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::config::Config;
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(builder: &mut GreenNodeBuilder, content: &str, config: &Config) {
61    builder.start_node(SyntaxKind::INLINE_FOOTNOTE.into());
62
63    // Opening marker
64    builder.token(SyntaxKind::INLINE_FOOTNOTE_START.into(), "^[");
65
66    // Parse the content recursively for nested inline elements
67    parse_inline_text(builder, content, config, false);
68
69    // Closing marker
70    builder.token(SyntaxKind::INLINE_FOOTNOTE_END.into(), "]");
71
72    builder.finish_node();
73}
74
75/// Try to parse a footnote reference: [^id]
76/// Returns Some((length, id)) if successful.
77pub(crate) fn try_parse_footnote_reference(text: &str) -> Option<(usize, String)> {
78    let bytes = text.as_bytes();
79
80    // Must start with [^
81    if bytes.len() < 4 || bytes[0] != b'[' || bytes[1] != b'^' {
82        return None;
83    }
84
85    // Find the closing ]
86    let mut pos = 2;
87    while pos < bytes.len() && bytes[pos] != b']' && bytes[pos] != b'\n' && bytes[pos] != b'\r' {
88        pos += 1;
89    }
90
91    if pos >= bytes.len() || bytes[pos] != b']' {
92        return None;
93    }
94
95    let id = &text[2..pos];
96    if id.is_empty() {
97        return None;
98    }
99
100    Some((pos + 1, id.to_string()))
101}
102
103/// Emit a footnote reference node to the builder.
104pub(crate) fn emit_footnote_reference(builder: &mut GreenNodeBuilder, id: &str) {
105    builder.start_node(SyntaxKind::FOOTNOTE_REFERENCE.into());
106    builder.token(SyntaxKind::FOOTNOTE_LABEL_START.into(), "[^");
107    builder.token(SyntaxKind::FOOTNOTE_LABEL_ID.into(), id);
108    builder.token(SyntaxKind::FOOTNOTE_LABEL_END.into(), "]");
109    builder.finish_node();
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_parse_simple_inline_footnote() {
118        let result = try_parse_inline_footnote("^[This is a note]");
119        assert_eq!(result, Some((17, "This is a note")));
120    }
121
122    #[test]
123    fn test_parse_inline_footnote_with_trailing_text() {
124        let result = try_parse_inline_footnote("^[Note text] and more");
125        assert_eq!(result, Some((12, "Note text")));
126    }
127
128    #[test]
129    fn test_parse_inline_footnote_with_brackets_inside() {
130        let result = try_parse_inline_footnote("^[Text with [nested] brackets]");
131        assert_eq!(result, Some((30, "Text with [nested] brackets")));
132    }
133
134    #[test]
135    fn test_parse_inline_footnote_with_escaped_bracket() {
136        let result = try_parse_inline_footnote("^[Text with \\] escaped]");
137        assert_eq!(result, Some((23, "Text with \\] escaped")));
138    }
139
140    #[test]
141    fn test_not_inline_footnote_no_opening() {
142        let result = try_parse_inline_footnote("[Not a footnote]");
143        assert_eq!(result, None);
144    }
145
146    #[test]
147    fn test_not_inline_footnote_no_closing() {
148        let result = try_parse_inline_footnote("^[No closing bracket");
149        assert_eq!(result, None);
150    }
151
152    #[test]
153    fn test_not_inline_footnote_just_caret() {
154        let result = try_parse_inline_footnote("^Not a footnote");
155        assert_eq!(result, None);
156    }
157
158    #[test]
159    fn test_empty_inline_footnote() {
160        let result = try_parse_inline_footnote("^[]");
161        assert_eq!(result, Some((3, "")));
162    }
163
164    #[test]
165    fn test_inline_footnote_multiline() {
166        // Inline footnotes can span multiple lines in the source
167        let result = try_parse_inline_footnote("^[This is\na multiline\nnote]");
168        assert_eq!(result, Some((27, "This is\na multiline\nnote")));
169    }
170
171    #[test]
172    fn test_inline_footnote_with_code() {
173        let result = try_parse_inline_footnote("^[Contains `code` inside]");
174        assert_eq!(result, Some((25, "Contains `code` inside")));
175    }
176
177    #[test]
178    fn test_footnote_reference_with_crlf() {
179        // Footnote reference IDs should not span lines with CRLF
180        let input = "[^foo\r\nbar]";
181        let result = try_parse_footnote_reference(input);
182
183        // Should fail to parse because ID contains line break
184        assert_eq!(
185            result, None,
186            "Should not parse footnote reference with CRLF in ID"
187        );
188    }
189
190    #[test]
191    fn test_footnote_reference_with_lf() {
192        // Footnote reference IDs should not span lines with LF either
193        let input = "[^foo\nbar]";
194        let result = try_parse_footnote_reference(input);
195
196        // Should fail to parse because ID contains line break
197        assert_eq!(
198            result, None,
199            "Should not parse footnote reference with LF in ID"
200        );
201    }
202}