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
use std::{
fs,
io::{stdin, Read},
path::PathBuf,
process,
};
use clap::{Parser, ValueEnum};
use sous::{Cookbook, MarkdownRenderer, Recipe, Renderer, SousError, TemplateRenderer};
#[derive(Clone, Debug, Default, ValueEnum)]
enum RenderMode {
/// Use the built-in Markdown renderer.
#[default]
Markdown,
/// Use the Tera template renderer.
Template,
}
/// Convert YAML culinary recipes to Markdown.
#[derive(Parser, Debug)]
#[command()]
struct Args {
/// Cookbook or single YAML-formatted recipe to convert.
///
/// Single-file mode or Cookbook mode will automatically be selected based on whether INPUT
/// points to a file or directory.
#[arg()]
input: PathBuf,
/// Output path.
///
/// In single-file mode, output to the specified file instead of printing to stdout. In
/// Cookbook mode, specify the directory in which to output (will be created if necessary).
#[arg(short, long)]
output: Option<PathBuf>,
/// Render mode selection.
///
/// Markdown mode uses the built-in renderer to generate markdown files.
/// Template mode uses the provided Tera template to render recipes.
#[arg(short, long, value_enum, default_value_t = RenderMode::Markdown)]
mode: RenderMode,
/// File to use for the template renderer.
///
/// If a template file is not specified, sous will read a template from standard input.
#[arg(short, long)]
template: Option<PathBuf>,
/// Override number of servings (Only applies to the Markdown renderer).
///
/// Render recipes with a specific number of servings. Ingredient amounts will be adjusted
/// accordingly.
#[arg(short, long, value_parser = clap::value_parser!(u32).range(1..))]
servings: Option<u32>,
/// Use front matter instead of pure Markdown (Only applies to Markdown renderer).
///
/// This option enables outputting some metadata content to YAML front matter instead of using
/// Markdown headers. Useful for static site generators.
#[arg(short, long)]
front_matter: bool,
}
fn create_renderer(args: &Args) -> Result<Box<dyn Renderer>, SousError> {
let renderer: Box<dyn Renderer> = match args.mode {
RenderMode::Markdown => Box::new(MarkdownRenderer {
servings: args.servings,
front_matter: args.front_matter,
..Default::default()
}),
RenderMode::Template => match &args.template {
Some(path) => Box::new(TemplateRenderer::from_path(&path)?),
None => {
let mut template = String::new();
stdin().read_to_string(&mut template)?;
Box::new(TemplateRenderer::from_str(&template)?)
}
},
};
Ok(renderer)
}
fn main() {
let args = Args::parse();
let renderer = create_renderer(&args).unwrap_or_else(|e| {
eprintln!("failed to initialize renderer: {e}");
process::exit(1);
});
if args.input.is_dir() {
let cookbook = Cookbook::open(&args.input).unwrap_or_else(|e| {
eprintln!("failed to open cookbook: {e}");
process::exit(1);
});
let output = args.output.unwrap_or_else(|| PathBuf::from("render"));
if !output.is_dir() {
fs::create_dir(&output).unwrap_or_else(|e| {
eprintln!("failed to open output directory: {e}");
});
}
for file in cookbook.recipes() {
let recipe = cookbook.load_recipe(file).unwrap_or_else(|e| {
eprintln!("failed to load recipe {file}: {e}");
process::exit(1);
});
let rendered = renderer.render(&recipe).unwrap_or_else(|e| {
eprintln!("failed to render recipe {file}: {e}");
process::exit(3);
});
fs::write(
&output.join(PathBuf::from(file).with_extension("md")),
rendered,
)
.unwrap_or_else(|e| {
eprintln!("failed to write file for recipe {file}: {e}");
process::exit(2);
});
}
} else {
let recipe = Recipe::from_file(&args.input).unwrap_or_else(|e| {
eprintln!("failed to load recipe: {e}");
process::exit(1);
});
let output = renderer.render(&recipe).unwrap_or_else(|e| {
eprintln!("failed to render recipe: {e}");
process::exit(3);
});
match args.output {
Some(file) => {
fs::write(&file, output).unwrap_or_else(|e| {
eprintln!("failed to write file: {e}");
process::exit(2);
});
}
None => {
print!("{}", output);
}
}
}
}