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#[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 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 pub fn variant(filename: &str) -> Self {
38 CSSFILES.iter().find(|&(_, f)| &filename == f).unwrap().0
39 }
40}
41
42#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct Item<'a>(&'a str);
46
47impl 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#[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#[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
98impl<'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
109impl Ready<'_> {
111 #[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 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 #[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 pub fn get(&self) -> &str {
188 &self.0
189 }
190
191 pub fn get_mut(&mut self) -> &mut String {
193 &mut self.0
194 }
195
196 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 Ok(())
208 }
209
210 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 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 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 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 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, 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 pub fn process(self) -> Self {
321 self.cssfile().content().write_theme_file()
322 }
323
324 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 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 #[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 _ => (), }
354 }
355
356 fn ready(mut self, cssfile: CssFile) -> Self {
358 self.cssfile = cssfile;
359 self.ready = Ready::get_defualt(cssfile);
360 self.process()
361 }
362
363 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 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 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
391impl Theme<'_> {
393 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 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 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 item_ = ":root- font-size";
425 }
426 self.content.fore_arg(item_, value.get());
427 }
428 }
429
430 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}