use std::collections::HashMap;
use std::str::FromStr;
use crate::convert::{Converter, PhysicalQuantity};
use crate::error::{label, CowStr, PassResult, SourceDiag, SourceReport};
use crate::located::Located;
use crate::metadata::{check_std_entry, StdKey};
use crate::parser::{
self, BlockKind, Event, IntermediateData, IntermediateRefMode, IntermediateTargetKind,
Modifiers,
};
use crate::quantity::{Quantity, Value};
use crate::span::Span;
use crate::text::Text;
use crate::{model::*, Extensions, ParseOptions};
use super::{AnalysisResult, CheckOptions, DefineMode, DuplicateMode};
macro_rules! error {
($msg:expr, $label:expr $(,)?) => {
$crate::error::SourceDiag::error($msg, $label, $crate::error::Stage::Analysis)
};
($msg:expr) => {
$crate::error::SourceDiag::unlabeled(
$msg,
$crate::error::Severity::Error,
$crate::error::Stage::Analysis,
)
};
}
macro_rules! warning {
($msg:expr, $label:expr $(,)?) => {
$crate::error::SourceDiag::warning($msg, $label, $crate::error::Stage::Analysis)
};
($msg:expr) => {
$crate::error::SourceDiag::unlabeled(
$msg,
$crate::error::Severity::Warning,
$crate::error::Stage::Analysis,
)
};
}
#[tracing::instrument(level = "debug", skip_all, target = "cooklang::analysis")]
pub fn parse_events<'i, 'c>(
events: impl Iterator<Item = Event<'i>>,
input: &'i str,
extensions: Extensions,
converter: &Converter,
parse_options: ParseOptions,
) -> AnalysisResult {
let col = RecipeCollector {
input,
extensions,
converter,
parse_options,
content: Recipe {
metadata: Default::default(),
sections: Default::default(),
ingredients: Default::default(),
cookware: Default::default(),
timers: Default::default(),
inline_quantities: Default::default(),
},
current_section: Section::default(),
define_mode: DefineMode::All,
duplicate_mode: DuplicateMode::New,
old_style_metadata: true,
old_style_metadata_used: vec![],
ctx: SourceReport::empty(),
locations: Default::default(),
step_counter: 1,
};
col.parse_events(events)
}
struct RecipeCollector<'i, 'c> {
input: &'i str,
extensions: Extensions,
converter: &'c Converter,
parse_options: ParseOptions<'c>,
content: Recipe,
current_section: Section,
define_mode: DefineMode,
duplicate_mode: DuplicateMode,
old_style_metadata: bool,
old_style_metadata_used: Vec<Span>,
ctx: SourceReport,
locations: Locations<'i>,
step_counter: u32,
}
#[derive(Default)]
struct Locations<'i> {
ingredients: Vec<Located<parser::Ingredient<'i>>>,
cookware: Vec<Located<parser::Cookware<'i>>>,
metadata: HashMap<StdKey, (Text<'i>, Text<'i>)>,
}
const IMPLICIT_REF_WARN: &str = "The reference (&) is implicit";
impl<'i> RecipeCollector<'i, '_> {
fn parse_events(mut self, mut events: impl Iterator<Item = Event<'i>>) -> AnalysisResult {
enum BlockBuffer {
Step(Vec<Item>),
Text(String),
}
let mut current_block = None;
let events = events.by_ref();
while let Some(event) = events.next() {
match event {
Event::YAMLFrontMatter(yaml_text) => {
self.old_style_metadata = true;
self.process_frontmatter(yaml_text);
}
Event::Metadata { key, value } => self.metadata(key, value),
Event::Section { name } => {
self.step_counter = 1;
if !self.current_section.is_empty() {
self.content.sections.push(self.current_section);
}
self.current_section =
Section::new(name.map(|t| t.text_trimmed().into_owned()));
}
Event::Start(kind) => {
let buffer = if self.define_mode == DefineMode::Text {
BlockBuffer::Text(String::new())
} else {
match kind {
BlockKind::Step => BlockBuffer::Step(Vec::new()),
BlockKind::Text => BlockBuffer::Text(String::new()),
}
};
current_block = Some(buffer)
}
Event::End(kind) => {
let new_content = match current_block {
Some(BlockBuffer::Step(items)) => {
assert_eq!(kind, BlockKind::Step);
Content::Step(Step {
items,
number: self.step_counter,
})
}
Some(BlockBuffer::Text(text)) => {
assert!(
kind == BlockKind::Text || self.define_mode == DefineMode::Text,
);
Content::Text(text)
}
None => panic!("End event without Start"),
};
if self.define_mode != DefineMode::Components || new_content.is_text() {
if new_content.is_step() {
self.step_counter += 1;
}
self.current_section.content.push(new_content);
}
current_block = None;
}
item @ (Event::Text(_)
| Event::Ingredient(_)
| Event::Cookware(_)
| Event::Timer(_)) => match &mut current_block {
Some(BlockBuffer::Step(items)) => self.in_step(item, items),
Some(BlockBuffer::Text(text)) => self.in_text(item, text),
None => panic!("Content outside block"),
},
Event::Error(e) => {
self.ctx.error(e);
events.for_each(|e| match e {
Event::Error(e) | Event::Warning(e) => self.ctx.push(e),
_ => {}
});
self.ctx.retain(|e| e.stage == crate::error::Stage::Parse);
return PassResult::new(None, self.ctx);
}
Event::Warning(w) => self.ctx.warn(w),
}
}
if !self.current_section.is_empty() {
self.content.sections.push(self.current_section);
}
if !self.old_style_metadata_used.is_empty() {
let mut diag =
warning!("The '>>' syntax for metadata is deprecated, use a YAML frontmatter");
for span in self.old_style_metadata_used {
diag.add_label(label!(span));
}
if let Ok(yaml_hint) = serde_yaml::to_string(&self.content.metadata.map) {
diag.add_hint(format!("Replace the entries with this at the top of the document:\n---\n{yaml_hint}---\n"));
}
self.ctx.warn(diag);
}
PassResult::new(Some(self.content), self.ctx)
}
fn process_frontmatter(&mut self, yaml_text: Text<'i>) {
self.old_style_metadata = false;
let yaml_str = yaml_text.text();
let mut yaml_map = match serde_yaml::from_str::<serde_yaml::Mapping>(&yaml_str) {
Ok(yaml_map) => yaml_map,
Err(err) => {
let mut diag = warning!(format!("Invalid YAML frontmatter syntax: {}", err));
let err_span = err
.location()
.map(|loc| Span::pos(yaml_text.span().start() + loc.index()));
if let Some(loc) = err_span {
diag = diag.label(label!(loc));
}
diag.add_hint(
"The frontmatter will be ignored. Fix the YAML syntax to use metadata.",
);
self.ctx.warn(diag);
return;
}
};
let mut to_remove = Vec::new();
for (key, value) in yaml_map.iter() {
let mut action = CheckOptions::default();
if let Some(validator) = self.parse_options.metadata_validator.as_mut() {
let res = validator(key, value, &mut action);
if let Some(mut diag) = res.into_source_diag(|| "Invalid metadata entry") {
if let Some(key_s) = key.as_str() {
if let Some(pos) = yaml_find_key_position(&yaml_str, key_s) {
diag.add_label(label!(Span::pos(yaml_text.span().start() + pos)));
}
}
self.ctx.push(diag);
}
if !action.include {
to_remove.push(key.clone());
continue;
}
}
if !action.run_std_checks {
continue;
}
if let Some(sk) = key.as_str().and_then(|s| StdKey::from_str(s).ok()) {
if let Err(err) = check_std_entry(sk, value, self.converter) {
let mut diag = warning!(format!(
"Unsupported value for key: '{}'",
key.as_str().unwrap()
))
.set_source(err);
if let Some(key_s) = key.as_str() {
if let Some(pos) = yaml_find_key_position(&yaml_str, key_s) {
diag.add_label(label!(Span::pos(yaml_text.span().start() + pos)));
}
}
self.ctx.warn(diag);
}
}
}
for key in &to_remove {
yaml_map.shift_remove(key);
}
if yaml_map.contains_key(StdKey::Time.as_ref()) {
let loc = |key: StdKey| -> Option<usize> {
yaml_map
.contains_key(key.as_ref())
.then(|| yaml_find_key_position(&yaml_str, key.as_ref()))
.flatten()
};
let prep = loc(StdKey::PrepTime);
let cook = loc(StdKey::CookTime);
const OVERRIDEN: &str = "this entry is overriden";
const OVERRIDES: &str = "this entry has preference";
if prep.is_some() || cook.is_some() {
let mut w = warning!("Time overriden");
if let Some(p) = prep {
w.add_label(label!(Span::pos(yaml_text.span().start() + p), OVERRIDEN));
}
if let Some(p) = cook {
w.add_label(label!(Span::pos(yaml_text.span().start() + p), OVERRIDEN));
}
if let Some(p) = yaml_find_key_position(&yaml_str, StdKey::Time.as_ref()) {
w.add_label(label!(Span::pos(yaml_text.span().start() + p), OVERRIDES));
}
w.add_hint(
"Top level 'prep time' and/or 'cook time' are not compatible with 'time'",
);
self.ctx.warn(w);
}
}
self.content.metadata.map = yaml_map;
}
fn metadata(&mut self, key: Text<'i>, value: Text<'i>) {
let key_t = key.text_trimmed();
let value_t = value.text_outer_trimmed();
let invalid_value = |possible| {
error!(
format!("Invalid value for config key '{key_t}': {value_t}"),
label!(value.span(), "this value")
)
.label(label!(key.span(), "this key does not support"))
.hint(format!("Possible values are: {possible:?}"))
};
if self.extensions.contains(Extensions::MODES)
&& key_t.starts_with('[')
&& key_t.ends_with(']')
{
let config_key = &key_t[1..key_t.len() - 1];
match config_key {
"define" | "mode" => match value_t.as_ref() {
"all" | "default" => self.define_mode = DefineMode::All,
"components" | "ingredients" => self.define_mode = DefineMode::Components,
"steps" => self.define_mode = DefineMode::Steps,
"text" => self.define_mode = DefineMode::Text,
_ => self
.ctx
.error(invalid_value(vec!["all", "components", "steps", "text"])),
},
"duplicate" => match value_t.as_ref() {
"new" | "default" => self.duplicate_mode = DuplicateMode::New,
"reference" | "ref" => self.duplicate_mode = DuplicateMode::Reference,
_ => self.ctx.error(invalid_value(vec!["new", "reference"])),
},
_ => {
self.ctx.warn(
warning!(
format!("Unknown config metadata key: {key_t}"),
label!(key.span())
)
.hint("Possible config keys are '[mode]' and '[duplicate]''"),
);
if self.old_style_metadata {
self.content.metadata.map.insert(
serde_yaml::Value::String(key_t.into_owned()),
serde_yaml::Value::String(value_t.into_owned()),
);
}
}
}
return;
}
self.old_style_metadata_used
.push(Span::new(key.span().start(), value.span().end()));
let yaml_key = serde_yaml::Value::String(key_t.to_string());
let yaml_value = serde_yaml::Value::String(value_t.to_string());
let mut action = CheckOptions::default();
if let Some(validator) = self.parse_options.metadata_validator.as_mut() {
let res = validator(&yaml_key, &yaml_value, &mut action);
if let Some(mut diag) = res.into_source_diag(|| "Invalid metadata entry") {
diag.add_label(label!(key.span()));
diag.add_label(label!(value.span()));
self.ctx.push(diag);
}
if !action.include {
return;
}
}
self.content.metadata.map.insert(yaml_key, yaml_value);
if !action.run_std_checks {
return;
}
if let Ok(sp_key) = StdKey::from_str(&key_t) {
let check_result = crate::metadata::check_std_entry(
sp_key,
self.content.metadata.map.get(key_t.as_ref()).unwrap(),
self.converter,
);
if let Err(err) = check_result {
self.ctx.warn(
warning!(
format!("Unsupported value for key: '{}'", key.text_trimmed()),
label!(value.span(), "this value"),
)
.label(label!(key.span(), "this key does not support"))
.hint("It will be a regular metadata entry")
.set_source(err),
);
return;
}
self.locations
.metadata
.insert(sp_key, (key.clone(), value.clone()));
if matches!(sp_key, StdKey::Time | StdKey::PrepTime | StdKey::CookTime) {
self.time_override_check(sp_key)
}
}
}
fn time_override_check(&mut self, new: StdKey) {
let locs = |keys: &[StdKey]| {
assert!(!keys.is_empty());
let mut v = keys
.iter()
.filter_map(|k| {
self.locations
.metadata
.get(k)
.map(|e| Span::new(e.0.span().start(), e.1.span().end()))
})
.collect::<Vec<_>>();
v.sort_unstable();
v
};
let overrides = locs(&[new])[0];
let overriden_keys: &[StdKey] = match new {
StdKey::Time => &[StdKey::PrepTime, StdKey::CookTime],
StdKey::PrepTime | StdKey::CookTime => &[StdKey::Time],
_ => panic!("unknown time special key"),
};
let overriden = locs(overriden_keys);
for k in overriden_keys {
self.locations.metadata.remove(k); }
if overriden.is_empty() {
return;
}
let mut overriden = overriden.iter();
const OVERRIDEN: &str = "this entry is overriden";
const OVERRIDES: &str = "by this entry";
let mut warn = warning!(
"Time overridden",
label!(overriden.next().unwrap(), OVERRIDEN)
);
for e in overriden {
warn.add_label(label!(e, OVERRIDEN));
}
warn.add_label(label!(overrides, OVERRIDES));
warn.add_hint("Prep time and/or cook time overrides total time and vice versa");
self.ctx.warn(warn);
}
fn in_step(&mut self, item: Event<'i>, items: &mut Vec<Item>) {
match item {
Event::Text(text) => {
let t = text.text();
if self.define_mode == DefineMode::Components {
if t.contains(|c: char| c.is_alphanumeric()) {
self.ctx.warn(warning!(
"Ignoring text in define components mode",
label!(text.span())
));
}
return; }
if self.extensions.contains(Extensions::INLINE_QUANTITIES) {
let mut haystack = t.as_ref();
while let Some((before, temperature, after)) =
find_inline_quantity(haystack, self.converter)
{
if !before.is_empty() {
items.push(Item::Text {
value: before.to_string(),
});
}
items.push(Item::InlineQuantity {
index: self.content.inline_quantities.len(),
});
self.content.inline_quantities.push(temperature);
haystack = after;
}
if !haystack.is_empty() {
items.push(Item::Text {
value: haystack.to_string(),
});
}
} else {
items.push(Item::Text {
value: t.into_owned(),
});
}
}
Event::Ingredient(i) => items.push(Item::Ingredient {
index: self.ingredient(i),
}),
Event::Cookware(i) => items.push(Item::Cookware {
index: self.cookware(i),
}),
Event::Timer(i) => items.push(Item::Timer {
index: self.timer(i),
}),
_ => panic!("Unexpected event in step: {item:?}"),
};
}
fn in_text(&mut self, ev: Event<'i>, s: &mut String) {
match ev {
Event::Text(t) => s.push_str(t.text().as_ref()),
Event::Ingredient(_) | Event::Cookware(_) | Event::Timer(_) => {
assert_eq!(
self.define_mode,
DefineMode::Text,
"Non text event in text block outside define mode text"
);
let (c, span) = match ev {
Event::Ingredient(i) => ("ingredient", i.span()),
Event::Cookware(c) => ("cookware", c.span()),
Event::Timer(t) => ("timer", t.span()),
_ => unreachable!(),
};
self.ctx
.warn(warning!(format!("Ignoring {c} in text mode"), label!(span)));
s.push_str(&self.input[span.range()]);
}
_ => panic!("Unexpected event in text block: {ev:?}"),
}
}
fn ingredient(&mut self, ingredient: Located<parser::Ingredient<'i>>) -> usize {
let located_ingredient = ingredient.clone();
let (ingredient, location) = ingredient.take_pair();
let mut name = ingredient.name.text_trimmed();
let reference = parse_reference(&name);
if let Some(reference) = &reference {
name = reference.name.clone().into();
}
let mut new_igr = Ingredient {
name: name.into_owned(),
alias: ingredient.alias.map(|t| t.text_trimmed().into_owned()),
quantity: ingredient.quantity.clone().map(|q| self.quantity(q, true)),
note: ingredient.note.map(|n| n.text_trimmed().into_owned()),
reference,
modifiers: ingredient.modifiers.into_inner(),
relation: IngredientRelation::definition(
Vec::new(),
self.define_mode != DefineMode::Components,
),
};
if let Some(inter_data) = ingredient.intermediate_data {
assert!(new_igr.modifiers().contains(Modifiers::REF));
let invalid_modifiers = Modifiers::RECIPE | Modifiers::HIDDEN | Modifiers::NEW;
if new_igr.modifiers().intersects(invalid_modifiers) {
self.ctx.error(
error!(
"Conflicting modifiers with intermediate preparation reference",
label!(ingredient.modifiers.span())
)
.hint(format!(
"Remove the following modifiers: {}",
new_igr.modifiers() & invalid_modifiers
)),
);
}
match self.resolve_intermediate_ref(inter_data) {
Ok(relation) => new_igr.relation = relation,
Err(error) => self.ctx.error(error),
}
} else if let Some((references_to, implicit)) =
self.resolve_reference(&mut new_igr, location, located_ingredient.modifiers.span())
{
assert!(ingredient.intermediate_data.is_none());
let definition = &self.content.ingredients[references_to];
let definition_location = &self.locations.ingredients[references_to];
assert!(definition.relation.is_definition());
if self.extensions.contains(Extensions::ADVANCED_UNITS) {
if let Some(new_quantity) = &new_igr.quantity {
let all_quantities = std::iter::once(references_to)
.chain(definition.relation.referenced_from().iter().copied())
.filter_map(|index| {
self.content.ingredients[index]
.quantity
.as_ref()
.map(|q| (index, q))
});
for (index, q) in all_quantities {
if let Err(e) = q.compatible_unit(new_quantity, self.converter) {
let old_q_loc =
self.locations.ingredients[index].quantity.as_ref().unwrap();
let old = old_q_loc
.unit
.as_ref()
.map(|l| l.span())
.unwrap_or(old_q_loc.span());
let new_q_loc = located_ingredient.quantity.as_ref().unwrap();
let new = new_q_loc
.unit
.as_ref()
.map(|l| l.span())
.unwrap_or(new_q_loc.span());
let (main_label, support_label) = match &e {
crate::quantity::IncompatibleUnits::MissingUnit { lhs, .. } => {
let m = "value missing unit";
let f = "found unit";
if *lhs {
(label!(new, m), label!(old, f))
} else {
(label!(new, f), label!(old, m))
}
}
crate::quantity::IncompatibleUnits::DifferentPhysicalQuantities {
a: a_q,
b: b_q,
} => {
(label!(new, b_q.to_string()), label!(old, a_q.to_string()))
}
crate::quantity::IncompatibleUnits::UnknownDifferentUnits { .. } => {
(label!(new), label!(old))
}
};
self.ctx.warn(
warning!(
"Incompatible units prevent calculating total amount",
main_label
)
.label(support_label)
.set_source(e),
)
}
}
}
}
if let Some(note) = &located_ingredient.note {
self.ctx.error(note_reference_error(
note.span(),
implicit,
definition_location.span(),
definition_location.note.as_ref().map(|n| n.span()),
));
}
if definition.quantity.is_some()
&& new_igr.quantity.is_some()
&& !definition
.relation
.is_defined_in_step()
.expect("definition")
{
self.ctx.error(conflicting_reference_quantity_error(
ingredient.quantity.unwrap().span(),
definition_location.span(),
implicit,
));
}
if let Some((ref_q, def_q)) =
&new_igr.quantity.as_ref().zip(definition.quantity.as_ref())
{
let ref_is_text = ref_q.value().is_text();
let def_is_text = def_q.value().is_text();
if ref_is_text != def_is_text {
let ref_q_loc = located_ingredient.quantity.as_ref().unwrap().span();
let def_q_loc = definition_location.quantity.as_ref().unwrap().span();
let (text_quantity_span, number_quantity_span) = if ref_is_text {
(ref_q_loc, def_q_loc)
} else {
(def_q_loc, ref_q_loc)
};
self.ctx.warn(text_val_in_ref_warn(
text_quantity_span,
number_quantity_span,
implicit,
));
}
}
Ingredient::set_referenced_from(&mut self.content.ingredients, references_to);
}
if new_igr.modifiers.contains(Modifiers::RECIPE)
&& !new_igr.modifiers.contains(Modifiers::REF)
{
if let Some(checker) = self.parse_options.recipe_ref_check.as_mut() {
let res = checker(&new_igr.name);
if let Some(mut diag) = res
.into_source_diag(|| format!("Referenced recipe not found: {}", new_igr.name))
{
diag.add_label(label!(location));
self.ctx.push(diag);
}
}
}
self.locations.ingredients.push(located_ingredient);
self.content.ingredients.push(new_igr);
self.content.ingredients.len() - 1
}
fn resolve_intermediate_ref(
&mut self,
inter_data: Located<IntermediateData>,
) -> Result<IngredientRelation, SourceDiag> {
use IntermediateRefMode as Mode;
use IntermediateTargetKind as Kind;
assert!(!inter_data.val.is_negative());
let val = inter_data.val as u32;
const INVALID: &str = "Invalid intermediate preparation reference";
if val == 0 {
match inter_data.ref_mode {
Mode::Number => {
return Err(error!(
format!("{INVALID}: number is 0"),
label!(inter_data.span())
)
.hint("Step and section numbers start at 1"));
}
Mode::Relative => {
return Err(error!(
format!("{INVALID}: relative reference to self"),
label!(inter_data.span())
)
.hint("Relative reference value has to be greater than 0"));
}
}
}
let bounds = |help: String| {
Err(error!(
format!("{INVALID}: value out of bounds"),
label!(inter_data.span())
)
.hint(help))
};
let relation = match (inter_data.target_kind, inter_data.ref_mode) {
(Kind::Step, Mode::Number) => {
let index = self
.current_section
.content
.iter()
.enumerate()
.filter_map(|(i, c)| c.is_step().then_some(i))
.nth((val - 1) as usize);
if index.is_none() {
return bounds(format!(
"The value has to be a previous step number: {}",
match self.step_counter.saturating_sub(1) {
0 => "no steps before this one".to_string(),
1 => "1".to_string(),
max => format!("1 to {max}"),
}
));
}
IngredientRelation::reference(index.unwrap(), IngredientReferenceTarget::Step)
}
(Kind::Step, Mode::Relative) => {
let index = self
.current_section
.content
.iter()
.enumerate()
.filter_map(|(i, c)| c.is_step().then_some(i))
.nth_back((val - 1) as usize);
if index.is_none() {
return bounds(format!(
"The current section {} steps before this one",
match self.step_counter.saturating_sub(1) {
0 => "has no".to_string(),
before => format!("only has {before}"),
}
));
}
IngredientRelation::reference(index.unwrap(), IngredientReferenceTarget::Step)
}
(Kind::Section, Mode::Number) => {
let index = (val - 1) as usize;
if index >= self.content.sections.len() {
return bounds(format!(
"The value has to be a previous section number: {}",
match self.content.sections.len() {
0 => "no sections before this one".to_string(),
1 => "1".to_string(),
max => format!("1 to {max}"),
}
));
}
IngredientRelation::reference(index, IngredientReferenceTarget::Section)
}
(Kind::Section, Mode::Relative) => {
let val = val as usize;
if val > self.content.sections.len() {
return bounds(format!(
"The recipe {} sections before this one",
match self.content.sections.len() {
0 => "has no".to_string(),
before => format!("only has {before}"),
}
));
}
let index = self.content.sections.len().saturating_sub(val);
IngredientRelation::reference(index, IngredientReferenceTarget::Section)
}
};
Ok(relation)
}
fn cookware(&mut self, cookware: Located<parser::Cookware<'i>>) -> usize {
let located_cookware = cookware.clone();
let (cookware, location) = cookware.take_pair();
let mut new_cw = Cookware {
name: cookware.name.text_trimmed().into_owned(),
alias: cookware.alias.map(|t| t.text_trimmed().into_owned()),
quantity: cookware.quantity.clone().map(|q| self.quantity(q, false)),
note: cookware.note.map(|n| n.text_trimmed().into_owned()),
modifiers: cookware.modifiers.into_inner(),
relation: ComponentRelation::Definition {
referenced_from: Vec::new(),
defined_in_step: self.define_mode != DefineMode::Components,
},
};
if let Some((references_to, implicit)) =
self.resolve_reference(&mut new_cw, location, located_cookware.modifiers.span())
{
let definition = &self.content.cookware[references_to];
let definition_location = &self.locations.cookware[references_to];
assert!(definition.relation.is_definition());
if let Some(note) = &located_cookware.note {
self.ctx.error(note_reference_error(
note.span(),
implicit,
definition_location.span(),
definition_location.note.as_ref().map(|n| n.span()),
));
}
if definition.quantity.is_some()
&& new_cw.quantity.is_some()
&& !definition
.relation
.is_defined_in_step()
.expect("definition")
{
self.ctx.error(conflicting_reference_quantity_error(
located_cookware.quantity.as_ref().unwrap().span(),
definition_location.span(),
implicit,
));
}
if let Some((ref_q, def_q)) =
&new_cw.quantity.as_ref().zip(definition.quantity.as_ref())
{
let ref_is_text = ref_q.value().is_text();
let def_is_text = def_q.value().is_text();
if ref_is_text != def_is_text {
let ref_q_loc = located_cookware.quantity.as_ref().unwrap().span();
let def_q_loc = definition_location.quantity.as_ref().unwrap().span();
let (text_quantity_span, number_quantity_span) = if ref_is_text {
(ref_q_loc, def_q_loc)
} else {
(def_q_loc, ref_q_loc)
};
self.ctx.warn(text_val_in_ref_warn(
text_quantity_span,
number_quantity_span,
implicit,
));
}
}
Cookware::set_referenced_from(&mut self.content.cookware, references_to);
}
self.locations.cookware.push(located_cookware);
self.content.cookware.push(new_cw);
self.content.cookware.len() - 1
}
fn timer(&mut self, timer: Located<parser::Timer<'i>>) -> usize {
let located_timer = timer.clone();
let (timer, _span) = timer.take_pair();
let quantity = timer.quantity.map(|q| {
let quantity = self.quantity(q, false);
if self.extensions.contains(Extensions::ADVANCED_UNITS) {
let located_quantity = located_timer.quantity.as_ref().unwrap();
if quantity.value().is_text() {
self.ctx.error(error!(
format!("Timer value is text: {}", quantity.value()),
label!(located_quantity.value.span(), "expected a number here")
));
}
if let Some(unit_text) = quantity.unit() {
let unit_span = located_quantity.unit.as_ref().unwrap().span();
match quantity.unit_info(self.converter) {
Some(unit) => {
if unit.physical_quantity != PhysicalQuantity::Time {
self.ctx.error(error!(
format!("Timer unit is not time: {unit}"),
label!(
unit_span,
"expected time, not {}",
unit.physical_quantity
)
));
}
}
None => self.ctx.error(error!(
format!("Unknown timer unit: {unit_text}"),
label!(unit_span, "expected time unit")
)),
}
}
}
quantity
});
let new_timer = Timer {
name: timer.name.map(|t| t.text_trimmed().into_owned()),
quantity,
};
self.content.timers.push(new_timer);
self.content.timers.len() - 1
}
fn quantity(
&mut self,
quantity: Located<parser::Quantity<'i>>,
is_ingredient: bool,
) -> Quantity {
let parser::Quantity { value, unit, .. } = quantity.into_inner();
let (value, scalable) = self.value(value, is_ingredient);
Quantity {
value,
unit: unit.map(|t| t.text_trimmed().into_owned()),
scalable,
}
}
fn value(&mut self, value: parser::QuantityValue, is_ingredient: bool) -> (Value, bool) {
let parser::QuantityValue {
value,
scaling_lock,
} = value;
let has_scaling_lock = scaling_lock.is_some();
let is_text = value.is_text();
if is_ingredient && !is_text && !has_scaling_lock {
return (value.into_inner(), true);
}
if has_scaling_lock {
let mut warning = warning!(
"Unnecessary scaling lock modifier",
label!(value.span(), "this scaling lock has no effect")
);
if !is_ingredient {
warning.add_hint("Only ingredients can be scaled, scaling lock is not needed here");
} else if is_text {
warning.add_hint("Text values cannot be scaled, scaling lock is not needed here");
}
self.ctx.warn(warning);
}
(value.into_inner(), false)
}
fn resolve_reference<C: RefComponent>(
&mut self,
new: &mut C,
location: Span,
modifiers_location: Span,
) -> Option<(usize, bool)> {
let new_name = unicase::UniCase::new(new.name());
let all = C::all(&self.content);
let same_name_cell = std::cell::OnceCell::new();
let same_name = || {
*same_name_cell.get_or_init(|| {
C::all(&self.content).iter().rposition(|other: &C| {
!other.modifiers().contains(Modifiers::REF)
&& new_name == unicase::UniCase::new(other.name())
})
})
};
let conflicing_modifiers = |conflict: Modifiers, help: CowStr, implicit: bool| {
let mut e = error!(
format!("Unsupported modifier combination with reference: {conflict}"),
label!(modifiers_location)
)
.hint(help);
if implicit {
e.add_hint(IMPLICIT_REF_WARN);
}
e
};
let redundant_modifier = |redundant: &'static str, help: String| {
warning!(
format!("Redundant {redundant} modifier"),
label!(modifiers_location)
)
.hint(help)
.hint(format!(
"In the current mode, by default, {}",
match (self.define_mode, self.duplicate_mode) {
(DefineMode::Steps, _) => "all components are references",
(_, DuplicateMode::Reference) =>
"components are definitions but duplicates are references",
_ => "all components are definitions",
}
))
};
if new.modifiers().contains(Modifiers::NEW | Modifiers::REF) {
self.ctx.error(conflicing_modifiers(
*new.modifiers(),
"New (+) can never be combined with ref (&)".into(),
false,
));
return None;
}
if new.modifiers().contains(Modifiers::NEW) {
if self.define_mode != DefineMode::Steps {
if self.duplicate_mode == DuplicateMode::Reference && same_name().is_none() {
self.ctx.warn(redundant_modifier(
"new (+)",
format!("There are no {}s with the same name before", C::container()),
));
} else if self.duplicate_mode == DuplicateMode::New {
self.ctx.warn(redundant_modifier(
"new (+)",
format!("This {} is already a definition", C::container()),
));
}
}
return None;
}
if (self.duplicate_mode == DuplicateMode::Reference
|| self.define_mode == DefineMode::Steps)
&& new.modifiers().contains(Modifiers::REF)
{
self.ctx.warn(redundant_modifier(
"reference (&)",
format!("This {} is already a reference", C::container()),
));
}
let treat_as_reference = new.modifiers().contains(Modifiers::REF)
|| self.define_mode == DefineMode::Steps
|| self.duplicate_mode == DuplicateMode::Reference && same_name().is_some();
if !treat_as_reference {
return None;
}
let implicit = !new.modifiers().contains(Modifiers::REF);
if let Some(references_to) = same_name() {
let referenced = &all[references_to];
assert!(!referenced.modifiers().contains(Modifiers::REF));
let inherited = *referenced.modifiers() & C::inherit_modifiers();
let conflict = *new.modifiers() & !inherited & !Modifiers::REF;
*new.modifiers_mut() |= inherited;
*new.modifiers_mut() |= Modifiers::REF;
new.set_reference(references_to);
if !conflict.is_empty() {
let help = {
let extra = conflict
.iter_names()
.map(|(s, _)| s.to_lowercase())
.collect::<Vec<_>>()
.join(", ");
if implicit {
format!("Mark the definition as {extra} or add new (+) to this")
} else {
format!("Mark the definition as {extra} or remove the reference (&)")
}
};
self.ctx
.error(conflicing_modifiers(conflict, help.into(), implicit));
}
Some((references_to, implicit))
} else {
self.ctx.error({
let mut e = error!(
format!("Reference not found: {}", new.name()),
label!(location)
)
.hint(format!(
"A non reference {} with the same name defined BEFORE cannot be found",
C::container()
));
if implicit {
e.add_hint(IMPLICIT_REF_WARN);
}
e
});
None
}
}
}
trait RefComponent: Sized {
fn name(&self) -> &str;
fn modifiers(&self) -> &Modifiers;
fn modifiers_mut(&mut self) -> &mut Modifiers;
fn inherit_modifiers() -> Modifiers;
fn container() -> &'static str;
fn set_reference(&mut self, references_to: usize);
fn set_referenced_from(all: &mut [Self], references_to: usize);
fn all(content: &Recipe) -> &[Self];
}
impl RefComponent for Ingredient {
#[inline]
fn name(&self) -> &str {
&self.name
}
#[inline]
fn modifiers(&self) -> &Modifiers {
&self.modifiers
}
#[inline]
fn modifiers_mut(&mut self) -> &mut Modifiers {
&mut self.modifiers
}
#[inline]
fn inherit_modifiers() -> Modifiers {
Modifiers::HIDDEN | Modifiers::OPT | Modifiers::RECIPE
}
#[inline]
fn container() -> &'static str {
"ingredient"
}
#[inline]
fn set_reference(&mut self, references_to: usize) {
self.relation =
IngredientRelation::reference(references_to, IngredientReferenceTarget::Ingredient);
}
fn set_referenced_from(all: &mut [Self], references_to: usize) {
let new_index = all.len();
match all[references_to].relation.referenced_from_mut() {
Some(referenced_from) => {
referenced_from.push(new_index);
}
None => panic!("Reference to reference"),
}
}
#[inline]
fn all(content: &Recipe) -> &[Self] {
&content.ingredients
}
}
impl RefComponent for Cookware {
#[inline]
fn name(&self) -> &str {
&self.name
}
#[inline]
fn modifiers(&self) -> &Modifiers {
&self.modifiers
}
#[inline]
fn modifiers_mut(&mut self) -> &mut Modifiers {
&mut self.modifiers
}
#[inline]
fn inherit_modifiers() -> Modifiers {
Modifiers::HIDDEN | Modifiers::OPT
}
#[inline]
fn container() -> &'static str {
"cookware item"
}
#[inline]
fn set_reference(&mut self, references_to: usize) {
self.relation = ComponentRelation::Reference { references_to };
}
fn set_referenced_from(all: &mut [Self], references_to: usize) {
let new_index = all.len();
match &mut all[references_to].relation {
ComponentRelation::Definition {
referenced_from, ..
} => referenced_from.push(new_index),
ComponentRelation::Reference { .. } => panic!("Reference to reference"),
}
}
#[inline]
fn all(content: &Recipe) -> &[Self] {
&content.cookware
}
}
fn find_inline_quantity<'a>(
text: &'a str,
converter: &Converter,
) -> Option<(&'a str, Quantity, &'a str)> {
let mut i = 0;
fn eat_word<'a>(text: &'a str, i: &mut usize) -> Option<&'a str> {
let s = &text[*i..];
let offset = s.find(|c: char| c.is_whitespace()).or({
if !s.is_empty() {
Some(s.len())
} else {
None
}
})?;
let word = &s[..offset];
*i += offset;
Some(word)
}
fn eat_whitespace<'a>(text: &'a str, i: &mut usize) -> Option<&'a str> {
let offset = text[*i..].find(|c: char| !c.is_whitespace())?;
let ws = &text[*i..*i + offset];
*i += offset;
Some(ws)
}
#[cfg(debug_assertions)]
let mut prev = 0;
while let Some(offset) = text[i..].find(|c: char| c.is_ascii_digit()) {
i += offset;
let before: &str;
let neg: bool;
if i > 0 && text.as_bytes()[i - 1] == b'-' {
before = &text[..i - 1];
neg = true;
} else {
before = &text[..i];
neg = false;
}
let w1 = eat_word(text, &mut i)?; let first_non_digit =
w1.find(|c: char| !c.is_ascii_digit() && c != '.' && !c.is_whitespace());
let (mut number, mut unit) = if let Some(mid) = first_non_digit {
w1.split_at(mid)
} else {
let _ = eat_whitespace(text, &mut i);
let w2 = eat_word(text, &mut i)?; (w1, w2)
};
number = number.trim();
unit = unit.trim();
#[cfg(debug_assertions)]
{
debug_assert!(prev < i); prev = i;
}
let after = &text[i..];
let Ok(mut number) = number.parse::<f64>() else {
continue;
};
if converter.find_unit(unit).is_none() {
continue;
};
if neg {
number = -number;
}
let q = Quantity::new(Value::from(number), Some(unit.to_string()));
return Some((before, q, after));
}
None
}
fn note_reference_error(
span: Span,
implicit: bool,
def_span: Span,
def_note_span: Option<Span>,
) -> SourceDiag {
let span = Span::new(span.start().saturating_sub(1), span.end() + 1);
let mut e = error!("Note not allowed in reference", label!(span, "remove this"));
if let Some(sp) = def_note_span {
e.add_label(label!(sp, "the definition already has a note"));
} else {
e.add_hint("Add the note in the definition of the ingredient");
e.add_label(label!(Span::pos(def_span.end()), "add the note here"));
}
if implicit {
e.add_hint(IMPLICIT_REF_WARN);
}
e
}
fn conflicting_reference_quantity_error(
ref_quantity_span: Span,
def_span: Span,
implicit: bool,
) -> SourceDiag {
let mut e = error!(
"Conflicting component reference quantities",
label!(ref_quantity_span, "reference with quantity")
)
.label(label!(
def_span,
"definition with quantity outside a step"
))
.hint("If the component is not defined in a step and has a quantity, its references cannot have a quantity");
if implicit {
e.add_hint(IMPLICIT_REF_WARN);
}
e
}
fn text_val_in_ref_warn(
text_quantity_span: Span,
number_quantity_span: Span,
implicit: bool,
) -> SourceDiag {
let mut w = warning!(
"Text value may prevent calculating total amount",
label!(text_quantity_span, "can't operate with text value")
)
.label(label!(number_quantity_span, "numeric value"))
.hint("Use numeric values so they can be added together");
if implicit {
w.add_hint(IMPLICIT_REF_WARN);
}
w
}
fn yaml_find_key_position(text: &str, key: &str) -> Option<usize> {
let mut offset = 0;
for line in text.split_inclusive('\n') {
let l_offset = offset;
offset += line.len();
let line = line.trim_start();
let Some((k, _)) = line.split_once(':') else {
continue;
};
let Some(start) = k.find(|c: char| !c.is_ascii_whitespace()) else {
continue;
};
if k[start..].trim_ascii_end() == key {
return Some(l_offset + start);
}
}
None
}
fn parse_reference(name: &str) -> Option<RecipeReference> {
if name.starts_with("./")
|| name.starts_with("../")
|| name.starts_with(".\\")
|| name.starts_with("..\\")
{
let path = name.replace('\\', "/");
let mut components: Vec<String> = path.split('/').map(String::from).collect();
let file_stem = components.pop().unwrap();
if !file_stem.is_empty() {
Some(RecipeReference {
components,
name: file_stem,
})
} else {
None
}
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_reference() {
assert_eq!(
parse_reference("./pasta/spaghetti"),
Some(RecipeReference {
components: vec![".".to_string(), "pasta".to_string()],
name: "spaghetti".into()
})
);
assert_eq!(
parse_reference("../sauces/tomato"),
Some(RecipeReference {
components: vec!["..".to_string(), "sauces".to_string()],
name: "tomato".into()
})
);
assert_eq!(
parse_reference(r#".\pasta\spaghetti"#),
Some(RecipeReference {
components: vec![".".to_string(), "pasta".to_string()],
name: "spaghetti".into()
})
);
assert_eq!(
parse_reference(r#"..\sauces\tomato"#),
Some(RecipeReference {
components: vec!["..".to_string(), "sauces".to_string()],
name: "tomato".into()
})
);
assert_eq!(
parse_reference("./recipes/italian/pasta/spaghetti"),
Some(RecipeReference {
components: vec![
".".to_string(),
"recipes".to_string(),
"italian".to_string(),
"pasta".to_string()
],
name: "spaghetti".into()
})
);
assert_eq!(
parse_reference("./spaghetti"),
Some(RecipeReference {
components: vec![".".to_string()],
name: "spaghetti".into()
})
);
assert_eq!(
parse_reference("./../../spaghetti"),
Some(RecipeReference {
components: vec![".".to_string(), "..".to_string(), "..".to_string()],
name: "spaghetti".into()
})
);
assert_eq!(parse_reference("spaghetti"), None);
assert_eq!(parse_reference("pasta/spaghetti"), None);
assert_eq!(parse_reference("pasta\\spaghetti"), None);
assert_eq!(parse_reference("/pasta/spaghetti"), None);
assert_eq!(parse_reference("\\pasta\\spaghetti"), None);
assert_eq!(parse_reference("./"), None);
assert_eq!(parse_reference(".\\"), None);
assert_eq!(parse_reference("../"), None);
assert_eq!(parse_reference("..\\"), None);
let reference = RecipeReference {
components: vec![".".to_string()],
name: "Sicilian-style Scottadito Lamb Chops".into(),
};
assert_eq!(
"./Sicilian-style Scottadito Lamb Chops",
reference.path("/")
);
}
}