Skip to main content

panache_parser/parser/inlines/
inline_footnotes.rs

1//! Inline footnote parsing for Pandoc's inline_notes extension.
2//!
3//! Syntax: `^[footnote text]` for inline footnotes
4//! Syntax: `[^id]` for reference footnotes
5
6use crate::syntax::SyntaxKind;
7use rowan::GreenNodeBuilder;
8
9use super::core::parse_inline_text;
10use crate::options::ParserOptions;
11
12/// Try to parse an inline footnote starting at the current position.
13/// Returns Some((length, content)) if successful, None otherwise.
14///
15/// Inline footnotes have the syntax: ^[text]
16/// The text can contain most inline elements but not nested footnotes.
17pub(crate) fn try_parse_inline_footnote(text: &str) -> Option<(usize, &str)> {
18    let bytes = text.as_bytes();
19
20    // Must start with ^[
21    if bytes.len() < 3 || bytes[0] != b'^' || bytes[1] != b'[' {
22        return None;
23    }
24
25    // Find the closing ]
26    let mut pos = 2;
27    let mut bracket_depth = 1; // Already opened one bracket
28
29    while pos < bytes.len() {
30        match bytes[pos] {
31            b'\\' => {
32                // Skip escaped character
33                pos += 2;
34                continue;
35            }
36            b'[' => {
37                bracket_depth += 1;
38                pos += 1;
39            }
40            b']' => {
41                bracket_depth -= 1;
42                if bracket_depth == 0 {
43                    // Found the closing bracket
44                    let content = &text[2..pos];
45                    return Some((pos + 1, content));
46                }
47                pos += 1;
48            }
49            _ => {
50                pos += 1;
51            }
52        }
53    }
54
55    // No closing bracket found
56    None
57}
58
59/// Emit an inline footnote node to the builder.
60pub(crate) fn emit_inline_footnote(
61    builder: &mut GreenNodeBuilder,
62    content: &str,
63    config: &ParserOptions,
64) {
65    builder.start_node(SyntaxKind::INLINE_FOOTNOTE.into());
66
67    // Opening marker
68    builder.token(SyntaxKind::INLINE_FOOTNOTE_START.into(), "^[");
69
70    // Parse the content recursively for nested inline elements
71    parse_inline_text(builder, content, config, false);
72
73    // Closing marker
74    builder.token(SyntaxKind::INLINE_FOOTNOTE_END.into(), "]");
75
76    builder.finish_node();
77}
78
79/// Try to parse a footnote reference: [^id]
80/// Returns Some((length, id)) if successful.
81pub(crate) fn try_parse_footnote_reference(text: &str) -> Option<(usize, String)> {
82    let bytes = text.as_bytes();
83
84    // Must start with [^
85    if bytes.len() < 4 || bytes[0] != b'[' || bytes[1] != b'^' {
86        return None;
87    }
88
89    // Find the closing ]
90    let mut pos = 2;
91    while pos < bytes.len() && bytes[pos] != b']' && bytes[pos] != b'\n' && bytes[pos] != b'\r' {
92        pos += 1;
93    }
94
95    if pos >= bytes.len() || bytes[pos] != b']' {
96        return None;
97    }
98
99    let id = &text[2..pos];
100    if id.is_empty() {
101        return None;
102    }
103
104    Some((pos + 1, id.to_string()))
105}
106
107/// Emit a footnote reference node to the builder.
108pub(crate) fn emit_footnote_reference(builder: &mut GreenNodeBuilder, id: &str) {
109    builder.start_node(SyntaxKind::FOOTNOTE_REFERENCE.into());
110    builder.token(SyntaxKind::FOOTNOTE_LABEL_START.into(), "[^");
111    builder.token(SyntaxKind::FOOTNOTE_LABEL_ID.into(), id);
112    builder.token(SyntaxKind::FOOTNOTE_LABEL_END.into(), "]");
113    builder.finish_node();
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_parse_simple_inline_footnote() {
122        let result = try_parse_inline_footnote("^[This is a note]");
123        assert_eq!(result, Some((17, "This is a note")));
124    }
125
126    #[test]
127    fn test_parse_inline_footnote_with_trailing_text() {
128        let result = try_parse_inline_footnote("^[Note text] and more");
129        assert_eq!(result, Some((12, "Note text")));
130    }
131
132    #[test]
133    fn test_parse_inline_footnote_with_brackets_inside() {
134        let result = try_parse_inline_footnote("^[Text with [nested] brackets]");
135        assert_eq!(result, Some((30, "Text with [nested] brackets")));
136    }
137
138    #[test]
139    fn test_parse_inline_footnote_with_escaped_bracket() {
140        let result = try_parse_inline_footnote("^[Text with \\] escaped]");
141        assert_eq!(result, Some((23, "Text with \\] escaped")));
142    }
143
144    #[test]
145    fn test_not_inline_footnote_no_opening() {
146        let result = try_parse_inline_footnote("[Not a footnote]");
147        assert_eq!(result, None);
148    }
149
150    #[test]
151    fn test_not_inline_footnote_no_closing() {
152        let result = try_parse_inline_footnote("^[No closing bracket");
153        assert_eq!(result, None);
154    }
155
156    #[test]
157    fn test_not_inline_footnote_just_caret() {
158        let result = try_parse_inline_footnote("^Not a footnote");
159        assert_eq!(result, None);
160    }
161
162    #[test]
163    fn test_empty_inline_footnote() {
164        let result = try_parse_inline_footnote("^[]");
165        assert_eq!(result, Some((3, "")));
166    }
167
168    #[test]
169    fn test_inline_footnote_multiline() {
170        // Inline footnotes can span multiple lines in the source
171        let result = try_parse_inline_footnote("^[This is\na multiline\nnote]");
172        assert_eq!(result, Some((27, "This is\na multiline\nnote")));
173    }
174
175    #[test]
176    fn test_inline_footnote_with_code() {
177        let result = try_parse_inline_footnote("^[Contains `code` inside]");
178        assert_eq!(result, Some((25, "Contains `code` inside")));
179    }
180
181    #[test]
182    fn test_footnote_reference_with_crlf() {
183        // Footnote reference IDs should not span lines with CRLF
184        let input = "[^foo\r\nbar]";
185        let result = try_parse_footnote_reference(input);
186
187        // Should fail to parse because ID contains line break
188        assert_eq!(
189            result, None,
190            "Should not parse footnote reference with CRLF in ID"
191        );
192    }
193
194    #[test]
195    fn test_footnote_reference_with_lf() {
196        // Footnote reference IDs should not span lines with LF either
197        let input = "[^foo\nbar]";
198        let result = try_parse_footnote_reference(input);
199
200        // Should fail to parse because ID contains line break
201        assert_eq!(
202            result, None,
203            "Should not parse footnote reference with LF in ID"
204        );
205    }
206}