1use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11use serde::Deserialize;
12
13pub const CONFIG_FILE_NAME: &str = ".hongdown.toml";
15
16fn default_git_aware() -> bool {
18 true
19}
20
21#[derive(Debug, Clone, Deserialize, PartialEq)]
23#[serde(default)]
24pub struct Config {
25 #[serde(default)]
28 pub no_inherit: bool,
29
30 pub line_width: LineWidth,
32
33 pub include: Vec<String>,
36
37 pub exclude: Vec<String>,
39
40 #[serde(default = "default_git_aware")]
42 pub git_aware: bool,
43
44 pub heading: HeadingConfig,
46
47 pub unordered_list: UnorderedListConfig,
49
50 pub ordered_list: OrderedListConfig,
52
53 pub code_block: CodeBlockConfig,
55
56 pub thematic_break: ThematicBreakConfig,
58
59 pub punctuation: PunctuationConfig,
61}
62
63impl Default for Config {
64 fn default() -> Self {
65 Self {
66 no_inherit: false,
67 line_width: LineWidth::default(),
68 include: Vec::new(),
69 exclude: Vec::new(),
70 git_aware: true,
71 heading: HeadingConfig::default(),
72 unordered_list: UnorderedListConfig::default(),
73 ordered_list: OrderedListConfig::default(),
74 code_block: CodeBlockConfig::default(),
75 thematic_break: ThematicBreakConfig::default(),
76 punctuation: PunctuationConfig::default(),
77 }
78 }
79}
80
81#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
87#[serde(default)]
88pub struct ConfigLayer {
89 #[serde(default)]
91 pub no_inherit: bool,
92
93 pub line_width: Option<LineWidth>,
95
96 pub include: Option<Vec<String>>,
98
99 pub exclude: Option<Vec<String>>,
101
102 pub git_aware: Option<bool>,
104
105 pub heading: Option<HeadingConfig>,
107
108 pub unordered_list: Option<UnorderedListConfig>,
110
111 pub ordered_list: Option<OrderedListConfig>,
113
114 pub code_block: Option<CodeBlockConfig>,
116
117 pub thematic_break: Option<ThematicBreakConfig>,
119
120 pub punctuation: Option<PunctuationConfig>,
122}
123
124impl ConfigLayer {
125 pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
129 let content =
130 std::fs::read_to_string(path).map_err(|e| ConfigError::Io(path.to_path_buf(), e))?;
131 toml::from_str(&content).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))
132 }
133
134 pub fn merge_over(self, mut base: Config) -> Config {
140 base.no_inherit = self.no_inherit;
142
143 if let Some(line_width) = self.line_width {
144 base.line_width = line_width;
145 }
146 if let Some(include) = self.include {
147 base.include = include;
148 }
149 if let Some(exclude) = self.exclude {
150 base.exclude = exclude;
151 }
152 if let Some(git_aware) = self.git_aware {
153 base.git_aware = git_aware;
154 }
155 if let Some(heading) = self.heading {
156 base.heading = heading;
157 }
158 if let Some(unordered_list) = self.unordered_list {
159 base.unordered_list = unordered_list;
160 }
161 if let Some(ordered_list) = self.ordered_list {
162 base.ordered_list = ordered_list;
163 }
164 if let Some(code_block) = self.code_block {
165 base.code_block = code_block;
166 }
167 if let Some(thematic_break) = self.thematic_break {
168 base.thematic_break = thematic_break;
169 }
170 if let Some(punctuation) = self.punctuation {
171 base.punctuation = punctuation;
172 }
173 base
174 }
175}
176
177#[derive(Debug, Clone, Deserialize, PartialEq)]
179#[serde(default)]
180pub struct HeadingConfig {
181 pub setext_h1: bool,
183
184 pub setext_h2: bool,
186
187 pub sentence_case: bool,
189
190 pub proper_nouns: Vec<String>,
193
194 pub common_nouns: Vec<String>,
199}
200
201impl Default for HeadingConfig {
202 fn default() -> Self {
203 Self {
204 setext_h1: true,
205 setext_h2: true,
206 sentence_case: false,
207 proper_nouns: Vec::new(),
208 common_nouns: Vec::new(),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
215pub enum UnorderedMarker {
216 #[default]
218 #[serde(rename = "-")]
219 Hyphen,
220 #[serde(rename = "*")]
222 Asterisk,
223 #[serde(rename = "+")]
225 Plus,
226}
227
228impl UnorderedMarker {
229 pub fn as_char(self) -> char {
231 match self {
232 Self::Hyphen => '-',
233 Self::Asterisk => '*',
234 Self::Plus => '+',
235 }
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub struct LeadingSpaces(usize);
242
243impl LeadingSpaces {
244 pub const MAX: usize = 3;
246
247 pub fn new(value: usize) -> Result<Self, String> {
251 if value > Self::MAX {
252 Err(format!(
253 "leading_spaces must be at most {}, got {}.",
254 Self::MAX,
255 value
256 ))
257 } else {
258 Ok(Self(value))
259 }
260 }
261
262 pub fn get(self) -> usize {
264 self.0
265 }
266}
267
268impl Default for LeadingSpaces {
269 fn default() -> Self {
270 Self(1)
271 }
272}
273
274impl<'de> serde::Deserialize<'de> for LeadingSpaces {
275 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276 where
277 D: serde::Deserializer<'de>,
278 {
279 let value = usize::deserialize(deserializer)?;
280 Self::new(value).map_err(serde::de::Error::custom)
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286pub struct TrailingSpaces(usize);
287
288impl TrailingSpaces {
289 pub const MAX: usize = 3;
291
292 pub fn new(value: usize) -> Result<Self, String> {
296 if value > Self::MAX {
297 Err(format!(
298 "trailing_spaces must be at most {}, got {}.",
299 Self::MAX,
300 value
301 ))
302 } else {
303 Ok(Self(value))
304 }
305 }
306
307 pub fn get(self) -> usize {
309 self.0
310 }
311}
312
313impl Default for TrailingSpaces {
314 fn default() -> Self {
315 Self(2)
316 }
317}
318
319impl<'de> serde::Deserialize<'de> for TrailingSpaces {
320 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
321 where
322 D: serde::Deserializer<'de>,
323 {
324 let value = usize::deserialize(deserializer)?;
325 Self::new(value).map_err(serde::de::Error::custom)
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub struct IndentWidth(usize);
332
333impl IndentWidth {
334 pub const MIN: usize = 1;
336
337 pub fn new(value: usize) -> Result<Self, String> {
341 if value < Self::MIN {
342 Err(format!(
343 "indent_width must be at least {}, got {}.",
344 Self::MIN,
345 value
346 ))
347 } else {
348 Ok(Self(value))
349 }
350 }
351
352 pub fn get(self) -> usize {
354 self.0
355 }
356}
357
358impl Default for IndentWidth {
359 fn default() -> Self {
360 Self(4)
361 }
362}
363
364impl<'de> serde::Deserialize<'de> for IndentWidth {
365 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
366 where
367 D: serde::Deserializer<'de>,
368 {
369 let value = usize::deserialize(deserializer)?;
370 Self::new(value).map_err(serde::de::Error::custom)
371 }
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
376pub struct LineWidth(usize);
377
378impl LineWidth {
379 pub const MIN: usize = 8;
381
382 pub const RECOMMENDED_MIN: usize = 40;
384
385 pub fn new(value: usize) -> Result<Self, String> {
389 if value < Self::MIN {
390 Err(format!(
391 "line_width must be at least {}, got {}.",
392 Self::MIN,
393 value
394 ))
395 } else {
396 Ok(Self(value))
397 }
398 }
399
400 pub fn get(self) -> usize {
402 self.0
403 }
404
405 pub fn is_below_recommended(self) -> bool {
407 self.0 < Self::RECOMMENDED_MIN
408 }
409}
410
411impl Default for LineWidth {
412 fn default() -> Self {
413 Self(80)
414 }
415}
416
417impl<'de> serde::Deserialize<'de> for LineWidth {
418 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
419 where
420 D: serde::Deserializer<'de>,
421 {
422 let value = usize::deserialize(deserializer)?;
423 Self::new(value).map_err(serde::de::Error::custom)
424 }
425}
426
427#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
429#[serde(default)]
430pub struct UnorderedListConfig {
431 pub unordered_marker: UnorderedMarker,
433
434 pub leading_spaces: LeadingSpaces,
436
437 pub trailing_spaces: TrailingSpaces,
439
440 pub indent_width: IndentWidth,
442}
443
444#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
446pub enum OrderedMarker {
447 #[default]
449 #[serde(rename = ".")]
450 Period,
451 #[serde(rename = ")")]
453 Parenthesis,
454}
455
456impl OrderedMarker {
457 pub fn as_char(self) -> char {
459 match self {
460 Self::Period => '.',
461 Self::Parenthesis => ')',
462 }
463 }
464}
465
466#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Default)]
468#[serde(rename_all = "lowercase")]
469pub enum OrderedListPad {
470 #[default]
472 Start,
473 End,
475}
476
477#[derive(Debug, Clone, Deserialize, PartialEq)]
479#[serde(default)]
480pub struct OrderedListConfig {
481 pub odd_level_marker: OrderedMarker,
483
484 pub even_level_marker: OrderedMarker,
486
487 pub pad: OrderedListPad,
489
490 pub indent_width: IndentWidth,
492}
493
494impl Default for OrderedListConfig {
495 fn default() -> Self {
496 Self {
497 odd_level_marker: OrderedMarker::default(),
498 even_level_marker: OrderedMarker::Parenthesis,
499 pad: OrderedListPad::Start,
500 indent_width: IndentWidth::default(),
501 }
502 }
503}
504
505fn default_formatter_timeout() -> u64 {
507 5
508}
509
510#[derive(Debug, Clone, Deserialize, PartialEq)]
516#[serde(untagged)]
517pub enum FormatterConfig {
518 Simple(Vec<String>),
520 Full {
522 command: Vec<String>,
524 #[serde(default = "default_formatter_timeout")]
526 timeout: u64,
527 },
528}
529
530impl FormatterConfig {
531 pub fn command(&self) -> &[String] {
533 match self {
534 FormatterConfig::Simple(cmd) => cmd,
535 FormatterConfig::Full { command, .. } => command,
536 }
537 }
538
539 pub fn timeout(&self) -> u64 {
541 match self {
542 FormatterConfig::Simple(_) => default_formatter_timeout(),
543 FormatterConfig::Full { timeout, .. } => *timeout,
544 }
545 }
546
547 pub fn validate(&self) -> Result<(), String> {
551 if self.command().is_empty() {
552 return Err("formatter command cannot be empty".to_string());
553 }
554 Ok(())
555 }
556}
557
558#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Default)]
560pub enum FenceChar {
561 #[default]
563 #[serde(rename = "~")]
564 Tilde,
565 #[serde(rename = "`")]
567 Backtick,
568}
569
570impl FenceChar {
571 pub fn as_char(self) -> char {
573 match self {
574 Self::Tilde => '~',
575 Self::Backtick => '`',
576 }
577 }
578}
579
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
582pub struct MinFenceLength(usize);
583
584impl MinFenceLength {
585 pub const MIN: usize = 3;
587
588 pub fn new(value: usize) -> Result<Self, String> {
592 if value < Self::MIN {
593 Err(format!(
594 "min_fence_length must be at least {}, got {}.",
595 Self::MIN,
596 value
597 ))
598 } else {
599 Ok(Self(value))
600 }
601 }
602
603 pub fn get(self) -> usize {
605 self.0
606 }
607}
608
609impl Default for MinFenceLength {
610 fn default() -> Self {
611 Self(4)
612 }
613}
614
615impl<'de> serde::Deserialize<'de> for MinFenceLength {
616 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
617 where
618 D: serde::Deserializer<'de>,
619 {
620 let value = usize::deserialize(deserializer)?;
621 Self::new(value).map_err(serde::de::Error::custom)
622 }
623}
624
625#[derive(Debug, Clone, Deserialize, PartialEq)]
627#[serde(default)]
628pub struct CodeBlockConfig {
629 pub fence_char: FenceChar,
631
632 pub min_fence_length: MinFenceLength,
634
635 pub space_after_fence: bool,
637
638 pub default_language: String,
642
643 pub formatters: HashMap<String, FormatterConfig>,
648}
649
650impl Default for CodeBlockConfig {
651 fn default() -> Self {
652 Self {
653 fence_char: FenceChar::default(),
654 min_fence_length: MinFenceLength::default(),
655 space_after_fence: true,
656 default_language: String::new(),
657 formatters: HashMap::new(),
658 }
659 }
660}
661
662#[derive(Debug, Clone, PartialEq, Eq)]
668pub struct ThematicBreakStyle(String);
669
670impl ThematicBreakStyle {
671 pub const MIN_MARKERS: usize = 3;
673
674 pub fn new(style: String) -> Result<Self, String> {
678 let trimmed = style.trim();
684 if trimmed.is_empty() {
685 return Err("thematic_break style cannot be empty.".to_string());
686 }
687
688 let mut asterisk_count = 0;
690 let mut hyphen_count = 0;
691 let mut underscore_count = 0;
692 let mut has_other = false;
693
694 for ch in trimmed.chars() {
695 match ch {
696 '*' => asterisk_count += 1,
697 '-' => hyphen_count += 1,
698 '_' => underscore_count += 1,
699 ' ' | '\t' => {} _ => {
701 has_other = true;
702 break;
703 }
704 }
705 }
706
707 if has_other {
708 return Err(format!(
709 "thematic_break style must only contain *, -, or _ (with optional spaces), got {:?}.",
710 style
711 ));
712 }
713
714 let marker_counts = [
716 (asterisk_count, '*'),
717 (hyphen_count, '-'),
718 (underscore_count, '_'),
719 ];
720
721 let valid_markers: Vec<_> = marker_counts
722 .iter()
723 .filter(|(count, _)| *count >= Self::MIN_MARKERS)
724 .collect();
725
726 if valid_markers.is_empty() {
727 return Err(format!(
728 "thematic_break style must contain at least {} of the same marker (*, -, or _), got {:?}.",
729 Self::MIN_MARKERS,
730 style
731 ));
732 }
733
734 if valid_markers.len() > 1 {
735 return Err(format!(
736 "thematic_break style must use only one type of marker (*, -, or _), got {:?}.",
737 style
738 ));
739 }
740
741 Ok(Self(style))
742 }
743
744 pub fn as_str(&self) -> &str {
746 &self.0
747 }
748}
749
750impl Default for ThematicBreakStyle {
751 fn default() -> Self {
752 Self(
753 "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -".to_string(),
754 )
755 }
756}
757
758impl<'de> serde::Deserialize<'de> for ThematicBreakStyle {
759 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
760 where
761 D: serde::Deserializer<'de>,
762 {
763 let value = String::deserialize(deserializer)?;
764 Self::new(value).map_err(serde::de::Error::custom)
765 }
766}
767
768#[derive(Debug, Clone, Deserialize, PartialEq)]
770#[serde(default)]
771pub struct ThematicBreakConfig {
772 pub style: ThematicBreakStyle,
774
775 pub leading_spaces: LeadingSpaces,
778}
779
780impl Default for ThematicBreakConfig {
781 fn default() -> Self {
782 Self {
783 style: ThematicBreakStyle::default(),
784 leading_spaces: LeadingSpaces::new(3).unwrap(),
785 }
786 }
787}
788
789#[derive(Debug, Clone, PartialEq, Eq)]
792pub struct DashPattern(String);
793
794impl DashPattern {
795 pub fn new(pattern: String) -> Result<Self, String> {
799 if pattern.is_empty() {
800 return Err("dash pattern cannot be empty.".to_string());
801 }
802
803 if pattern.chars().any(|c| !c.is_ascii_graphic()) {
805 return Err(format!(
806 "dash pattern must only contain printable ASCII characters (no whitespace), got {:?}.",
807 pattern
808 ));
809 }
810
811 Ok(Self(pattern))
812 }
813
814 pub fn as_str(&self) -> &str {
816 &self.0
817 }
818}
819
820impl<'de> serde::Deserialize<'de> for DashPattern {
821 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
822 where
823 D: serde::Deserializer<'de>,
824 {
825 let value = String::deserialize(deserializer)?;
826 Self::new(value).map_err(serde::de::Error::custom)
827 }
828}
829
830#[derive(Debug, Clone, PartialEq, Default)]
833pub enum DashSetting {
834 #[default]
836 Disabled,
837 Pattern(DashPattern),
839}
840
841impl<'de> Deserialize<'de> for DashSetting {
842 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
843 where
844 D: serde::Deserializer<'de>,
845 {
846 use serde::de::{self, Visitor};
847
848 struct DashSettingVisitor;
849
850 impl<'de> Visitor<'de> for DashSettingVisitor {
851 type Value = DashSetting;
852
853 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
854 formatter.write_str("false or a string pattern")
855 }
856
857 fn visit_bool<E>(self, value: bool) -> Result<DashSetting, E>
858 where
859 E: de::Error,
860 {
861 if value {
862 Err(de::Error::custom(
863 "expected false or a string pattern, got true",
864 ))
865 } else {
866 Ok(DashSetting::Disabled)
867 }
868 }
869
870 fn visit_str<E>(self, value: &str) -> Result<DashSetting, E>
871 where
872 E: de::Error,
873 {
874 DashPattern::new(value.to_string())
875 .map(DashSetting::Pattern)
876 .map_err(de::Error::custom)
877 }
878
879 fn visit_string<E>(self, value: String) -> Result<DashSetting, E>
880 where
881 E: de::Error,
882 {
883 DashPattern::new(value)
884 .map(DashSetting::Pattern)
885 .map_err(de::Error::custom)
886 }
887 }
888
889 deserializer.deserialize_any(DashSettingVisitor)
890 }
891}
892
893#[derive(Debug, Clone, Deserialize, PartialEq)]
895#[serde(default)]
896pub struct PunctuationConfig {
897 pub curly_double_quotes: bool,
900
901 pub curly_single_quotes: bool,
904
905 pub curly_apostrophes: bool,
908
909 pub ellipsis: bool,
912
913 pub en_dash: DashSetting,
917
918 pub em_dash: DashSetting,
922}
923
924impl Default for PunctuationConfig {
925 fn default() -> Self {
926 Self {
927 curly_double_quotes: true,
928 curly_single_quotes: true,
929 curly_apostrophes: false,
930 ellipsis: true,
931 en_dash: DashSetting::Disabled,
932 em_dash: DashSetting::Pattern(DashPattern::new("--".to_string()).unwrap()),
933 }
934 }
935}
936
937impl Config {
938 pub fn from_toml(toml_str: &str) -> Result<Self, toml::de::Error> {
940 toml::from_str(toml_str)
941 }
942
943 pub fn from_file(path: &Path) -> Result<Self, ConfigError> {
945 let content =
946 std::fs::read_to_string(path).map_err(|e| ConfigError::Io(path.to_path_buf(), e))?;
947 Self::from_toml(&content).map_err(|e| ConfigError::Parse(path.to_path_buf(), e))
948 }
949
950 pub fn discover(start_dir: &Path) -> Result<Option<(PathBuf, Self)>, ConfigError> {
956 let mut current = start_dir.to_path_buf();
957 loop {
958 let config_path = current.join(CONFIG_FILE_NAME);
959 if config_path.exists() {
960 let config = Self::from_file(&config_path)?;
961 return Ok(Some((config_path, config)));
962 }
963 if !current.pop() {
964 break;
965 }
966 }
967 Ok(None)
968 }
969
970 pub fn load_cascading(start_dir: &Path) -> Result<(Self, Option<PathBuf>), ConfigError> {
982 let mut layers = Vec::new();
983 let mut project_config_path = None;
984
985 if let Some(layer) = Self::load_system_config()? {
987 layers.push(layer);
988 }
989
990 if let Some(layer) = Self::load_user_legacy_config()? {
992 layers.push(layer);
993 }
994
995 if let Some(layer) = Self::load_user_xdg_config()? {
997 layers.push(layer);
998 }
999
1000 if let Some((path, layer)) = Self::discover_project_config(start_dir)? {
1002 project_config_path = Some(path);
1003
1004 if layer.no_inherit {
1006 layers.clear();
1007 }
1008
1009 layers.push(layer);
1010 }
1011
1012 let mut config = Self::default();
1014 for layer in layers {
1015 config = layer.merge_over(config);
1016 }
1017
1018 Ok((config, project_config_path))
1019 }
1020
1021 fn load_system_config() -> Result<Option<ConfigLayer>, ConfigError> {
1023 Self::try_load_layer(Path::new("/etc/hongdown/config.toml"))
1024 }
1025
1026 fn load_user_legacy_config() -> Result<Option<ConfigLayer>, ConfigError> {
1028 if let Some(home) = dirs::home_dir() {
1029 Self::try_load_layer(&home.join(".hongdown.toml"))
1030 } else {
1031 Ok(None)
1032 }
1033 }
1034
1035 fn load_user_xdg_config() -> Result<Option<ConfigLayer>, ConfigError> {
1037 if let Some(config_dir) = dirs::config_dir() {
1038 Self::try_load_layer(&config_dir.join("hongdown/config.toml"))
1039 } else {
1040 Ok(None)
1041 }
1042 }
1043
1044 fn discover_project_config(
1046 start_dir: &Path,
1047 ) -> Result<Option<(PathBuf, ConfigLayer)>, ConfigError> {
1048 let mut current = start_dir.to_path_buf();
1049 loop {
1050 let config_path = current.join(CONFIG_FILE_NAME);
1051 if config_path.exists() {
1052 let layer = ConfigLayer::from_file(&config_path)?;
1053 return Ok(Some((config_path, layer)));
1054 }
1055 if !current.pop() {
1056 break;
1057 }
1058 }
1059 Ok(None)
1060 }
1061
1062 fn try_load_layer(path: &Path) -> Result<Option<ConfigLayer>, ConfigError> {
1064 if !path.exists() {
1065 return Ok(None);
1066 }
1067 ConfigLayer::from_file(path).map(Some)
1068 }
1069
1070 pub fn collect_files(&self, base_dir: &Path) -> Result<Vec<PathBuf>, ConfigError> {
1076 use ignore::WalkBuilder;
1077 use ignore::overrides::OverrideBuilder;
1078
1079 if self.include.is_empty() {
1080 return Ok(Vec::new());
1081 }
1082
1083 let mut override_builder = OverrideBuilder::new(base_dir);
1086
1087 for pattern in &self.include {
1089 override_builder.add(pattern).map_err(ConfigError::Ignore)?;
1090 }
1091
1092 for pattern in &self.exclude {
1094 override_builder
1095 .add(&format!("!{}", pattern))
1096 .map_err(ConfigError::Ignore)?;
1097 }
1098
1099 let overrides = override_builder.build().map_err(ConfigError::Ignore)?;
1100
1101 let walker = WalkBuilder::new(base_dir)
1104 .hidden(false) .git_ignore(self.git_aware) .git_global(false) .git_exclude(self.git_aware) .overrides(overrides)
1109 .build();
1110
1111 let mut files = Vec::new();
1113 for entry in walker {
1114 let entry = entry.map_err(ConfigError::Ignore)?;
1115 let path = entry.path();
1116 if path.is_file() {
1117 files.push(path.to_path_buf());
1118 }
1119 }
1120
1121 files.sort();
1123
1124 Ok(files)
1125 }
1126}
1127
1128#[derive(Debug)]
1130pub enum ConfigError {
1131 Io(PathBuf, std::io::Error),
1133 Parse(PathBuf, toml::de::Error),
1135 Glob(String, glob::PatternError),
1137 GlobIo(glob::GlobError),
1139 Ignore(ignore::Error),
1141}
1142
1143impl std::fmt::Display for ConfigError {
1144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1145 match self {
1146 ConfigError::Io(path, err) => {
1147 write!(f, "failed to read {}: {}", path.display(), err)
1148 }
1149 ConfigError::Parse(path, err) => {
1150 write!(f, "failed to parse {}: {}", path.display(), err)
1151 }
1152 ConfigError::Glob(pattern, err) => {
1153 write!(f, "invalid glob pattern '{}': {}", pattern, err)
1154 }
1155 ConfigError::GlobIo(err) => {
1156 write!(f, "error reading file: {}", err)
1157 }
1158 ConfigError::Ignore(err) => {
1159 write!(f, "error during file traversal: {}", err)
1160 }
1161 }
1162 }
1163}
1164
1165impl std::error::Error for ConfigError {
1166 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1167 match self {
1168 ConfigError::Io(_, err) => Some(err),
1169 ConfigError::Parse(_, err) => Some(err),
1170 ConfigError::Glob(_, err) => Some(err),
1171 ConfigError::GlobIo(err) => Some(err),
1172 ConfigError::Ignore(err) => Some(err),
1173 }
1174 }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 use super::*;
1180
1181 #[test]
1182 fn test_default_config() {
1183 let config = Config::default();
1184 assert_eq!(config.line_width.get(), 80);
1185 assert!(config.git_aware);
1186 assert!(config.heading.setext_h1);
1187 assert!(config.heading.setext_h2);
1188 assert_eq!(
1189 config.unordered_list.unordered_marker,
1190 UnorderedMarker::Hyphen
1191 );
1192 assert_eq!(config.unordered_list.leading_spaces.get(), 1);
1193 assert_eq!(config.unordered_list.trailing_spaces.get(), 2);
1194 assert_eq!(config.unordered_list.indent_width.get(), 4);
1195 assert_eq!(config.ordered_list.odd_level_marker, OrderedMarker::Period);
1196 assert_eq!(
1197 config.ordered_list.even_level_marker,
1198 OrderedMarker::Parenthesis
1199 );
1200 assert_eq!(config.ordered_list.pad, OrderedListPad::Start);
1201 assert_eq!(config.ordered_list.indent_width.get(), 4);
1202 assert_eq!(config.code_block.fence_char, FenceChar::Tilde);
1203 assert_eq!(config.code_block.min_fence_length.get(), 4);
1204 assert!(config.code_block.space_after_fence);
1205 assert_eq!(config.code_block.default_language, "");
1206 assert_eq!(
1207 config.thematic_break.style.as_str(),
1208 "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
1209 );
1210 assert_eq!(config.thematic_break.leading_spaces.get(), 3);
1211 }
1212
1213 #[test]
1214 fn test_parse_empty_toml() {
1215 let config = Config::from_toml("").unwrap();
1216 assert_eq!(config, Config::default());
1217 }
1218
1219 #[test]
1220 fn test_parse_line_width() {
1221 let config = Config::from_toml("line_width = 100").unwrap();
1222 assert_eq!(config.line_width.get(), 100);
1223 }
1224
1225 #[test]
1226 fn test_parse_heading_config() {
1227 let config = Config::from_toml(
1228 r#"
1229[heading]
1230setext_h1 = false
1231setext_h2 = false
1232"#,
1233 )
1234 .unwrap();
1235 assert!(!config.heading.setext_h1);
1236 assert!(!config.heading.setext_h2);
1237 }
1238
1239 #[test]
1240 fn test_parse_heading_sentence_case() {
1241 let config = Config::from_toml(
1242 r#"
1243[heading]
1244sentence_case = true
1245"#,
1246 )
1247 .unwrap();
1248 assert!(config.heading.sentence_case);
1249 }
1250
1251 #[test]
1252 fn test_parse_heading_proper_nouns() {
1253 let config = Config::from_toml(
1254 r#"
1255[heading]
1256proper_nouns = ["Hongdown", "MyCompany", "MyProduct"]
1257"#,
1258 )
1259 .unwrap();
1260 assert_eq!(
1261 config.heading.proper_nouns,
1262 vec!["Hongdown", "MyCompany", "MyProduct"]
1263 );
1264 }
1265
1266 #[test]
1267 fn test_parse_heading_sentence_case_with_proper_nouns() {
1268 let config = Config::from_toml(
1269 r#"
1270[heading]
1271sentence_case = true
1272proper_nouns = ["Hongdown", "MyAPI"]
1273"#,
1274 )
1275 .unwrap();
1276 assert!(config.heading.sentence_case);
1277 assert_eq!(config.heading.proper_nouns, vec!["Hongdown", "MyAPI"]);
1278 }
1279
1280 #[test]
1281 fn test_parse_heading_common_nouns() {
1282 let config = Config::from_toml(
1283 r#"
1284[heading]
1285common_nouns = ["Go", "Swift"]
1286"#,
1287 )
1288 .unwrap();
1289 assert_eq!(config.heading.common_nouns, vec!["Go", "Swift"]);
1290 }
1291
1292 #[test]
1293 fn test_parse_heading_with_proper_and_common_nouns() {
1294 let config = Config::from_toml(
1295 r#"
1296[heading]
1297sentence_case = true
1298proper_nouns = ["MyAPI"]
1299common_nouns = ["Go"]
1300"#,
1301 )
1302 .unwrap();
1303 assert!(config.heading.sentence_case);
1304 assert_eq!(config.heading.proper_nouns, vec!["MyAPI"]);
1305 assert_eq!(config.heading.common_nouns, vec!["Go"]);
1306 }
1307
1308 #[test]
1309 fn test_parse_unordered_list_config() {
1310 let config = Config::from_toml(
1311 r#"
1312[unordered_list]
1313unordered_marker = "*"
1314leading_spaces = 0
1315trailing_spaces = 1
1316indent_width = 2
1317"#,
1318 )
1319 .unwrap();
1320 assert_eq!(
1321 config.unordered_list.unordered_marker,
1322 UnorderedMarker::Asterisk
1323 );
1324 assert_eq!(config.unordered_list.leading_spaces.get(), 0);
1325 assert_eq!(config.unordered_list.trailing_spaces.get(), 1);
1326 assert_eq!(config.unordered_list.indent_width.get(), 2);
1327 }
1328
1329 #[test]
1330 fn test_parse_ordered_list_config() {
1331 let config = Config::from_toml(
1332 r#"
1333[ordered_list]
1334odd_level_marker = ")"
1335even_level_marker = "."
1336"#,
1337 )
1338 .unwrap();
1339 assert_eq!(
1340 config.ordered_list.odd_level_marker,
1341 OrderedMarker::Parenthesis
1342 );
1343 assert_eq!(config.ordered_list.even_level_marker, OrderedMarker::Period);
1344 assert_eq!(config.ordered_list.pad, OrderedListPad::Start); }
1346
1347 #[test]
1348 fn test_parse_ordered_list_pad_end() {
1349 let config = Config::from_toml(
1350 r#"
1351[ordered_list]
1352pad = "end"
1353"#,
1354 )
1355 .unwrap();
1356 assert_eq!(config.ordered_list.pad, OrderedListPad::End);
1357 }
1358
1359 #[test]
1360 fn test_parse_ordered_list_pad_start() {
1361 let config = Config::from_toml(
1362 r#"
1363[ordered_list]
1364pad = "start"
1365"#,
1366 )
1367 .unwrap();
1368 assert_eq!(config.ordered_list.pad, OrderedListPad::Start);
1369 }
1370
1371 #[test]
1372 fn test_parse_code_block_config() {
1373 let config = Config::from_toml(
1374 r#"
1375[code_block]
1376fence_char = "`"
1377min_fence_length = 3
1378space_after_fence = false
1379"#,
1380 )
1381 .unwrap();
1382 assert_eq!(config.code_block.fence_char, FenceChar::Backtick);
1383 assert_eq!(config.code_block.min_fence_length.get(), 3);
1384 assert!(!config.code_block.space_after_fence);
1385 assert_eq!(config.code_block.default_language, ""); }
1387
1388 #[test]
1389 fn test_parse_code_block_default_language() {
1390 let config = Config::from_toml(
1391 r#"
1392[code_block]
1393default_language = "text"
1394"#,
1395 )
1396 .unwrap();
1397 assert_eq!(config.code_block.default_language, "text");
1398 }
1399
1400 #[test]
1401 fn test_parse_full_config() {
1402 let config = Config::from_toml(
1403 r#"
1404line_width = 80
1405
1406[heading]
1407setext_h1 = true
1408setext_h2 = true
1409
1410[unordered_list]
1411unordered_marker = "-"
1412leading_spaces = 1
1413trailing_spaces = 2
1414indent_width = 4
1415
1416[ordered_list]
1417odd_level_marker = "."
1418even_level_marker = ")"
1419
1420[code_block]
1421fence_char = "~"
1422min_fence_length = 4
1423space_after_fence = true
1424
1425[thematic_break]
1426style = "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
1427leading_spaces = 3
1428"#,
1429 )
1430 .unwrap();
1431 assert_eq!(config, Config::default());
1432 }
1433
1434 #[test]
1435 fn test_parse_thematic_break_config() {
1436 let config = Config::from_toml(
1437 r#"
1438[thematic_break]
1439style = "---"
1440"#,
1441 )
1442 .unwrap();
1443 assert_eq!(config.thematic_break.style.as_str(), "---");
1444 }
1445
1446 #[test]
1447 fn test_parse_invalid_toml() {
1448 let result = Config::from_toml("line_width = \"not a number\"");
1449 assert!(result.is_err());
1450 }
1451
1452 #[test]
1453 fn test_discover_config_in_parent_dir() {
1454 let temp_dir = std::env::temp_dir().join("hongdown_test_parent");
1455 let _ = std::fs::remove_dir_all(&temp_dir);
1456 let sub_dir = temp_dir.join("subdir").join("nested");
1457 std::fs::create_dir_all(&sub_dir).unwrap();
1458 let config_path = temp_dir.join(CONFIG_FILE_NAME);
1459 std::fs::write(&config_path, "line_width = 90").unwrap();
1460
1461 let result = Config::discover(&sub_dir).unwrap();
1462 assert!(result.is_some());
1463 let (path, config) = result.unwrap();
1464 assert_eq!(path, config_path);
1465 assert_eq!(config.line_width.get(), 90);
1466
1467 let _ = std::fs::remove_dir_all(&temp_dir);
1468 }
1469
1470 #[test]
1471 fn test_default_include_exclude() {
1472 let config = Config::default();
1473 assert!(config.include.is_empty());
1474 assert!(config.exclude.is_empty());
1475 }
1476
1477 #[test]
1478 fn test_parse_include_patterns() {
1479 let config = Config::from_toml(
1480 r#"
1481include = ["*.md", "docs/**/*.md"]
1482"#,
1483 )
1484 .unwrap();
1485 assert_eq!(config.include, vec!["*.md", "docs/**/*.md"]);
1486 }
1487
1488 #[test]
1489 fn test_parse_exclude_patterns() {
1490 let config = Config::from_toml(
1491 r#"
1492exclude = ["node_modules/**", "target/**"]
1493"#,
1494 )
1495 .unwrap();
1496 assert_eq!(config.exclude, vec!["node_modules/**", "target/**"]);
1497 }
1498
1499 #[test]
1500 fn test_parse_include_and_exclude() {
1501 let config = Config::from_toml(
1502 r#"
1503include = ["**/*.md"]
1504exclude = ["vendor/**"]
1505"#,
1506 )
1507 .unwrap();
1508 assert_eq!(config.include, vec!["**/*.md"]);
1509 assert_eq!(config.exclude, vec!["vendor/**"]);
1510 }
1511
1512 #[test]
1513 fn test_collect_files_with_include() {
1514 let temp_dir = std::env::temp_dir().join("hongdown_test_collect");
1515 let _ = std::fs::remove_dir_all(&temp_dir);
1516 std::fs::create_dir_all(&temp_dir).unwrap();
1517 std::fs::write(temp_dir.join("README.md"), "# Test").unwrap();
1518 std::fs::write(temp_dir.join("CHANGELOG.md"), "# Changes").unwrap();
1519 std::fs::write(temp_dir.join("main.rs"), "fn main() {}").unwrap();
1520
1521 let config = Config::from_toml(r#"include = ["*.md"]"#).unwrap();
1522 let files = config.collect_files(&temp_dir).unwrap();
1523
1524 assert_eq!(files.len(), 2);
1525 assert!(files.iter().any(|p| p.ends_with("README.md")));
1526 assert!(files.iter().any(|p| p.ends_with("CHANGELOG.md")));
1527
1528 let _ = std::fs::remove_dir_all(&temp_dir);
1529 }
1530
1531 #[test]
1532 fn test_collect_files_with_exclude() {
1533 let temp_dir = std::env::temp_dir().join("hongdown_test_exclude");
1534 let _ = std::fs::remove_dir_all(&temp_dir);
1535 std::fs::create_dir_all(&temp_dir).unwrap();
1536 std::fs::create_dir_all(temp_dir.join("vendor")).unwrap();
1537 std::fs::write(temp_dir.join("README.md"), "# Test").unwrap();
1538 std::fs::write(temp_dir.join("vendor").join("lib.md"), "# Lib").unwrap();
1539
1540 let config = Config::from_toml(
1541 r#"
1542include = ["**/*.md"]
1543exclude = ["vendor/**"]
1544"#,
1545 )
1546 .unwrap();
1547 let files = config.collect_files(&temp_dir).unwrap();
1548
1549 assert_eq!(files.len(), 1);
1550 assert!(files[0].ends_with("README.md"));
1551
1552 let _ = std::fs::remove_dir_all(&temp_dir);
1553 }
1554
1555 #[test]
1556 fn test_collect_files_empty_include() {
1557 let temp_dir = std::env::temp_dir().join("hongdown_test_empty");
1558 let _ = std::fs::remove_dir_all(&temp_dir);
1559 std::fs::create_dir_all(&temp_dir).unwrap();
1560 std::fs::write(temp_dir.join("README.md"), "# Test").unwrap();
1561
1562 let config = Config::default();
1563 let files = config.collect_files(&temp_dir).unwrap();
1564
1565 assert!(files.is_empty());
1566
1567 let _ = std::fs::remove_dir_all(&temp_dir);
1568 }
1569
1570 #[test]
1571 fn test_default_punctuation_config() {
1572 let config = PunctuationConfig::default();
1573 assert!(config.curly_double_quotes);
1574 assert!(config.curly_single_quotes);
1575 assert!(!config.curly_apostrophes);
1576 assert!(config.ellipsis);
1577 assert_eq!(config.en_dash, DashSetting::Disabled);
1578 assert_eq!(
1579 config.em_dash,
1580 DashSetting::Pattern(DashPattern::new("--".to_string()).unwrap())
1581 );
1582 }
1583
1584 #[test]
1585 fn test_parse_punctuation_config_all_options() {
1586 let config = Config::from_toml(
1587 r#"
1588[punctuation]
1589curly_double_quotes = false
1590curly_single_quotes = false
1591curly_apostrophes = true
1592ellipsis = false
1593en_dash = "--"
1594em_dash = "---"
1595"#,
1596 )
1597 .unwrap();
1598 assert!(!config.punctuation.curly_double_quotes);
1599 assert!(!config.punctuation.curly_single_quotes);
1600 assert!(config.punctuation.curly_apostrophes);
1601 assert!(!config.punctuation.ellipsis);
1602 assert_eq!(
1603 config.punctuation.en_dash,
1604 DashSetting::Pattern(DashPattern::new("--".to_string()).unwrap())
1605 );
1606 assert_eq!(
1607 config.punctuation.em_dash,
1608 DashSetting::Pattern(DashPattern::new("---".to_string()).unwrap())
1609 );
1610 }
1611
1612 #[test]
1613 fn test_parse_dash_setting_disabled() {
1614 let config = Config::from_toml(
1615 r#"
1616[punctuation]
1617em_dash = false
1618"#,
1619 )
1620 .unwrap();
1621 assert_eq!(config.punctuation.em_dash, DashSetting::Disabled);
1622 }
1623
1624 #[test]
1625 fn test_parse_dash_setting_pattern() {
1626 let config = Config::from_toml(
1627 r#"
1628[punctuation]
1629en_dash = "---"
1630"#,
1631 )
1632 .unwrap();
1633 assert_eq!(
1634 config.punctuation.en_dash,
1635 DashSetting::Pattern(DashPattern::new("---".to_string()).unwrap())
1636 );
1637 }
1638
1639 #[test]
1640 fn test_punctuation_config_in_full_config() {
1641 let config = Config::from_toml(
1642 r#"
1643line_width = 100
1644
1645[punctuation]
1646curly_double_quotes = true
1647em_dash = "--"
1648"#,
1649 )
1650 .unwrap();
1651 assert_eq!(config.line_width.get(), 100);
1652 assert!(config.punctuation.curly_double_quotes);
1653 assert_eq!(
1654 config.punctuation.em_dash,
1655 DashSetting::Pattern(DashPattern::new("--".to_string()).unwrap())
1656 );
1657 }
1658
1659 #[test]
1660 fn test_default_code_block_formatters() {
1661 let config = Config::default();
1662 assert!(config.code_block.formatters.is_empty());
1663 }
1664
1665 #[test]
1666 fn test_parse_formatter_simple() {
1667 let config = Config::from_toml(
1668 r#"
1669[code_block.formatters]
1670javascript = ["deno", "fmt", "-"]
1671"#,
1672 )
1673 .unwrap();
1674 let formatter = config.code_block.formatters.get("javascript").unwrap();
1675 assert_eq!(formatter.command(), &["deno", "fmt", "-"]);
1676 assert_eq!(formatter.timeout(), 5);
1677 }
1678
1679 #[test]
1680 fn test_parse_formatter_full() {
1681 let config = Config::from_toml(
1682 r#"
1683[code_block.formatters.python]
1684command = ["black", "-"]
1685timeout = 10
1686"#,
1687 )
1688 .unwrap();
1689 let formatter = config.code_block.formatters.get("python").unwrap();
1690 assert_eq!(formatter.command(), &["black", "-"]);
1691 assert_eq!(formatter.timeout(), 10);
1692 }
1693
1694 #[test]
1695 fn test_parse_formatter_full_default_timeout() {
1696 let config = Config::from_toml(
1697 r#"
1698[code_block.formatters.rust]
1699command = ["rustfmt"]
1700"#,
1701 )
1702 .unwrap();
1703 let formatter = config.code_block.formatters.get("rust").unwrap();
1704 assert_eq!(formatter.command(), &["rustfmt"]);
1705 assert_eq!(formatter.timeout(), 5);
1706 }
1707
1708 #[test]
1709 fn test_parse_multiple_formatters() {
1710 let config = Config::from_toml(
1711 r#"
1712[code_block.formatters]
1713javascript = ["deno", "fmt", "-"]
1714typescript = ["deno", "fmt", "-"]
1715
1716[code_block.formatters.python]
1717command = ["black", "-"]
1718timeout = 10
1719"#,
1720 )
1721 .unwrap();
1722 assert_eq!(config.code_block.formatters.len(), 3);
1723 assert!(config.code_block.formatters.contains_key("javascript"));
1724 assert!(config.code_block.formatters.contains_key("typescript"));
1725 assert!(config.code_block.formatters.contains_key("python"));
1726 }
1727
1728 #[test]
1729 fn test_formatter_empty_command_validation() {
1730 let config = Config::from_toml(
1731 r#"
1732[code_block.formatters]
1733javascript = []
1734"#,
1735 )
1736 .unwrap();
1737 assert!(
1738 config
1739 .code_block
1740 .formatters
1741 .get("javascript")
1742 .unwrap()
1743 .validate()
1744 .is_err()
1745 );
1746 }
1747
1748 #[test]
1749 fn test_formatter_valid_command_validation() {
1750 let config = Config::from_toml(
1751 r#"
1752[code_block.formatters]
1753javascript = ["deno", "fmt", "-"]
1754"#,
1755 )
1756 .unwrap();
1757 assert!(
1758 config
1759 .code_block
1760 .formatters
1761 .get("javascript")
1762 .unwrap()
1763 .validate()
1764 .is_ok()
1765 );
1766 }
1767}
1768
1769#[cfg(test)]
1770mod unordered_marker_tests {
1771 use super::*;
1772
1773 #[test]
1774 fn test_unordered_marker_default() {
1775 let marker = UnorderedMarker::default();
1776 assert_eq!(marker, UnorderedMarker::Hyphen);
1777 assert_eq!(marker.as_char(), '-');
1778 }
1779
1780 #[test]
1781 fn test_unordered_marker_hyphen() {
1782 let config = Config::from_toml(
1783 r#"
1784[unordered_list]
1785unordered_marker = "-"
1786"#,
1787 )
1788 .unwrap();
1789 assert_eq!(
1790 config.unordered_list.unordered_marker,
1791 UnorderedMarker::Hyphen
1792 );
1793 assert_eq!(config.unordered_list.unordered_marker.as_char(), '-');
1794 }
1795
1796 #[test]
1797 fn test_unordered_marker_asterisk() {
1798 let config = Config::from_toml(
1799 r#"
1800[unordered_list]
1801unordered_marker = "*"
1802"#,
1803 )
1804 .unwrap();
1805 assert_eq!(
1806 config.unordered_list.unordered_marker,
1807 UnorderedMarker::Asterisk
1808 );
1809 assert_eq!(config.unordered_list.unordered_marker.as_char(), '*');
1810 }
1811
1812 #[test]
1813 fn test_unordered_marker_plus() {
1814 let config = Config::from_toml(
1815 r#"
1816[unordered_list]
1817unordered_marker = "+"
1818"#,
1819 )
1820 .unwrap();
1821 assert_eq!(
1822 config.unordered_list.unordered_marker,
1823 UnorderedMarker::Plus
1824 );
1825 assert_eq!(config.unordered_list.unordered_marker.as_char(), '+');
1826 }
1827
1828 #[test]
1829 fn test_unordered_marker_invalid_period() {
1830 let result = Config::from_toml(
1831 r#"
1832[unordered_list]
1833unordered_marker = "."
1834"#,
1835 );
1836 assert!(result.is_err());
1837 let err_msg = result.unwrap_err().to_string();
1838 assert!(err_msg.contains("unordered_marker"));
1839 }
1840
1841 #[test]
1842 fn test_unordered_marker_invalid_letter() {
1843 let result = Config::from_toml(
1844 r#"
1845[unordered_list]
1846unordered_marker = "x"
1847"#,
1848 );
1849 assert!(result.is_err());
1850 }
1851
1852 #[test]
1853 fn test_unordered_marker_invalid_number() {
1854 let result = Config::from_toml(
1855 r#"
1856[unordered_list]
1857unordered_marker = "1"
1858"#,
1859 );
1860 assert!(result.is_err());
1861 }
1862
1863 #[test]
1864 fn test_unordered_marker_invalid_empty() {
1865 let result = Config::from_toml(
1866 r#"
1867[unordered_list]
1868unordered_marker = ""
1869"#,
1870 );
1871 assert!(result.is_err());
1872 }
1873}
1874
1875#[cfg(test)]
1876mod ordered_marker_tests {
1877 use super::*;
1878
1879 #[test]
1880 fn test_ordered_marker_default() {
1881 let marker = OrderedMarker::default();
1882 assert_eq!(marker, OrderedMarker::Period);
1883 assert_eq!(marker.as_char(), '.');
1884 }
1885
1886 #[test]
1887 fn test_ordered_marker_period() {
1888 let config = Config::from_toml(
1889 r#"
1890[ordered_list]
1891odd_level_marker = "."
1892"#,
1893 )
1894 .unwrap();
1895 assert_eq!(config.ordered_list.odd_level_marker, OrderedMarker::Period);
1896 assert_eq!(config.ordered_list.odd_level_marker.as_char(), '.');
1897 }
1898
1899 #[test]
1900 fn test_ordered_marker_parenthesis() {
1901 let config = Config::from_toml(
1902 r#"
1903[ordered_list]
1904even_level_marker = ")"
1905"#,
1906 )
1907 .unwrap();
1908 assert_eq!(
1909 config.ordered_list.even_level_marker,
1910 OrderedMarker::Parenthesis
1911 );
1912 assert_eq!(config.ordered_list.even_level_marker.as_char(), ')');
1913 }
1914
1915 #[test]
1916 fn test_ordered_marker_invalid_hyphen() {
1917 let result = Config::from_toml(
1918 r#"
1919[ordered_list]
1920odd_level_marker = "-"
1921"#,
1922 );
1923 assert!(result.is_err());
1924 let err_msg = result.unwrap_err().to_string();
1925 assert!(err_msg.contains("odd_level_marker"));
1926 }
1927
1928 #[test]
1929 fn test_ordered_marker_invalid_asterisk() {
1930 let result = Config::from_toml(
1931 r#"
1932[ordered_list]
1933even_level_marker = "*"
1934"#,
1935 );
1936 assert!(result.is_err());
1937 }
1938
1939 #[test]
1940 fn test_ordered_marker_invalid_letter() {
1941 let result = Config::from_toml(
1942 r#"
1943[ordered_list]
1944odd_level_marker = "a"
1945"#,
1946 );
1947 assert!(result.is_err());
1948 }
1949
1950 #[test]
1951 fn test_ordered_marker_invalid_empty() {
1952 let result = Config::from_toml(
1953 r#"
1954[ordered_list]
1955odd_level_marker = ""
1956"#,
1957 );
1958 assert!(result.is_err());
1959 }
1960}
1961
1962#[cfg(test)]
1963mod fence_char_tests {
1964 use super::*;
1965
1966 #[test]
1967 fn test_fence_char_default_is_tilde() {
1968 assert_eq!(FenceChar::default(), FenceChar::Tilde);
1969 }
1970
1971 #[test]
1972 fn test_fence_char_as_char() {
1973 assert_eq!(FenceChar::Tilde.as_char(), '~');
1974 assert_eq!(FenceChar::Backtick.as_char(), '`');
1975 }
1976
1977 #[test]
1978 fn test_fence_char_parse_tilde() {
1979 let config = Config::from_toml(
1980 r#"
1981[code_block]
1982fence_char = "~"
1983"#,
1984 )
1985 .unwrap();
1986 assert_eq!(config.code_block.fence_char, FenceChar::Tilde);
1987 }
1988
1989 #[test]
1990 fn test_fence_char_parse_backtick() {
1991 let config = Config::from_toml(
1992 r#"
1993[code_block]
1994fence_char = "`"
1995"#,
1996 )
1997 .unwrap();
1998 assert_eq!(config.code_block.fence_char, FenceChar::Backtick);
1999 }
2000
2001 #[test]
2002 fn test_fence_char_invalid_char() {
2003 let result = Config::from_toml(
2004 r##"
2005[code_block]
2006fence_char = "#"
2007"##,
2008 );
2009 assert!(result.is_err());
2010 }
2011
2012 #[test]
2013 fn test_fence_char_invalid_empty() {
2014 let result = Config::from_toml(
2015 r#"
2016[code_block]
2017fence_char = ""
2018"#,
2019 );
2020 assert!(result.is_err());
2021 }
2022}
2023
2024#[cfg(test)]
2025mod min_fence_length_tests {
2026 use super::*;
2027
2028 #[test]
2029 fn test_min_fence_length_default() {
2030 assert_eq!(MinFenceLength::default().get(), 4);
2031 }
2032
2033 #[test]
2034 fn test_min_fence_length_valid() {
2035 assert_eq!(MinFenceLength::new(3).unwrap().get(), 3);
2036 assert_eq!(MinFenceLength::new(4).unwrap().get(), 4);
2037 assert_eq!(MinFenceLength::new(10).unwrap().get(), 10);
2038 }
2039
2040 #[test]
2041 fn test_min_fence_length_too_small() {
2042 assert!(MinFenceLength::new(0).is_err());
2043 assert!(MinFenceLength::new(1).is_err());
2044 assert!(MinFenceLength::new(2).is_err());
2045 }
2046
2047 #[test]
2048 fn test_min_fence_length_parse_valid() {
2049 let config = Config::from_toml(
2050 r#"
2051[code_block]
2052min_fence_length = 3
2053"#,
2054 )
2055 .unwrap();
2056 assert_eq!(config.code_block.min_fence_length.get(), 3);
2057 }
2058
2059 #[test]
2060 fn test_min_fence_length_parse_invalid() {
2061 let result = Config::from_toml(
2062 r#"
2063[code_block]
2064min_fence_length = 2
2065"#,
2066 );
2067 assert!(result.is_err());
2068 }
2069
2070 #[test]
2071 fn test_min_fence_length_parse_zero() {
2072 let result = Config::from_toml(
2073 r#"
2074[code_block]
2075min_fence_length = 0
2076"#,
2077 );
2078 assert!(result.is_err());
2079 }
2080}
2081
2082#[cfg(test)]
2083mod line_width_tests {
2084 use super::*;
2085
2086 #[test]
2087 fn test_line_width_default() {
2088 assert_eq!(LineWidth::default().get(), 80);
2089 }
2090
2091 #[test]
2092 fn test_line_width_valid() {
2093 assert_eq!(LineWidth::new(8).unwrap().get(), 8);
2094 assert_eq!(LineWidth::new(40).unwrap().get(), 40);
2095 assert_eq!(LineWidth::new(80).unwrap().get(), 80);
2096 assert_eq!(LineWidth::new(120).unwrap().get(), 120);
2097 }
2098
2099 #[test]
2100 fn test_line_width_below_recommended() {
2101 assert!(LineWidth::new(8).unwrap().is_below_recommended());
2102 assert!(LineWidth::new(39).unwrap().is_below_recommended());
2103 assert!(!LineWidth::new(40).unwrap().is_below_recommended());
2104 assert!(!LineWidth::new(80).unwrap().is_below_recommended());
2105 }
2106
2107 #[test]
2108 fn test_line_width_invalid() {
2109 assert!(LineWidth::new(0).is_err());
2110 assert!(LineWidth::new(7).is_err());
2111 assert_eq!(
2112 LineWidth::new(5).unwrap_err(),
2113 "line_width must be at least 8, got 5."
2114 );
2115 }
2116
2117 #[test]
2118 fn test_line_width_parse_valid() {
2119 let config = Config::from_toml("line_width = 100").unwrap();
2120 assert_eq!(config.line_width.get(), 100);
2121 }
2122
2123 #[test]
2124 fn test_line_width_parse_invalid() {
2125 let result = Config::from_toml("line_width = 5");
2126 assert!(result.is_err());
2127 let err = result.unwrap_err().to_string();
2128 assert!(err.contains("line_width must be at least 8"));
2129 }
2130}
2131
2132#[cfg(test)]
2133mod thematic_break_style_tests {
2134 use super::*;
2135
2136 #[test]
2137 fn test_thematic_break_style_default() {
2138 let style = ThematicBreakStyle::default();
2139 assert_eq!(
2140 style.as_str(),
2141 "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"
2142 );
2143 }
2144
2145 #[test]
2146 fn test_thematic_break_style_valid_hyphens() {
2147 assert!(ThematicBreakStyle::new("---".to_string()).is_ok());
2148 assert!(ThematicBreakStyle::new("- - -".to_string()).is_ok());
2149 assert!(ThematicBreakStyle::new("-----".to_string()).is_ok());
2150 assert_eq!(
2151 ThematicBreakStyle::new("---".to_string()).unwrap().as_str(),
2152 "---"
2153 );
2154 }
2155
2156 #[test]
2157 fn test_thematic_break_style_valid_asterisks() {
2158 assert!(ThematicBreakStyle::new("***".to_string()).is_ok());
2159 assert!(ThematicBreakStyle::new("* * *".to_string()).is_ok());
2160 assert!(ThematicBreakStyle::new("*****".to_string()).is_ok());
2161 }
2162
2163 #[test]
2164 fn test_thematic_break_style_valid_underscores() {
2165 assert!(ThematicBreakStyle::new("___".to_string()).is_ok());
2166 assert!(ThematicBreakStyle::new("_ _ _".to_string()).is_ok());
2167 assert!(ThematicBreakStyle::new("_____".to_string()).is_ok());
2168 }
2169
2170 #[test]
2171 fn test_thematic_break_style_empty() {
2172 let result = ThematicBreakStyle::new("".to_string());
2173 assert!(result.is_err());
2174 assert_eq!(result.unwrap_err(), "thematic_break style cannot be empty.");
2175 }
2176
2177 #[test]
2178 fn test_thematic_break_style_too_few_markers() {
2179 assert!(ThematicBreakStyle::new("--".to_string()).is_err());
2180 assert!(ThematicBreakStyle::new("**".to_string()).is_err());
2181 assert!(ThematicBreakStyle::new("__".to_string()).is_err());
2182 let result = ThematicBreakStyle::new("--".to_string());
2183 assert!(
2184 result
2185 .unwrap_err()
2186 .contains("must contain at least 3 of the same marker")
2187 );
2188 }
2189
2190 #[test]
2191 fn test_thematic_break_style_mixed_markers() {
2192 let result = ThematicBreakStyle::new("---***".to_string());
2193 assert!(result.is_err());
2194 assert!(result.unwrap_err().contains("only one type of marker"));
2195 }
2196
2197 #[test]
2198 fn test_thematic_break_style_invalid_characters() {
2199 let result = ThematicBreakStyle::new("---abc".to_string());
2200 assert!(result.is_err());
2201 assert!(result.unwrap_err().contains("must only contain *, -, or _"));
2202 }
2203
2204 #[test]
2205 fn test_thematic_break_style_parse_valid() {
2206 let config = Config::from_toml(
2207 r#"
2208[thematic_break]
2209style = "***"
2210"#,
2211 )
2212 .unwrap();
2213 assert_eq!(config.thematic_break.style.as_str(), "***");
2214 }
2215
2216 #[test]
2217 fn test_thematic_break_style_parse_invalid() {
2218 let result = Config::from_toml(
2219 r#"
2220[thematic_break]
2221style = "--"
2222"#,
2223 );
2224 assert!(result.is_err());
2225 let err = result.unwrap_err().to_string();
2226 assert!(err.contains("must contain at least 3"));
2227 }
2228}
2229
2230#[cfg(test)]
2231mod dash_pattern_tests {
2232 use super::*;
2233
2234 #[test]
2235 fn test_dash_pattern_valid() {
2236 assert!(DashPattern::new("--".to_string()).is_ok());
2237 assert!(DashPattern::new("---".to_string()).is_ok());
2238 assert!(DashPattern::new("-".to_string()).is_ok());
2239 assert_eq!(DashPattern::new("--".to_string()).unwrap().as_str(), "--");
2240 }
2241
2242 #[test]
2243 fn test_dash_pattern_empty() {
2244 let result = DashPattern::new("".to_string());
2245 assert!(result.is_err());
2246 assert_eq!(result.unwrap_err(), "dash pattern cannot be empty.");
2247 }
2248
2249 #[test]
2250 fn test_dash_pattern_with_whitespace() {
2251 assert!(DashPattern::new("- -".to_string()).is_err());
2253 assert!(DashPattern::new(" --".to_string()).is_err());
2254 assert!(DashPattern::new("-- ".to_string()).is_err());
2255 let result = DashPattern::new("- -".to_string());
2256 assert!(
2257 result
2258 .unwrap_err()
2259 .contains("must only contain printable ASCII characters")
2260 );
2261 }
2262
2263 #[test]
2264 fn test_dash_pattern_parse_valid() {
2265 let config = Config::from_toml(
2266 r#"
2267[punctuation]
2268em_dash = "--"
2269"#,
2270 )
2271 .unwrap();
2272 if let DashSetting::Pattern(p) = &config.punctuation.em_dash {
2273 assert_eq!(p.as_str(), "--");
2274 } else {
2275 panic!("Expected Pattern");
2276 }
2277 }
2278
2279 #[test]
2280 fn test_dash_pattern_parse_invalid() {
2281 let result = Config::from_toml(
2282 r#"
2283[punctuation]
2284em_dash = ""
2285"#,
2286 );
2287 assert!(result.is_err());
2288 let err = result.unwrap_err().to_string();
2289 assert!(err.contains("dash pattern cannot be empty"));
2290 }
2291}
2292
2293#[cfg(test)]
2294mod leading_spaces_tests {
2295 use super::*;
2296
2297 #[test]
2298 fn test_leading_spaces_default() {
2299 assert_eq!(LeadingSpaces::default().get(), 1);
2300 }
2301
2302 #[test]
2303 fn test_leading_spaces_valid() {
2304 assert_eq!(LeadingSpaces::new(0).unwrap().get(), 0);
2305 assert_eq!(LeadingSpaces::new(1).unwrap().get(), 1);
2306 assert_eq!(LeadingSpaces::new(2).unwrap().get(), 2);
2307 assert_eq!(LeadingSpaces::new(3).unwrap().get(), 3);
2308 }
2309
2310 #[test]
2311 fn test_leading_spaces_invalid() {
2312 assert!(LeadingSpaces::new(4).is_err());
2313 assert!(LeadingSpaces::new(5).is_err());
2314 assert_eq!(
2315 LeadingSpaces::new(4).unwrap_err(),
2316 "leading_spaces must be at most 3, got 4."
2317 );
2318 }
2319
2320 #[test]
2321 fn test_leading_spaces_parse_valid() {
2322 let config = Config::from_toml(
2323 r#"
2324[unordered_list]
2325leading_spaces = 2
2326"#,
2327 )
2328 .unwrap();
2329 assert_eq!(config.unordered_list.leading_spaces.get(), 2);
2330 }
2331
2332 #[test]
2333 fn test_leading_spaces_parse_invalid() {
2334 let result = Config::from_toml(
2335 r#"
2336[unordered_list]
2337leading_spaces = 5
2338"#,
2339 );
2340 assert!(result.is_err());
2341 let err = result.unwrap_err().to_string();
2342 assert!(err.contains("leading_spaces must be at most 3"));
2343 }
2344}
2345
2346#[cfg(test)]
2347mod trailing_spaces_tests {
2348 use super::*;
2349
2350 #[test]
2351 fn test_trailing_spaces_default() {
2352 assert_eq!(TrailingSpaces::default().get(), 2);
2353 }
2354
2355 #[test]
2356 fn test_trailing_spaces_valid() {
2357 assert_eq!(TrailingSpaces::new(0).unwrap().get(), 0);
2358 assert_eq!(TrailingSpaces::new(1).unwrap().get(), 1);
2359 assert_eq!(TrailingSpaces::new(2).unwrap().get(), 2);
2360 assert_eq!(TrailingSpaces::new(3).unwrap().get(), 3);
2361 }
2362
2363 #[test]
2364 fn test_trailing_spaces_invalid() {
2365 assert!(TrailingSpaces::new(4).is_err());
2366 assert!(TrailingSpaces::new(5).is_err());
2367 assert_eq!(
2368 TrailingSpaces::new(4).unwrap_err(),
2369 "trailing_spaces must be at most 3, got 4."
2370 );
2371 }
2372
2373 #[test]
2374 fn test_trailing_spaces_parse_valid() {
2375 let config = Config::from_toml(
2376 r#"
2377[unordered_list]
2378trailing_spaces = 1
2379"#,
2380 )
2381 .unwrap();
2382 assert_eq!(config.unordered_list.trailing_spaces.get(), 1);
2383 }
2384
2385 #[test]
2386 fn test_trailing_spaces_parse_invalid() {
2387 let result = Config::from_toml(
2388 r#"
2389[unordered_list]
2390trailing_spaces = 4
2391"#,
2392 );
2393 assert!(result.is_err());
2394 let err = result.unwrap_err().to_string();
2395 assert!(err.contains("trailing_spaces must be at most 3"));
2396 }
2397}
2398
2399#[cfg(test)]
2400mod indent_width_tests {
2401 use super::*;
2402
2403 #[test]
2404 fn test_indent_width_default() {
2405 assert_eq!(IndentWidth::default().get(), 4);
2406 }
2407
2408 #[test]
2409 fn test_indent_width_valid() {
2410 assert_eq!(IndentWidth::new(1).unwrap().get(), 1);
2411 assert_eq!(IndentWidth::new(2).unwrap().get(), 2);
2412 assert_eq!(IndentWidth::new(4).unwrap().get(), 4);
2413 assert_eq!(IndentWidth::new(8).unwrap().get(), 8);
2414 }
2415
2416 #[test]
2417 fn test_indent_width_invalid() {
2418 assert!(IndentWidth::new(0).is_err());
2419 assert_eq!(
2420 IndentWidth::new(0).unwrap_err(),
2421 "indent_width must be at least 1, got 0."
2422 );
2423 }
2424
2425 #[test]
2426 fn test_indent_width_parse_valid() {
2427 let config = Config::from_toml(
2428 r#"
2429[unordered_list]
2430indent_width = 2
2431"#,
2432 )
2433 .unwrap();
2434 assert_eq!(config.unordered_list.indent_width.get(), 2);
2435 }
2436
2437 #[test]
2438 fn test_indent_width_parse_invalid() {
2439 let result = Config::from_toml(
2440 r#"
2441[unordered_list]
2442indent_width = 0
2443"#,
2444 );
2445 assert!(result.is_err());
2446 let err = result.unwrap_err().to_string();
2447 assert!(err.contains("indent_width must be at least 1"));
2448 }
2449}
2450
2451#[cfg(test)]
2452mod config_layer_tests {
2453 use super::*;
2454 use tempfile::TempDir;
2455
2456 #[test]
2457 fn test_config_layer_from_file_empty() {
2458 let temp_dir = TempDir::new().unwrap();
2459 let config_path = temp_dir.path().join(".hongdown.toml");
2460 std::fs::write(&config_path, "").unwrap();
2461
2462 let layer = ConfigLayer::from_file(&config_path).unwrap();
2463 assert_eq!(layer, ConfigLayer::default());
2464 }
2465
2466 #[test]
2467 fn test_config_layer_from_file_partial() {
2468 let temp_dir = TempDir::new().unwrap();
2469 let config_path = temp_dir.path().join(".hongdown.toml");
2470 std::fs::write(
2471 &config_path,
2472 r#"
2473line_width = 100
2474git_aware = false
2475"#,
2476 )
2477 .unwrap();
2478
2479 let layer = ConfigLayer::from_file(&config_path).unwrap();
2480 assert_eq!(layer.line_width, Some(LineWidth::new(100).unwrap()));
2481 assert_eq!(layer.git_aware, Some(false));
2482 assert_eq!(layer.include, None);
2483 assert_eq!(layer.heading, None);
2484 }
2485
2486 #[test]
2487 fn test_config_layer_from_file_no_inherit() {
2488 let temp_dir = TempDir::new().unwrap();
2489 let config_path = temp_dir.path().join(".hongdown.toml");
2490 std::fs::write(
2491 &config_path,
2492 r#"
2493no_inherit = true
2494line_width = 100
2495"#,
2496 )
2497 .unwrap();
2498
2499 let layer = ConfigLayer::from_file(&config_path).unwrap();
2500 assert_eq!(layer.no_inherit, true);
2501 assert_eq!(layer.line_width, Some(LineWidth::new(100).unwrap()));
2502 }
2503
2504 #[test]
2505 fn test_config_layer_from_file_not_found() {
2506 let result = ConfigLayer::from_file(Path::new("/nonexistent/.hongdown.toml"));
2507 assert!(result.is_err());
2508 }
2509
2510 #[test]
2511 fn test_config_layer_merge_simple_fields() {
2512 let base = Config {
2513 line_width: LineWidth::new(80).unwrap(),
2514 git_aware: true,
2515 ..Config::default()
2516 };
2517
2518 let layer = ConfigLayer {
2519 line_width: Some(LineWidth::new(100).unwrap()),
2520 git_aware: Some(false),
2521 ..ConfigLayer::default()
2522 };
2523
2524 let merged = layer.merge_over(base);
2525 assert_eq!(merged.line_width.get(), 100);
2526 assert_eq!(merged.git_aware, false);
2527 }
2528
2529 #[test]
2530 fn test_config_layer_merge_preserves_unset() {
2531 let base = Config {
2532 line_width: LineWidth::new(120).unwrap(),
2533 git_aware: false,
2534 include: vec!["*.md".to_string()],
2535 ..Config::default()
2536 };
2537
2538 let layer = ConfigLayer {
2539 line_width: Some(LineWidth::new(100).unwrap()),
2540 ..ConfigLayer::default()
2542 };
2543
2544 let merged = layer.merge_over(base);
2545 assert_eq!(merged.line_width.get(), 100);
2546 assert_eq!(merged.git_aware, false); assert_eq!(merged.include, vec!["*.md".to_string()]); }
2549
2550 #[test]
2551 fn test_config_layer_merge_nested_structs() {
2552 let base = Config {
2553 heading: HeadingConfig {
2554 setext_h1: true,
2555 setext_h2: true,
2556 sentence_case: false,
2557 proper_nouns: vec!["Rust".to_string()],
2558 common_nouns: Vec::new(),
2559 },
2560 ..Config::default()
2561 };
2562
2563 let layer = ConfigLayer {
2564 heading: Some(HeadingConfig {
2565 setext_h1: false,
2566 setext_h2: false,
2567 sentence_case: true,
2568 proper_nouns: vec!["Python".to_string()],
2569 common_nouns: Vec::new(),
2570 }),
2571 ..ConfigLayer::default()
2572 };
2573
2574 let merged = layer.merge_over(base);
2575 assert_eq!(merged.heading.setext_h1, false);
2576 assert_eq!(merged.heading.setext_h2, false);
2577 assert_eq!(merged.heading.sentence_case, true);
2578 assert_eq!(merged.heading.proper_nouns, vec!["Python".to_string()]);
2579 }
2580
2581 #[test]
2582 fn test_config_layer_merge_vec_replacement() {
2583 let base = Config {
2584 include: vec!["*.md".to_string(), "*.txt".to_string()],
2585 exclude: vec!["test/**".to_string()],
2586 ..Config::default()
2587 };
2588
2589 let layer = ConfigLayer {
2590 include: Some(vec!["docs/*.md".to_string()]),
2591 ..ConfigLayer::default()
2593 };
2594
2595 let merged = layer.merge_over(base);
2596 assert_eq!(merged.include, vec!["docs/*.md".to_string()]);
2597 assert_eq!(merged.exclude, vec!["test/**".to_string()]); }
2599}
2600
2601#[cfg(test)]
2602mod cascading_config_tests {
2603 use super::*;
2604 use tempfile::TempDir;
2605
2606 #[test]
2607 fn test_try_load_layer_file_exists() {
2608 let temp_dir = TempDir::new().unwrap();
2609 let config_path = temp_dir.path().join(".hongdown.toml");
2610 std::fs::write(
2611 &config_path,
2612 r#"
2613line_width = 100
2614"#,
2615 )
2616 .unwrap();
2617
2618 let layer = Config::try_load_layer(&config_path).unwrap();
2619 assert!(layer.is_some());
2620 assert_eq!(
2621 layer.unwrap().line_width,
2622 Some(LineWidth::new(100).unwrap())
2623 );
2624 }
2625
2626 #[test]
2627 fn test_try_load_layer_file_not_exists() {
2628 let result = Config::try_load_layer(Path::new("/nonexistent/.hongdown.toml")).unwrap();
2629 assert!(result.is_none());
2630 }
2631
2632 #[test]
2633 fn test_discover_project_config() {
2634 let temp_dir = TempDir::new().unwrap();
2635 let parent = temp_dir.path();
2636 let child = parent.join("project");
2637 std::fs::create_dir(&child).unwrap();
2638
2639 let config_path = parent.join(".hongdown.toml");
2640 std::fs::write(&config_path, "line_width = 100").unwrap();
2641
2642 let result = Config::discover_project_config(&child).unwrap();
2643 assert!(result.is_some());
2644 let (path, layer) = result.unwrap();
2645 assert_eq!(path, config_path);
2646 assert_eq!(layer.line_width, Some(LineWidth::new(100).unwrap()));
2647 }
2648
2649 #[test]
2650 fn test_discover_project_config_not_found() {
2651 let temp_dir = TempDir::new().unwrap();
2652 let result = Config::discover_project_config(temp_dir.path()).unwrap();
2653 assert!(result.is_none());
2654 }
2655
2656 #[test]
2657 fn test_load_cascading_project_only() {
2658 let temp_dir = TempDir::new().unwrap();
2659 let config_path = temp_dir.path().join(".hongdown.toml");
2660 std::fs::write(&config_path, "line_width = 100").unwrap();
2661
2662 let (config, path) = Config::load_cascading(temp_dir.path()).unwrap();
2663 assert_eq!(path, Some(config_path));
2664 assert_eq!(config.line_width.get(), 100);
2665 }
2666
2667 #[test]
2668 fn test_load_cascading_no_config() {
2669 let temp_dir = TempDir::new().unwrap();
2670 let (config, path) = Config::load_cascading(temp_dir.path()).unwrap();
2671 assert_eq!(path, None);
2672 assert_eq!(config, Config::default());
2673 }
2674
2675 #[test]
2676 fn test_load_cascading_with_no_inherit() {
2677 let temp_dir = TempDir::new().unwrap();
2678 let config_path = temp_dir.path().join(".hongdown.toml");
2679
2680 std::fs::write(
2682 &config_path,
2683 r#"
2684no_inherit = true
2685line_width = 100
2686"#,
2687 )
2688 .unwrap();
2689
2690 let (config, _) = Config::load_cascading(temp_dir.path()).unwrap();
2691
2692 assert_eq!(config.line_width.get(), 100);
2694 assert_eq!(config.no_inherit, true);
2695 }
2696
2697 #[test]
2698 fn test_load_cascading_nearest_project_config() {
2699 let temp_dir = TempDir::new().unwrap();
2700 let parent = temp_dir.path();
2701 let child = parent.join("project");
2702 std::fs::create_dir(&child).unwrap();
2703
2704 std::fs::write(parent.join(".hongdown.toml"), "line_width = 120").unwrap();
2706
2707 std::fs::write(child.join(".hongdown.toml"), "line_width = 100").unwrap();
2709
2710 let (config, path) = Config::load_cascading(&child).unwrap();
2711
2712 assert_eq!(config.line_width.get(), 100);
2714 assert_eq!(path, Some(child.join(".hongdown.toml")));
2715 }
2716
2717 #[test]
2718 fn test_load_cascading_searches_parent_dirs() {
2719 let temp_dir = TempDir::new().unwrap();
2720 let parent = temp_dir.path();
2721 let child = parent.join("project");
2722 std::fs::create_dir(&child).unwrap();
2723
2724 std::fs::write(
2726 parent.join(".hongdown.toml"),
2727 r#"
2728line_width = 120
2729git_aware = false
2730"#,
2731 )
2732 .unwrap();
2733
2734 let (config, path) = Config::load_cascading(&child).unwrap();
2735
2736 assert_eq!(config.line_width.get(), 120);
2738 assert_eq!(config.git_aware, false);
2739 assert_eq!(path, Some(parent.join(".hongdown.toml")));
2740 }
2741}