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