1extern crate percent_encoding;
2
3use percent_encoding::{percent_encode, CONTROLS};
4use std::path::PathBuf;
5use std::str::FromStr;
6
7fn slugify(text: &str) -> String {
8 percent_encode(
9 text.replace(" ", "-").to_lowercase().as_bytes(),
10 CONTROLS,
11 )
12 .to_string()
13}
14
15pub struct Heading {
16 pub depth: usize,
17 pub title: String,
18}
19
20impl FromStr for Heading {
21 type Err = ();
22
23 fn from_str(s: &str) -> Result<Self, Self::Err> {
24 let trimmed = s.trim_end();
25 if trimmed.starts_with("#") {
26 let mut depth = 0usize;
27 let title = trimmed
28 .chars()
29 .skip_while(|c| {
30 if *c == '#' {
31 depth += 1;
32 true
33 } else {
34 false
35 }
36 })
37 .collect::<String>()
38 .trim_start()
39 .to_owned();
40 Ok(Heading {
41 depth: depth - 1,
42 title,
43 })
44 } else {
45 Err(())
46 }
47 }
48}
49
50impl Heading {
51 pub fn format(&self, config: &Config) -> Option<String> {
52 if self.depth >= config.min_depth
53 && config.max_depth.map(|d| self.depth <= d).unwrap_or(true)
54 {
55 Some(format!(
56 "{}{} {}",
57 " ".repeat(config.indent)
58 .repeat(self.depth - config.min_depth),
59 &config.bullet,
60 if config.no_link {
61 self.title.clone()
62 } else {
63 format!("[{}](#{})", &self.title, slugify(&self.title))
64 }
65 ))
66 } else {
67 None
68 }
69 }
70}
71
72pub enum InputFile {
73 Path(PathBuf),
74 StdIn,
75}
76
77pub struct Config {
84 pub input_file: InputFile,
85 pub bullet: String,
86 pub indent: usize,
87 pub max_depth: Option<usize>,
88 pub min_depth: usize,
89 pub header: Option<String>,
90 pub no_link: bool,
91 }
93
94impl Default for Config {
95 fn default() -> Self {
96 Config {
97 input_file: InputFile::StdIn,
98 bullet: String::from("1."),
99 indent: 4,
100 max_depth: None,
101 min_depth: 0,
102 no_link: false,
103 header: Some(String::from("## Table of Contents")),
104 }
106 }
107}