1#[cfg(test)]
43mod tests;
44
45mod fence;
46mod rustdoc_parse;
47
48use fs_err as fs;
49use std::{
50 env,
51 iter::zip,
52 path::{Path, PathBuf},
53};
54
55use anyhow::{bail, Context, Result};
56use fence::find_fences;
57
58pub fn process_includes_document(document: &mut String, template_dir: &Path) -> Result<()> {
59 let mut fences = find_fences(&document, template_dir)?;
60 fences.sort_by_key(|f| f.priority());
61
62 for fence in fences {
63 fence.run(document)?;
64 }
65 Ok(())
66}
67
68pub fn update<P1: AsRef<Path>, P2: AsRef<Path>>(
69 template_file: P1,
70 destination_file: P2,
71) -> Result<()> {
72 let is_ci = env::var("CI").map(|_| true).unwrap_or(false);
73
74 let template_file = template_file.as_ref();
75
76 let template_dir = template_file
77 .parent()
78 .map(|p| p.to_path_buf())
79 .unwrap_or_else(|| PathBuf::from(""));
80 let mut generated_doc = fs::read_to_string(&template_file)
81 .context(format!(
82 "current working directory: {:?}",
83 env::current_dir()
84 ))
85 .context(format!("failed to read template"))?;
86 process_includes_document(&mut generated_doc, &template_dir)?;
87
88 let file = template_file
89 .components()
90 .map(|c| c.as_os_str().to_string_lossy())
91 .collect::<Vec<_>>()
92 .join("/");
93 let generated_doc = format!(
94 r#"<!--
95Please don't edit. This document has been generated from {file:?}
96-->
97{generated_doc}"#
98 );
99
100 let dest_path = destination_file.as_ref();
101
102 let current_doc = if dest_path.exists() {
103 fs::read_to_string(&dest_path)?
104 } else {
105 "".to_string()
106 };
107
108 if let Some(diff_str) = diff(&generated_doc, ¤t_doc) {
109 if is_ci {
110 bail!(
111 "The markdown document {dest_path:?} is out of sync with {template_file:?}.
112 Please re-run the tests and commit the updated file.
113 This message is generated because the test is run on CI (the CI environment variable is set).\n{diff_str}"
114 );
115 } else {
116 fs::write(&dest_path, generated_doc.as_bytes())?;
117 }
118 }
119
120 Ok(())
121}
122
123fn diff(doc1: &str, doc2: &str) -> Option<String> {
124 if zip(doc1.lines(), doc2.lines()).any(|(l1, l2)| l1.trim() != l2.trim()) {
125 Some(
126 zip(doc1.lines(), doc2.lines())
127 .filter(|(l1, l2)| l1.trim() != l2.trim())
128 .map(|(l1, l2)| format!("> {}\n< {}\n", l1.trim(), l2.trim()))
129 .take(5)
130 .collect::<Vec<_>>()
131 .join(", "),
132 )
133 } else {
134 None
135 }
136}
137
138#[test]
139fn update_readme() {
140 update(
141 &Path::new("src").join("README.tpl.md"),
142 Path::new("README.md"),
143 )
144 .unwrap();
145}