1use std::fs::{self, File};
2
3use anyhow::{anyhow, Context as _};
4use indexmap::IndexMap;
5use mdbook::config::HtmlConfig;
6use once_cell::sync::Lazy;
7use serde::{Deserialize, Serialize};
8
9mod book;
10use book::Book;
11
12mod css;
13mod html;
14mod latex;
15mod pandoc;
16
17mod preprocess;
18use preprocess::Preprocessor;
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22struct Config {
23 #[serde(rename = "profile", default = "Default::default")]
24 pub profiles: IndexMap<String, pandoc::Profile>,
25 #[serde(default = "defaults::enabled")]
26 pub keep_preprocessed: bool,
27 pub hosted_html: Option<String>,
28 #[serde(default = "Default::default")]
30 pub code: CodeConfig,
31 #[serde(default = "Default::default")]
33 pub disabled: bool,
34 #[serde(default = "Default::default")]
36 pub markdown: MarkdownConfig,
37}
38
39#[derive(Clone, Debug, Default, Serialize, Deserialize)]
41#[serde(rename_all = "kebab-case")]
42struct MarkdownConfig {
43 pub extensions: MarkdownExtensionConfig,
45}
46
47#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
49#[serde(rename_all = "kebab-case")]
50struct MarkdownExtensionConfig {
51 #[serde(default = "defaults::disabled")]
53 pub gfm: bool,
54 #[serde(default = "defaults::disabled")]
56 pub math: bool,
57 #[serde(default = "defaults::disabled")]
59 pub definition_lists: bool,
60 #[serde(default = "defaults::disabled")]
62 pub superscript: bool,
63 #[serde(default = "defaults::disabled")]
65 pub subscript: bool,
66}
67
68#[derive(Clone, Debug, Default, Serialize, Deserialize)]
70#[serde(rename_all = "kebab-case")]
71struct CodeConfig {
72 pub show_hidden_lines: bool,
73}
74
75mod defaults {
76 pub fn enabled() -> bool {
77 true
78 }
79
80 pub fn disabled() -> bool {
81 false
82 }
83}
84
85#[derive(Default)]
87pub struct Renderer {
88 logfile: Option<File>,
89}
90
91impl Renderer {
92 pub fn new() -> Self {
93 Self { logfile: None }
94 }
95
96 const NAME: &'static str = "pandoc";
97 const CONFIG_KEY: &'static str = "output.pandoc";
98}
99
100impl mdbook::Renderer for Renderer {
101 fn name(&self) -> &str {
102 Self::NAME
103 }
104
105 fn render(&self, ctx: &mdbook::renderer::RenderContext) -> anyhow::Result<()> {
106 static MDBOOK_VERSION_REQ: Lazy<semver::VersionReq> = Lazy::new(|| {
109 let compiled_mdbook_version = semver::Version::parse(mdbook::MDBOOK_VERSION).unwrap();
110 semver::VersionReq {
111 comparators: vec![semver::Comparator {
112 op: semver::Op::Caret,
113 major: compiled_mdbook_version.major,
114 minor: Some(compiled_mdbook_version.minor),
115 patch: None,
116 pre: Default::default(),
117 }],
118 }
119 });
120 let mdbook_server_version = semver::Version::parse(&ctx.version).unwrap();
121 if !MDBOOK_VERSION_REQ.matches(&mdbook_server_version) {
122 log::warn!(
123 "{} is semver-incompatible with mdbook {} (requires {})",
124 env!("CARGO_PKG_NAME"),
125 mdbook_server_version,
126 *MDBOOK_VERSION_REQ,
127 );
128 }
129
130 let cfg: Config = ctx
131 .config
132 .get_deserialized_opt(Self::CONFIG_KEY)
133 .with_context(|| format!("Unable to deserialize {}", Self::CONFIG_KEY))?
134 .ok_or(anyhow!("No {} table found", Self::CONFIG_KEY))?;
135
136 if cfg.disabled {
137 log::info!("Skipping rendering since `disabled` is set");
138 return Ok(());
139 }
140
141 pandoc::check_compatibility()?;
142
143 let html_cfg: Option<HtmlConfig> = ctx
144 .config
145 .get_deserialized_opt("output.html")
146 .unwrap_or_default();
147
148 let book = Book::new(ctx)?;
149
150 let stylesheets;
151 let mut css = css::Css::default();
152 if let Some(cfg) = &html_cfg {
153 stylesheets = css::read_stylesheets(cfg, &book).collect::<Vec<_>>();
154 for (stylesheet, stylesheet_css) in &stylesheets {
155 css.load(stylesheet, stylesheet_css);
156 }
157 }
158
159 for (name, profile) in cfg.profiles {
160 let ctx = pandoc::RenderContext {
161 book: &book,
162 mdbook_cfg: &ctx.config,
163 destination: book.destination.join(name),
164 output: profile.output_format(),
165 columns: profile.columns,
166 cur_list_depth: 0,
167 max_list_depth: 0,
168 code: &cfg.code,
169 html: html_cfg.as_ref(),
170 css: &css,
171 };
172
173 let mut preprocessor = Preprocessor::new(ctx, &cfg.markdown)?;
175
176 if let Some(uri) = cfg.hosted_html.as_deref() {
177 preprocessor.hosted_html(uri);
178 }
179
180 if let Some(redirects) = html_cfg.as_ref().map(|cfg| &cfg.redirect) {
181 if !redirects.is_empty() {
182 log::debug!("Processing redirects in [output.html.redirect]");
183 let redirects = redirects
184 .iter()
185 .map(|(src, dst)| (src.as_str(), dst.as_str()));
186 #[cfg(test)]
188 let redirects = redirects
189 .collect::<std::collections::BTreeMap<_, _>>()
190 .into_iter();
191 preprocessor.add_redirects(redirects);
192 }
193 }
194
195 let mut preprocessed = preprocessor.preprocess();
196
197 let mut renderer = pandoc::Renderer::new();
199
200 renderer.current_dir(&book.root);
202 for input in &mut preprocessed {
203 renderer.input(input?);
204 }
205
206 if preprocessed.unresolved_links() {
207 log::warn!(
208 "Unable to resolve one or more relative links within the book, \
209 consider setting the `hosted-html` option in `[output.pandoc]`"
210 );
211 }
212
213 if let Some(logfile) = &self.logfile {
214 renderer.stderr(logfile.try_clone()?);
215 }
216
217 renderer.render(profile, preprocessed.render_context())?;
219
220 if !cfg.keep_preprocessed {
221 fs::remove_dir_all(preprocessed.output_dir())?;
222 }
223 }
224
225 Ok(())
226 }
227}
228
229#[cfg(test)]
230mod tests;