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