mdbook_plotly/preprocessor/
config.rs1use log::warn;
2use serde::{Deserialize, Serialize, de::DeserializeOwned};
3use toml::{Value, value::Table};
4
5pub const SUPPORTED_MDBOOK_VERSION: &str = "0.5.2";
6pub const PREPROCESSOR_CONFIG_KEY: &str = "preprocessor.plotly";
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(default, rename_all = "kebab-case")]
11pub struct PreprocessorConfig {
12 pub output_type: PlotlyOutputType,
19
20 pub input_type: PlotlyInputType,
26
27 pub map_eval: MapEvalConfig,
30}
31
32impl PreprocessorConfig {
33 pub fn from_toml(value: &Value) -> Self {
34 let Some(table) = value.as_table() else {
35 warn!(
36 "Illegal config format for '{}': expected a table; using default configuration.",
37 PREPROCESSOR_CONFIG_KEY
38 );
39 return Self::default();
40 };
41
42 warn_unknown_keys(
43 PREPROCESSOR_CONFIG_KEY,
44 table,
45 &["output-type", "input-type", "map-eval"],
46 );
47
48 Self {
49 output_type: parse_field(
50 table,
51 "output-type",
52 &format!("{}.output-type", PREPROCESSOR_CONFIG_KEY),
53 Self::default().output_type,
54 ),
55 input_type: parse_field(
56 table,
57 "input-type",
58 &format!("{}.input-type", PREPROCESSOR_CONFIG_KEY),
59 Self::default().input_type,
60 ),
61 map_eval: match table.get("map-eval") {
62 Some(map_eval) => MapEvalConfig::from_toml(map_eval),
63 None => MapEvalConfig::default(),
64 },
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(default, rename_all = "kebab-case")]
71pub struct MapEvalConfig {
72 pub enabled: bool,
73 pub reuse_slab: bool,
74 pub compile_expressions: bool,
75 pub namespace_scope: MapNamespaceScope,
76}
77
78impl MapEvalConfig {
79 pub fn from_toml(value: &Value) -> Self {
80 let Some(table) = value.as_table() else {
81 warn!(
82 "Illegal config format for '{}.map-eval': expected a table; using default configuration.",
83 PREPROCESSOR_CONFIG_KEY
84 );
85 return Self::default();
86 };
87
88 warn_unknown_keys(
89 &format!("{}.map-eval", PREPROCESSOR_CONFIG_KEY),
90 table,
91 &[
92 "enabled",
93 "reuse-slab",
94 "compile-expressions",
95 "namespace-scope",
96 ],
97 );
98
99 Self {
100 enabled: parse_field(
101 table,
102 "enabled",
103 &format!("{}.map-eval.enabled", PREPROCESSOR_CONFIG_KEY),
104 Self::default().enabled,
105 ),
106 reuse_slab: parse_field(
107 table,
108 "reuse-slab",
109 &format!("{}.map-eval.reuse-slab", PREPROCESSOR_CONFIG_KEY),
110 Self::default().reuse_slab,
111 ),
112 compile_expressions: parse_field(
113 table,
114 "compile-expressions",
115 &format!("{}.map-eval.compile-expressions", PREPROCESSOR_CONFIG_KEY),
116 Self::default().compile_expressions,
117 ),
118 namespace_scope: parse_field(
119 table,
120 "namespace-scope",
121 &format!("{}.map-eval.namespace-scope", PREPROCESSOR_CONFIG_KEY),
122 Self::default().namespace_scope,
123 ),
124 }
125 }
126}
127
128fn parse_field<T>(table: &Table, key: &str, path: &str, default: T) -> T
129where
130 T: DeserializeOwned,
131{
132 match table.get(key) {
133 Some(value) => deserialize_value(value).unwrap_or_else(|e| {
134 warn!(
135 "Failed to parse config field '{}': {}; using default value.",
136 path, e
137 );
138 default
139 }),
140 None => default,
141 }
142}
143
144fn deserialize_value<T>(value: &Value) -> Result<T, toml::de::Error>
145where
146 T: DeserializeOwned,
147{
148 #[derive(Deserialize)]
149 struct Wrapper<T> {
150 value: T,
151 }
152
153 toml::from_str::<Wrapper<T>>(&format!("value = {}", value)).map(|wrapper| wrapper.value)
154}
155
156fn warn_unknown_keys(path: &str, table: &Table, known_keys: &[&str]) {
157 for key in table.keys() {
158 if !known_keys.contains(&key.as_str()) {
159 warn!("Unknown config key '{}.{}' will be ignored.", path, key);
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::sync::Once;
168
169 fn init_logger() {
170 static INIT: Once = Once::new();
171 INIT.call_once(|| {
172 let _ = env_logger::builder().is_test(true).try_init();
173 });
174 }
175
176 #[test]
177 fn ignores_unknown_top_level_keys() {
178 init_logger();
179
180 let value = toml::from_str::<Value>(
181 r#"
182 output-type = "plotly-html"
183 input-type = "toml-input"
184 unexpected = 1
185 [map-eval]
186 enabled = false
187 extra = true
188 "#,
189 )
190 .unwrap();
191
192 let config = PreprocessorConfig::from_toml(&value);
193
194 assert_eq!(config.output_type, PlotlyOutputType::PlotlyHtml);
195 assert_eq!(config.input_type, PlotlyInputType::TOMLInput);
196 assert!(!config.map_eval.enabled);
197 }
198
199 #[test]
200 fn falls_back_for_only_the_bad_field() {
201 init_logger();
202
203 let value = toml::from_str::<Value>(
204 r#"
205 output-type = "plotly-html"
206 input-type = 42
207 [map-eval]
208 enabled = false
209 reuse-slab = false
210 compile-expressions = false
211 namespace-scope = "exports-only"
212 "#,
213 )
214 .unwrap();
215
216 let config = PreprocessorConfig::from_toml(&value);
217
218 assert_eq!(config.output_type, PlotlyOutputType::PlotlyHtml);
219 assert_eq!(config.input_type, PlotlyInputType::JSONInput);
220 assert_eq!(
221 config.map_eval.namespace_scope,
222 MapNamespaceScope::ExportsOnly
223 );
224 assert!(!config.map_eval.enabled);
225 assert!(!config.map_eval.reuse_slab);
226 assert!(!config.map_eval.compile_expressions);
227 }
228
229 #[test]
230 fn falls_back_for_only_the_nested_bad_field() {
231 init_logger();
232
233 let value = toml::from_str::<Value>(
234 r#"
235 output-type = "plotly-html"
236 input-type = "toml-input"
237 [map-eval]
238 enabled = "nope"
239 reuse-slab = false
240 compile-expressions = false
241 namespace-scope = "exports-only"
242 "#,
243 )
244 .unwrap();
245
246 let config = PreprocessorConfig::from_toml(&value);
247
248 assert_eq!(config.output_type, PlotlyOutputType::PlotlyHtml);
249 assert_eq!(config.input_type, PlotlyInputType::TOMLInput);
250 assert!(config.map_eval.enabled);
251 assert!(!config.map_eval.reuse_slab);
252 assert!(!config.map_eval.compile_expressions);
253 assert_eq!(
254 config.map_eval.namespace_scope,
255 MapNamespaceScope::ExportsOnly
256 );
257 }
258}
259
260impl Default for MapEvalConfig {
261 fn default() -> Self {
262 Self {
263 enabled: true,
264 reuse_slab: true,
265 compile_expressions: true,
266 namespace_scope: MapNamespaceScope::FullMap,
267 }
268 }
269}
270
271#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
272#[serde(rename_all = "kebab-case")]
273pub enum MapNamespaceScope {
274 #[default]
275 #[serde(rename = "full-map")]
276 FullMap,
277 #[serde(rename = "exports-only")]
278 ExportsOnly,
279}
280
281#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
283pub enum PlotlyOutputType {
284 #[default]
286 #[cfg(feature = "plotly-html-handler")]
287 #[serde(rename = "plotly-html")]
288 PlotlyHtml,
289
290 #[cfg(feature = "plotly-svg-handler")]
292 #[serde(rename = "plotly-svg")]
293 PlotlySvg,
294}
295
296#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
298pub enum PlotlyInputType {
299 #[default]
302 #[serde(rename = "json-input")]
303 JSONInput,
304
305 #[serde(rename = "toml-input")]
307 TOMLInput,
308}