//! Block quotes occur in the [document][] content type. //! //! ## Grammar //! //! Block quotes form with the following BNF //! (see [construct][crate::construct] for character groups): //! //! ```bnf //! block_quote_start ::= '>' [ space_or_tab ] //! block_quote_cont ::= '>' [ space_or_tab ] //! ``` //! //! Further lines that are not prefixed with `block_quote_cont` cause the block //! quote to be exited, except when those lines are lazy continuation. //! Like so many things in markdown, block quotes too are complex. //! See [*§ Phase 1: block structure* in `CommonMark`][commonmark-block] for //! more on parsing details. //! //! As block quote is a container, it takes several bytes from the start of the //! line, while the rest of the line includes more containers or flow. //! //! ## HTML //! //! Block quote relates to the `
` element in HTML. //! See [*§ 4.4.4 The `blockquote` element*][html-blockquote] in the HTML spec //! for more info. //! //! ## Recommendation //! //! Always use a single space after a block quote marker (`>`). //! Never use lazy continuation. //! //! ## Tokens //! //! * [`BlockQuote`][Name::BlockQuote] //! * [`BlockQuoteMarker`][Name::BlockQuoteMarker] //! * [`BlockQuotePrefix`][Name::BlockQuotePrefix] //! * [`SpaceOrTab`][Name::SpaceOrTab] //! //! ## References //! //! * [`block-quote.js` in `micromark`](https://github.com/micromark/micromark/blob/main/packages/micromark-core-commonmark/dev/lib/block-quote.js) //! * [*§ 5.1 Block quotes* in `CommonMark`](https://spec.commonmark.org/0.31/#block-quotes) //! //! [document]: crate::construct::document //! [html-blockquote]: https://html.spec.whatwg.org/multipage/grouping-content.html#the-blockquote-element //! [commonmark-block]: https://spec.commonmark.org/0.31/#phase-1-block-structure use crate::construct::partial_space_or_tab::space_or_tab_min_max; use crate::event::Name; use crate::state::{Name as StateName, State}; use crate::tokenizer::Tokenizer; use crate::util::constant::TAB_SIZE; /// Start of block quote. /// /// ```markdown /// > | > a /// ^ /// ``` pub fn start(tokenizer: &mut Tokenizer) -> State { if tokenizer.parse_state.options.constructs.block_quote { tokenizer.enter(Name::BlockQuote); State::Retry(StateName::BlockQuoteContStart) } else { State::Nok } } /// Start of block quote continuation. /// /// Also used to parse the first block quote opening. /// /// ```markdown /// | > a /// > | > b /// ^ /// ``` pub fn cont_start(tokenizer: &mut Tokenizer) -> State { if matches!(tokenizer.current, Some(b'\t' | b' ')) { tokenizer.attempt(State::Next(StateName::BlockQuoteContBefore), State::Nok); State::Retry(space_or_tab_min_max( tokenizer, 1, if tokenizer.parse_state.options.constructs.code_indented { TAB_SIZE - 1 } else { usize::MAX }, )) } else { State::Retry(StateName::BlockQuoteContBefore) } } /// At `>`, after optional whitespace. /// /// Also used to parse the first block quote opening. /// /// ```markdown /// | > a /// > | > b /// ^ /// ``` pub fn cont_before(tokenizer: &mut Tokenizer) -> State { match tokenizer.current { Some(b'>') => { tokenizer.enter(Name::BlockQuotePrefix); tokenizer.enter(Name::BlockQuoteMarker); tokenizer.consume(); tokenizer.exit(Name::BlockQuoteMarker); // If we are looking for obsidian block quote metadata... if tokenizer.parse_state.options.constructs.obs_block_quote { // Clean up our flags tokenizer.tokenize_state.seen = false; // Start looking for the metadata! tokenizer.attempt( State::Next(StateName::BlockQuoteContBeforeNoCallout), State::Next(StateName::BlockQuoteContAfter), ); return State::Next(StateName::ObsidianBlockQuoteCalloutStart); } State::Next(StateName::BlockQuoteContAfter) } _ => State::Nok, } } // Just the original version of the previous function, without looking for // Obsidian callout blocks... pub fn cont_before_no_callout(tokenizer: &mut Tokenizer) -> State { match tokenizer.current { Some(b'>') => { tokenizer.enter(Name::BlockQuotePrefix); tokenizer.enter(Name::BlockQuoteMarker); tokenizer.consume(); tokenizer.exit(Name::BlockQuoteMarker); State::Next(StateName::BlockQuoteContAfter) } _ => State::Nok, } } /// After `>`, before optional whitespace. /// /// ```markdown /// > | > a /// ^ /// > | >b /// ^ /// ``` pub fn cont_after(tokenizer: &mut Tokenizer) -> State { if let Some(b'\t' | b' ') = tokenizer.current { tokenizer.enter(Name::SpaceOrTab); tokenizer.consume(); tokenizer.exit(Name::SpaceOrTab); } tokenizer.exit(Name::BlockQuotePrefix); State::Ok } pub fn obs_callout_start(tokenizer: &mut Tokenizer) -> State { match tokenizer.current { // Eat all whitespace before a callout Some(b' ' | b'\t') => { tokenizer.attempt( State::Next(StateName::ObsidianBlockQuoteCalloutStart), // State::Next(StateName::BlockQuoteContAfter), State::Nok, ); State::Retry(space_or_tab_min_max( tokenizer, 1, if tokenizer.parse_state.options.constructs.code_indented { TAB_SIZE - 1 } else { usize::MAX }, )) } Some(b'[') => { // We reuse state to mark that we've seen a possible callout start tokenizer.tokenize_state.seen = true; tokenizer.consume(); State::Next(StateName::ObsidianBlockQuoteCalloutStart) } Some(b'!') if tokenizer.tokenize_state.seen => { // We're inside a callout now tokenizer.enter(Name::ObsidianBlockQuoteCallout); tokenizer.enter(Name::ObsidianBlockQuoteCalloutType); tokenizer.consume(); // We reuse this state variable until we've found a closing ] tokenizer.tokenize_state.seen = true; State::Next(StateName::ObsidianBlockQuoteCalloutInner) } _ => State::Nok, } } /// ```markdown /// > [!blahblah] /// ^ /// ``` pub fn obs_callout_inner(tokenizer: &mut Tokenizer) -> State { match tokenizer.current { Some(b']') if tokenizer.tokenize_state.seen => { // We've seen the end of the callout declaration tokenizer.tokenize_state.seen = false; // TODO: Optionally check for - character tokenizer.consume(); tokenizer.exit(Name::ObsidianBlockQuoteCalloutType); tokenizer.enter(Name::ObsidianText); State::Next(StateName::ObsidianBlockQuoteCalloutInner) } Some(b'\n') => { // Resume normal flow! tokenizer.consume(); tokenizer.exit(Name::ObsidianText); tokenizer.exit(Name::ObsidianBlockQuoteCallout); tokenizer.tokenize_state.seen = false; // State::Next(StateName::BlockQuoteContBefore) State::Ok } _ if tokenizer.tokenize_state.seen => { tokenizer.consume(); State::Next(StateName::ObsidianBlockQuoteCalloutInner) } _ => { // We just save whatever this is as text tokenizer.consume(); State::Next(StateName::ObsidianBlockQuoteCalloutInner) } } }