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(), 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(); }
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 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}