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