//! Several helpers to parse whitespace (`space_or_tab`, `space_or_tab_eol`). //! //! ## References //! //! * [`micromark-factory-space/index.js` in `micromark`](https://github.com/micromark/micromark/blob/main/packages/micromark-factory-space/dev/index.js) use crate::subtokenize::link; use crate::token::Token; use crate::tokenizer::{Code, ContentType, State, StateFn, StateFnResult, Tokenizer}; /// Options to parse `space_or_tab`. #[derive(Debug)] pub struct Options { /// Minimum allowed characters (inclusive). pub min: usize, /// Maximum allowed characters (inclusive). pub max: usize, /// Token type to use for whitespace events. pub kind: Token, /// Connect this whitespace to the previous. pub connect: bool, /// Embedded content type to use. pub content_type: Option, } /// Options to parse `space_or_tab` and one optional eol, but no blank line. #[derive(Debug)] pub struct EolOptions { /// Connect this whitespace to the previous. pub connect: bool, /// Embedded content type to use. pub content_type: Option, } /// State needed to parse `space_or_tab`. #[derive(Debug)] struct Info { /// Current size. size: usize, /// Configuration. options: Options, } /// State needed to parse `space_or_tab_eol`. #[derive(Debug)] struct EolInfo { /// Whether to connect the next whitespace to the event before. connect: bool, /// Whether there was initial whitespace. ok: bool, /// Configuration. options: EolOptions, } /// One or more `space_or_tab`. /// /// ```bnf /// space_or_tab ::= 1*( ' ' '\t' ) /// ``` pub fn space_or_tab() -> Box { space_or_tab_min_max(1, usize::MAX) } /// Between `x` and `y` `space_or_tab`. /// /// ```bnf /// space_or_tab_min_max ::= x*y( ' ' '\t' ) /// ``` pub fn space_or_tab_min_max(min: usize, max: usize) -> Box { space_or_tab_with_options(Options { kind: Token::SpaceOrTab, min, max, content_type: None, connect: false, }) } /// `space_or_tab`, with the given options. pub fn space_or_tab_with_options(options: Options) -> Box { Box::new(|t, c| start(t, c, Info { size: 0, options })) } /// `space_or_tab`, or optionally `space_or_tab`, one `eol`, and /// optionally `space_or_tab`. /// /// ```bnf /// space_or_tab_eol ::= 1*( ' ' '\t' ) | 0*( ' ' '\t' ) eol 0*( ' ' '\t' ) /// ``` pub fn space_or_tab_eol() -> Box { space_or_tab_eol_with_options(EolOptions { content_type: None, connect: false, }) } /// `space_or_tab_eol`, with the given options. pub fn space_or_tab_eol_with_options(options: EolOptions) -> Box { Box::new(move |tokenizer, code| { let mut info = EolInfo { connect: false, ok: false, options, }; tokenizer.attempt( space_or_tab_with_options(Options { kind: Token::SpaceOrTab, min: 1, max: usize::MAX, content_type: info.options.content_type.clone(), connect: info.options.connect, }), move |ok| { if ok { info.ok = ok; if info.options.content_type.is_some() { info.connect = true; } } Box::new(|t, c| after_space_or_tab(t, c, info)) }, )(tokenizer, code) }) } /// Before `space_or_tab`. /// /// ```markdown /// > | a␠␠b /// ^ /// ``` fn start(tokenizer: &mut Tokenizer, code: Code, mut info: Info) -> StateFnResult { match code { Code::VirtualSpace | Code::Char('\t' | ' ') if info.options.max > 0 => { tokenizer .enter_with_content(info.options.kind.clone(), info.options.content_type.clone()); if info.options.content_type.is_some() { let index = tokenizer.events.len() - 1; link(&mut tokenizer.events, index); } tokenizer.consume(code); info.size += 1; (State::Fn(Box::new(|t, c| inside(t, c, info))), 0) } _ => { if info.options.min == 0 { (State::Ok, if matches!(code, Code::None) { 0 } else { 1 }) } else { (State::Nok, 0) } } } } /// In `space_or_tab`. /// /// ```markdown /// > | a␠␠b /// ^ /// ``` fn inside(tokenizer: &mut Tokenizer, code: Code, mut info: Info) -> StateFnResult { match code { Code::VirtualSpace | Code::Char('\t' | ' ') if info.size < info.options.max => { tokenizer.consume(code); info.size += 1; (State::Fn(Box::new(|t, c| inside(t, c, info))), 0) } _ => { tokenizer.exit(info.options.kind.clone()); if info.size >= info.options.min { (State::Ok, if matches!(code, Code::None) { 0 } else { 1 }) } else { (State::Nok, 0) } } } } /// `space_or_tab_eol`: after optionally first `space_or_tab`. /// /// ```markdown /// > | a /// ^ /// | b /// ``` fn after_space_or_tab(tokenizer: &mut Tokenizer, code: Code, mut info: EolInfo) -> StateFnResult { match code { Code::CarriageReturnLineFeed | Code::Char('\n' | '\r') => { tokenizer.enter_with_content(Token::LineEnding, info.options.content_type.clone()); if info.connect { let index = tokenizer.events.len() - 1; link(&mut tokenizer.events, index); } else if info.options.content_type.is_some() { info.connect = true; } tokenizer.consume(code); tokenizer.exit(Token::LineEnding); (State::Fn(Box::new(|t, c| after_eol(t, c, info))), 0) } _ if info.ok => (State::Ok, if matches!(code, Code::None) { 0 } else { 1 }), _ => (State::Nok, 0), } } /// `space_or_tab_eol`: after eol. /// /// ```markdown /// | a /// > | b /// ^ /// ``` #[allow(clippy::needless_pass_by_value)] fn after_eol(tokenizer: &mut Tokenizer, code: Code, info: EolInfo) -> StateFnResult { tokenizer.attempt_opt( space_or_tab_with_options(Options { kind: Token::SpaceOrTab, min: 1, max: usize::MAX, content_type: info.options.content_type, connect: info.connect, }), after_more_space_or_tab, )(tokenizer, code) } /// `space_or_tab_eol`: after more (optional) `space_or_tab`. /// /// ```markdown /// | a /// > | b /// ^ /// ``` fn after_more_space_or_tab(_tokenizer: &mut Tokenizer, code: Code) -> StateFnResult { // Blank line not allowed. if matches!( code, Code::None | Code::CarriageReturnLineFeed | Code::Char('\n' | '\r') ) { (State::Nok, 0) } else { (State::Ok, if matches!(code, Code::None) { 0 } else { 1 }) } }