1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
use nadi_plugin::nadi_internal_plugin;
#[nadi_internal_plugin]
mod render {
use crate::prelude::*;
use abi_stable::std_types::{RHashMap, RString};
use nadi_plugin::{env_func, network_func, node_func};
use std::collections::HashMap;
use std::path::PathBuf;
/// Render the template based on the node attributes
///
/// If you have the safe=true, then if the rendering fails, it
/// returns the original string template. For more details on the
/// template system. Refer to the String Template section of the
/// NADI book.
///
/// ```task
/// env assert_eq(render("abc {x}", x="ab"), "abc ab")
/// env assert_eq(render("abc {x}", x=23), "abc 23")
/// env assert_eq(render("abc {x} {a}", safe=true), "abc {x} {a}")
/// ```
///
/// If safe parameter is true, then it doesn't error out even if
/// the variable is not present, and will just return the original
/// template. By default it errors out if there are any variables
/// in the template without a value.
///
/// ```task
/// env assert_eq(render("abc {x}", safe=true), "abc {x}")
/// ```
#[env_func(safe = false)]
fn render(
/// String template to render
template: Template,
/// if render fails keep it as it is instead of exiting
safe: bool,
#[kwargs] keyval: HashMap<&RString, Attribute>,
) -> Result<String, String> {
let attrmap: RHashMap<_, _> = keyval.into_iter().map(|(k, v)| (k.clone(), v)).collect();
let res = template.render(&attrmap);
let text = if safe {
res.unwrap_or_else(|_| template.original().to_string())
} else {
res.map_err(|e| e.to_string())?
};
Ok(text)
}
/// Render the template based on the node attributes
///
/// For more details on the template system. Refer to the String
/// Template section of the NADI book.
///
/// ```task
/// network load_str("a -> b")
/// nodes.x = 13
/// nodes assert_eq(render("abc {x}"), "abc 13")
/// nodes assert_eq(render("abc {x} {a}", safe=true), "abc {x} {a}")
/// ```
#[node_func(safe = false)]
fn render(
node: &NodeInner,
/// String template to render
template: Template,
/// if render fails keep it as it is instead of exiting
safe: bool,
) -> Result<String, String> {
let res = template.render(node);
let text = if safe {
res.unwrap_or_else(|_| template.original().to_string())
} else {
res.map_err(|e| e.to_string())?
};
Ok(text)
}
/// Render from network attributes
///
/// ```task
/// network.x = 13
/// network assert_eq(render("abc {x}"), "abc 13")
/// network assert_eq(render("abc {x} {a}", safe=true), "abc {x} {a}")
/// ```
#[network_func(safe = false)]
fn render(
network: &Network,
/// Path to the template file
template: Template,
/// if render fails keep it as it is instead of exiting
safe: bool,
) -> Result<String, String> {
let res = template.render(network);
let text = if safe {
res.unwrap_or_else(|_| template.original().to_string())
} else {
res.map_err(|e| e.to_string())?
};
Ok(text)
}
/// Render each node of the network and combine to same variable
///
/// ```task
/// network load_str("a -> b")
/// nodes.x = INDEX + 1
/// network assert_eq(render_nodes("abc {x}"), "abc 1\nabc 2")
/// ```
#[network_func(safe = false, join = "\n")]
fn render_nodes(
network: &Network,
/// Path to the template file
template: Template,
/// if render fails keep it as it is instead of exiting
safe: bool,
/// String to join the render results
join: &str,
) -> Result<String, String> {
let lines: Vec<String> = network
.nodes()
.map(|n| {
let n: &NodeInner = &n.lock();
let res = template.render(n);
if safe {
Ok(res.unwrap_or_else(|_| template.original().to_string()))
} else {
res.map_err(|e| e.to_string())
}
})
.collect::<Result<Vec<_>, String>>()?;
Ok(lines.join(join))
}
/// Render a File template for the nodes in the whole network
///
/// Write the file with templates for input variables in the same
/// way you write string templates. It's useful for markdown
/// files, as the curly braces syntax won't be used for anything
/// else that way. Do be careful about that. And the program will
/// replace those templates with their values when you run it with
/// inputs.
///
/// It'll repeat the same template for each node and render them.
/// If you want only a portion of the file repeated for nodes
/// inclose them with lines with `---8<---` on both start and the
/// end. The lines containing the clip syntax will be ignored,
/// ideally you can put them in comments.
///
/// You can also use `---include:<filename>[::line_range]` syntax to
/// include a file, the line_range syntax, if present, should be
/// in the form of `start[:increment]:end`, you can exclude start
/// or end to denote the line 1 or last line (e.g. `:5` is 1:5,
/// and `3:` is from line 3 to the end)
///
/// # Arguments
/// - `template`: Path to the template file
/// - `outfile` [Optional]: Path to save the template file, if none it'll be printed in stdout
#[network_func]
fn render_template(
network: &Network,
/// Path to the template file
template: PathBuf,
) -> anyhow::Result<String> {
let template = super::render_utils::RenderFileContents::read_file(&template)?;
template.render(network)
}
}
mod render_utils {
use crate::network::Propagation;
use crate::prelude::*;
use anyhow::{Context, Error};
use number_range::NumberRangeOptions;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::str::FromStr;
pub enum RenderFileContentsType {
Include(PathBuf, String),
Literal(String),
Snippet(Template, Box<Propagation>),
}
pub struct RenderFileContents {
contents: Vec<RenderFileContentsType>,
}
fn insert_till_now(
lines: &mut String,
batch: Option<Propagation>,
filecontents: &mut RenderFileContents,
) -> Result<(), Error> {
let p = if let Some(batch) = batch {
RenderFileContentsType::Snippet(Template::from_str(lines)?, Box::new(batch))
} else {
RenderFileContentsType::Literal(lines.clone())
};
filecontents.contents.push(p);
lines.clear();
Ok(())
}
impl RenderFileContents {
pub fn read_file(filename: &Path) -> Result<Self, Error> {
let file = match File::open(filename) {
Ok(f) => f,
Err(e) => {
return Err(Error::msg(format!(
"Couldn't open input file: {:?}\n{:?}",
filename.to_string_lossy(),
e
)));
}
};
let reader_lines = BufReader::new(file).lines();
let mut snippet = false;
let mut filecontents = RenderFileContents {
contents: Vec::new(),
};
let mut lines = String::new();
let mut batch: Option<Propagation> = None;
for line in reader_lines {
let l = line.unwrap();
if l.contains("---8<---") {
insert_till_now(&mut lines, batch.clone(), &mut filecontents)?;
batch = if snippet {
// if in a snippet already, we're exiting
None
} else if let Some((_, s)) = l.split_once(':') {
let s = s.split_once(':').map(|(s, _)| s).unwrap_or(s);
let prop = Propagation::from_str(s)?;
Some(prop)
} else {
Some(Propagation::default())
};
snippet = !snippet;
} else if l.contains("---include:") {
if snippet {
// todo let it include files globally, as well as inside snippets
return Err(Error::msg("Cannot have file in render snippet"));
}
insert_till_now(&mut lines, None, &mut filecontents)?;
let (_, fname) = l.split_once(':').unwrap();
let (fname, lines) = fname.split_once("::").unwrap_or((fname, ":"));
filecontents.contents.push(RenderFileContentsType::Include(
PathBuf::from(filename).parent().unwrap().join(fname.trim()),
lines.to_string(),
))
} else {
lines.push_str(&l);
lines.push('\n');
}
}
if filecontents.contents.is_empty() {
// if there is no ---8<--- in file, consider the whole
// file as snippet
batch = Some(Propagation::default());
}
if !lines.is_empty() {
insert_till_now(&mut lines, batch, &mut filecontents)?;
}
Ok(filecontents)
}
fn _snippet(templ: &str, batch: Propagation) -> Result<Self, Error> {
Ok(Self {
contents: vec![RenderFileContentsType::Snippet(
Template::from_str(templ)?,
Box::new(batch),
)],
})
}
pub fn render(&self, net: &Network) -> anyhow::Result<String> {
let mut output = String::new();
for part in &self.contents {
match part {
RenderFileContentsType::Include(filename, lines) => {
let file = File::open(filename)
.with_context(|| format!("File {filename:?} not found"))?;
let reader_lines: Vec<String> = BufReader::new(file)
.lines()
.collect::<Result<Vec<String>, std::io::Error>>()?;
let lines = NumberRangeOptions::default()
.with_default_start(1)
.with_default_end(reader_lines.len())
.parse(lines)?;
for l in lines {
output.push_str(&reader_lines[l - 1]);
output.push('\n');
}
}
RenderFileContentsType::Literal(s) => output.push_str(s),
RenderFileContentsType::Snippet(templ, prop) => {
for node in net
.nodes_select(&prop.order, &prop.nodes)
.map_err(anyhow::Error::msg)?
{
let n: &NodeInner = &node.lock();
output.push_str(&templ.render(n)?);
}
}
}
}
Ok(output)
}
}
}