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#[derive(Debug, StructOpt, Default, Clone)]
26#[structopt(name = "md-inc", about = "Include files in Markdown docs")]
27pub struct Args {
28 #[structopt(parse(from_os_str))]
32 files: Vec<PathBuf>,
33
34 #[structopt(long = "out", parse(from_os_str))]
39 out_dir: Option<PathBuf>,
40
41 #[structopt(
45 short = "O",
46 long,
47 help = "Tag used for opening commands (default: '<!--|')"
48 )]
49 open_tag: Option<String>,
50
51 #[structopt(
55 short = "C",
56 long,
57 help = "Tag used for closing commands (default: '|-->')"
58 )]
59 close_tag: Option<String>,
60
61 #[structopt(short, long, help = "Command used to end a block (default: 'end')")]
65 end_command: Option<String>,
66
67 #[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 #[structopt(
82 short = "d",
83 long = "dir",
84 parse(from_os_str),
85 help = "Working directories"
86 )]
87 working_dir: Vec<PathBuf>,
88
89 #[structopt(
93 short,
94 long = "ignore-config",
95 help = "Ignore '.md-inc.toml' files in the directory"
96 )]
97 ignore_config: bool,
98
99 #[structopt(short, long, parse(from_os_str), help = "Path to a config file")]
103 config: Option<PathBuf>,
104
105 #[structopt(short = "R", long = "read-only", help = "Skip writing output to file")]
109 read_only: bool,
110
111 #[structopt(
115 short = "r",
116 long = "recursive",
117 help = "Run for all subfolders containing '.md-inc.toml'"
118 )]
119 recursive: bool,
120
121 #[structopt(
126 short = "g",
127 long = "glob",
128 help = "Custom globs used to match config files"
129 )]
130 glob: Vec<String>,
131
132 #[structopt(short, long, help = "Print output to stdout")]
136 print: bool,
137}
138
139pub 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
191pub 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 let contents = read_to_string(&path)?;
230 if contents == res {
231 println!(" [[No changes]]");
232 return Ok(res); }
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
258pub 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}