panache_parser/parser/inlines/
inline_footnotes.rs1use crate::syntax::SyntaxKind;
7use rowan::GreenNodeBuilder;
8
9use super::core::parse_inline_text;
10use crate::options::ParserOptions;
11
12pub(crate) fn try_parse_inline_footnote(text: &str) -> Option<(usize, &str)> {
18 let bytes = text.as_bytes();
19
20 if bytes.len() < 3 || bytes[0] != b'^' || bytes[1] != b'[' {
22 return None;
23 }
24
25 let mut pos = 2;
27 let mut bracket_depth = 1; while pos < bytes.len() {
30 match bytes[pos] {
31 b'\\' => {
32 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 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 None
57}
58
59pub(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 builder.token(SyntaxKind::INLINE_FOOTNOTE_START.into(), "^[");
69
70 parse_inline_text(builder, content, config, false);
72
73 builder.token(SyntaxKind::INLINE_FOOTNOTE_END.into(), "]");
75
76 builder.finish_node();
77}
78
79pub(crate) fn try_parse_footnote_reference(text: &str) -> Option<(usize, String)> {
82 let bytes = text.as_bytes();
83
84 if bytes.len() < 4 || bytes[0] != b'[' || bytes[1] != b'^' {
86 return None;
87 }
88
89 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
107pub(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 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 let input = "[^foo\r\nbar]";
185 let result = try_parse_footnote_reference(input);
186
187 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 let input = "[^foo\nbar]";
198 let result = try_parse_footnote_reference(input);
199
200 assert_eq!(
202 result, None,
203 "Should not parse footnote reference with LF in ID"
204 );
205 }
206}