mdbook_theme/
ace.rs

1use super::{Error, Result};
2use std::io::Read;
3use std::path::PathBuf;
4
5use serde_derive::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize)]
8#[serde(default, rename_all = "kebab-case")]
9pub struct Ace {
10    pub theme_white: String,
11    pub theme_dark: String,
12    pub below_build_dir: bool,
13    pub build_dir: PathBuf,   // generally `full-path/book`
14    pub theme_dir: PathBuf,   // generally `theme`
15    pub destination: PathBuf, // generally `full-path/book/theme-ace`
16}
17
18impl Ace {
19    /// default: ace/theme/*.css in this crate;
20    /// user's: ./theme/ace.css in user's book.
21    /// First matched `.(*) ` is considered to be a `cssClass` .
22    ///
23    /// If a user both set the config in `book.toml` and have `ace-*.css` file,
24    /// the config will be ignored.
25    pub fn css_class_text(&self, dark: bool) -> Result<(String, String)> {
26        let mut css_text = String::new();
27        let ace_file = format!("ace-{}.css", if dark { "dark" } else { "white" });
28        let path = self.theme_dir.join(ace_file);
29
30        if path.exists() || self.theme_dir.join("ace.css").exists() {
31            std::fs::File::open(path)
32                .unwrap()
33                .read_to_string(&mut css_text)
34                .unwrap();
35        } else if let Some(v) = self.defult_css(dark) {
36            css_text = String::from(unsafe { std::str::from_utf8_unchecked(v) });
37        } else {
38            return Err(Error::AceNotFound);
39        }
40
41        css_text = css_text.replace(['\n', '"'], " ");
42        let p1 = css_text.find(".ace-").ok_or(Error::StrNotFound)?;
43        let css_class =
44            css_text[p1 + 1..p1 + css_text[p1..].find(' ').ok_or(Error::StrNotFound)?].to_string();
45        Ok((css_class, css_text))
46    }
47
48    /// get the defult css bytes matched with the user's config and a local css not found
49    #[rustfmt::skip]
50    pub fn defult_css(&self, dark: bool) -> Option<&[u8]> {
51        ACE_DEFAULT.iter()
52                   .find(|&(path, _)| {
53                       if dark { path.strip_suffix(".css").unwrap().ends_with(&self.theme_dark) }
54                       else { path.strip_suffix(".css").unwrap().ends_with(&self.theme_white) }
55                   })
56                   .map(|&(_, bytes)| bytes)
57    }
58
59    /// get the target content to be written
60    pub fn write(&self, css_: (String, String), dark: bool) -> Result<()> {
61        let file = if dark {
62            "theme-tomorrow_night.js"
63        } else {
64            "theme-dawn.js"
65        };
66        let path = &self.build_dir.join("html").join(file);
67
68        let (css_class, css_text) = css_;
69        let mut content = String::new();
70        std::fs::File::open(path)
71            .unwrap()
72            .read_to_string(&mut content)
73            .unwrap();
74        content.replace_range(find(&content, "cssClass=\"")?, &css_class);
75        content.replace_range(find(&content, "cssText=\"")?, &css_text);
76
77        std::fs::write(path, content).map_err(|_| Error::FileNotWritten)?;
78        Ok(())
79    }
80
81    /// organize the workflow
82    pub fn run(self) -> Result<()> {
83        for dark in [true, false] {
84            self.write(self.css_class_text(dark)?, dark)?;
85        }
86        self.below_build_dir()?;
87        self.remove_destination();
88        Ok(())
89    }
90
91    /// move `book/html` to `book/`
92    pub fn below_build_dir(&self) -> Result<()> {
93        if self.below_build_dir {
94            use mdbook::utils::fs::copy_files_except_ext as copy;
95            let html = self.build_dir.join("html");
96            copy(&html, &self.build_dir, true, None, &[]).map_err(|_| Error::DirNotCreated)?;
97            std::fs::remove_dir_all(html).map_err(|_| Error::DirNotRemoved)?;
98        }
99        Ok(())
100    }
101
102    /// Remove `book/theme-ace`: if it's not empty, it'll not be removed.
103    /// But for now, it should be empty.
104    fn remove_destination(&self) {
105        std::fs::remove_dir(&self.destination).unwrap_or_default();
106    }
107}
108
109impl Default for Ace {
110    fn default() -> Self {
111        Self {
112            theme_white: String::from(""),
113            theme_dark: String::from(""),
114            build_dir: PathBuf::from(""),
115            theme_dir: PathBuf::from(""),
116            destination: PathBuf::from(""),
117            below_build_dir: true,
118        }
119    }
120}
121
122/// find the positions of double quotation marks behind cssClass or cssText
123/// target: "cssClass=\"" | "cssText=\""
124fn find(content: &str, target: &str) -> Result<std::ops::Range<usize>> {
125    let p1 = content.find(target).ok_or(Error::StrNotFound)? + target.len();
126    let p2 = p1 + content[p1..].find('"').ok_or(Error::StrNotFound)?;
127    Ok(p1..p2)
128}
129
130default! {
131    "./ace/theme/ambiance.css",                AMBIANCE;
132    "./ace/theme/chaos.css",                   CHAOS;
133    "./ace/theme/chrome.css",                  CHROME;
134    "./ace/theme/clouds.css",                  CLOUDS;
135    "./ace/theme/clouds_midnight.css",         CLOUDS_MIDNIGHT;
136    "./ace/theme/cobalt.css",                  COBALT;
137    "./ace/theme/crimson_editor.css",          CRIMSON_EDITOR;
138    "./ace/theme/dawn.css",                    DAWN;
139    "./ace/theme/dracula.css",                 DRACULA;
140    "./ace/theme/dreamweaver.css",             DREAMWEAVER;
141    "./ace/theme/eclipse.css",                 ECLIPSE;
142    "./ace/theme/github.css",                  GITHUB;
143    "./ace/theme/gob.css",                     GOB;
144    "./ace/theme/gruvbox.css",                 GRUVBOX;
145    "./ace/theme/idle_fingers.css",            IDLE_FINGERS;
146    "./ace/theme/iplastic.css",                IPLASTIC;
147    "./ace/theme/katzenmilch.css",             KATZENMILCH;
148    "./ace/theme/kr_theme.css",                KR_THEME;
149    "./ace/theme/kuroir.css",                  KUROIR;
150    "./ace/theme/merbivore.css",               MERBIVORE;
151    "./ace/theme/merbivore_soft.css",          MERBIVORE_SOFT;
152    "./ace/theme/mono_industrial.css",         MONO_INDUSTRIAL;
153    "./ace/theme/monokai.css",                 MONOKAI;
154    "./ace/theme/nord_dark.css",               NORD_DARK;
155    "./ace/theme/one_dark.css",                ONE_DARK;
156    "./ace/theme/pastel_on_dark.css",          PASTEL_ON_DARK;
157    "./ace/theme/solarized_dark.css",          SOLARIZED_DARK;
158    "./ace/theme/solarized_light.css",         SOLARIZED_LIGHT;
159    "./ace/theme/sqlserver.css",               SQLSERVER;
160    "./ace/theme/terminal.css",                TERMINAL;
161    "./ace/theme/textmate.css",                TEXTMATE;
162    "./ace/theme/tomorrow.css",                TOMORROW;
163    "./ace/theme/tomorrow_night_blue.css",     TOMORROW_NIGHT_BLUE;
164    "./ace/theme/tomorrow_night_bright.css",   TOMORROW_NIGHT_BRIGHT;
165    "./ace/theme/tomorrow_night.css",          TOMORROW_NIGHT;
166    "./ace/theme/tomorrow_night_eighties.css", TOMORROW_NIGHT_EIGHTIES;
167    "./ace/theme/twilight.css",                TWILIGHT;
168    "./ace/theme/vibrant_ink.css",             VIBRANT_INK;
169    "./ace/theme/xcode.css",                   XCODE
170}