#![warn(missing_docs)]
pub mod cooklang_source;
pub mod human;
pub mod latex;
pub mod markdown;
pub mod number;
pub mod quantity;
pub mod schema;
pub mod typst;
pub use cooklang;
pub const REFERENCE_SEPARATOR: &str = "/";
use ::cooklang::{convert::Converter, Recipe};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Style {
#[default]
Plain,
Ansi,
}
impl Style {
pub fn is_ansi(self) -> bool {
matches!(self, Style::Ansi)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum PaperSize {
#[default]
A4,
Letter,
A5,
Legal,
}
impl PaperSize {
pub fn latex_name(self) -> &'static str {
match self {
PaperSize::A4 => "a4paper",
PaperSize::Letter => "letterpaper",
PaperSize::A5 => "a5paper",
PaperSize::Legal => "legalpaper",
}
}
pub fn typst_name(self) -> &'static str {
match self {
PaperSize::A4 => "a4",
PaperSize::Letter => "us-letter",
PaperSize::A5 => "a5",
PaperSize::Legal => "us-legal",
}
}
}
pub fn human_to_string(
recipe: &Recipe,
name: &str,
scale: f64,
converter: &Converter,
style: Style,
) -> std::io::Result<String> {
let mut buf = Vec::new();
human::print_human(recipe, name, scale, converter, style, &mut buf)?;
into_string(buf)
}
pub fn markdown_to_string(
recipe: &Recipe,
name: &str,
scale: f64,
converter: &Converter,
) -> std::io::Result<String> {
let mut buf = Vec::new();
markdown::print_md(recipe, name, scale, converter, &mut buf)?;
into_string(buf)
}
fn into_string(buf: Vec<u8>) -> std::io::Result<String> {
String::from_utf8(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
pub struct ReadmeDoctests;
#[cfg(test)]
pub(crate) mod test_support {
use cooklang::{Converter, CooklangParser, Extensions, Recipe};
use std::sync::LazyLock;
pub(crate) static PARSER: LazyLock<CooklangParser> =
LazyLock::new(|| CooklangParser::new(Extensions::empty(), Converter::default()));
pub(crate) struct Parsed {
pub(crate) value: Recipe,
pub(crate) diagnostics: Vec<String>,
}
pub(crate) fn parse_recipe(text: &str, name: &str, scale: f64) -> Result<Parsed, String> {
let parsed = PARSER.parse(text);
if parsed.report().has_errors() {
return Err(format!("{name} failed to parse"));
}
let diagnostics = parsed
.report()
.iter()
.map(|diag| diag.message.to_string())
.collect();
match parsed.into_result() {
Ok((mut recipe, _)) => {
recipe.scale(scale, PARSER.converter());
Ok(Parsed {
value: recipe,
diagnostics,
})
}
Err(_) => Err(format!("{name} produced no output")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{parse_recipe, PARSER};
const RECIPE: &str = "---\ntitle: Tea\ntags: [hot, quick]\n---\n\nBoil @water{2%cups} in a #pot for ~{5%minutes}.\n";
fn human(style: Style) -> String {
let recipe = parse_recipe(RECIPE, "Tea", 1.0).expect("parses").value;
human_to_string(&recipe, "Tea", 1.0, PARSER.converter(), style).expect("formats")
}
#[test]
fn plain_is_ansi_with_the_escapes_removed() {
let plain = human(Style::Plain);
let coloured = human(Style::Ansi);
assert!(
!plain.contains('\u{1b}'),
"Style::Plain must emit no escape codes: {plain:?}"
);
assert_eq!(
plain,
anstream::adapter::strip_str(&coloured).to_string(),
"Plain must be exactly Ansi with the escapes removed, not different text"
);
}
#[cfg(not(windows))]
#[test]
fn ansi_keeps_the_escape_codes() {
let coloured = human(Style::Ansi);
assert!(
coloured.contains('\u{1b}'),
"Style::Ansi must emit escape codes: {coloured:?}"
);
}
#[test]
fn paper_sizes_map_to_both_typesetter_spellings() {
let all = [
(PaperSize::A4, "a4paper", "a4"),
(PaperSize::Letter, "letterpaper", "us-letter"),
(PaperSize::A5, "a5paper", "a5"),
(PaperSize::Legal, "legalpaper", "us-legal"),
];
for (size, latex, typst) in all {
assert_eq!(size.latex_name(), latex, "latex name for {size:?}");
assert_eq!(size.typst_name(), typst, "typst name for {size:?}");
}
assert_eq!(PaperSize::default(), PaperSize::A4);
}
#[test]
fn style_default_is_plain_and_only_ansi_is_ansi() {
assert_eq!(Style::default(), Style::Plain);
assert!(Style::Ansi.is_ansi());
assert!(!Style::Plain.is_ansi());
}
#[test]
fn to_string_wrappers_match_the_writer_primitives() {
let recipe = parse_recipe(RECIPE, "Tea", 1.0).expect("parses").value;
let mut buf = Vec::new();
human::print_human(
&recipe,
"Tea",
1.0,
PARSER.converter(),
Style::Plain,
&mut buf,
)
.expect("formats");
assert_eq!(
human_to_string(&recipe, "Tea", 1.0, PARSER.converter(), Style::Plain).unwrap(),
String::from_utf8(buf).unwrap()
);
let mut buf = Vec::new();
markdown::print_md(&recipe, "Tea", 1.0, PARSER.converter(), &mut buf).expect("formats");
assert_eq!(
markdown_to_string(&recipe, "Tea", 1.0, PARSER.converter()).unwrap(),
String::from_utf8(buf).unwrap()
);
}
}