mdbook_theme/theme/
mod.rs

1use crate::{Error, Result};
2use default::*;
3use std::borrow::Borrow;
4use std::fmt;
5use std::hash::Hash;
6use std::iter::FromIterator;
7use std::path::{Path, PathBuf};
8
9pub mod config;
10pub mod default;
11
12/// All cssfiles to be modified.
13/// There are several aspects of configs:
14/// 1. pagetoc related
15/// 2. fontsize related
16/// 3. color related
17/// 
18/// but in practice all configs are processed in unit of single file.
19#[rustfmt::skip]
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub enum CssFile {
22    Variables, General, Chrome, Index, PagetocJs, PagetocCss,
23    Invalid, Pagetoc, Custom(&'static str)
24}
25
26impl CssFile {
27    /// get filename according to `CssFile` type
28    pub fn filename(&self) -> &'static str {
29        if let CssFile::Custom(filename) = self {
30            filename
31        } else {
32            CSSFILES.iter().find(|&(css, _)| *css == *self).unwrap().1
33        }
34    }
35
36    /// get `CssFile` variant according to filename
37    pub fn variant(filename: &str) -> Self {
38        CSSFILES.iter().find(|&(_, f)| &filename == f).unwrap().0
39    }
40}
41
42/// 1. supported items (config args)
43/// 2. item of `preprocessor.theme-pre` table in book.toml
44#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct Item<'a>(&'a str);
46
47/// useful when looking up in `HashMap<&Item, _>` just via `HashMap<&str, _>`
48impl Borrow<str> for Item<'_> {
49    fn borrow(&self) -> &str {
50        self.0
51    }
52}
53
54impl Item<'_> {
55    pub fn get(&self) -> &str {
56        self.0
57    }
58}
59
60impl fmt::Debug for Item<'_> {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        self.0.fmt(f)
63    }
64}
65
66/// by default or specified by a user
67#[derive(Clone, Copy, PartialEq, Eq, Hash)]
68pub struct Value<'a>(&'a str);
69
70impl Value<'_> {
71    pub fn get(&self) -> &str {
72        self.0
73    }
74}
75
76impl fmt::Debug for Value<'_> {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        self.0.fmt(f)
79    }
80}
81
82/// configs ready to go
83#[derive(Clone, Default)]
84pub struct Ready<'a>(Vec<(Item<'a>, Value<'a>)>);
85
86impl fmt::Display for Ready<'_> {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        f.debug_list().entries(self.0.iter()).finish()
89    }
90}
91
92impl fmt::Debug for Ready<'_> {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{}", self.0.len())
95    }
96}
97
98/// get `Ready` by using `iter.collect()`
99impl<'a> FromIterator<(Item<'a>, Value<'a>)> for Ready<'a> {
100    fn from_iter<I: IntoIterator<Item = (Item<'a>, Value<'a>)>>(iter: I) -> Self {
101        let mut r = Self::default();
102        for i in iter {
103            r.0.push(i);
104        }
105        r
106    }
107}
108
109/// yield default config or merge configs
110impl Ready<'_> {
111    /// To get a default config from a specific cssfile, which need modifying.
112    /// See [`DEFAULT`] to check detailed configs.
113    ///
114    /// [`DEFAULT`]: ./default/static.DEFAULT.html
115    #[rustfmt::skip]
116    pub fn get_defualt(css: CssFile) -> Self {
117        match css {
118            c @ CssFile::Variables => Ready::from(c),
119            c @ CssFile::General   => Ready::from(c),
120            c @ CssFile::Chrome    => Ready::from(c),
121            _                      => Self::default(),
122        }
123    }
124
125    /// help to simplify `.get()`
126    fn from(css: CssFile) -> Self {
127        DEFAULT
128            .iter()
129            .filter(|(c, _, _)| *c == css)
130            .map(|(_, i, v)| (*i, *v))
131            .collect()
132    }
133
134    pub fn item_value(&self) -> &Vec<(Item, Value)> {
135        &self.0
136    }
137}
138
139#[derive(Clone, PartialEq, Default)]
140pub struct Content(String);
141
142impl fmt::Display for Content {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        self.0.fmt(f)
145    }
146}
147
148impl fmt::Debug for Content {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        write!(f, "Content(...)")
151    }
152}
153
154impl Content {
155    /// All contents that are to modify or directly use.
156    #[rustfmt::skip]
157    pub fn from(cssfile: CssFile, dir: &Path) -> Self {
158        use mdbook::theme::*;
159        match cssfile {
160            CssFile::Custom(f)  => Content::from_file(dir, f),
161            CssFile::Variables  => Content::from_static(VARIABLES_CSS),
162            CssFile::Index      => Content::from_static(INDEX),
163            CssFile::PagetocJs  => Content::from_static(PAGETOCJS),
164            CssFile::PagetocCss => Content::from_static(PAGETOCCSS),
165            CssFile::Chrome     => Content::from_static(CHROME_CSS),
166            CssFile::General    => Content::from_static(GENERAL_CSS),
167            _                   => Content::default(),
168        }
169    }
170
171    fn from_static(v: &[u8]) -> Self {
172        Content(String::from(unsafe { std::str::from_utf8_unchecked(v) }))
173    }
174
175    fn from_file(dir: &Path, filename: &str) -> Self {
176        use std::fs::File;
177        use std::io::Read;
178        let mut s = String::new();
179        File::open(dir.join(filename))
180            .unwrap()
181            .read_to_string(&mut s)
182            .unwrap();
183        Content(s)
184    }
185
186    /// for viewing the content
187    pub fn get(&self) -> &str {
188        &self.0
189    }
190
191    /// for modifying the content
192    pub fn get_mut(&mut self) -> &mut String {
193        &mut self.0
194    }
195
196    /// Update the content: directly relapce a value.
197    /// Useful when the item is exact or replace the first one.
198    ///
199    /// Hypothesis: `item: value;` .
200    /// Better to use [`regex`](https://docs.rs/regex/*/regex/), but for now I'm not ready :(
201    fn replace(&mut self, item: &str, value: &str) -> Result<()> {
202        let text = self.get();
203        let p1 = text.find(item).ok_or(Error::StrNotFound)? + item.len() + 2;
204        let p2 = p1 + text[p1..].find(';').ok_or(Error::StrNotFound)?;
205        self.get_mut().replace_range(p1..p2, value);
206        // eprintln!("\n{}", &self.get()[p1 - 20..p2 + 10]);
207        Ok(())
208    }
209
210    /// update the content with information of context:
211    /// it's common to see homonymous args items in different context,
212    /// so this function takes an additional foregoing hint (need two locations).
213    fn fore_replace(&mut self, fore: &str, item: &str, value: &str) -> Result<()> {
214        let text = self.get();
215        let pfore = text.find(fore).ok_or(Error::StrNotFound)?;
216        let p1 = text[pfore..].find(item).ok_or(Error::StrNotFound)? + pfore + item.len() + 2;
217        let p2 = p1 + text[p1..].find(';').ok_or(Error::StrNotFound)?;
218        self.get_mut().replace_range(p1..p2, value);
219        Ok(())
220    }
221
222    /// Insert content, and need two str to find.
223    /// The first is to find backwards;
224    /// the second is to locate the inserted space right one char ahead.
225    fn insert(&mut self, insert: &str, find1: &str, find2: &str) -> Result<()> {
226        let text = self.get();
227        let mut pos = text.find(find1).ok_or(Error::StrNotFound)?;
228        pos = pos + text[pos..].find(find2).ok_or(Error::StrNotFound)? - 1;
229        self.get_mut().replace_range(pos..pos + 1, insert);
230        Ok(())
231    }
232
233    /// content processing in `variables.css`
234    fn variables(&mut self, item: &str, value: &str) {
235        if item == "mobile-content-max-width" {
236            let media_on_screen = "@media only screen and (max-width:1439px)";
237            if self.get().contains(media_on_screen) {
238                return;
239            }
240            let content = format!(
241                "\n{media_on_screen} {{
242 :root{{
243    --content-max-width: {};
244  }}
245}}\n\n",
246                value
247            );
248            self.insert(&content, "}", "/* Themes */").unwrap();
249        } else if item.starts_with("light")
250            | item.starts_with("ayu")
251            | item.starts_with("rust")
252            | item.starts_with("navy")
253            | item.starts_with("coal")
254        {
255            self.fore_arg(item, value);
256        } else if self.replace(item, value).is_err() {
257            self.insert(&format!("\n    --{}: {};\n", item, value), ":root", "}\n")
258                .unwrap();
259        }
260    }
261
262    /// deal with the config named `fore-arg: value;`
263    fn fore_arg(&mut self, item: &str, value: &str) {
264        for n in 2..item.split('-').count() + 1 {
265            for d in [true, false] {
266                for j in [" ", "-"] {
267                    let (fore, arg) = Content::fore_check(item, n, d, j);
268                    if self.fore_replace(&fore, arg, value).is_ok() {
269                        return;
270                    }
271                }
272            }
273        }
274    }
275
276    /// parse `fore-arg`:
277    /// `fore` may have multiple meaning, and it's complex:
278    /// 1. one word begins with/without `.` , or even `:` : `.content` | `body` | `:root`
279    /// 2. one word will very likely join more words with ` ` or `-`:
280    ///    `.content main` | `.nav-chapters`
281    fn fore_check<'a>(item: &'a str, n: usize, dot: bool, joint: &'a str) -> (String, &'a str) {
282        let v: Vec<&str> = item.splitn(n, '-').collect();
283        let d = if dot { "." } else { "" };
284        let fore = format!("\n{}{} {{", d, v[..n - 1].join(joint));
285        (fore, v[n - 1])
286    }
287}
288
289#[derive(Debug, Clone)]
290pub struct Theme<'a> {
291    pub cssfile: CssFile,
292    pub content: Content, // ultimate str to be processed
293    content_cmp: Content,
294    pub ready: Ready<'a>,
295    pub dir: PathBuf,
296    path: PathBuf,
297}
298
299impl Default for Theme<'_> {
300    fn default() -> Self {
301        Self {
302            cssfile: CssFile::Custom(""),
303            content: Content::default(),
304            ready: Ready::default(),
305            dir: PathBuf::new(),
306            content_cmp: Content::default(),
307            path: PathBuf::new(),
308        }
309    }
310}
311
312impl<'a> Theme<'a> {
313    #[rustfmt::skip]
314    pub fn from(cssfile: CssFile, ready: Ready<'a>, dir: PathBuf) -> Self {
315        Self { cssfile, ready, content: Content::default(), path: PathBuf::new(),
316        dir, content_cmp: Content::default() }
317    }
318
319    /// canonical procedure
320    pub fn process(self) -> Self {
321        self.cssfile().content().write_theme_file()
322    }
323
324    /// Give a default or custom virtual css file marked to help content processing.
325    fn cssfile(mut self) -> Self {
326        let filename = self.cssfile.filename();
327        self.path = self.dir.join(filename);
328        if self.path.exists() {
329            self.cssfile = CssFile::Custom(filename);
330        }
331        self
332    }
333
334    /// The **ultimate** content to be written into `theme` dir.
335    /// An empty content means not having processed the content.
336    fn content(mut self) -> Self {
337        self.content = Content::from(self.cssfile, &self.dir);
338        self.content_cmp = self.content.clone();
339        self.content_process(None);
340        self
341    }
342
343    /// process contents of different files
344    #[rustfmt::skip]
345    fn content_process(&mut self, filename: Option<&str>) {
346        match filename.map_or_else(|| self.cssfile, CssFile::variant) {
347            CssFile::Custom(f) => self.content_process(Some(f)),
348            CssFile::Variables => self.process_variables(),
349            CssFile::General   => self.process_general(),
350            CssFile::Chrome    => self.process_chrome(),
351            CssFile::Index     => self.process_index(),
352            _ => (), // skip content processing
353        }
354    }
355
356    /// Swich to another cssfile and process its content, which can repeat.
357    fn ready(mut self, cssfile: CssFile) -> Self {
358        self.cssfile = cssfile;
359        self.ready = Ready::get_defualt(cssfile);
360        self.process()
361    }
362
363    /// When `pagetoc = true` , a bunch of files need to change; if NOT true, don't call this.
364    fn pagetoc(self) {
365        self.ready(CssFile::Variables)
366            .ready(CssFile::Index)
367            .ready(CssFile::PagetocJs)
368            .ready(CssFile::PagetocCss)
369            .ready(CssFile::General)
370            .ready(CssFile::Chrome);
371    }
372
373    /// create a css file on demand
374    fn write_theme_file(self) -> Self {
375        if self.content != self.content_cmp
376            || ((self.cssfile == CssFile::PagetocJs || self.cssfile == CssFile::PagetocCss)
377                && !self.path.exists())
378        {
379            std::fs::write(&self.path, self.content.get().as_bytes()).unwrap();
380        }
381        self
382    }
383
384    /// create the dirs on demand
385    pub(self) fn create_theme_dirs(dir: PathBuf) -> Result<()> {
386        std::fs::create_dir_all(dir.join("css")).map_err(|_| Error::DirNotCreated)?;
387        Ok(())
388    }
389}
390
391/// content processing
392impl Theme<'_> {
393    /// update content in `variables.css`
394    fn process_variables(&mut self) {
395        for (item, value) in self.ready.item_value() {
396            self.content.variables(item.get(), value.get());
397        }
398    }
399
400    /// update content in `index.hbs`, if and only if `pagetoc = true` for now
401    fn process_index(&mut self) {
402        let comment = "<!-- Page table of contents -->";
403        if self.content.get().contains(comment) {
404            return;
405        }
406        let insert = format!(
407            r#" {comment}
408                        <div class="sidetoc"><nav class="pagetoc"></nav></div>
409
410                        "#
411        );
412        self.content
413            .insert(&insert, "<main>", "{{{ content }}}")
414            .unwrap();
415    }
416
417    /// update content in `css/general.css`
418    fn process_general(&mut self) {
419        for (item, value) in self.ready.item_value() {
420            let mut item_ = item.get();
421            if item_ == "root-font-size" {
422                // This case is annoying:
423                // field starts with `:` and value mixes with a comment
424                item_ = ":root-    font-size";
425            }
426            self.content.fore_arg(item_, value.get());
427        }
428    }
429
430    /// update content in `css/chrome.css`
431    fn process_chrome(&mut self) {
432        for (item, value) in self.ready.item_value() {
433            self.content.fore_arg(item.get(), value.get());
434        }
435    }
436}