mdbook_tocjs/
lib.rs

1use mdbook::book::Book;
2use mdbook::errors::{Error, Result};
3use mdbook::preprocess::{Preprocessor, PreprocessorContext};
4
5use std::path::{Path, PathBuf};
6use std::fs::{self, File};
7use std::io::prelude::*;
8
9mod blobs;
10use blobs::*;
11
12
13pub struct Config {
14  save_dir: PathBuf,
15  block_marker_id: String,
16  wing_marker_id: String,
17  theme_dir: PathBuf,
18  base_url: String,
19}
20
21static DEFAULT_SAVE_DIR: &str = "lib";
22static DEFAULT_BLOCK_MARKER_ID: &str = "tock";
23static DEFAULT_WING_MARKER_ID: &str = "tocw";
24static DEFAULT_THEME_DIR: &str = "theme";
25static DEFAULT_BASE_URL: &str = "/";
26
27
28fn get_value_to_str(cfg: &toml::map::Map<String, toml::value::Value>, key: &str) -> Option<Result<String>> {
29  if let Some(x) = cfg.get(key) {
30    let res = if let Some(x) = x.as_str() {
31      Ok(x.to_string())
32    } else {
33      Err(Error::msg(format!("{key} {x:?} is not a valid string")))
34    };
35    Some(res)
36  } else {
37    None
38  }
39}
40
41
42impl Config {
43  fn new(preprocessor_name: &str, ctx: &PreprocessorContext) -> Result<Self> {
44
45    let mut config = Self {
46      save_dir: ctx.config.book.src.join(DEFAULT_SAVE_DIR),
47      block_marker_id: String::from(DEFAULT_BLOCK_MARKER_ID),
48      wing_marker_id: String::from(DEFAULT_WING_MARKER_ID),
49      theme_dir: Path::new(DEFAULT_THEME_DIR).to_path_buf(), // This one is not under /src
50      base_url: String::from(DEFAULT_BASE_URL)
51    };
52
53    let Some(cfg) = ctx.config.get_preprocessor(preprocessor_name) else {
54      return Ok(config)
55    };
56
57    if let Some(x) = get_value_to_str(cfg, "save_dir") {
58      config.save_dir = ctx.config.book.src.join(x?.as_str());
59    }
60    if let Some(x) = get_value_to_str(cfg, "block_marker_id") {
61      config.block_marker_id = x?;
62    }
63    if let Some(x) = get_value_to_str(cfg, "wing_marker_id") {
64      config.wing_marker_id = x?;
65    }
66    if let Some(x) = get_value_to_str(cfg, "theme_dir") {
67      config.theme_dir = Path::new(x?.as_str()).to_path_buf(); // This one is not under /src
68    }
69    if let Some(x) = get_value_to_str(cfg, "base_url") {
70      config.base_url = x?;
71    }
72
73    if !config.save_dir.exists() {
74      fs::create_dir(config.save_dir.as_path())?;
75    }
76    if !config.theme_dir.exists() {
77      fs::create_dir(config.theme_dir.as_path())?;
78    }
79
80    Ok(config)
81  }
82
83  fn format_js(&self, literal: &str) -> String {
84    format!(r#"
85      {}
86      window.addEventListener("load", (e)=>{{
87        const toc_maker = new TocMaker();
88        toc_maker.build_block("{}");
89        toc_maker.build_wing("{}");
90      }});
91      "#,
92      literal,
93      self.block_marker_id,
94      self.wing_marker_id
95    )
96  }
97}
98
99
100
101pub struct TocJsMaker;
102
103impl TocJsMaker {
104  pub fn new() -> Self { Self }
105}
106
107
108impl Preprocessor for TocJsMaker {
109
110  fn name(&self) -> &str {
111    "tocjs"
112  }
113
114  fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
115        
116    let cfg = Config::new(self.name(), ctx)?;
117
118    let mut f = File::create(cfg.save_dir.join("toc.css")).unwrap();
119    f.write_all(TOC_CSS.as_bytes())?;
120
121    let mut f = File::create(cfg.save_dir.join("toc.js")).unwrap();
122    f.write_all(cfg.format_js(TOC_JS).as_bytes())?;
123
124    // make theme/head.hbs
125    let head_hbs_literals = [
126      format!(r#"<link rel="stylesheet" href="{}lib/toc.css">"#, cfg.base_url),
127      format!(r#"<script src="{}lib/toc.js"></script>"#, cfg.base_url)
128    ];
129      
130    let head_hbs_path = cfg.theme_dir.join("head.hbs");
131
132    match head_hbs_path.exists() {
133      true => {
134        let mut f = std::fs::OpenOptions::new()
135          .read(true).write(true).append(true).open(head_hbs_path.as_path()).unwrap();
136
137        let mut buf: String = String::new();
138        f.read_to_string(&mut buf)?;
139
140        for literal in head_hbs_literals {
141          if !buf.contains(&literal) {
142            f.write_all(literal.as_bytes())?;
143          }
144        }
145      },
146      false => {
147        let mut f = File::create(head_hbs_path.as_path()).unwrap();
148        f.write_all(head_hbs_literals.join("").as_bytes())?;
149      }
150    }
151
152    Ok(book)
153  }
154}