Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ target/
releases
node_modules/
static/css/output.css
.vscode
4 changes: 3 additions & 1 deletion src/recipe/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ use clap::{Args, Subcommand};

use crate::Context;

mod read;
pub mod read;

pub use read::CLIExtensions;

#[derive(Debug, Args)]
#[command(args_conflicts_with_subcommands = true)]
Expand Down
39 changes: 34 additions & 5 deletions src/recipe/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ use std::io::Read;
use camino::Utf8PathBuf;

use crate::{
util::{split_recipe_name_and_scaling_factor, write_to_output, PARSER},
util::{
build_extensions_from_cli, parse_recipe_from_entry_with_parser,
split_recipe_name_and_scaling_factor, write_to_output,
},
Context,
};
use cooklang_find::RecipeEntry;
Expand Down Expand Up @@ -71,6 +74,13 @@ pub struct ReadArgs {
/// Has no effect on human, cooklang, or markdown formats.
#[arg(long)]
pretty: bool,

/// Extensions to enable
///
/// Can be specified multiple times to enable multiple extensions.
/// If not specified, no extensions are enabled.
#[arg(short, long, value_enum, num_args(0..), default_values_t = vec![CLIExtensions::None])]
extension: Vec<CLIExtensions>,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
Expand All @@ -85,9 +95,28 @@ enum OutputFormat {
Markdown,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum CLIExtensions {
All,
None,
Compat,
ComponentModifiers,
ComponentAlias,
AdvancedUnits,
Modes,
InlineQuantities,
RangeValues,
TimerRequiresTime,
IntermediatePreparations,
}

pub fn run(ctx: &Context, args: ReadArgs) -> Result<()> {
let mut scale = args.input.scale;

// Build extensions from CLI args
let extensions = build_extensions_from_cli(&args.extension);
let parser = cooklang::CooklangParser::new(extensions, cooklang::Converter::default());

let (recipe, title) = if let Some(query) = args.input.recipe {
let (name, scaling_factor) = split_recipe_name_and_scaling_factor(query.as_str())
.map(|(name, scaling_factor)| {
Expand All @@ -109,7 +138,7 @@ pub fn run(ctx: &Context, args: ReadArgs) -> Result<()> {

let recipe_entry = cooklang_find::get_recipe(vec![ctx.base_path().clone()], name.into())
.map_err(|e| anyhow::anyhow!("Recipe not found: {}", e))?;
let recipe = crate::util::parse_recipe_from_entry(&recipe_entry, scale)?;
let recipe = parse_recipe_from_entry_with_parser(&recipe_entry, scale, &parser)?;
(recipe, recipe_entry.name().clone().unwrap_or(String::new()))
} else {
// Read from stdin and create a RecipeEntry
Expand All @@ -123,7 +152,7 @@ pub fn run(ctx: &Context, args: ReadArgs) -> Result<()> {
.context("Failed to create recipe entry from stdin")?;

// Use the same parsing function as for file-based recipes
let recipe = crate::util::parse_recipe_from_entry(&recipe_entry, scale)?;
let recipe = parse_recipe_from_entry_with_parser(&recipe_entry, scale, &parser)?;
(recipe, recipe_entry.name().clone().unwrap_or(String::new()))
};

Expand All @@ -145,7 +174,7 @@ pub fn run(ctx: &Context, args: ReadArgs) -> Result<()> {
&recipe,
&title,
scale,
PARSER.converter(),
parser.converter(),
writer,
)?,
OutputFormat::Json => {
Expand All @@ -163,7 +192,7 @@ pub fn run(ctx: &Context, args: ReadArgs) -> Result<()> {
&recipe,
&title,
scale,
PARSER.converter(),
parser.converter(),
writer,
)?,
}
Expand Down
55 changes: 54 additions & 1 deletion src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,63 @@ use tracing::warn;
pub const RECIPE_SCALING_DELIMITER: char = ':';

pub static PARSER: Lazy<CooklangParser> = Lazy::new(|| {
// Use no extensions but with default converter for basic unit support
// Default: all extensions enabled for legacy use
CooklangParser::new(Extensions::empty(), Converter::default())
});

/// Build Extensions from CLI args (CLIExtensions)
use crate::recipe::CLIExtensions;
pub fn build_extensions_from_cli(exts: &[CLIExtensions]) -> Extensions {
if exts.iter().any(|e| matches!(e, CLIExtensions::All)) {
return Extensions::all();
}
if exts.iter().any(|e| matches!(e, CLIExtensions::None)) {
return Extensions::empty();
}
let mut extensions = Extensions::empty();
for ext in exts {
match ext {
CLIExtensions::Compat => extensions |= Extensions::COMPAT,
CLIExtensions::ComponentModifiers => extensions |= Extensions::COMPONENT_MODIFIERS,
CLIExtensions::ComponentAlias => extensions |= Extensions::COMPONENT_ALIAS,
CLIExtensions::AdvancedUnits => extensions |= Extensions::ADVANCED_UNITS,
CLIExtensions::Modes => extensions |= Extensions::MODES,
CLIExtensions::InlineQuantities => extensions |= Extensions::INLINE_QUANTITIES,
CLIExtensions::RangeValues => extensions |= Extensions::RANGE_VALUES,
CLIExtensions::TimerRequiresTime => extensions |= Extensions::TIMER_REQUIRES_TIME,
CLIExtensions::IntermediatePreparations => {
extensions |= Extensions::INTERMEDIATE_PREPARATIONS
}
CLIExtensions::All | CLIExtensions::None => {} // handled above
}
}
extensions
}

/// Parse a Recipe from a RecipeEntry with the given scaling factor and parser
pub fn parse_recipe_from_entry_with_parser(
entry: &RecipeEntry,
scaling_factor: f64,
parser: &CooklangParser,
) -> Result<Arc<Recipe>> {
let content = entry.content().context("Failed to read recipe content")?;
let parsed = parser.parse(&content);

// Log any warnings
if parsed.report().has_warnings() {
let recipe_name = entry.name().as_deref().unwrap_or("unknown");
for warning in parsed.report().warnings() {
warn!("Recipe '{}': {}", recipe_name, warning);
}
}

let (mut recipe, _warnings) = parsed.into_result().context("Failed to parse recipe")?;

// Scale the recipe
recipe.scale(scaling_factor, parser.converter());
Ok(Arc::new(recipe))
}

/// Parse a Recipe from a RecipeEntry with the given scaling factor
pub fn parse_recipe_from_entry(entry: &RecipeEntry, scaling_factor: f64) -> Result<Arc<Recipe>> {
let content = entry.content().context("Failed to read recipe content")?;
Expand Down
19 changes: 19 additions & 0 deletions tests/cli_integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,22 @@ fn test_cli_recipe_from_subdirectory() {
.success()
.stdout(predicate::str::contains("Pancakes"));
}

#[test]
fn test_cli_recipe_with_extensions() {
let temp_dir = common::setup_test_recipes().unwrap();

Command::cargo_bin("cook")
.unwrap()
.current_dir(temp_dir.path())
.arg("recipe")
.arg("read")
.arg("aliases.cook")
.arg("-f")
.arg("json")
.arg("--extension")
.arg("component-alias")
.assert()
.success()
.stdout(predicate::str::contains("table salt"));
}
14 changes: 14 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ Add @garlic{2%cloves} and cook for ~{2%minutes}.
"#,
)?;

// Create a recipe with component aliases
fs::write(
recipes_dir.join("aliases.cook"),
r#"---
title: Simple Recipe with Aliases
servings: 2
---

Boil @water{2%cups} for ~{5%minutes}.
Add @table salt|salt{1%tsp} and @pasta{200%g}.
Cook in a #pot for another ~{10%minutes}.
"#,
)?;

// Create a recipe with errors for doctor testing
fs::write(
recipes_dir.join("with_errors.cook"),
Expand Down
2 changes: 1 addition & 1 deletion tests/snapshots/snapshot_test__doctor_validate_output.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ expression: sorted_output
❌ Missing reference: nonexistent

=== Validation Summary ===
Total recipes scanned: 5
Total recipes scanned: 6
❌ 1 error(s) found in 0 recipe(s)
⚠️ 1 warning(s) found in 1 recipe(s)
Loading