Skip to main content

panache_parser/parser/inlines/
inline_executable.rs

1use super::sink::InlineSink;
2use crate::syntax::SyntaxKind;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub(crate) enum InlineExecutableVariant {
6    RMarkdown,
7    Quarto,
8}
9
10pub(crate) struct InlineExecutableMatch<'a> {
11    pub(crate) total_len: usize,
12    pub(crate) backtick_count: usize,
13    pub(crate) prefix: &'a str,
14    pub(crate) spacing_after_marker: &'a str,
15    pub(crate) code: &'a str,
16    pub(crate) variant: InlineExecutableVariant,
17}
18
19pub(crate) fn try_parse_inline_executable(
20    text: &str,
21    allow_rmarkdown: bool,
22    allow_quarto: bool,
23) -> Option<InlineExecutableMatch<'_>> {
24    let (code_span_len, prefix, backtick_count, attrs) =
25        super::code_spans::try_parse_code_span(text)?;
26    if backtick_count != 1 || attrs.is_some() {
27        return None;
28    }
29
30    let remaining = &text[code_span_len..];
31    let line_len = remaining.find('\n').unwrap_or(remaining.len());
32    let tail = &remaining[..line_len];
33    if !tail.ends_with("``") {
34        return None;
35    }
36
37    parse_tail(tail, allow_rmarkdown, allow_quarto).map(|(spacing_after_marker, code, variant)| {
38        InlineExecutableMatch {
39            total_len: code_span_len + line_len,
40            backtick_count,
41            prefix,
42            spacing_after_marker,
43            code,
44            variant,
45        }
46    })
47}
48
49fn parse_tail(
50    tail: &str,
51    allow_rmarkdown: bool,
52    allow_quarto: bool,
53) -> Option<(&str, &str, InlineExecutableVariant)> {
54    if allow_rmarkdown && tail.starts_with('r') {
55        return parse_marker_and_code(tail, "r", InlineExecutableVariant::RMarkdown);
56    }
57    if allow_quarto && tail.starts_with("{r}") {
58        return parse_marker_and_code(tail, "{r}", InlineExecutableVariant::Quarto);
59    }
60    None
61}
62
63fn parse_marker_and_code<'a>(
64    tail: &'a str,
65    marker: &'a str,
66    variant: InlineExecutableVariant,
67) -> Option<(&'a str, &'a str, InlineExecutableVariant)> {
68    let suffix = &tail[marker.len()..];
69    let spacing_len = suffix
70        .chars()
71        .take_while(|ch| ch.is_whitespace())
72        .map(char::len_utf8)
73        .sum::<usize>();
74    if spacing_len == 0 {
75        return None;
76    }
77    let spacing_after_marker = &suffix[..spacing_len];
78    let mut code = &suffix[spacing_len..];
79    if let Some(stripped) = code.strip_suffix("``") {
80        code = stripped;
81    } else {
82        return None;
83    }
84    if code.trim().is_empty() {
85        return None;
86    }
87    Some((spacing_after_marker, code, variant))
88}
89
90pub(crate) fn emit_inline_executable(builder: &mut impl InlineSink, m: &InlineExecutableMatch<'_>) {
91    builder.start_node(SyntaxKind::INLINE_EXEC.into());
92    builder.token(
93        SyntaxKind::INLINE_EXEC_MARKER.into(),
94        &"`".repeat(m.backtick_count),
95    );
96    if !m.prefix.is_empty() {
97        builder.token(SyntaxKind::TEXT.into(), m.prefix);
98    }
99    let lang = match m.variant {
100        InlineExecutableVariant::RMarkdown => "r",
101        InlineExecutableVariant::Quarto => "{r}",
102    };
103    builder.token(SyntaxKind::INLINE_EXEC_LANG.into(), lang);
104    builder.token(SyntaxKind::WHITESPACE.into(), m.spacing_after_marker);
105    builder.token(SyntaxKind::INLINE_EXEC_CONTENT.into(), m.code);
106    builder.token(
107        SyntaxKind::INLINE_EXEC_MARKER.into(),
108        &"`".repeat(m.backtick_count),
109    );
110    builder.finish_node();
111}