Skip to main content

panache_parser/parser/inlines/
code_spans.rs

1/// Parsing for inline code spans (`code`)
2use crate::syntax::SyntaxKind;
3use rowan::GreenNodeBuilder;
4
5// Import the attribute parsing from utils
6use crate::parser::utils::attributes::{
7    AttributeBlock, emit_attributes, try_parse_trailing_attributes,
8};
9
10/// Try to parse a code span starting at the current position.
11/// Returns (total_len, code_content, backtick_count, optional_attributes) if successful.
12pub fn try_parse_code_span(text: &str) -> Option<(usize, &str, usize, Option<AttributeBlock>)> {
13    // Count opening backticks
14    let opening_backticks = text.bytes().take_while(|&b| b == b'`').count();
15    if opening_backticks == 0 {
16        return None;
17    }
18
19    let rest = &text[opening_backticks..];
20
21    // Look for matching closing backticks
22    let mut pos = 0;
23    while pos < rest.len() {
24        if rest[pos..].starts_with('`') {
25            let closing_backticks = rest[pos..].bytes().take_while(|&b| b == b'`').count();
26
27            if closing_backticks == opening_backticks {
28                // Found matching close
29                let code_content = &rest[..pos];
30                let after_close = opening_backticks + pos + closing_backticks;
31
32                // Check for trailing attributes {#id .class key=value}
33                let remaining = &text[after_close..];
34                if remaining.starts_with('{') {
35                    // Find the closing brace
36                    if let Some(close_brace_pos) = remaining.find('}') {
37                        let attr_text = &remaining[..=close_brace_pos];
38                        // Try to parse as attributes
39                        if let Some((attrs, _)) = try_parse_trailing_attributes(attr_text) {
40                            let total_len = after_close + close_brace_pos + 1;
41                            return Some((total_len, code_content, opening_backticks, Some(attrs)));
42                        }
43                    }
44                }
45
46                // No attributes, just return the code span
47                return Some((after_close, code_content, opening_backticks, None));
48            }
49            // Skip these backticks and continue searching
50            pos += closing_backticks;
51        } else {
52            // Move to next character (handle UTF-8 properly)
53            pos += rest[pos..].chars().next()?.len_utf8();
54        }
55    }
56
57    // No matching close found
58    None
59}
60
61/// Emit a code span node to the builder.
62pub fn emit_code_span(
63    builder: &mut GreenNodeBuilder,
64    content: &str,
65    backtick_count: usize,
66    attributes: Option<AttributeBlock>,
67) {
68    builder.start_node(SyntaxKind::INLINE_CODE.into());
69
70    // Opening backticks
71    builder.token(
72        SyntaxKind::INLINE_CODE_MARKER.into(),
73        &"`".repeat(backtick_count),
74    );
75
76    // Code content
77    builder.token(SyntaxKind::INLINE_CODE_CONTENT.into(), content);
78
79    // Closing backticks
80    builder.token(
81        SyntaxKind::INLINE_CODE_MARKER.into(),
82        &"`".repeat(backtick_count),
83    );
84
85    // Emit attributes if present
86    if let Some(attrs) = attributes {
87        emit_attributes(builder, &attrs);
88    }
89
90    builder.finish_node();
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_parse_simple_code_span() {
99        let result = try_parse_code_span("`code`");
100        assert_eq!(result, Some((6, "code", 1, None)));
101    }
102
103    #[test]
104    fn test_parse_code_span_with_backticks() {
105        let result = try_parse_code_span("`` `backtick` ``");
106        assert_eq!(result, Some((16, " `backtick` ", 2, None)));
107    }
108
109    #[test]
110    fn test_parse_code_span_triple_backticks() {
111        let result = try_parse_code_span("``` `` ```");
112        assert_eq!(result, Some((10, " `` ", 3, None)));
113    }
114
115    #[test]
116    fn test_parse_code_span_no_close() {
117        let result = try_parse_code_span("`no close");
118        assert_eq!(result, None);
119    }
120
121    #[test]
122    fn test_parse_code_span_mismatched_close() {
123        let result = try_parse_code_span("`single``");
124        assert_eq!(result, None);
125    }
126
127    #[test]
128    fn test_not_code_span() {
129        let result = try_parse_code_span("no backticks");
130        assert_eq!(result, None);
131    }
132
133    #[test]
134    fn test_code_span_with_trailing_text() {
135        let result = try_parse_code_span("`code` and more");
136        assert_eq!(result, Some((6, "code", 1, None)));
137    }
138
139    #[test]
140    fn test_code_span_with_simple_class() {
141        let result = try_parse_code_span("`code`{.python}");
142        let (len, content, backticks, attrs) = result.unwrap();
143        assert_eq!(len, 15);
144        assert_eq!(content, "code");
145        assert_eq!(backticks, 1);
146        assert!(attrs.is_some());
147        let attrs = attrs.unwrap();
148        assert_eq!(attrs.classes, vec!["python"]);
149    }
150
151    #[test]
152    fn test_code_span_with_id() {
153        let result = try_parse_code_span("`code`{#mycode}");
154        let (len, content, backticks, attrs) = result.unwrap();
155        assert_eq!(len, 15);
156        assert_eq!(content, "code");
157        assert_eq!(backticks, 1);
158        assert!(attrs.is_some());
159        let attrs = attrs.unwrap();
160        assert_eq!(attrs.identifier, Some("mycode".to_string()));
161    }
162
163    #[test]
164    fn test_code_span_with_full_attributes() {
165        let result = try_parse_code_span("`x + y`{#calc .haskell .eval}");
166        let (len, content, backticks, attrs) = result.unwrap();
167        assert_eq!(len, 29);
168        assert_eq!(content, "x + y");
169        assert_eq!(backticks, 1);
170        assert!(attrs.is_some());
171        let attrs = attrs.unwrap();
172        assert_eq!(attrs.identifier, Some("calc".to_string()));
173        assert_eq!(attrs.classes, vec!["haskell", "eval"]);
174    }
175
176    #[test]
177    fn test_code_span_attributes_must_be_adjacent() {
178        // Space between closing backtick and { should not parse attributes
179        let result = try_parse_code_span("`code` {.python}");
180        assert_eq!(result, Some((6, "code", 1, None)));
181    }
182}