1use super::{DirectiveValidationResult, DirectiveValidator, ParsedDirective};
28
29const CODE_BLOCK_OPTIONS: &[&str] = &[
31 "force",
32 "linenos",
33 "dedent",
34 "lineno-start",
35 "emphasize-lines",
36 "caption",
37 "class",
38 "name",
39];
40
41const ADMONITION_OPTIONS: &[&str] = &["class", "name"];
43
44const IMAGE_OPTIONS: &[&str] = &[
46 "alt", "height", "width", "scale", "align", "target", "loading", "class", "name",
47];
48
49const FIGURE_OPTIONS: &[&str] = &[
51 "alt", "height", "width", "scale", "align", "target", "loading", "class", "name", "figwidth",
52 "figclass", "figname",
53];
54
55const FIGURE_ONLY_OPTIONS: &[&str] = &["figwidth", "figclass", "figname"];
58
59const TOCTREE_OPTIONS: &[&str] = &[
61 "maxdepth",
62 "name",
63 "class",
64 "caption",
65 "glob",
66 "hidden",
67 "includehidden",
68 "numbered",
69 "titlesonly",
70 "reversed",
71];
72
73const INCLUDE_OPTIONS: &[&str] = &[
75 "literal",
76 "code",
77 "encoding",
78 "parser",
79 "tab-width",
80 "start-line",
81 "end-line",
82 "start-after",
83 "end-before",
84 "number-lines",
85 "class",
86 "name",
87];
88
89const LITERALINCLUDE_OPTIONS: &[&str] = &[
97 "dedent",
98 "linenos",
99 "lineno-start",
100 "lineno-match",
101 "tab-width",
102 "language",
103 "force",
104 "encoding",
105 "pyobject",
106 "lines",
107 "start-after",
108 "end-before",
109 "start-at",
110 "end-at",
111 "prepend",
112 "append",
113 "emphasize-lines",
114 "caption",
115 "class",
116 "name",
117 "diff",
118];
119
120const MATH_OPTIONS: &[&str] = &["label", "name", "class", "no-wrap", "nowrap"];
122
123fn names(options: &[&'static str]) -> Vec<String> {
126 options.iter().map(|name| (*name).to_string()).collect()
127}
128
129fn is_valid_length(value: &str) -> bool {
132 const UNITS: &[&str] = &["em", "ex", "px", "in", "cm", "mm", "pt", "pc", "%"];
133 let number = UNITS
134 .iter()
135 .find_map(|u| value.strip_suffix(u))
136 .unwrap_or(value);
137 !number.trim().is_empty() && number.trim().parse::<f64>().is_ok()
138}
139
140#[derive(Default)]
142pub struct CodeBlockValidator;
143
144impl CodeBlockValidator {
145 pub fn new() -> Self {
146 Self
147 }
148}
149
150impl DirectiveValidator for CodeBlockValidator {
151 fn name(&self) -> &str {
152 "code-block"
153 }
154
155 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
156 for (option, value) in &directive.options {
164 match option.as_str() {
165 "linenos" | "force" => {
169 if !value.is_empty() {
170 return DirectiveValidationResult::Error(format!(
171 "{option} option should not have a value"
172 ));
173 }
174 }
175 "emphasize-lines" => {
176 }
178 "caption" | "name" | "dedent" | "class" | "lineno-start" => {}
185 _ => {
186 return DirectiveValidationResult::Warning(format!(
187 "Unknown option '{}' for code-block directive",
188 option
189 ));
190 }
191 }
192 }
193
194 DirectiveValidationResult::Valid
195 }
196
197 fn expected_arguments(&self) -> Vec<String> {
198 vec!["language".to_string()]
199 }
200
201 fn valid_options(&self) -> Vec<String> {
202 names(CODE_BLOCK_OPTIONS)
203 }
204
205 fn requires_content(&self) -> bool {
206 false }
208
209 fn allows_content(&self) -> bool {
210 true
211 }
212}
213
214#[derive(Default)]
216pub struct NoteValidator;
217
218impl NoteValidator {
219 pub fn new() -> Self {
220 Self
221 }
222}
223
224impl DirectiveValidator for NoteValidator {
225 fn name(&self) -> &str {
226 "note"
227 }
228
229 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
230 if directive.content.trim().is_empty() {
233 return DirectiveValidationResult::Error("Note directive requires content".to_string());
234 }
235
236 for option in directive.options.keys() {
238 match option.as_str() {
239 "class" | "name" => {
240 }
242 _ => {
243 return DirectiveValidationResult::Warning(format!(
244 "Unknown option '{}' for note directive",
245 option
246 ));
247 }
248 }
249 }
250
251 DirectiveValidationResult::Valid
252 }
253
254 fn expected_arguments(&self) -> Vec<String> {
255 vec![]
256 }
257
258 fn valid_options(&self) -> Vec<String> {
259 names(ADMONITION_OPTIONS)
260 }
261
262 fn requires_content(&self) -> bool {
263 true
264 }
265
266 fn allows_content(&self) -> bool {
267 true
268 }
269}
270
271#[derive(Default)]
273pub struct WarningValidator;
274
275impl WarningValidator {
276 pub fn new() -> Self {
277 Self
278 }
279}
280
281impl DirectiveValidator for WarningValidator {
282 fn name(&self) -> &str {
283 "warning"
284 }
285
286 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
287 if directive.content.trim().is_empty() {
290 return DirectiveValidationResult::Error(
291 "Warning directive requires content".to_string(),
292 );
293 }
294
295 for option in directive.options.keys() {
297 match option.as_str() {
298 "class" | "name" => {
299 }
301 _ => {
302 return DirectiveValidationResult::Warning(format!(
303 "Unknown option '{}' for warning directive",
304 option
305 ));
306 }
307 }
308 }
309
310 DirectiveValidationResult::Valid
311 }
312
313 fn expected_arguments(&self) -> Vec<String> {
314 vec![]
315 }
316
317 fn valid_options(&self) -> Vec<String> {
318 names(ADMONITION_OPTIONS)
319 }
320
321 fn requires_content(&self) -> bool {
322 true
323 }
324
325 fn allows_content(&self) -> bool {
326 true
327 }
328}
329
330#[derive(Default)]
332pub struct ImageValidator;
333
334impl ImageValidator {
335 pub fn new() -> Self {
336 Self
337 }
338}
339
340impl DirectiveValidator for ImageValidator {
341 fn name(&self) -> &str {
342 "image"
343 }
344
345 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
346 if directive.arguments.is_empty() {
348 return DirectiveValidationResult::Error(
349 "Image directive requires a path argument".to_string(),
350 );
351 }
352
353 let image_path = &directive.arguments[0];
354 if image_path.is_empty() {
355 return DirectiveValidationResult::Error("Image path cannot be empty".to_string());
356 }
357
358 let valid_extensions = ["png", "jpg", "jpeg", "gif", "svg", "bmp", "webp"];
360 if let Some(extension) = image_path.split('.').next_back() {
361 if !valid_extensions.contains(&extension.to_lowercase().as_str()) {
362 return DirectiveValidationResult::Warning(format!(
363 "Unusual image extension: {}",
364 extension
365 ));
366 }
367 }
368
369 for (option, value) in &directive.options {
371 match option.as_str() {
372 "alt" | "target" | "class" | "name" | "loading" => {
376 }
378 "width" | "height" => {
379 if !is_valid_length(value) {
380 return DirectiveValidationResult::Warning(format!(
381 "{} is not a valid length: '{}'",
382 option, value
383 ));
384 }
385 }
386 "scale" => {
387 if value.parse::<f32>().is_err() {
388 return DirectiveValidationResult::Error(
389 "Scale must be a number".to_string(),
390 );
391 }
392 }
393 "align" => {
394 let valid_alignments = ["left", "center", "right", "top", "middle", "bottom"];
395 if !valid_alignments.contains(&value.as_str()) {
396 return DirectiveValidationResult::Error(format!(
397 "Invalid alignment: {}. Valid options: {}",
398 value,
399 valid_alignments.join(", ")
400 ));
401 }
402 }
403 _ => {
404 return DirectiveValidationResult::Warning(format!(
405 "Unknown option '{}' for image directive",
406 option
407 ));
408 }
409 }
410 }
411
412 DirectiveValidationResult::Valid
413 }
414
415 fn expected_arguments(&self) -> Vec<String> {
416 vec!["image_uri".to_string()]
417 }
418
419 fn valid_options(&self) -> Vec<String> {
420 names(IMAGE_OPTIONS)
421 }
422
423 fn requires_content(&self) -> bool {
424 false
425 }
426
427 fn allows_content(&self) -> bool {
428 false
429 }
430}
431
432#[derive(Default)]
434pub struct FigureValidator;
435
436impl FigureValidator {
437 pub fn new() -> Self {
438 Self
439 }
440}
441
442impl DirectiveValidator for FigureValidator {
443 fn name(&self) -> &str {
444 "figure"
445 }
446
447 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
448 if directive.arguments.is_empty() {
450 return DirectiveValidationResult::Error(
451 "Figure directive requires a path argument".to_string(),
452 );
453 }
454
455 let image_validator = ImageValidator::new();
462 let mut temp_directive = directive.clone();
463 temp_directive.name = "image".to_string();
464 for option in FIGURE_ONLY_OPTIONS {
465 temp_directive.options.remove(*option);
466 }
467 let image_result = image_validator.validate(&temp_directive);
468
469 match image_result {
471 DirectiveValidationResult::Valid => DirectiveValidationResult::Valid,
472 other => other,
473 }
474 }
475
476 fn expected_arguments(&self) -> Vec<String> {
477 vec!["image_uri".to_string()]
478 }
479
480 fn valid_options(&self) -> Vec<String> {
481 names(FIGURE_OPTIONS)
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 TocTreeValidator;
496
497impl TocTreeValidator {
498 pub fn new() -> Self {
499 Self
500 }
501}
502
503impl DirectiveValidator for TocTreeValidator {
504 fn name(&self) -> &str {
505 "toctree"
506 }
507
508 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
509 if directive.content.trim().is_empty() {
511 return DirectiveValidationResult::Warning("Toctree directive is empty".to_string());
512 }
513
514 for (option, value) in &directive.options {
516 match option.as_str() {
517 "maxdepth" => {}
525 "numbered" => {}
534 "titlesonly" | "glob" | "reversed" | "hidden" | "includehidden" => {
535 if !value.is_empty() {
537 return DirectiveValidationResult::Warning(format!(
538 "{} option should not have a value",
539 option
540 ));
541 }
542 }
543 "caption" | "name" | "class" => {
544 }
546 _ => {
547 return DirectiveValidationResult::Warning(format!(
548 "Unknown option '{}' for toctree directive",
549 option
550 ));
551 }
552 }
553 }
554
555 DirectiveValidationResult::Valid
556 }
557
558 fn expected_arguments(&self) -> Vec<String> {
559 vec![]
560 }
561
562 fn valid_options(&self) -> Vec<String> {
563 names(TOCTREE_OPTIONS)
564 }
565
566 fn requires_content(&self) -> bool {
567 false
568 }
569
570 fn allows_content(&self) -> bool {
571 true
572 }
573}
574
575#[derive(Default)]
577pub struct IncludeValidator;
578
579impl IncludeValidator {
580 pub fn new() -> Self {
581 Self
582 }
583}
584
585impl DirectiveValidator for IncludeValidator {
586 fn name(&self) -> &str {
587 "include"
588 }
589
590 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
591 if directive.arguments.is_empty() {
593 return DirectiveValidationResult::Error(
594 "Include directive requires a file path".to_string(),
595 );
596 }
597
598 let file_path = &directive.arguments[0];
599 if file_path.is_empty() {
600 return DirectiveValidationResult::Error(
601 "Include file path cannot be empty".to_string(),
602 );
603 }
604
605 DirectiveValidationResult::Valid
612 }
613
614 fn expected_arguments(&self) -> Vec<String> {
615 vec!["filename".to_string()]
616 }
617
618 fn valid_options(&self) -> Vec<String> {
619 names(INCLUDE_OPTIONS)
620 }
621
622 fn requires_content(&self) -> bool {
623 false
624 }
625
626 fn allows_content(&self) -> bool {
627 false
628 }
629}
630
631#[derive(Default)]
633pub struct LiteralIncludeValidator;
634
635impl LiteralIncludeValidator {
636 pub fn new() -> Self {
637 Self
638 }
639}
640
641impl DirectiveValidator for LiteralIncludeValidator {
642 fn name(&self) -> &str {
643 "literalinclude"
644 }
645
646 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
647 if directive.arguments.is_empty() {
649 return DirectiveValidationResult::Error(
650 "Literalinclude directive requires a file path".to_string(),
651 );
652 }
653
654 let file_path = &directive.arguments[0];
655 if file_path.is_empty() {
656 return DirectiveValidationResult::Error(
657 "Literalinclude file path cannot be empty".to_string(),
658 );
659 }
660
661 for (option, value) in &directive.options {
669 match option.as_str() {
670 "language" | "start-after" | "end-before" | "prepend" | "append" | "caption"
671 | "name" | "class" | "encoding" | "pyobject" | "diff" | "lineno-start"
672 | "tab-width" | "dedent" => {
673 }
675 "linenos" | "force" | "lineno-match" => {
676 if !value.is_empty() {
678 return DirectiveValidationResult::Warning(format!(
679 "{} option should not have a value",
680 option
681 ));
682 }
683 }
684 _ if LITERALINCLUDE_OPTIONS.contains(&option.as_str()) => {}
693 _ => {
694 return DirectiveValidationResult::Warning(format!(
695 "Unknown option '{}' for literalinclude directive",
696 option
697 ));
698 }
699 }
700 }
701
702 DirectiveValidationResult::Valid
703 }
704
705 fn expected_arguments(&self) -> Vec<String> {
706 vec!["filename".to_string()]
707 }
708
709 fn valid_options(&self) -> Vec<String> {
710 names(LITERALINCLUDE_OPTIONS)
711 }
712
713 fn requires_content(&self) -> bool {
714 false
715 }
716
717 fn allows_content(&self) -> bool {
718 false
719 }
720}
721
722#[derive(Default)]
724pub struct AdmonitionValidator;
725
726impl AdmonitionValidator {
727 pub fn new() -> Self {
728 Self
729 }
730}
731
732impl DirectiveValidator for AdmonitionValidator {
733 fn name(&self) -> &str {
734 "admonition"
735 }
736
737 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
738 if directive.arguments.is_empty() {
740 return DirectiveValidationResult::Error(
741 "Admonition directive requires a title argument".to_string(),
742 );
743 }
744
745 if directive.content.trim().is_empty() {
747 return DirectiveValidationResult::Warning(
748 "Admonition directive has no content".to_string(),
749 );
750 }
751
752 DirectiveValidationResult::Valid
753 }
754
755 fn expected_arguments(&self) -> Vec<String> {
756 vec!["title".to_string()]
757 }
758
759 fn valid_options(&self) -> Vec<String> {
760 names(ADMONITION_OPTIONS)
761 }
762
763 fn requires_content(&self) -> bool {
764 false
765 }
766
767 fn allows_content(&self) -> bool {
768 true
769 }
770}
771
772#[derive(Default)]
774pub struct MathValidator;
775
776impl MathValidator {
777 pub fn new() -> Self {
778 Self
779 }
780}
781
782impl DirectiveValidator for MathValidator {
783 fn name(&self) -> &str {
784 "math"
785 }
786
787 fn validate(&self, directive: &ParsedDirective) -> DirectiveValidationResult {
788 if directive.content.trim().is_empty() {
790 return DirectiveValidationResult::Error(
791 "Math directive requires LaTeX math content".to_string(),
792 );
793 }
794
795 let content = directive.content.trim();
797 let open_braces = content.matches('{').count();
798 let close_braces = content.matches('}').count();
799
800 if open_braces != close_braces {
801 return DirectiveValidationResult::Warning(
802 "Unmatched braces in math content".to_string(),
803 );
804 }
805
806 DirectiveValidationResult::Valid
807 }
808
809 fn expected_arguments(&self) -> Vec<String> {
810 vec![]
811 }
812
813 fn valid_options(&self) -> Vec<String> {
814 names(MATH_OPTIONS)
815 }
816
817 fn requires_content(&self) -> bool {
818 true
819 }
820
821 fn allows_content(&self) -> bool {
822 true
823 }
824}
825
826#[cfg(test)]
827mod tests {
828 use super::*;
829 use crate::directives::validation::SourceLocation;
830 use std::collections::HashMap;
831
832 fn create_test_directive(
833 name: &str,
834 args: Vec<String>,
835 options: HashMap<String, String>,
836 content: &str,
837 ) -> ParsedDirective {
838 ParsedDirective {
839 name: name.to_string(),
840 arguments: args,
841 options,
842 content: content.to_string(),
843 location: SourceLocation {
844 file: "test.rst".to_string(),
845 line: 1,
846 column: 1,
847 },
848 }
849 }
850
851 #[test]
852 fn test_code_block_validator() {
853 let validator = CodeBlockValidator::new();
854
855 let directive = create_test_directive(
857 "code-block",
858 vec!["python".to_string()],
859 HashMap::new(),
860 "print('Hello, world!')",
861 );
862 assert_eq!(
863 validator.validate(&directive),
864 DirectiveValidationResult::Valid
865 );
866
867 let directive = create_test_directive(
869 "code-block",
870 vec![],
871 HashMap::new(),
872 "print('Hello, world!')",
873 );
874 assert_eq!(
875 validator.validate(&directive),
876 DirectiveValidationResult::Valid
877 );
878
879 for width in ["100", "2cm", "50%", "1.5em", "12pt"] {
881 let mut options = HashMap::new();
882 options.insert("width".to_string(), width.to_string());
883 let directive = create_test_directive("image", vec!["x.png".to_string()], options, "");
884 assert_eq!(
885 ImageValidator::new().validate(&directive),
886 DirectiveValidationResult::Valid,
887 "width '{width}' must be accepted"
888 );
889 }
890 }
891
892 #[test]
893 fn test_note_validator() {
894 let validator = NoteValidator::new();
895
896 let directive = create_test_directive("note", vec![], HashMap::new(), "This is a note");
898 assert_eq!(
899 validator.validate(&directive),
900 DirectiveValidationResult::Valid
901 );
902
903 let directive = create_test_directive("note", vec![], HashMap::new(), "");
905 assert!(matches!(
906 validator.validate(&directive),
907 DirectiveValidationResult::Error(_)
908 ));
909 }
910
911 #[test]
912 fn test_image_validator() {
913 let validator = ImageValidator::new();
914
915 let directive =
917 create_test_directive("image", vec!["test.png".to_string()], HashMap::new(), "");
918 assert_eq!(
919 validator.validate(&directive),
920 DirectiveValidationResult::Valid
921 );
922
923 let directive = create_test_directive("image", vec![], HashMap::new(), "");
925 assert!(matches!(
926 validator.validate(&directive),
927 DirectiveValidationResult::Error(_)
928 ));
929 }
930
931 #[test]
932 fn test_math_validator() {
933 let validator = MathValidator::new();
934
935 let directive = create_test_directive("math", vec![], HashMap::new(), "x = \\frac{a}{b}");
937 assert_eq!(
938 validator.validate(&directive),
939 DirectiveValidationResult::Valid
940 );
941
942 let directive = create_test_directive("math", vec![], HashMap::new(), "");
944 assert!(matches!(
945 validator.validate(&directive),
946 DirectiveValidationResult::Error(_)
947 ));
948 }
949
950 #[test]
958 fn literalinclude_accepts_every_option_it_advertises() {
959 let validator = LiteralIncludeValidator::new();
960
961 for option in validator.valid_options() {
962 let value = if matches!(
966 option.as_str(),
967 "linenos" | "force" | "lineno-match" | "dedent"
968 ) {
969 String::new()
970 } else {
971 "1".to_string()
972 };
973 let mut options = HashMap::new();
974 options.insert(option.clone(), value);
975 let directive =
976 create_test_directive("literalinclude", vec!["f.py".to_string()], options, "");
977 assert_eq!(
978 validator.validate(&directive),
979 DirectiveValidationResult::Valid,
980 "option {option:?} is advertised by valid_options but does not validate"
981 );
982 }
983
984 let mut options = HashMap::new();
986 options.insert("no-such-option".to_string(), String::new());
987 let directive =
988 create_test_directive("literalinclude", vec!["f.py".to_string()], options, "");
989 assert_eq!(
990 validator.validate(&directive),
991 DirectiveValidationResult::Warning(
992 "Unknown option 'no-such-option' for literalinclude directive".to_string()
993 )
994 );
995 }
996
997 fn every_validator() -> Vec<(Box<dyn DirectiveValidator>, Vec<String>, &'static str)> {
1001 let arg = |s: &str| vec![s.to_string()];
1002 vec![
1003 (
1004 Box::new(CodeBlockValidator::new()),
1005 arg("python"),
1006 "print(1)",
1007 ),
1008 (Box::new(NoteValidator::new()), vec![], "body"),
1009 (Box::new(WarningValidator::new()), vec![], "body"),
1010 (Box::new(ImageValidator::new()), arg("x.png"), ""),
1011 (Box::new(FigureValidator::new()), arg("x.png"), "caption"),
1012 (Box::new(TocTreeValidator::new()), vec![], "a\nb"),
1013 (Box::new(IncludeValidator::new()), arg("inc.rst"), ""),
1014 (Box::new(LiteralIncludeValidator::new()), arg("f.py"), ""),
1015 (Box::new(AdmonitionValidator::new()), arg("Title"), "body"),
1016 (Box::new(MathValidator::new()), vec![], "x = 1"),
1017 ]
1018 }
1019
1020 #[test]
1038 fn every_validator_accepts_every_option_it_advertises() {
1039 for (validator, arguments, content) in every_validator() {
1040 for option in validator.valid_options() {
1041 for value in ["", "1", "left", "-1", "0"] {
1042 let mut options = HashMap::new();
1043 options.insert(option.clone(), value.to_string());
1044 let directive = create_test_directive(
1045 validator.name(),
1046 arguments.clone(),
1047 options,
1048 content,
1049 );
1050 if let DirectiveValidationResult::Warning(message)
1051 | DirectiveValidationResult::Error(message) = validator.validate(&directive)
1052 {
1053 assert!(
1054 !message.starts_with(&format!("Unknown option '{option}'")),
1055 "{}: option {option:?} is advertised by valid_options \
1056 but its validate() calls it unknown (value {value:?})",
1057 validator.name()
1058 );
1059 }
1060 }
1061 }
1062 }
1063 }
1064
1065 #[test]
1078 fn validator_option_lists_match_the_parser_spec() {
1079 use std::collections::BTreeSet;
1080 for (validator, _, _) in every_validator() {
1081 let name = validator.name();
1082 let spec: BTreeSet<String> = crate::rst::block::directive_option_names(name)
1083 .unwrap_or_else(|| panic!("{name}: no parse-time directive spec"))
1084 .into_iter()
1085 .map(str::to_string)
1086 .collect();
1087 let advertised: BTreeSet<String> = validator.valid_options().into_iter().collect();
1088 assert_eq!(
1089 advertised,
1090 spec,
1091 "{name}: valid_options and the parser's option_spec disagree.\n \
1092 advertised but not in the spec: {:?}\n \
1093 in the spec but not advertised: {:?}",
1094 advertised.difference(&spec).collect::<Vec<_>>(),
1095 spec.difference(&advertised).collect::<Vec<_>>(),
1096 );
1097 }
1098 }
1099
1100 #[test]
1104 fn start_line_and_end_line_are_include_only() {
1105 for option in ["start-line", "end-line"] {
1106 assert!(
1107 IncludeValidator::new()
1108 .valid_options()
1109 .contains(&option.to_string()),
1110 "include must still advertise {option:?}"
1111 );
1112 assert!(
1113 !LiteralIncludeValidator::new()
1114 .valid_options()
1115 .contains(&option.to_string()),
1116 "literalinclude must not advertise {option:?}: sphinx 9.1.0's \
1117 LiteralInclude.option_spec has no such key"
1118 );
1119 let mut options = HashMap::new();
1120 options.insert(option.to_string(), "2".to_string());
1121 let directive =
1122 create_test_directive("literalinclude", vec!["f.py".to_string()], options, "");
1123 assert_eq!(
1124 LiteralIncludeValidator::new().validate(&directive),
1125 DirectiveValidationResult::Warning(format!(
1126 "Unknown option '{option}' for literalinclude directive"
1127 ))
1128 );
1129 }
1130 }
1131 #[test]
1138 fn integer_options_accept_the_values_sphinxs_converters_accept() {
1139 let cases: &[(&str, Vec<String>, &str, &str, &str)] = &[
1140 (
1141 "literalinclude",
1142 vec!["f.py".to_string()],
1143 "",
1144 "tab-width",
1145 "-1",
1146 ),
1147 (
1148 "literalinclude",
1149 vec!["f.py".to_string()],
1150 "",
1151 "lineno-start",
1152 "-3",
1153 ),
1154 (
1155 "literalinclude",
1156 vec!["f.py".to_string()],
1157 "",
1158 "lineno-start",
1159 "0",
1160 ),
1161 (
1162 "literalinclude",
1163 vec!["f.py".to_string()],
1164 "",
1165 "dedent",
1166 "-2",
1167 ),
1168 ("literalinclude", vec!["f.py".to_string()], "", "dedent", ""),
1169 (
1170 "code-block",
1171 vec!["python".to_string()],
1172 "x = 1",
1173 "lineno-start",
1174 "-3",
1175 ),
1176 (
1177 "code-block",
1178 vec!["python".to_string()],
1179 "x = 1",
1180 "lineno-start",
1181 "0",
1182 ),
1183 ("code-block", vec![], "x = 1", "dedent", "-2"),
1184 ("toctree", vec![], "a\nb", "maxdepth", "-1"),
1185 ("toctree", vec![], "a\nb", "maxdepth", "99"),
1186 ];
1187 let registry = crate::directives::validation::DirectiveRegistry::with_builtin_validators();
1188 for (name, arguments, content, option, value) in cases {
1189 let mut options = HashMap::new();
1190 options.insert((*option).to_string(), (*value).to_string());
1191 let directive = create_test_directive(name, arguments.clone(), options, content);
1192 assert_eq!(
1193 registry.validate_directive(&directive),
1194 DirectiveValidationResult::Valid,
1195 "{name} :{option}: {value:?} is accepted by sphinx-build"
1196 );
1197 }
1198 }
1199
1200 #[test]
1205 fn include_has_no_opinion_on_the_targets_extension() {
1206 let registry = crate::directives::validation::DirectiveRegistry::with_builtin_validators();
1207 for (target, option) in [
1208 ("<isonum.txt>", None),
1209 ("snippet.py", Some("literal")),
1210 ("snippet.py", None),
1211 ("NOTES", None),
1212 ("data.csv", Some("code")),
1213 ] {
1214 let mut options = HashMap::new();
1215 if let Some(option) = option {
1216 options.insert(option.to_string(), String::new());
1217 }
1218 let directive = create_test_directive("include", vec![target.to_string()], options, "");
1219 assert_eq!(
1220 registry.validate_directive(&directive),
1221 DirectiveValidationResult::Valid,
1222 ".. include:: {target}"
1223 );
1224 }
1225 }
1226
1227 #[test]
1231 fn an_empty_code_block_is_not_a_finding() {
1232 let registry = crate::directives::validation::DirectiveRegistry::with_builtin_validators();
1233 for arguments in [vec![], vec!["python".to_string()]] {
1234 let directive = create_test_directive("code-block", arguments, HashMap::new(), "");
1235 assert_eq!(
1236 registry.validate_directive(&directive),
1237 DirectiveValidationResult::Valid
1238 );
1239 }
1240 }
1241}