panache-parser 0.3.0

Lossless CST parser and syntax wrappers for Pandoc markdown, Quarto, and RMarkdown
Documentation
use crate::syntax::SyntaxKind;
use rowan::GreenNodeBuilder;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum InlineExecutableVariant {
    RMarkdown,
    Quarto,
}

pub(crate) struct InlineExecutableMatch<'a> {
    pub(crate) total_len: usize,
    pub(crate) backtick_count: usize,
    pub(crate) prefix: &'a str,
    pub(crate) spacing_after_marker: &'a str,
    pub(crate) code: &'a str,
    pub(crate) variant: InlineExecutableVariant,
}

pub(crate) fn try_parse_inline_executable(
    text: &str,
    allow_rmarkdown: bool,
    allow_quarto: bool,
) -> Option<InlineExecutableMatch<'_>> {
    let (code_span_len, prefix, backtick_count, attrs) =
        super::code_spans::try_parse_code_span(text)?;
    if backtick_count != 1 || attrs.is_some() {
        return None;
    }

    let remaining = &text[code_span_len..];
    let line_len = remaining.find('\n').unwrap_or(remaining.len());
    let tail = &remaining[..line_len];
    if !tail.ends_with("``") {
        return None;
    }

    parse_tail(tail, allow_rmarkdown, allow_quarto).map(|(spacing_after_marker, code, variant)| {
        InlineExecutableMatch {
            total_len: code_span_len + line_len,
            backtick_count,
            prefix,
            spacing_after_marker,
            code,
            variant,
        }
    })
}

fn parse_tail(
    tail: &str,
    allow_rmarkdown: bool,
    allow_quarto: bool,
) -> Option<(&str, &str, InlineExecutableVariant)> {
    if allow_rmarkdown && tail.starts_with('r') {
        return parse_marker_and_code(tail, "r", InlineExecutableVariant::RMarkdown);
    }
    if allow_quarto && tail.starts_with("{r}") {
        return parse_marker_and_code(tail, "{r}", InlineExecutableVariant::Quarto);
    }
    None
}

fn parse_marker_and_code<'a>(
    tail: &'a str,
    marker: &'a str,
    variant: InlineExecutableVariant,
) -> Option<(&'a str, &'a str, InlineExecutableVariant)> {
    let suffix = &tail[marker.len()..];
    let spacing_len = suffix
        .chars()
        .take_while(|ch| ch.is_whitespace())
        .map(char::len_utf8)
        .sum::<usize>();
    if spacing_len == 0 {
        return None;
    }
    let spacing_after_marker = &suffix[..spacing_len];
    let mut code = &suffix[spacing_len..];
    if let Some(stripped) = code.strip_suffix("``") {
        code = stripped;
    } else {
        return None;
    }
    if code.trim().is_empty() {
        return None;
    }
    Some((spacing_after_marker, code, variant))
}

pub(crate) fn emit_inline_executable(
    builder: &mut GreenNodeBuilder,
    m: &InlineExecutableMatch<'_>,
) {
    builder.start_node(SyntaxKind::INLINE_EXEC.into());
    builder.token(
        SyntaxKind::INLINE_EXEC_MARKER.into(),
        &"`".repeat(m.backtick_count),
    );
    if !m.prefix.is_empty() {
        builder.token(SyntaxKind::TEXT.into(), m.prefix);
    }
    let lang = match m.variant {
        InlineExecutableVariant::RMarkdown => "r",
        InlineExecutableVariant::Quarto => "{r}",
    };
    builder.token(SyntaxKind::INLINE_EXEC_LANG.into(), lang);
    builder.token(SyntaxKind::WHITESPACE.into(), m.spacing_after_marker);
    builder.token(SyntaxKind::INLINE_EXEC_CONTENT.into(), m.code);
    builder.token(
        SyntaxKind::INLINE_EXEC_MARKER.into(),
        &"`".repeat(m.backtick_count),
    );
    builder.finish_node();
}