panache_parser/parser/inlines/
inline_executable.rs1use 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}