Skip to main content

panache_parser/parser/inlines/
code_spans.rs

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