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