1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! A simple way of including other files, rust doc and table of content in a markdown file.
//!
//! For a repo's README file, you'll create a _README.tpl.md_ which you can edit like a normal
//! markdown file, but with the added support for fenced includes which are TOML fences with
//! an extra name containing the configuration of the include.
//!
//! ## Example
//!
//! _src/README.tpl.md_:
//! > My title<br>
//! > <br>
//! > Include a table of content:<br>
//! > &#96;&#96;&#96;toml toc<br>
//! > header = "# Table of contents"<br>
//! > &#96;&#96;&#96;<br>
//! > <br>
//! > Extracted from lib.rs' rust doc:<br>
//! > <br>
//! > &#96;&#96;&#96;toml rustdoc<br>
//! > source = "lib.rs"<br>
//! > &#96;&#96;&#96;<br>
//!
//!
//! To generate a _README.md_ file you add a test:
//!
//! ```rust
//! #[test]
//! fn update_readme() {
//!     markdown_includes::update("src/README.tpl.md", "README.md").unwrap();
//! }
//! ```
//!
//! This test will update the README file if necessary, but if running
//! in a CI pipeline (the CI environment variable is set),
//! it will fail if the _README.md_ needs updating.
//!
#[cfg(test)]
mod tests;

mod fence;
mod rustdoc_parse;

use std::{
    env, fs,
    iter::zip,
    path::{Path, PathBuf},
};

use anyhow::{bail, Result};
use fence::find_fences;

pub fn process_includes_document(document: &mut String, template_dir: &Path) -> Result<()> {
    let mut fences = find_fences(&document, template_dir)?;
    fences.sort_by_key(|f| f.priority());

    for fence in fences {
        fence.run(document)?;
    }
    Ok(())
}

pub fn update(template_file: &str, destination_file: &str) -> Result<()> {
    let is_ci = env::var("CI").map(|_| true).unwrap_or(false);

    let template_path = PathBuf::from(template_file);
    let template_dir = template_path
        .parent()
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| PathBuf::from(""));
    let mut generated_doc = fs::read_to_string(&template_path)?;
    process_includes_document(&mut generated_doc, &template_dir)?;
    let generated_doc = format!(
        r#"<!-- 
Please don't edit. This document has been generated from {template_file}
--> 
{generated_doc}"#
    );

    let dest_path = PathBuf::from(destination_file);

    let current_doc = if dest_path.exists() {
        fs::read_to_string(&dest_path)?
    } else {
        "".to_string()
    };

    if let Some(diff_str) = diff(&generated_doc, &current_doc) {
        if is_ci {
            bail!(
                "The markdown document {dest_path:?} is out of sync with {template_path:?}. 
            Please re-run the tests and commit the updated file. 
            This message is generated because the test is run on CI (the CI environment variable is set).\n{diff_str}"
            );
        } else {
            fs::write(&dest_path, generated_doc.as_bytes())?;
        }
    }

    Ok(())
}

fn diff(doc1: &str, doc2: &str) -> Option<String> {
    if zip(doc1.lines(), doc2.lines()).any(|(l1, l2)| l1.trim() != l2.trim()) {
        Some(
            zip(doc1.lines(), doc2.lines())
                .map(|(l1, l2)| format!("> {}\n< {}\n", l1.trim(), l2.trim()))
                .collect::<Vec<_>>()
                .join(", "),
        )
    } else {
        None
    }
}

#[test]
fn update_readme() {
    update("src/README.tpl.md", "README.md").unwrap();
}