cooklang 0.18.6

Cooklang parser with opt-in extensions
Documentation
use crate::{error::label, lexer::T};

use super::{tokens_span, warning, BlockParser, Event};

pub(crate) fn section<'i>(block: &mut BlockParser<'_, 'i>) -> Option<Event<'i>> {
    block.consume(T![=])?;
    block.consume_while(|t| t == T![=]);
    let name_pos = block.current_offset();
    let name_tokens = block.consume_while(|t| t != T![=]);
    let name = block.text(name_pos, name_tokens);
    block.consume_while(|t| t == T![=]);
    block.ws_comments();

    if !block.rest().is_empty() {
        block.warn(
            warning!(
                "A section block is invalid and it will be a step",
                label!(tokens_span(block.rest()), "remove this"),
            )
            .hint("After the ending `=` the line must end for it to be a valid section"),
        );
        return None;
    }

    let name = if name.is_text_empty() {
        None
    } else {
        Some(name)
    };
    Some(Event::Section { name })
}

#[cfg(test)]
mod tests {
    use std::collections::VecDeque;

    use super::*;
    use crate::{
        parser::{token_stream::TokenStream, BlockParser},
        span::Span,
        Extensions,
    };
    use test_case::test_case;

    macro_rules! text {
        ($s:expr; $offset:expr) => {
            text!($s; $offset, $offset + $s.len())
        };
        ($s:expr; $start:expr, $end:expr) => {
            Some(($s.to_string(), Span::new($start, $end)))
        };
    }

    #[test_case("= section" => text!(" section"; 1); "single char")]
    #[test_case("== section ==" => text!(" section "; 2) ; "fenced")]
    #[test_case("=" => None ; "no name single char")]
    #[test_case("===" => None ; "no name multiple char")]
    #[test_case("= ==" => None ; "no name unbalanced")]
    #[test_case("= = ==" => panics "failed to parse section" ; "more than one split")]
    #[test_case("== section ==    " => text!(" section "; 2) ; "trailing whitespace")]
    #[test_case("== section ==  -- comment  " => text!(" section "; 2) ; "trailing line comment")]
    #[test_case("== section ==  [- comment -]  " => text!(" section "; 2) ; "trailing block comment")]
    #[test_case("== section [- and a comment = -] ==" => text!(" section  "; 2, 33) ; "in between block comment")]
    #[test_case("== section -- and a comment" => text!(" section "; 2) ; "in between line comment")]
    fn test_section(input: &'static str) -> Option<(String, Span)> {
        let tokens = TokenStream::new(input).collect::<Vec<_>>();
        let mut events = VecDeque::new();
        let mut bp = BlockParser::new(&tokens, input, &mut events, Extensions::all());
        let event = section(&mut bp).expect("failed to parse section");
        bp.finish();
        assert!(events.is_empty());
        let Event::Section { name } = event else {
            panic!()
        };
        name.map(|text| (text.text().into_owned(), text.span()))
    }
}