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 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 let mut nix: Option<String> = None;
62 let events = Parser::new(&chapter.content)
63 .filter_map(|event| {
64 match &event {
65 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 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 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 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 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 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 _ => 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