Skip to main content

panache_parser/parser/inlines/
inline_executable.rs

1use crate::syntax::SyntaxKind;
2use rowan::GreenNodeBuilder;
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(
91    builder: &mut GreenNodeBuilder,
92    m: &InlineExecutableMatch<'_>,
93) {
94    builder.start_node(SyntaxKind::INLINE_EXEC.into());
95    builder.token(
96        SyntaxKind::INLINE_EXEC_MARKER.into(),
97        &"`".repeat(m.backtick_count),
98    );
99    if !m.prefix.is_empty() {
100        builder.token(SyntaxKind::TEXT.into(), m.prefix);
101    }
102    let lang = match m.variant {
103        InlineExecutableVariant::RMarkdown => "r",
104        InlineExecutableVariant::Quarto => "{r}",
105    };
106    builder.token(SyntaxKind::INLINE_EXEC_LANG.into(), lang);
107    builder.token(SyntaxKind::WHITESPACE.into(), m.spacing_after_marker);
108    builder.token(SyntaxKind::INLINE_EXEC_CONTENT.into(), m.code);
109    builder.token(
110        SyntaxKind::INLINE_EXEC_MARKER.into(),
111        &"`".repeat(m.backtick_count),
112    );
113    builder.finish_node();
114}