mdbook_nix_eval/
lib.rs

1use std::fs::File;
2
3use mdbook::book::{Book, Chapter};
4use mdbook::errors::Error;
5use mdbook::preprocess::{Preprocessor, PreprocessorContext};
6use tempfile::tempdir;
7use serde_json::Value;
8
9pub struct NixEval;
10
11impl NixEval {
12    pub fn new() -> NixEval {
13        NixEval
14    }
15}
16
17struct NixConfig {
18    eval_command: String,
19    eval_args: String,
20}
21
22impl Preprocessor for NixEval {
23    fn name(&self) -> &str { "nix-eval" }
24
25    fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book, Error> {
26        let mut nix_config = NixConfig {
27            eval_command: "nix-instantiate".to_owned(),
28            eval_args: "".to_owned(),
29        };
30        if let Some(config) = _ctx.config.get_preprocessor("nix-eval") {
31            if let Some(toml::value::Value::String(v)) = config.get("eval_command") {
32                nix_config.eval_command = v.to_owned();
33            }
34            if let Some(toml::value::Value::String(v)) = config.get("eval_args") {
35                nix_config.eval_args = v.to_owned();
36            }
37        }
38        book.for_each_mut(|book| {
39            if let mdbook::BookItem::Chapter(chapter) = book {
40                // TODO: better error handling...
41                if let Err(e) = nix_eval(&nix_config, chapter) {
42                    eprintln!("nix-eval error: {:?}", e);
43                }
44            }
45        });
46
47        Ok(book)
48    }
49
50    fn supports_renderer(&self, renderer: &str) -> bool {
51        renderer == "html"
52    }
53}
54
55fn nix_eval(config: &NixConfig, chapter: &mut Chapter) -> Result<(), Error> {
56    use pulldown_cmark::{Parser, Event, Tag, CodeBlockKind, CowStr};
57
58    let chapter_temp_dir = tempdir()?;
59
60    // mini state machine for the current nix-eval tag
61    let mut nix: Option<String> = None;
62    let events = Parser::new(&chapter.content)
63        .filter_map(|event| {
64            match &event {
65                // a code block for the `nix-eval` language was started
66                Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
67                    if !(info.ends_with(".nix") || info.to_string() == "nix") {
68                        return Some(event);
69                    }
70                    nix = Some("".to_owned());
71                    None
72                }
73                // a code block for the `nix-eval` language was ended
74                Event::End(Tag::CodeBlock(CodeBlockKind::Fenced(info))) => {
75                    let is_file = info.ends_with(".nix");
76                    let is_eval = info.to_string() == "nix";
77                    let mut wrap_lambda = false;
78                    if !(is_file || is_eval) {
79                        return Some(event);
80                    }
81
82                    let nix_file_name = match is_file {
83                        true => info.as_ref(),
84                        false => "eval.nix",
85                    };
86                    let nix_file_path = chapter_temp_dir.path().join(nix_file_name);
87
88                    // extract the contents of the diagram
89                    let nix_src = nix.take().expect("nix was started");
90
91                    let mut out_file = File::create(nix_file_path.as_path()).expect("nix file created");
92                    out_file.write_all(nix_src.as_ref()).expect("wrote temp file");
93
94                    // eprintln!("writing temp file: {:?}", nix_file_path.as_path());
95
96                    // evaluate the nix expression
97                    use std::process::{Command, Stdio};
98                    use std::io::Write;
99
100                    let quick_eval = match Command::new(&config.eval_command)
101                        .current_dir(chapter_temp_dir.path())
102                        .arg("--eval")
103                        .arg(nix_file_path.as_path())
104                        .stdout(Stdio::piped())
105                        .stderr(Stdio::piped())
106                        .spawn() {
107                        Ok(c) => c,
108                        Err(e) => {
109                            eprintln!("failed to launch nix-eval, not rendering nix-eval block: {:?}", e);
110                            return None;
111                        }
112                    };
113                    let quick_eval_out = quick_eval.wait_with_output().expect("can launch nix-eval");
114                    let quick_eval_str = String::from_utf8(quick_eval_out.stdout).expect("valid utf-8");
115                    if quick_eval_str.trim() == "<LAMBDA>" {
116                        wrap_lambda = true;
117                    }
118
119                    let mut c_init = Command::new(&config.eval_command);
120                    let mut command = c_init.current_dir(chapter_temp_dir.path())
121                        .arg("--json")
122                        .arg("--eval")
123                        .stdout(Stdio::piped())
124                        .stderr(Stdio::piped());
125
126                    if is_eval || is_file {
127                        command = command.arg("--strict");
128                    }
129
130                    if config.eval_args.len() > 0 {
131                        command = command.args(config.eval_args.split(" "));
132                    }
133
134                    if wrap_lambda {
135                        command = command.arg("-E");
136                        command = command.arg(format!("import {} {}", nix_file_path.as_path().to_str().expect("invalid path"), "{}"));
137                    } else {
138                        command = command.arg(nix_file_path.as_path());
139                    }
140
141                    // eprintln!("{:?}", command);
142                    let child = match command.spawn() {
143                        Ok(c) => c,
144                        Err(e) => {
145                            eprintln!("failed to launch nix-eval, not rendering nix-eval block: {:?}", e);
146                            return None;
147                        }
148                    };
149
150                    let cmd_output = child.wait_with_output().expect("can launch nix-eval");
151
152                    let output: String = String::from_utf8(cmd_output.stdout).expect("valid utf-8");
153                    let trimmed_output = output.trim();
154                    let mut nix_eval_output = "".to_owned();
155                    if !cmd_output.status.success() {
156                        nix_eval_output = String::from_utf8(cmd_output.stderr).expect("valid utf-8");
157                    } else if trimmed_output.len() > 0 {
158                        let _decoded = match serde_json::from_str(trimmed_output) {
159                            Ok(v) => {
160                                let line = match v {
161                                    Value::String(s) => {
162                                        let trimmed = s.trim();
163                                        if trimmed.contains("\n") {
164                                            format!("\"\n{}\n\"", trimmed)
165                                        } else {
166                                            format!("\"{}\"", trimmed)
167                                        }
168                                    },
169                                    Value::Bool(b) => serde_json::to_string_pretty(&b).unwrap(),
170                                    Value::Null => "null".to_owned(),
171                                    Value::Number(n) => format!("{}", n),
172                                    Value::Array(a) => serde_json::to_string_pretty(&a).unwrap(),
173                                    Value::Object(o) => serde_json::to_string_pretty(&o).unwrap(),
174                                };
175                                nix_eval_output.push_str(line.as_str())
176                            },
177                            Err(_e) => {
178                                nix_eval_output.push_str(trimmed_output)
179                            }
180                        };
181                    } else {
182                        nix_eval_output.push_str("<< no output >>")
183                    }
184
185                    let input_header = match is_file {
186                        true => format!("**{}**\n", info.as_ref()),
187                        false => "".to_string(),
188                    };
189
190                    let input = format!("\n```nix\n{}\n```\n", nix_src.trim());
191                    let output = format!("\n```json\n{}\n```\n", nix_eval_output.trim());
192
193                    nix = None;
194                    Some(Event::Text(CowStr::from(format!("\n{}\n<div style='border-left: 2px solid;'>\n{}\n\n{}\n</div>\n\n", input_header, input, output))))
195                }
196                // intercept text events if we're currently in the code block state
197                Event::Text(txt) => {
198                    if let Some(nix) = nix.as_mut() {
199                        nix.push_str(&txt);
200                        None
201                    } else {
202                        Some(event)
203                    }
204                }
205                // don't touch other events
206                _ => Some(event),
207            }
208        });
209
210    let mut buf = String::with_capacity(chapter.content.len());
211    pulldown_cmark_to_cmark::cmark(events, &mut buf, None).expect("can re-render cmark");
212    chapter.content = buf;
213
214    Ok(())
215}
216