md_inc/
lib.rs

1#[macro_use]
2extern crate serde_derive;
3use anyhow::{Context, Result};
4use std::env::current_dir;
5use std::fs::{read_to_string, File};
6use std::io::Write;
7use std::path::{Path, PathBuf};
8pub use structopt::StructOpt;
9mod config;
10mod parse;
11use crate::config::ConfigAndPath;
12use crate::parse::Parser;
13pub use crate::{
14    config::{Config, OutputTo},
15    parse::ParserConfig,
16};
17
18#[cfg(test)]
19mod test;
20
21///
22/// Include files in Markdown docs
23/// Can be after from command-line arguments using `Args::from_args()` (uses the `StructOpt` trait)
24///
25#[derive(Debug, StructOpt, Default, Clone)]
26#[structopt(name = "md-inc", about = "Include files in Markdown docs")]
27pub struct Args {
28    ///
29    /// A list of files to transform
30    ///
31    #[structopt(parse(from_os_str))]
32    files: Vec<PathBuf>,
33
34    ///
35    /// An optional path to output the generated file to.
36    /// If not present, the files will be inserted inline
37    ///
38    #[structopt(long = "out", parse(from_os_str))]
39    out_dir: Option<PathBuf>,
40
41    ///
42    /// Override the opening tag for a command block
43    ///
44    #[structopt(
45        short = "O",
46        long,
47        help = "Tag used for opening commands (default: '<!--|')"
48    )]
49    open_tag: Option<String>,
50
51    ///
52    /// Override the closing tag for a command block
53    ///
54    #[structopt(
55        short = "C",
56        long,
57        help = "Tag used for closing commands (default: '|-->')"
58    )]
59    close_tag: Option<String>,
60
61    ///
62    /// Override the 'end' command name
63    ///
64    #[structopt(short, long, help = "Command used to end a block (default: 'end')")]
65    end_command: Option<String>,
66
67    ///
68    /// The base directory used to reference imported files
69    ///
70    #[structopt(
71        short = "b",
72        long = "base-dir",
73        parse(from_os_str),
74        help = "Base directory used when referencing imports"
75    )]
76    base_dir: Option<PathBuf>,
77
78    ///
79    /// Set 1 or more working directories that may contain a '.md-inc.toml' config file.
80    ///
81    #[structopt(
82        short = "d",
83        long = "dir",
84        parse(from_os_str),
85        help = "Working directories"
86    )]
87    working_dir: Vec<PathBuf>,
88
89    ///
90    /// Ignore automatic detection of '.md-inc.toml' config files in the working directory
91    ///
92    #[structopt(
93        short,
94        long = "ignore-config",
95        help = "Ignore '.md-inc.toml' files in the directory"
96    )]
97    ignore_config: bool,
98
99    ///
100    /// A custom '.toml' config file
101    ///
102    #[structopt(short, long, parse(from_os_str), help = "Path to a config file")]
103    config: Option<PathBuf>,
104
105    ///
106    /// If true, the output is not written back to the file
107    ///
108    #[structopt(short = "R", long = "read-only", help = "Skip writing output to file")]
109    read_only: bool,
110
111    ///
112    /// Scans all subdirectories for '.md-inc.toml' files
113    ///
114    #[structopt(
115        short = "r",
116        long = "recursive",
117        help = "Run for all subfolders containing '.md-inc.toml'"
118    )]
119    recursive: bool,
120
121    ///
122    /// Searches the working directory for all matching config files
123    /// and transforms files using each config file
124    ///
125    #[structopt(
126        short = "g",
127        long = "glob",
128        help = "Custom globs used to match config files"
129    )]
130    glob: Vec<String>,
131
132    ///
133    /// Prints the transformed files to stdout
134    ///
135    #[structopt(short, long, help = "Print output to stdout")]
136    print: bool,
137}
138
139///
140/// Transforms a list of input files
141///
142/// # Returns
143/// A Result containing the transformed input and vec of directories to also check
144///
145/// # Parameters
146/// * `args` An struct of configuration settings.
147///
148///
149pub fn transform_files_with_args(args: Args, config: Option<ConfigAndPath>) -> Result<Vec<String>> {
150    let mut out_dir: Option<PathBuf> = None;
151    if let Some(x) = args.working_dir.first() {
152        if x.exists() {
153            std::env::set_current_dir(&x)
154                .with_context(|| format!("Could not set working directory: {:?}", &x))?;
155        }
156    }
157    let (mut parser, files) = if let Some(cfg) = config {
158        let parent = cfg.parent_path()?;
159        out_dir = cfg.config.out_dir.as_ref().map(|x| parent.join(x));
160        cfg.into_parser()?
161    } else {
162        (ParserConfig::default(), args.files)
163    };
164
165    if let Some(x) = args.out_dir {
166        out_dir = Some(x);
167    }
168    if let Some(x) = args.open_tag {
169        parser.tags.opening = x;
170    }
171    if let Some(x) = args.close_tag {
172        parser.tags.closing = x;
173    }
174    if let Some(x) = args.end_command {
175        parser.end_command = x;
176    }
177    if let Some(x) = args.base_dir {
178        parser.base_dir = x;
179    }
180    transform_files(
181        parser,
182        &files,
183        OutputTo {
184            read_only: args.read_only,
185            print: args.print,
186            out_dir,
187        },
188    )
189}
190
191///
192/// Transforms files
193///
194/// # Parameters
195/// * `parser` A parser which contains override configuration and a base directory
196/// * `files` A list of files to be transformed
197/// * `prefs` Output configuration settings
198///
199/// # Example
200///
201/// ```
202/// use md_inc::{transform_files, OutputTo, ParserConfig};
203/// transform_files(ParserConfig::default(), &["README.md"], OutputTo::stdout());
204/// ```
205///
206pub fn transform_files<P: AsRef<Path>>(
207    parser: ParserConfig,
208    files: &[P],
209    prefs: OutputTo,
210) -> Result<Vec<String>> {
211    let (read_only, print, out_dir) = (prefs.read_only, prefs.print, prefs.out_dir);
212    Ok(files
213        .iter()
214        .map(|file| {
215            let file = file.as_ref();
216            print!(" {}", &file.to_str().unwrap_or_default());
217            let file_parser = Parser::new(parser.clone(), read_to_string(file.clone())?);
218            let res = file_parser.parse()?;
219            if !read_only {
220                match &out_dir {
221                    Some(path) => {
222                        let mut path = path.clone();
223                        if path.is_dir() {
224                            let name = file.file_name().and_then(|x| x.to_str()).unwrap_or("out");
225                            path = path.join(name)
226                        }
227                        if path.is_file() {
228                            // Check if contents has changed
229                            let contents = read_to_string(&path)?;
230                            if contents == res {
231                                println!(" [[No changes]]");
232                                return Ok(res); // Next file
233                            }
234                        }
235                        let mut f = File::create(&path)?;
236                        f.write_all(res.as_bytes())?;
237                        println!(" [[Updated!]]")
238                    }
239                    _ => {
240                        if res != file_parser.content {
241                            let mut f = File::create(&file)?;
242                            f.write_all(res.as_bytes())?;
243                            println!(" [[Updated!]]")
244                        } else {
245                            println!(" [[No changes]]");
246                        }
247                    }
248                }
249            }
250            if print {
251                println!("\n{}", res);
252            }
253            Ok(res)
254        })
255        .collect::<Result<Vec<_>>>()?)
256}
257
258///
259/// Transform files based on the arguments in `args`.
260///
261/// If `recursive` is true, the `glob` will be used to find matching config files,
262/// (or "**/.md-inc.toml" if not set)
263/// if `files` is set, they will be transformed, otherwise the `files` field in the config file(s)
264/// will be used.
265/// Similarly, any fields in the config file will be overridden if also set in `args`.
266///
267pub fn walk_transform(mut args: Args) -> Result<Vec<Vec<String>>> {
268    if let Some(x) = &args.working_dir.first() {
269        std::env::set_current_dir(x)?;
270    }
271    let mut subdirs: Vec<PathBuf> = args.working_dir.clone();
272    if args.recursive {
273        args.recursive = false;
274        let find_glob = |g| {
275            glob::glob(g)
276                .expect("Failed to read glob pattern")
277                .filter_map(|path| path.ok().and_then(|x| x.parent().map(|x| x.to_path_buf())))
278                .collect::<Vec<_>>()
279        };
280        if args.glob.is_empty() {
281            subdirs.append(&mut find_glob("**/.md-inc.toml"));
282        }
283        for g in &args.glob {
284            subdirs.append(&mut find_glob(g.as_str()));
285        }
286        if subdirs.is_empty() {
287            return Err(anyhow::anyhow!("Did not find any matches for globs"));
288        }
289    }
290
291    let config: Option<ConfigAndPath> = if let Some(path) = &args.config {
292        Some(ConfigAndPath {
293            config: Config::try_from_path(&path)?,
294            path: path.to_path_buf(),
295        })
296    } else if !args.ignore_config {
297        Config::try_from_dir(current_dir()?)?
298    } else {
299        None
300    };
301
302    let config = if let Some(x) = config {
303        let parent = x
304            .path
305            .parent()
306            .context("Directory of config file could not be determined")?;
307        if subdirs.is_empty() {
308            subdirs = vec![current_dir()?];
309        }
310        subdirs = x
311            .config
312            .depend_dirs
313            .iter()
314            .map(|x| parent.join(x))
315            .chain(subdirs.into_iter())
316            .chain(x.config.next_dirs.iter().map(|x| parent.join(x)))
317            .collect();
318        Some(x)
319    } else {
320        None
321    };
322
323    let res = if subdirs.is_empty() {
324        let res = transform_files_with_args(args.clone(), config.clone())?;
325        vec![res]
326    } else {
327        let mut res = vec![];
328        for x in subdirs {
329            println!(">> {}", x.to_str().unwrap_or_default());
330            match &config {
331                Some(cfg) if cfg.path == x => {
332                    args.working_dir = vec![x];
333                    res.push(transform_files_with_args(args.clone(), config.clone())?);
334                }
335                _ => {
336                    if let Some(config) = Config::try_from_dir(&x)? {
337                        args.working_dir = vec![x];
338                        res.push(transform_files_with_args(args.clone(), Some(config))?);
339                    }
340                }
341            }
342        }
343        res
344    };
345    Ok(res)
346}