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    let rest_bytes = rest.as_bytes();
21
22    // Look for matching closing backticks. Skip non-backtick bytes via
23    // memchr (compiles to vectorized scan) instead of stepping one
24    // UTF-8 char at a time — `try_parse_code_span` is called on every
25    // `` ` `` byte the dispatcher encounters and scans to end of input
26    // when no closer matches, so the inner skip dominates self-time.
27    let mut pos = 0;
28    while pos < rest_bytes.len() {
29        let next_tick = match rest_bytes[pos..].iter().position(|&b| b == b'`') {
30            Some(off) => pos + off,
31            None => break,
32        };
33        // Count the run of consecutive backticks starting at `next_tick`.
34        let mut closing_backticks = 0;
35        while next_tick + closing_backticks < rest_bytes.len()
36            && rest_bytes[next_tick + closing_backticks] == b'`'
37        {
38            closing_backticks += 1;
39        }
40
41        if closing_backticks == opening_backticks {
42            // Found matching close
43            let code_content = &rest[..next_tick];
44            let after_close = opening_backticks + next_tick + closing_backticks;
45
46            // Check for trailing attributes {#id .class key=value}
47            let remaining = &text[after_close..];
48            if remaining.starts_with('{') {
49                // Find the closing brace
50                if let Some(close_brace_pos) = remaining.find('}') {
51                    let attr_text = &remaining[..=close_brace_pos];
52                    // Try to parse as attributes
53                    if let Some((attrs, _)) = try_parse_trailing_attributes(attr_text) {
54                        let total_len = after_close + close_brace_pos + 1;
55                        return Some((total_len, code_content, opening_backticks, Some(attrs)));
56                    }
57                }
58            }
59
60            // No attributes, just return the code span
61            return Some((after_close, code_content, opening_backticks, None));
62        }
63        // Skip past this run of backticks and keep searching.
64        pos = next_tick + closing_backticks;
65    }
66
67    // No matching close found
68    None
69}
70
71/// Emit a code span node to the builder.
72pub fn emit_code_span(
73    builder: &mut GreenNodeBuilder,
74    content: &str,
75    backtick_count: usize,
76    attributes: Option<AttributeBlock>,
77) {
78    builder.start_node(SyntaxKind::INLINE_CODE.into());
79
80    // Opening backticks
81    builder.token(
82        SyntaxKind::INLINE_CODE_MARKER.into(),
83        &"`".repeat(backtick_count),
84    );
85
86    // Code content
87    builder.token(SyntaxKind::INLINE_CODE_CONTENT.into(), content);
88
89    // Closing backticks
90    builder.token(
91        SyntaxKind::INLINE_CODE_MARKER.into(),
92        &"`".repeat(backtick_count),
93    );
94
95    // Emit attributes if present
96    if let Some(attrs) = attributes {
97        emit_attributes(builder, &attrs);
98    }
99
100    builder.finish_node();
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_parse_simple_code_span() {
109        let result = try_parse_code_span("`code`");
110        assert_eq!(result, Some((6, "code", 1, None)));
111    }
112
113    #[test]
114    fn test_parse_code_span_with_backticks() {
115        let result = try_parse_code_span("`` `backtick` ``");
116        assert_eq!(result, Some((16, " `backtick` ", 2, None)));
117    }
118
119    #[test]
120    fn test_parse_code_span_triple_backticks() {
121        let result = try_parse_code_span("``` `` ```");
122        assert_eq!(result, Some((10, " `` ", 3, None)));
123    }
124
125    #[test]
126    fn test_parse_code_span_no_close() {
127        let result = try_parse_code_span("`no close");
128        assert_eq!(result, None);
129    }
130
131    #[test]
132    fn test_parse_code_span_mismatched_close() {
133        let result = try_parse_code_span("`single``");
134        assert_eq!(result, None);
135    }
136
137    #[test]
138    fn test_not_code_span() {
139        let result = try_parse_code_span("no backticks");
140        assert_eq!(result, None);
141    }
142
143    #[test]
144    fn test_code_span_with_trailing_text() {
145        let result = try_parse_code_span("`code` and more");
146        assert_eq!(result, Some((6, "code", 1, None)));
147    }
148
149    #[test]
150    fn test_code_span_with_simple_class() {
151        let result = try_parse_code_span("`code`{.python}");
152        let (len, content, backticks, attrs) = result.unwrap();
153        assert_eq!(len, 15);
154        assert_eq!(content, "code");
155        assert_eq!(backticks, 1);
156        assert!(attrs.is_some());
157        let attrs = attrs.unwrap();
158        assert_eq!(attrs.classes, vec!["python"]);
159    }
160
161    #[test]
162    fn test_code_span_with_id() {
163        let result = try_parse_code_span("`code`{#mycode}");
164        let (len, content, backticks, attrs) = result.unwrap();
165        assert_eq!(len, 15);
166        assert_eq!(content, "code");
167        assert_eq!(backticks, 1);
168        assert!(attrs.is_some());
169        let attrs = attrs.unwrap();
170        assert_eq!(attrs.identifier, Some("mycode".to_string()));
171    }
172
173    #[test]
174    fn test_code_span_with_full_attributes() {
175        let result = try_parse_code_span("`x + y`{#calc .haskell .eval}");
176        let (len, content, backticks, attrs) = result.unwrap();
177        assert_eq!(len, 29);
178        assert_eq!(content, "x + y");
179        assert_eq!(backticks, 1);
180        assert!(attrs.is_some());
181        let attrs = attrs.unwrap();
182        assert_eq!(attrs.identifier, Some("calc".to_string()));
183        assert_eq!(attrs.classes, vec!["haskell", "eval"]);
184    }
185
186    #[test]
187    fn test_code_span_attributes_must_be_adjacent() {
188        // Space between closing backtick and { should not parse attributes
189        let result = try_parse_code_span("`code` {.python}");
190        assert_eq!(result, Some((6, "code", 1, None)));
191    }
192}