mdbook_utils/
lib.rs

1//! # mdbook-utils
2//!
3//! For installation and usage instructions for the `mdbook-utils` command-line
4//! tool, consult the [User Guide](https://john-cd.com/mdbook-utils/).
5//!
6//! A list of available commands is displayed when entering `mdbook-utils` at a
7//! shell prompt.
8//!
9//! The following (<https://docs.rs/mdbook-utils/>) contains the **library API** doc.
10//! Please consult the [Public API](https://john-cd.com/mdbook-utils/public_api.html) page as well.
11//!
12//! You will want to use the Public API (over the CLI) to:
13//!
14//! - Integrate it in your project, for example call it from a `build.rs` build
15//!   script,
16//! - Extend its capabilities,
17//! - ...
18
19#![deny(missing_docs)]
20#![deny(rust_2018_idioms)]
21#![doc(html_playground_url = "https://play.rust-lang.org/")]
22// #![doc(html_favicon_url = "https://example.com/favicon.ico")]
23// #![doc(html_logo_url = "https://example.com/logo.jpg")]
24
25mod build_book;
26mod dependencies;
27mod fs;
28mod gen;
29mod link;
30pub mod markdown;
31mod parser;
32mod sitemap;
33pub mod test_markdown;
34mod write_from_parser;
35
36use std::fs::File;
37use std::io::BufWriter;
38use std::io::Write;
39use std::path::Path;
40
41use anyhow::bail;
42use anyhow::Context;
43use anyhow::Result;
44use pulldown_cmark::LinkType;
45use pulldown_cmark::Parser;
46
47/// Helper function:
48///
49/// Checks if the source directory exists,
50/// create the destination directory if it doesn't exist,
51/// create the destination file,
52/// parse all the Markdown files in the source directory,
53/// and invoke a closure that uses the parser to write to the file.
54fn helper<P1, P2, F>(src_dir_path: P1, dest_file_path: P2, func: F) -> Result<()>
55where
56    P1: AsRef<Path>,
57    P2: AsRef<Path>,
58    F: for<'a, 'b> FnOnce(&'a mut Parser<'a>, &'b mut File) -> Result<()>,
59{
60    let src_dir_path = fs::check_is_dir(src_dir_path)?;
61
62    fs::create_parent_dir_for(dest_file_path.as_ref())?;
63
64    let mut f = File::create(dest_file_path.as_ref()).with_context(|| {
65        format!(
66            "[helper] Could not create file {}",
67            dest_file_path.as_ref().display()
68        )
69    })?;
70
71    let all_markdown = fs::read_to_string_all_markdown_files_in(src_dir_path)?;
72    let mut parser = parser::get_parser(all_markdown.as_ref());
73
74    func(&mut parser, &mut f)?;
75    Ok(())
76}
77
78// Public Functions
79
80// DEBUG
81
82/// Parse Markdown from all .md files in a given source directory and
83/// write all raw events to a file for debugging purposes.
84///
85/// src_dir_path: path to the source directory.
86///
87/// dest_file_path: path to the file to create and write into.
88pub fn debug_parse_to<P1, P2>(src_dir_path: P1, dest_file_path: P2) -> Result<()>
89where
90    P1: AsRef<Path>,
91    P2: AsRef<Path>,
92{
93    helper(
94        src_dir_path,
95        dest_file_path,
96        write_from_parser::write_raw_to,
97    )?;
98    Ok(())
99}
100
101/// Test function that uses fake Markdown.
102pub fn test() -> Result<()> {
103    fs::create_dir("./book/temp/")?;
104
105    let dest_file_path = "./book/temp/test.log";
106    let mut f = BufWriter::new(File::create(dest_file_path).context(
107        "[test] Failed to create the destination file. Does the full directory path exist?",
108    )?);
109
110    let test_markdown = test_markdown::get_test_markdown();
111    let mut parser = parser::get_parser(test_markdown.as_ref());
112    write_from_parser::write_raw_to(&mut parser, &mut f)?;
113    f.flush()
114        .context("Not all bytes could be written due to I/O errors or EOF being reached.")?;
115    Ok(())
116}
117
118// REFERENCE DEFINITIONS
119
120/// Parse Markdown from all .md files in a given source directory
121/// and write reference definitions found therein to a file.
122///
123/// src_dir_path: path to the source directory.
124///
125/// dest_file_path: path to the file to create and write into.
126pub fn write_refdefs_to<P1, P2>(src_dir_path: P1, dest_file_path: P2) -> Result<()>
127where
128    P1: AsRef<Path>,
129    P2: AsRef<Path>,
130{
131    helper(
132        src_dir_path,
133        dest_file_path,
134        write_from_parser::write_refdefs_to,
135    )?;
136    Ok(())
137}
138
139/// Parse Markdown from all .md files in a given source directory,
140/// extract existing reference definitions,
141/// identify URLs that are GitHub repos,
142/// create badge URLs for these links,
143/// and write to a file.
144///
145/// src_dir_path: path to the source directory.
146///
147/// dest_file_path: path to the file to create and write into.
148pub fn generate_badges<P1, P2>(src_dir_path: P1, dest_file_path: P2) -> Result<()>
149where
150    P1: AsRef<Path>,
151    P2: AsRef<Path>,
152{
153    helper(
154        src_dir_path,
155        dest_file_path,
156        write_from_parser::write_github_repo_badge_refdefs,
157    )?;
158    Ok(())
159}
160
161// LINKS
162
163// TODO need to remove internal links
164
165/// Parse Markdown from all .md files in a given source directory,
166/// write all inline links and autolinks (i.e., not written as
167/// reference-style links) found therein to a file.
168///
169/// src_dir_path: path to the source directory.
170///
171/// dest_file_path: path to the file to create and write into.
172pub fn write_inline_links<P1, P2>(src_dir_path: P1, dest_file_path: P2) -> Result<()>
173where
174    P1: AsRef<Path>,
175    P2: AsRef<Path>,
176{
177    helper(src_dir_path, dest_file_path, |parser, f| {
178        let links: Vec<link::Link<'_>> = parser::extract_links(parser);
179        let links: Vec<_> = links
180            .into_iter()
181            .filter(|l| {
182                [LinkType::Inline, LinkType::Autolink]
183                    .iter()
184                    .any(|&x| l.get_link_type().unwrap() == x)
185            })
186            .collect();
187        link::write_reference_style_links_to(links, f)?;
188        Ok(())
189    })?;
190
191    Ok(())
192}
193
194/// Parse Markdown from all .md files in a given source directory,
195/// write all links found therein to a file.
196///
197/// src_dir_path: path to the source directory.
198///
199/// dest_file_path: path to the file to create and write into.
200pub fn write_all_links<P1, P2>(src_dir_path: P1, dest_file_path: P2) -> Result<()>
201where
202    P1: AsRef<Path>,
203    P2: AsRef<Path>,
204{
205    helper(src_dir_path, dest_file_path, |parser, f| {
206        let links: Vec<link::Link<'_>> = parser::extract_links(parser);
207        link::write_reference_style_links_to(links, f)?;
208        Ok(())
209    })?;
210
211    Ok(())
212}
213
214/// Parse Markdown from all .md files in a given source directory,
215/// write duplicated links found therein to a file.
216///
217/// src_dir_path: path to the source directory.
218///
219/// dest_file_path: path to the file to create and write into.
220pub fn write_duplicate_links<P1, P2>(src_dir_path: P1, dest_file_path: P2) -> Result<()>
221where
222    P1: AsRef<Path>,
223    P2: AsRef<Path>,
224{
225    helper(src_dir_path, dest_file_path, |parser, _f| {
226        let _links: Vec<link::Link<'_>> = parser::extract_links(parser);
227        // TODO ::write_duplicate_links_to(links, f)?;
228        println!("NOT IMPLEMENTED!");
229        Ok(())
230    })?;
231
232    Ok(())
233}
234
235/// Parse Markdown from all .md files in a given source directory,
236/// write duplicated links found therein to a file.
237///
238/// src_dir_path: path to the source directory.
239///
240/// dest_file_path: path to the file to create and write into.
241pub fn write_broken_links<P1, P2>(src_dir_path: P1, dest_file_path: P2) -> Result<()>
242where
243    P1: AsRef<Path>,
244    P2: AsRef<Path>,
245{
246    helper(src_dir_path, dest_file_path, |parser, _f| {
247        let _links: Vec<link::Link<'_>> = parser::extract_links(parser);
248        // TODO ::write_broken_links_to(links, f)?;
249        println!("NOT IMPLEMENTED!");
250        Ok(())
251    })?;
252
253    Ok(())
254}
255
256// GENERATE REF DEFS FROM DEPENDENCIES
257
258/// Given a Cargo.toml path,
259/// generate reference definitions from code dependencies
260/// and write them to a file.
261///
262/// cargo_toml_dir_path: path to the directory containing `Cargo.toml`.
263///
264/// markdown_dir_path: path to the directory containing Markdown files.
265///
266/// refdef_dest_file_path: path to the file to create and
267/// write into.
268pub fn generate_refdefs_to<P1, P2, P3>(
269    cargo_toml_dir_path: P1,
270    markdown_dir_path: P2,
271    refdef_dest_file_path: P3,
272) -> Result<()>
273where
274    P1: AsRef<Path>,
275    P2: AsRef<Path>,
276    P3: AsRef<Path>,
277{
278    // Generate ref defs from dependencies
279    let deps = dependencies::get_dependencies(&cargo_toml_dir_path)?;
280    // for (_, d) in &deps {
281    //     tracing::info!("{:?}", d);
282    // }
283    let mut new_links = gen::generate_refdefs_from(deps);
284
285    // TODO can we read just the *-refs.md files?
286    helper(markdown_dir_path, refdef_dest_file_path, |parser, f| {
287        // Read existing ref defs
288        let _sorted_linkdefs: std::collections::BTreeMap<_, _> =
289            parser.reference_definitions().iter().collect();
290        // TODO
291        println!("NOT IMPLEMENTED!");
292        let existing_links = Vec::new();
293
294        let links = gen::merge_links(existing_links, &mut new_links);
295        link::write_refdefs_to(links, f)?;
296        Ok(())
297    })?;
298    Ok(())
299}
300
301// SITEMAP
302
303/// Create a sitemap.xml file from the list of Markdown files in a
304/// source directory.
305///
306/// src_dir_path: path to the source directory.
307///
308/// domain: base URL e.g. <https://john-cd.com/rust_howto/>.
309///
310/// dest_file_path: the path to the destination file e.g.
311/// book/html/sitemap.xml.
312pub fn generate_sitemap<P1, P2>(
313    markdown_src_dir_path: P1,
314    base_url: url::Url,
315    sitemap_dest_file_path: P2,
316) -> Result<()>
317where
318    P1: AsRef<Path>,
319    P2: AsRef<Path>,
320{
321    // Returns an error whether the base URL is a cannot-be-a-base URL,
322    // meaning that parsing a relative URL string with this URL
323    // as the base will return an error.
324    if base_url.cannot_be_a_base() {
325        bail!("Invalid URL - cannot be a base: {}", base_url);
326    }
327
328    // Verify source path
329    let markdown_src_dir_path = fs::check_is_dir(markdown_src_dir_path)?;
330
331    // Create the parent folders of the destination file, if needed
332    fs::create_parent_dir_for(sitemap_dest_file_path.as_ref())?;
333
334    // Create the `sitemap.xml` file
335    // File::create will create a file if it does not exist,
336    // and will truncate it if it does.
337    let mut f = File::create(sitemap_dest_file_path.as_ref()).with_context(|| {
338        format!(
339            "Failed to create the sitemap file {}. The full directory path may not exist or required permissions may be missing.",
340            sitemap_dest_file_path.as_ref().display()
341        )
342    })?;
343
344    let summary_md_path = markdown_src_dir_path.join("SUMMARY.md");
345    tracing::debug!("SUMMARY.md path: {}", summary_md_path.display());
346    let markdown = std::fs::read_to_string(summary_md_path.clone()).with_context(|| {
347        format!(
348            "[generate_sitemap] Could not read {}. Does the file exist?",
349            summary_md_path.display()
350        )
351    })?;
352    let mut parser = parser::get_parser(markdown.as_str());
353    let links: Vec<link::Link<'_>> = parser::extract_links(&mut parser);
354
355    sitemap::generate_sitemap(links, base_url, &mut f)?;
356
357    Ok(())
358}
359
360#[cfg(test)]
361mod test {
362    // use super::*;
363
364    // #[test]
365    // fn test() {
366    // }
367}