//! Space or tab (eol) occurs in [destination][], [label][], and [title][]. //! //! ## Grammar //! //! Space or tab (eol) forms with the following BNF //! (see [construct][crate::construct] for character groups): //! //! ```bnf //! space_or_tab_eol ::= 1*space_or_tab | *space_or_tab eol *space_or_tab //! ``` //! //! Importantly, this allows one line ending, but not blank lines. //! //! ## References //! //! * [`micromark-factory-space/index.js` in `micromark`](https://github.com/micromark/micromark/blob/main/packages/micromark-factory-space/dev/index.js) //! //! [destination]: crate::construct::partial_destination //! [label]: crate::construct::partial_label //! [title]: crate::construct::partial_title use crate::construct::partial_space_or_tab::{ space_or_tab_with_options, Options as SpaceOrTabOptions, }; use crate::event::{Content, Link, Name}; use crate::state::{Name as StateName, State}; use crate::subtokenize::link; use crate::tokenizer::Tokenizer; /// Configuration. #[derive(Debug)] pub struct Options { /// Connect this whitespace to the previous. pub connect: bool, /// Embedded content type to use. pub content: Option, } /// `space_or_tab_eol` pub fn space_or_tab_eol(tokenizer: &mut Tokenizer) -> StateName { space_or_tab_eol_with_options( tokenizer, Options { content: None, connect: false, }, ) } /// `space_or_tab_eol`, with the given options. pub fn space_or_tab_eol_with_options(tokenizer: &mut Tokenizer, options: Options) -> StateName { tokenizer.tokenize_state.space_or_tab_eol_content = options.content; tokenizer.tokenize_state.space_or_tab_eol_connect = options.connect; StateName::SpaceOrTabEolStart } /// Start of whitespace with at most one eol. /// /// ```markdown /// > | a␠␠b /// ^ /// > | a␠␠␊ /// ^ /// | ␠␠b /// ``` pub fn start(tokenizer: &mut Tokenizer) -> State { match tokenizer.current { Some(b'\t' | b' ') => { tokenizer.attempt( State::Next(StateName::SpaceOrTabEolAfterFirst), State::Next(StateName::SpaceOrTabEolAtEol), ); State::Retry(space_or_tab_with_options( tokenizer, SpaceOrTabOptions { kind: Name::SpaceOrTab, min: 1, max: usize::MAX, content: tokenizer.tokenize_state.space_or_tab_eol_content.clone(), connect: tokenizer.tokenize_state.space_or_tab_eol_connect, }, )) } _ => State::Retry(StateName::SpaceOrTabEolAtEol), } } /// After initial whitespace, at optional eol. /// /// ```markdown /// > | a␠␠b /// ^ /// > | a␠␠␊ /// ^ /// | ␠␠b /// ``` pub fn after_first(tokenizer: &mut Tokenizer) -> State { tokenizer.tokenize_state.space_or_tab_eol_ok = true; debug_assert!( tokenizer.tokenize_state.space_or_tab_eol_content.is_none(), "expected no content" ); // If the above ever errors, set `tokenizer.tokenize_state.space_or_tab_eol_connect: true` in that case. State::Retry(StateName::SpaceOrTabEolAtEol) } /// After optional whitespace, at eol. /// /// ```markdown /// > | a␠␠b /// ^ /// > | a␠␠␊ /// ^ /// | ␠␠b /// > | a␊ /// ^ /// | ␠␠b /// ``` pub fn at_eol(tokenizer: &mut Tokenizer) -> State { if let Some(b'\n') = tokenizer.current { if let Some(ref content) = tokenizer.tokenize_state.space_or_tab_eol_content { tokenizer.enter_link( Name::LineEnding, Link { previous: None, next: None, content: content.clone(), }, ); } else { tokenizer.enter(Name::LineEnding); } if tokenizer.tokenize_state.space_or_tab_eol_connect { let index = tokenizer.events.len() - 1; link(&mut tokenizer.events, index); } else if tokenizer.tokenize_state.space_or_tab_eol_content.is_some() { tokenizer.tokenize_state.space_or_tab_eol_connect = true; } tokenizer.consume(); tokenizer.exit(Name::LineEnding); State::Next(StateName::SpaceOrTabEolAfterEol) } else { let ok = tokenizer.tokenize_state.space_or_tab_eol_ok; tokenizer.tokenize_state.space_or_tab_eol_content = None; tokenizer.tokenize_state.space_or_tab_eol_connect = false; tokenizer.tokenize_state.space_or_tab_eol_ok = false; if ok { State::Ok } else { State::Nok } } } /// After eol. /// /// ```markdown /// | a␠␠␊ /// > | ␠␠b /// ^ /// | a␊ /// > | ␠␠b /// ^ /// ``` pub fn after_eol(tokenizer: &mut Tokenizer) -> State { if matches!(tokenizer.current, Some(b'\t' | b' ')) { tokenizer.attempt(State::Next(StateName::SpaceOrTabEolAfterMore), State::Nok); State::Retry(space_or_tab_with_options( tokenizer, SpaceOrTabOptions { kind: Name::SpaceOrTab, min: 1, max: usize::MAX, content: tokenizer.tokenize_state.space_or_tab_eol_content.clone(), connect: tokenizer.tokenize_state.space_or_tab_eol_connect, }, )) } else { State::Retry(StateName::SpaceOrTabEolAfterMore) } } /// After optional final whitespace. /// /// ```markdown /// | a␠␠␊ /// > | ␠␠b /// ^ /// | a␊ /// > | ␠␠b /// ^ /// ``` pub fn after_more(tokenizer: &mut Tokenizer) -> State { debug_assert!( !matches!(tokenizer.current, None | Some(b'\n')), "did not expect blank line" ); // If the above ever starts erroring, gracefully `State::Nok` on it. // Currently it doesn’t happen, as we only use this in content, which does // not allow blank lines. tokenizer.tokenize_state.space_or_tab_eol_content = None; tokenizer.tokenize_state.space_or_tab_eol_connect = false; tokenizer.tokenize_state.space_or_tab_eol_ok = false; State::Ok }