Skip to main content

mdbook_plotly/preprocessor/
config.rs

1use 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/// NOTE: These configurations are printed as kebab-case names. Please pay attention when using.
9#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(default, rename_all = "kebab-case")]
11pub struct PreprocessorConfig {
12    /// About the output form of the chart.
13    /// This output format may affect the presentation of the chart.
14    ///
15    /// In addition, in most cases, the different output forms can significantly affect the time at which the book is compiled.
16    ///
17    /// Other: The inner is an enumeration.
18    pub output_type: PlotlyOutputType,
19
20    /// About the input form of the chart.
21    ///
22    /// Charts are usually in the form of code in a markdown document. At the time of input, we allow the code to be presented in different forms.
23    ///
24    /// The two forms we consider for adoption are: a general script and a configuration file organized in a specific form. In theory, you can read and operate files directly from the current path by turning on some of the functions that come with MDBook.
25    pub input_type: PlotlyInputType,
26
27    /// Controls map expression evaluation behavior such as namespace visibility
28    /// and whether fasteval optimizations should be enabled.
29    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
164impl Default for MapEvalConfig {
165    fn default() -> Self {
166        Self {
167            enabled: true,
168            reuse_slab: true,
169            compile_expressions: true,
170            namespace_scope: MapNamespaceScope::FullMap,
171        }
172    }
173}
174
175#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
176#[serde(rename_all = "kebab-case")]
177pub enum MapNamespaceScope {
178    #[default]
179    #[serde(rename = "full-map")]
180    FullMap,
181    #[serde(rename = "exports-only")]
182    ExportsOnly,
183}
184
185/// NOTE: These configurations are printed as kebab-case names. Please pay attention when using.
186#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
187pub enum PlotlyOutputType {
188    /// After the code is executed, it is compiled into an `<div>` for display.
189    #[default]
190    #[cfg(feature = "plotly-html-handler")]
191    #[serde(rename = "plotly-html")]
192    PlotlyHtml,
193
194    /// After the code is executed, it is compiled into an SVG for display.
195    #[cfg(feature = "plotly-svg-handler")]
196    #[serde(rename = "plotly-svg")]
197    PlotlySvg,
198}
199
200/// NOTE: These configurations are printed as kebab-case names. Please pay attention when using.
201#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
202pub enum PlotlyInputType {
203    /// Translates the Json format into an actual plotly object.
204    /// NOTE: In the `PlotlyOutputType = PlotlySvg` state, this method may cause some performance loss due to multiple packaging.
205    #[default]
206    #[serde(rename = "json-input")]
207    JSONInput,
208
209    /// Translates the TOML format into JSON value first, then reuses the existing plot parser.
210    #[serde(rename = "toml-input")]
211    TOMLInput,
212}