1use super::{DirectiveValidationResult, DirectiveValidator, ParsedDirective};
4
5fn is_valid_length(value: &str) -> bool {
8 const UNITS: &[&str] = &["em", "ex", "px", "in", "cm", "mm", "pt", "pc", "%"];
9 let number = UNITS
10 .iter()
11 .find_map(|u| value.strip_suffix(u))
12 .unwrap_or(value);
13 !number.trim().is_empty() && number.trim().parse::<f64>().is_ok()
14}
15
16#[derive(Default)]
18pub struct CodeBlockValidator;
19
20impl CodeBlockValidator {
21 pub fn new() -> Self {
22 Self
23 }
24}
25
26impl DirectiveValidator for CodeBlockValidator {
27 fn name(&self) -> &str {
28 "code-block"
29 }
30
31 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
32 if directive.content.trim().is_empty() {
37 return DirectiveValidationResult::Warning(
38 "Code-block directive has no content".to_string(),
39 );
40 }
41
42 for (option, value) in &directive.options {
44 match option.as_str() {
45 "linenos" => {
46 if !value.is_empty() {
47 return DirectiveValidationResult::Error(
48 "linenos option should not have a value".to_string(),
49 );
50 }
51 }
52 "lineno-start" => {
53 if value.parse::<u32>().is_err() {
54 return DirectiveValidationResult::Error(
55 "lineno-start must be a positive integer".to_string(),
56 );
57 }
58 }
59 "emphasize-lines" => {
60 }
62 "caption" | "name" | "dedent" => {
63 }
65 _ => {
66 return DirectiveValidationResult::Warning(format!(
67 "Unknown option '{}' for code-block directive",
68 option
69 ));
70 }
71 }
72 }
73
74 DirectiveValidationResult::Valid
75 }
76
77 fn expected_arguments(&self) -> Vec<String> {
78 vec!["language".to_string()]
79 }
80
81 fn valid_options(&self) -> Vec<String> {
82 vec![
83 "linenos".to_string(),
84 "lineno-start".to_string(),
85 "emphasize-lines".to_string(),
86 "caption".to_string(),
87 "name".to_string(),
88 "dedent".to_string(),
89 "force".to_string(),
90 ]
91 }
92
93 fn requires_content(&self) -> bool {
94 false }
96
97 fn allows_content(&self) -> bool {
98 true
99 }
100}
101
102#[derive(Default)]
104pub struct NoteValidator;
105
106impl NoteValidator {
107 pub fn new() -> Self {
108 Self
109 }
110}
111
112impl DirectiveValidator for NoteValidator {
113 fn name(&self) -> &str {
114 "note"
115 }
116
117 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
118 if directive.content.trim().is_empty() {
121 return DirectiveValidationResult::Error("Note directive requires content".to_string());
122 }
123
124 for option in directive.options.keys() {
126 match option.as_str() {
127 "class" | "name" => {
128 }
130 _ => {
131 return DirectiveValidationResult::Warning(format!(
132 "Unknown option '{}' for note directive",
133 option
134 ));
135 }
136 }
137 }
138
139 DirectiveValidationResult::Valid
140 }
141
142 fn expected_arguments(&self) -> Vec<String> {
143 vec![]
144 }
145
146 fn valid_options(&self) -> Vec<String> {
147 vec!["class".to_string(), "name".to_string()]
148 }
149
150 fn requires_content(&self) -> bool {
151 true
152 }
153
154 fn allows_content(&self) -> bool {
155 true
156 }
157}
158
159#[derive(Default)]
161pub struct WarningValidator;
162
163impl WarningValidator {
164 pub fn new() -> Self {
165 Self
166 }
167}
168
169impl DirectiveValidator for WarningValidator {
170 fn name(&self) -> &str {
171 "warning"
172 }
173
174 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
175 if directive.content.trim().is_empty() {
178 return DirectiveValidationResult::Error(
179 "Warning directive requires content".to_string(),
180 );
181 }
182
183 for option in directive.options.keys() {
185 match option.as_str() {
186 "class" | "name" => {
187 }
189 _ => {
190 return DirectiveValidationResult::Warning(format!(
191 "Unknown option '{}' for warning directive",
192 option
193 ));
194 }
195 }
196 }
197
198 DirectiveValidationResult::Valid
199 }
200
201 fn expected_arguments(&self) -> Vec<String> {
202 vec![]
203 }
204
205 fn valid_options(&self) -> Vec<String> {
206 vec!["class".to_string(), "name".to_string()]
207 }
208
209 fn requires_content(&self) -> bool {
210 true
211 }
212
213 fn allows_content(&self) -> bool {
214 true
215 }
216}
217
218#[derive(Default)]
220pub struct ImageValidator;
221
222impl ImageValidator {
223 pub fn new() -> Self {
224 Self
225 }
226}
227
228impl DirectiveValidator for ImageValidator {
229 fn name(&self) -> &str {
230 "image"
231 }
232
233 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
234 if directive.arguments.is_empty() {
236 return DirectiveValidationResult::Error(
237 "Image directive requires a path argument".to_string(),
238 );
239 }
240
241 let image_path = &directive.arguments[0];
242 if image_path.is_empty() {
243 return DirectiveValidationResult::Error("Image path cannot be empty".to_string());
244 }
245
246 let valid_extensions = ["png", "jpg", "jpeg", "gif", "svg", "bmp", "webp"];
248 if let Some(extension) = image_path.split('.').next_back() {
249 if !valid_extensions.contains(&extension.to_lowercase().as_str()) {
250 return DirectiveValidationResult::Warning(format!(
251 "Unusual image extension: {}",
252 extension
253 ));
254 }
255 }
256
257 for (option, value) in &directive.options {
259 match option.as_str() {
260 "alt" | "target" | "class" | "name" => {
261 }
263 "width" | "height" => {
264 if !is_valid_length(value) {
265 return DirectiveValidationResult::Warning(format!(
266 "{} is not a valid length: '{}'",
267 option, value
268 ));
269 }
270 }
271 "scale" => {
272 if value.parse::<f32>().is_err() {
273 return DirectiveValidationResult::Error(
274 "Scale must be a number".to_string(),
275 );
276 }
277 }
278 "align" => {
279 let valid_alignments = ["left", "center", "right", "top", "middle", "bottom"];
280 if !valid_alignments.contains(&value.as_str()) {
281 return DirectiveValidationResult::Error(format!(
282 "Invalid alignment: {}. Valid options: {}",
283 value,
284 valid_alignments.join(", ")
285 ));
286 }
287 }
288 _ => {
289 return DirectiveValidationResult::Warning(format!(
290 "Unknown option '{}' for image directive",
291 option
292 ));
293 }
294 }
295 }
296
297 DirectiveValidationResult::Valid
298 }
299
300 fn expected_arguments(&self) -> Vec<String> {
301 vec!["image_uri".to_string()]
302 }
303
304 fn valid_options(&self) -> Vec<String> {
305 vec![
306 "alt".to_string(),
307 "height".to_string(),
308 "width".to_string(),
309 "scale".to_string(),
310 "align".to_string(),
311 "target".to_string(),
312 "class".to_string(),
313 "name".to_string(),
314 ]
315 }
316
317 fn requires_content(&self) -> bool {
318 false
319 }
320
321 fn allows_content(&self) -> bool {
322 false
323 }
324}
325
326#[derive(Default)]
328pub struct FigureValidator;
329
330impl FigureValidator {
331 pub fn new() -> Self {
332 Self
333 }
334}
335
336impl DirectiveValidator for FigureValidator {
337 fn name(&self) -> &str {
338 "figure"
339 }
340
341 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
342 if directive.arguments.is_empty() {
344 return DirectiveValidationResult::Error(
345 "Figure directive requires a path argument".to_string(),
346 );
347 }
348
349 let image_validator = ImageValidator::new();
351 let mut temp_directive = directive.clone();
352 temp_directive.name = "image".to_string();
353 let image_result = image_validator.validate(&temp_directive);
354
355 match image_result {
357 DirectiveValidationResult::Valid => DirectiveValidationResult::Valid,
358 other => other,
359 }
360 }
361
362 fn expected_arguments(&self) -> Vec<String> {
363 vec!["image_uri".to_string()]
364 }
365
366 fn valid_options(&self) -> Vec<String> {
367 vec![
368 "alt".to_string(),
369 "height".to_string(),
370 "width".to_string(),
371 "scale".to_string(),
372 "align".to_string(),
373 "target".to_string(),
374 "class".to_string(),
375 "name".to_string(),
376 "figwidth".to_string(),
377 "figclass".to_string(),
378 ]
379 }
380
381 fn requires_content(&self) -> bool {
382 false
383 }
384
385 fn allows_content(&self) -> bool {
386 true
387 }
388}
389
390#[derive(Default)]
392pub struct TocTreeValidator;
393
394impl TocTreeValidator {
395 pub fn new() -> Self {
396 Self
397 }
398}
399
400impl DirectiveValidator for TocTreeValidator {
401 fn name(&self) -> &str {
402 "toctree"
403 }
404
405 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
406 if directive.content.trim().is_empty() {
408 return DirectiveValidationResult::Warning("Toctree directive is empty".to_string());
409 }
410
411 for (option, value) in &directive.options {
413 match option.as_str() {
414 "maxdepth" => {
415 if let Ok(depth) = value.parse::<u32>() {
416 if depth > 10 {
417 return DirectiveValidationResult::Warning(
418 "Very deep toctree depth may cause performance issues".to_string(),
419 );
420 }
421 } else {
422 return DirectiveValidationResult::Error(
423 "maxdepth must be a positive integer".to_string(),
424 );
425 }
426 }
427 "numbered" | "titlesonly" | "glob" | "reversed" | "hidden" | "includehidden" => {
428 if !value.is_empty() {
430 return DirectiveValidationResult::Warning(format!(
431 "{} option should not have a value",
432 option
433 ));
434 }
435 }
436 "caption" | "name" | "class" => {
437 }
439 _ => {
440 return DirectiveValidationResult::Warning(format!(
441 "Unknown option '{}' for toctree directive",
442 option
443 ));
444 }
445 }
446 }
447
448 DirectiveValidationResult::Valid
449 }
450
451 fn expected_arguments(&self) -> Vec<String> {
452 vec![]
453 }
454
455 fn valid_options(&self) -> Vec<String> {
456 vec![
457 "maxdepth".to_string(),
458 "numbered".to_string(),
459 "titlesonly".to_string(),
460 "glob".to_string(),
461 "reversed".to_string(),
462 "hidden".to_string(),
463 "includehidden".to_string(),
464 "caption".to_string(),
465 "name".to_string(),
466 "class".to_string(),
467 ]
468 }
469
470 fn requires_content(&self) -> bool {
471 false
472 }
473
474 fn allows_content(&self) -> bool {
475 true
476 }
477}
478
479#[derive(Default)]
481pub struct IncludeValidator;
482
483impl IncludeValidator {
484 pub fn new() -> Self {
485 Self
486 }
487}
488
489impl DirectiveValidator for IncludeValidator {
490 fn name(&self) -> &str {
491 "include"
492 }
493
494 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
495 if directive.arguments.is_empty() {
497 return DirectiveValidationResult::Error(
498 "Include directive requires a file path".to_string(),
499 );
500 }
501
502 let file_path = &directive.arguments[0];
503 if file_path.is_empty() {
504 return DirectiveValidationResult::Error(
505 "Include file path cannot be empty".to_string(),
506 );
507 }
508
509 if let Some(extension) = file_path.split('.').next_back() {
511 let valid_extensions = ["rst", "txt", "md", "inc"];
512 if !valid_extensions.contains(&extension.to_lowercase().as_str()) {
513 return DirectiveValidationResult::Warning(format!(
514 "Unusual file extension for include: {}",
515 extension
516 ));
517 }
518 }
519
520 DirectiveValidationResult::Valid
521 }
522
523 fn expected_arguments(&self) -> Vec<String> {
524 vec!["filename".to_string()]
525 }
526
527 fn valid_options(&self) -> Vec<String> {
528 vec![
529 "start-line".to_string(),
530 "end-line".to_string(),
531 "start-after".to_string(),
532 "end-before".to_string(),
533 "literal".to_string(),
534 "code".to_string(),
535 "number-lines".to_string(),
536 "encoding".to_string(),
537 "tab-width".to_string(),
538 ]
539 }
540
541 fn requires_content(&self) -> bool {
542 false
543 }
544
545 fn allows_content(&self) -> bool {
546 false
547 }
548}
549
550#[derive(Default)]
552pub struct LiteralIncludeValidator;
553
554impl LiteralIncludeValidator {
555 pub fn new() -> Self {
556 Self
557 }
558}
559
560impl DirectiveValidator for LiteralIncludeValidator {
561 fn name(&self) -> &str {
562 "literalinclude"
563 }
564
565 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
566 if directive.arguments.is_empty() {
568 return DirectiveValidationResult::Error(
569 "Literalinclude directive requires a file path".to_string(),
570 );
571 }
572
573 let file_path = &directive.arguments[0];
574 if file_path.is_empty() {
575 return DirectiveValidationResult::Error(
576 "Literalinclude file path cannot be empty".to_string(),
577 );
578 }
579
580 for (option, value) in &directive.options {
582 match option.as_str() {
583 "start-line" | "end-line" | "lineno-start" | "tab-width" => {
584 if value.parse::<u32>().is_err() {
585 return DirectiveValidationResult::Error(format!(
586 "{} must be a positive integer",
587 option
588 ));
589 }
590 }
591 "dedent" => {
592 if !value.is_empty() && value.parse::<u32>().is_err() {
593 return DirectiveValidationResult::Error(
594 "dedent must be a positive integer".to_string(),
595 );
596 }
597 }
598 "language" | "start-after" | "end-before" | "prepend" | "append" | "caption"
599 | "name" | "class" | "encoding" | "pyobject" | "diff" => {
600 }
602 "linenos" | "force" => {
603 if !value.is_empty() {
605 return DirectiveValidationResult::Warning(format!(
606 "{} option should not have a value",
607 option
608 ));
609 }
610 }
611 _ => {
612 return DirectiveValidationResult::Warning(format!(
613 "Unknown option '{}' for literalinclude directive",
614 option
615 ));
616 }
617 }
618 }
619
620 DirectiveValidationResult::Valid
621 }
622
623 fn expected_arguments(&self) -> Vec<String> {
624 vec!["filename".to_string()]
625 }
626
627 fn valid_options(&self) -> Vec<String> {
628 vec![
629 "language".to_string(),
630 "linenos".to_string(),
631 "lineno-start".to_string(),
632 "emphasize-lines".to_string(),
633 "lines".to_string(),
634 "start-line".to_string(),
635 "end-line".to_string(),
636 "start-after".to_string(),
637 "end-before".to_string(),
638 "prepend".to_string(),
639 "append".to_string(),
640 "dedent".to_string(),
641 "tab-width".to_string(),
642 "encoding".to_string(),
643 "pyobject".to_string(),
644 "caption".to_string(),
645 "name".to_string(),
646 "class".to_string(),
647 "diff".to_string(),
648 "force".to_string(),
649 ]
650 }
651
652 fn requires_content(&self) -> bool {
653 false
654 }
655
656 fn allows_content(&self) -> bool {
657 false
658 }
659}
660
661#[derive(Default)]
663pub struct AdmonitionValidator;
664
665impl AdmonitionValidator {
666 pub fn new() -> Self {
667 Self
668 }
669}
670
671impl DirectiveValidator for AdmonitionValidator {
672 fn name(&self) -> &str {
673 "admonition"
674 }
675
676 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
677 if directive.arguments.is_empty() {
679 return DirectiveValidationResult::Error(
680 "Admonition directive requires a title argument".to_string(),
681 );
682 }
683
684 if directive.content.trim().is_empty() {
686 return DirectiveValidationResult::Warning(
687 "Admonition directive has no content".to_string(),
688 );
689 }
690
691 DirectiveValidationResult::Valid
692 }
693
694 fn expected_arguments(&self) -> Vec<String> {
695 vec!["title".to_string()]
696 }
697
698 fn valid_options(&self) -> Vec<String> {
699 vec!["class".to_string(), "name".to_string()]
700 }
701
702 fn requires_content(&self) -> bool {
703 false
704 }
705
706 fn allows_content(&self) -> bool {
707 true
708 }
709}
710
711#[derive(Default)]
713pub struct MathValidator;
714
715impl MathValidator {
716 pub fn new() -> Self {
717 Self
718 }
719}
720
721impl DirectiveValidator for MathValidator {
722 fn name(&self) -> &str {
723 "math"
724 }
725
726 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
727 if directive.content.trim().is_empty() {
729 return DirectiveValidationResult::Error(
730 "Math directive requires LaTeX math content".to_string(),
731 );
732 }
733
734 let content = directive.content.trim();
736 let open_braces = content.matches('{').count();
737 let close_braces = content.matches('}').count();
738
739 if open_braces != close_braces {
740 return DirectiveValidationResult::Warning(
741 "Unmatched braces in math content".to_string(),
742 );
743 }
744
745 DirectiveValidationResult::Valid
746 }
747
748 fn expected_arguments(&self) -> Vec<String> {
749 vec![]
750 }
751
752 fn valid_options(&self) -> Vec<String> {
753 vec!["label".to_string(), "name".to_string(), "class".to_string()]
754 }
755
756 fn requires_content(&self) -> bool {
757 true
758 }
759
760 fn allows_content(&self) -> bool {
761 true
762 }
763}
764
765#[cfg(test)]
766mod tests {
767 use super::*;
768 use crate::directives::validation::SourceLocation;
769 use std::collections::HashMap;
770
771 fn create_test_directive(
772 name: &str,
773 args: Vec<String>,
774 options: HashMap<String, String>,
775 content: &str,
776 ) -> ParsedDirective {
777 ParsedDirective {
778 name: name.to_string(),
779 arguments: args,
780 options,
781 content: content.to_string(),
782 location: SourceLocation {
783 file: "test.rst".to_string(),
784 line: 1,
785 column: 1,
786 },
787 }
788 }
789
790 #[test]
791 fn test_code_block_validator() {
792 let validator = CodeBlockValidator::new();
793
794 let directive = create_test_directive(
796 "code-block",
797 vec!["python".to_string()],
798 HashMap::new(),
799 "print('Hello, world!')",
800 );
801 assert_eq!(
802 validator.validate(&directive),
803 DirectiveValidationResult::Valid
804 );
805
806 let directive = create_test_directive(
808 "code-block",
809 vec![],
810 HashMap::new(),
811 "print('Hello, world!')",
812 );
813 assert_eq!(
814 validator.validate(&directive),
815 DirectiveValidationResult::Valid
816 );
817
818 for width in ["100", "2cm", "50%", "1.5em", "12pt"] {
820 let mut options = HashMap::new();
821 options.insert("width".to_string(), width.to_string());
822 let directive = create_test_directive("image", vec!["x.png".to_string()], options, "");
823 assert_eq!(
824 ImageValidator::new().validate(&directive),
825 DirectiveValidationResult::Valid,
826 "width '{width}' must be accepted"
827 );
828 }
829 }
830
831 #[test]
832 fn test_note_validator() {
833 let validator = NoteValidator::new();
834
835 let directive = create_test_directive("note", vec![], HashMap::new(), "This is a note");
837 assert_eq!(
838 validator.validate(&directive),
839 DirectiveValidationResult::Valid
840 );
841
842 let directive = create_test_directive("note", vec![], HashMap::new(), "");
844 assert!(matches!(
845 validator.validate(&directive),
846 DirectiveValidationResult::Error(_)
847 ));
848 }
849
850 #[test]
851 fn test_image_validator() {
852 let validator = ImageValidator::new();
853
854 let directive =
856 create_test_directive("image", vec!["test.png".to_string()], HashMap::new(), "");
857 assert_eq!(
858 validator.validate(&directive),
859 DirectiveValidationResult::Valid
860 );
861
862 let directive = create_test_directive("image", vec![], HashMap::new(), "");
864 assert!(matches!(
865 validator.validate(&directive),
866 DirectiveValidationResult::Error(_)
867 ));
868 }
869
870 #[test]
871 fn test_math_validator() {
872 let validator = MathValidator::new();
873
874 let directive = create_test_directive("math", vec![], HashMap::new(), "x = \\frac{a}{b}");
876 assert_eq!(
877 validator.validate(&directive),
878 DirectiveValidationResult::Valid
879 );
880
881 let directive = create_test_directive("math", vec![], HashMap::new(), "");
883 assert!(matches!(
884 validator.validate(&directive),
885 DirectiveValidationResult::Error(_)
886 ));
887 }
888}