aboutsummaryrefslogblamecommitdiffstats
path: root/src/construct/mdx_esm.rs
blob: 4fb6b5093b7c5ba0c6a3b1eba4c50cc4e0161ee8 (plain) (tree)
































                                                                                                                                                
                                                      

































































































































































                                                                                             

                                    

                                              
            




                                               







                                                                     














                                                                                 
//! MDX ESM occurs in the [flow][] content type.
//!
//! ## Grammar
//!
//! MDX expression (flow) forms with the following BNF
//! (<small>see [construct][crate::construct] for character groups</small>):
//!
//! ```bnf
//! mdx_esm ::= word *line *(eol *line)
//!
//! word ::= 'e' 'x' 'p' 'o' 'r' 't' | 'i' 'm' 'p' 'o' 'r' 't'
//! ```
//!
//! This construct must be followed by a blank line or eof (end of file).
//! It can include blank lines if [`MdxEsmParse`][crate::MdxEsmParse] passed in
//! `options.mdx_esm_parse` allows it.
//!
//! ## Tokens
//!
//! *   [`LineEnding`][Name::LineEnding]
//! *   [`MdxEsm`][Name::MdxEsm]
//! *   [`MdxEsmData`][Name::MdxEsmData]
//!
//! ## References
//!
//! *   [`syntax.js` in `micromark-extension-mdxjs-esm`](https://github.com/micromark/micromark-extension-mdxjs-esm/blob/main/dev/lib/syntax.js)
//! *   [`mdxjs.com`](https://mdxjs.com)
//!
//! [flow]: crate::construct::flow

use crate::event::Name;
use crate::state::{Name as StateName, State};
use crate::tokenizer::Tokenizer;
use crate::util::{mdx_collect::collect, slice::Slice};
use crate::MdxSignal;
use alloc::format;

/// Start of MDX ESM.
///
/// ```markdown
/// > | import a from 'b'
///     ^
/// ```
pub fn start(tokenizer: &mut Tokenizer) -> State {
    // If it’s turned on.
    if tokenizer.parse_state.options.constructs.mdx_esm
        // If there is a gnostic parser.
        && tokenizer.parse_state.options.mdx_esm_parse.is_some()
        // When not interrupting.
        && !tokenizer.interrupt
        // Only at the start of a line, not at whitespace or in a container.
        && tokenizer.point.column == 1
        && matches!(tokenizer.current, Some(b'e' | b'i'))
    {
        // Place where keyword starts.
        tokenizer.tokenize_state.start = tokenizer.point.index;
        tokenizer.enter(Name::MdxEsm);
        tokenizer.enter(Name::MdxEsmData);
        tokenizer.consume();
        State::Next(StateName::MdxEsmWord)
    } else {
        State::Nok
    }
}

/// In keyword.
///
/// ```markdown
/// > | import a from 'b'
///     ^^^^^^
/// ```
pub fn word(tokenizer: &mut Tokenizer) -> State {
    if matches!(tokenizer.current, Some(b'a'..=b'z')) {
        tokenizer.consume();
        State::Next(StateName::MdxEsmWord)
    } else {
        let slice = Slice::from_indices(
            tokenizer.parse_state.bytes,
            tokenizer.tokenize_state.start,
            tokenizer.point.index,
        );

        if matches!(slice.as_str(), "export" | "import") && tokenizer.current == Some(b' ') {
            tokenizer.concrete = true;
            tokenizer.tokenize_state.start = tokenizer.events.len() - 1;
            tokenizer.consume();
            State::Next(StateName::MdxEsmInside)
        } else {
            tokenizer.tokenize_state.start = 0;
            State::Nok
        }
    }
}

/// In data.
///
/// ```markdown
/// > | import a from 'b'
///           ^
/// ```
pub fn inside(tokenizer: &mut Tokenizer) -> State {
    match tokenizer.current {
        None | Some(b'\n') => {
            tokenizer.exit(Name::MdxEsmData);
            State::Retry(StateName::MdxEsmLineStart)
        }
        _ => {
            tokenizer.consume();
            State::Next(StateName::MdxEsmInside)
        }
    }
}

/// At start of line.
///
/// ```markdown
///   | import a from 'b'
/// > | export {a}
///     ^
/// ```
pub fn line_start(tokenizer: &mut Tokenizer) -> State {
    match tokenizer.current {
        None => State::Retry(StateName::MdxEsmAtEnd),
        Some(b'\n') => {
            tokenizer.check(
                State::Next(StateName::MdxEsmAtEnd),
                State::Next(StateName::MdxEsmContinuationStart),
            );
            State::Retry(StateName::MdxEsmBlankLineBefore)
        }
        _ => {
            tokenizer.enter(Name::MdxEsmData);
            tokenizer.consume();
            State::Next(StateName::MdxEsmInside)
        }
    }
}

/// At start of line that continues.
///
/// ```markdown
///   | import a from 'b'
/// > | export {a}
///     ^
/// ```
pub fn continuation_start(tokenizer: &mut Tokenizer) -> State {
    tokenizer.enter(Name::LineEnding);
    tokenizer.consume();
    tokenizer.exit(Name::LineEnding);
    State::Next(StateName::MdxEsmLineStart)
}

/// At start of a potentially blank line.
///
/// ```markdown
///   | import a from 'b'
/// > | export {a}
///     ^
/// ```
pub fn blank_line_before(tokenizer: &mut Tokenizer) -> State {
    tokenizer.enter(Name::LineEnding);
    tokenizer.consume();
    tokenizer.exit(Name::LineEnding);
    State::Next(StateName::BlankLineStart)
}

/// At end of line (blank or eof).
///
/// ```markdown
/// > | import a from 'b'
///                      ^
/// ```
pub fn at_end(tokenizer: &mut Tokenizer) -> State {
    let result = parse_esm(tokenizer);

    // Done!.
    if matches!(result, State::Ok) {
        tokenizer.concrete = false;
        tokenizer.exit(Name::MdxEsm);
    }

    result
}

/// Parse ESM with a given function.
fn parse_esm(tokenizer: &mut Tokenizer) -> State {
    // We can `unwrap` because we don’t parse if this is `None`.
    let parse = tokenizer
        .parse_state
        .options
        .mdx_esm_parse
        .as_ref()
        .unwrap();

    // Collect the body of the ESM and positional info for each run of it.
    let result = collect(
        &tokenizer.events,
        tokenizer.parse_state.bytes,
        tokenizer.tokenize_state.start,
        &[Name::MdxEsmData, Name::LineEnding],
        &[],
    );

    // Parse and handle what was signaled back.
    match parse(&result.value) {
        MdxSignal::Ok => State::Ok,
        MdxSignal::Error(message, relative) => {
            let point = tokenizer
                .parse_state
                .location
                .as_ref()
                .expect("expected location index if aware mdx is on")
                .relative_to_point(&result.stops, relative)
                .expect("expected non-empty string");
            State::Error(format!("{}:{}: {}", point.line, point.column, message))
        }
        MdxSignal::Eof(message) => {
            if tokenizer.current == None {
                State::Error(format!(
                    "{}:{}: {}",
                    tokenizer.point.line, tokenizer.point.column, message
                ))
            } else {
                tokenizer.tokenize_state.mdx_last_parse_error = Some(message);
                State::Retry(StateName::MdxEsmContinuationStart)
            }
        }
    }
}