mod block_parser;
mod frontmatter;
mod metadata;
mod model;
mod quantity;
mod section;
mod step;
mod text_block;
mod token_stream;
pub use model::*;
pub use quantity::ParsedQuantity;
use std::collections::VecDeque;
use crate::{
error::SourceDiag,
lexer::T,
located::Located,
parser::{
metadata::metadata_entry, section::section, step::parse_step, text_block::parse_text_block,
},
span::Span,
text::Text,
Extensions,
};
pub(crate) use block_parser::BlockParser;
use token_stream::{Token, TokenStream};
#[derive(Debug, Clone, PartialEq)]
pub enum Event<'i> {
YAMLFrontMatter(Text<'i>),
Metadata { key: Text<'i>, value: Text<'i> },
Section { name: Option<Text<'i>> },
Start(BlockKind),
End(BlockKind),
Text(Text<'i>),
Ingredient(Located<Ingredient<'i>>),
Cookware(Located<Cookware<'i>>),
Timer(Located<Timer<'i>>),
Error(SourceDiag),
Warning(SourceDiag),
}
#[derive(Debug, Clone, PartialEq)]
pub enum BlockKind {
Step,
Text,
}
#[derive(Debug)]
pub struct PullParser<'i, T>
where
T: Iterator<Item = Token>,
{
input: &'i str,
tokens: std::iter::Peekable<T>,
block: Vec<Token>,
queue: VecDeque<Event<'i>>,
extensions: Extensions,
old_style_metadata: bool,
}
impl<'i> PullParser<'i, TokenStream<'i>> {
pub fn new(input: &'i str, extensions: Extensions) -> Self {
if let Some(fm) = frontmatter::parse_frontmatter(input) {
let mut events = VecDeque::new();
events.push_back(Event::YAMLFrontMatter(Text::from_str(
fm.yaml_text,
fm.yaml_offset,
)));
let mut tokens = TokenStream::new(fm.cooklang_text);
tokens.offset(fm.cooklang_offset);
Self {
input,
tokens: tokens.peekable(),
block: Vec::new(),
extensions,
queue: events,
old_style_metadata: false,
}
} else {
let tokens = TokenStream::new(input);
Self {
input,
tokens: tokens.peekable(),
block: Vec::new(),
extensions,
queue: VecDeque::new(),
old_style_metadata: true,
}
}
}
}
impl<'i, T> PullParser<'i, T>
where
T: Iterator<Item = Token>,
{
pub fn into_meta_iter(mut self) -> impl Iterator<Item = Event<'i>> {
std::iter::from_fn(move || self.next_metadata())
}
}
fn is_empty_token(tok: &Token) -> bool {
matches!(
tok.kind,
T![ws] | T![block comment] | T![line comment] | T![newline]
)
}
fn is_single_line_marker(first: Option<&Token>) -> bool {
matches!(first, Some(mt![meta | =]))
}
struct LineInfo {
is_empty: bool,
is_single_line: bool,
}
impl<'i, T> PullParser<'i, T>
where
T: Iterator<Item = Token>,
{
fn pull_line(&mut self) -> Option<LineInfo> {
let mut is_empty = true;
let mut no_tokens = true;
let is_single_line = is_single_line_marker(self.tokens.peek());
for tok in self.tokens.by_ref() {
self.block.push(tok);
no_tokens = false;
if !is_empty_token(&tok) {
is_empty = false;
}
if tok.kind == T![newline] {
break;
}
}
if no_tokens {
None
} else {
Some(LineInfo {
is_empty,
is_single_line,
})
}
}
pub(crate) fn next_block(&mut self) -> Option<()> {
self.block.clear();
let mut start = 0;
let mut end;
let mut current_line = self.pull_line()?;
while current_line.is_empty {
start = self.block.len();
current_line = self.pull_line()?;
}
let multiline = !current_line.is_single_line;
end = self.block.len();
if multiline {
loop {
if is_single_line_marker(self.tokens.peek()) {
break;
}
match self.pull_line() {
None => break,
Some(line) if line.is_empty => break,
_ => {}
}
end = self.block.len();
}
}
while let mt![newline] = self.block[end - 1] {
if end <= start {
break;
}
end -= 1;
}
let trimmed_block = &self.block[start..end];
if trimmed_block.is_empty() {
return None;
}
let mut bp = BlockParser::new(trimmed_block, self.input, &mut self.queue, self.extensions);
parse_block(&mut bp, self.old_style_metadata);
bp.finish();
Some(())
}
fn next_metadata_block(&mut self) -> Option<()> {
if !self.old_style_metadata {
return None;
}
self.block.clear();
let mut last = T![newline];
loop {
let curr = self.tokens.peek()?.kind;
if last == T![newline] && curr == T![meta] {
break;
}
self.tokens.next();
last = curr;
}
for tok in self.tokens.by_ref() {
if tok.kind == T![newline] {
break;
}
self.block.push(tok);
}
let mut bp = BlockParser::new(&self.block, self.input, &mut self.queue, self.extensions);
if let Some(ev) = metadata_entry(&mut bp) {
bp.event(ev);
bp.finish(); }
Some(())
}
pub(crate) fn next_metadata(&mut self) -> Option<Event<'i>> {
self.queue.pop_front().or_else(|| {
self.next_metadata_block()?;
self.next_metadata()
})
}
}
impl<'i, T> Iterator for PullParser<'i, T>
where
T: Iterator<Item = Token>,
{
type Item = Event<'i>;
fn next(&mut self) -> Option<Self::Item> {
self.queue.pop_front().or_else(|| {
self.next_block()?;
self.next()
})
}
}
fn parse_block(block: &mut BlockParser, old_style_metadata: bool) {
let meta_or_section = match block.peek() {
T![meta] => block.with_recover(|bp| {
metadata_entry(bp).filter(|ev| {
let Event::Metadata { key, .. } = ev else {
unreachable!()
};
let key_t = key.text_outer_trimmed();
let is_config_key = key_t.starts_with('[') && key_t.ends_with(']');
let modes_active = bp.extension(Extensions::MODES);
(is_config_key && modes_active) || old_style_metadata
})
}),
T![=] => block.with_recover(section),
_ => None,
};
if let Some(ev) = meta_or_section {
block.event(ev);
} else {
parse_multiline_block(block);
}
}
fn parse_multiline_block(bp: &mut BlockParser) {
debug_assert!(bp
.tokens()
.last()
.map(|t| t.kind != T![newline])
.unwrap_or(true));
let is_empty = bp.tokens().iter().all(|t| {
matches!(
t.kind,
T![ws] | T![line comment] | T![block comment] | T![newline]
)
});
if is_empty {
bp.consume_rest();
return;
}
let is_text = bp.peek() == T![>];
if is_text {
parse_text_block(bp);
} else {
parse_step(bp);
}
}
pub(crate) fn tokens_span(tokens: &[Token]) -> Span {
debug_assert!(!tokens.is_empty(), "tokens_span tokens empty");
let start = tokens.first().unwrap().span.start();
let end = tokens.last().unwrap().span.end();
Span::new(start, end)
}
macro_rules! mt {
($($reprs:tt)|*) => {
$($crate::parser::token_stream::Token {
kind: T![$reprs],
..
})|+
}
}
pub(crate) use mt;
macro_rules! error {
($msg:expr, $label:expr $(,)?) => {
$crate::error::SourceDiag::error($msg, $label, $crate::error::Stage::Parse)
};
}
use error;
macro_rules! warning {
($msg:expr, $label:expr $(,)?) => {
$crate::error::SourceDiag::warning($msg, $label, $crate::error::Stage::Parse)
};
}
use warning;
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::*;
use crate::ast::*;
#[test]
fn just_metadata() {
let parser = PullParser::new(
indoc! {r#">> entry: true
a test @step @salt{1%mg} more text
a test @step @salt{1%mg} more text
a test @step @salt{1%mg} more text
>> entry2: uwu
a test @step @salt{1%mg} more text
"#},
Extensions::empty(),
);
let events = parser.into_meta_iter().collect::<Vec<_>>();
assert_eq!(
events,
vec![
Event::Metadata {
key: Text::from_str(" entry", 2),
value: Text::from_str(" true", 10)
},
Event::Metadata {
key: Text::from_str(" entry2", 126),
value: Text::from_str(" uwu", 134)
},
]
);
}
#[test]
fn multiline_spaces() {
let parser = PullParser::new(
" This is a step -- comment\n and this line continues -- another comment",
Extensions::empty(),
);
let (ast, report) = build_ast(parser).into_tuple();
let (err, warn) = report.unzip();
assert!(warn.is_empty());
assert!(err.is_empty());
assert_eq!(
ast.unwrap().blocks,
vec![Block::Step {
items: vec![Item::Text({
let mut t = Text::empty(0);
t.append_str(" This is a step ", 0);
t.append_fragment(crate::text::TextFragment::soft_break("\n", 37));
t.append_str(" and this line continues ", 39);
t
})]
}]
);
}
}