Skip to main content

panache_parser/parser/inlines/
bracketed_spans.rs

1//! Bracketed span parsing for Pandoc's `bracketed_spans` extension.
2//!
3//! Syntax: `[inline content]{.class key="val"}`
4
5use super::core::parse_inline_text;
6use super::sink::InlineSink;
7use crate::options::ParserOptions;
8use crate::parser::utils::attributes::emit_span_attributes_node;
9use crate::syntax::SyntaxKind;
10
11/// Try to parse a bracketed span starting from the current position.
12/// Returns (total_length, content, attributes) if successful.
13///
14/// A bracketed span is: [content]{attributes}
15/// - Must have matching brackets
16/// - Must be immediately followed by attributes in braces
17/// - Can contain nested inline elements
18pub(crate) fn try_parse_bracketed_span(text: &str) -> Option<(usize, String, String)> {
19    let bytes = text.as_bytes();
20
21    if bytes.first() != Some(&b'[') {
22        return None;
23    }
24
25    // Find the closing bracket, tracking nesting
26    let mut pos = 1;
27    let mut depth = 1;
28    let mut escaped = false;
29
30    while pos < text.len() {
31        if escaped {
32            escaped = false;
33            pos += 1;
34            continue;
35        }
36
37        match bytes[pos] {
38            b'\\' => escaped = true,
39            b'[' => depth += 1,
40            b']' => {
41                depth -= 1;
42                if depth == 0 {
43                    // Found closing bracket, now check for attributes
44                    let content = &text[1..pos];
45
46                    // Must be immediately followed by {attributes}
47                    if pos + 1 >= text.len() || bytes[pos + 1] != b'{' {
48                        return None;
49                    }
50
51                    // Find the closing brace for attributes
52                    let attr_start = pos + 2;
53                    let mut attr_pos = attr_start;
54                    let mut attr_escaped = false;
55
56                    while attr_pos < text.len() {
57                        if attr_escaped {
58                            attr_escaped = false;
59                            attr_pos += 1;
60                            continue;
61                        }
62
63                        match bytes[attr_pos] {
64                            b'\\' => attr_escaped = true,
65                            b'}' => {
66                                // Found closing brace
67                                let attributes = &text[attr_start..attr_pos];
68                                let total_len = attr_pos + 1;
69                                return Some((
70                                    total_len,
71                                    content.to_string(),
72                                    attributes.to_string(),
73                                ));
74                            }
75                            _ => {}
76                        }
77                        attr_pos += 1;
78                    }
79
80                    // No closing brace found
81                    return None;
82                }
83            }
84            _ => {}
85        }
86        pos += 1;
87    }
88
89    None
90}
91
92/// Emit a bracketed span node
93pub(crate) fn emit_bracketed_span(
94    builder: &mut impl InlineSink,
95    content: &str,
96    attributes: &str,
97    config: &ParserOptions,
98    suppress_footnote_refs: bool,
99) {
100    builder.start_node(SyntaxKind::BRACKETED_SPAN.into());
101
102    // Opening bracket
103    builder.token(SyntaxKind::SPAN_BRACKET_OPEN.into(), "[");
104
105    // Content (with recursive inline parsing)
106    builder.start_node(SyntaxKind::SPAN_CONTENT.into());
107    parse_inline_text(builder, content, config, false, suppress_footnote_refs);
108    builder.finish_node(); // SpanContent
109
110    // Closing bracket
111    builder.token(SyntaxKind::SPAN_BRACKET_CLOSE.into(), "]");
112
113    // Attributes: structure the Pandoc `{...}` body into ATTR_* children,
114    // wrapping the original source bytes (formatter normalizes on output).
115    // `attributes` is the inner content; reconstruct the full `{...}` slice.
116    emit_span_attributes_node(builder, &format!("{{{attributes}}}"));
117
118    builder.finish_node(); // BracketedSpan
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn parses_simple_span() {
127        let result = try_parse_bracketed_span("[text]{.class}");
128        assert!(result.is_some());
129        let (len, content, attrs) = result.unwrap();
130        assert_eq!(len, 14);
131        assert_eq!(content, "text");
132        assert_eq!(attrs, ".class");
133    }
134
135    #[test]
136    fn parses_span_with_multiple_attributes() {
137        let result = try_parse_bracketed_span("[text]{.class key=\"val\"}");
138        assert!(result.is_some());
139        let (len, content, attrs) = result.unwrap();
140        assert_eq!(len, 24);
141        assert_eq!(content, "text");
142        assert_eq!(attrs, ".class key=\"val\"");
143    }
144
145    #[test]
146    fn parses_span_with_emphasis() {
147        let result = try_parse_bracketed_span("[**bold** text]{.highlight}");
148        assert!(result.is_some());
149        let (len, content, attrs) = result.unwrap();
150        assert_eq!(len, 27);
151        assert_eq!(content, "**bold** text");
152        assert_eq!(attrs, ".highlight");
153    }
154
155    #[test]
156    fn handles_nested_brackets() {
157        let result = try_parse_bracketed_span("[[nested]]{.class}");
158        assert!(result.is_some());
159        let (len, content, attrs) = result.unwrap();
160        assert_eq!(len, 18);
161        assert_eq!(content, "[nested]");
162        assert_eq!(attrs, ".class");
163    }
164
165    #[test]
166    fn requires_attributes() {
167        // Without attributes, should not parse
168        let result = try_parse_bracketed_span("[text]");
169        assert!(result.is_none());
170    }
171
172    #[test]
173    fn requires_immediate_attributes() {
174        // Space between ] and { should not parse
175        let result = try_parse_bracketed_span("[text] {.class}");
176        assert!(result.is_none());
177    }
178
179    #[test]
180    fn handles_escaped_brackets() {
181        let result = try_parse_bracketed_span(r"[text \] more]{.class}");
182        assert!(result.is_some());
183        let (len, content, _) = result.unwrap();
184        assert_eq!(len, 22);
185        assert_eq!(content, r"text \] more");
186    }
187
188    #[test]
189    fn handles_escaped_braces_in_attributes() {
190        let result = try_parse_bracketed_span(r"[text]{key=\}}");
191        assert!(result.is_some());
192        let (len, _, attrs) = result.unwrap();
193        assert_eq!(len, 14);
194        assert_eq!(attrs, r"key=\}");
195    }
196}