panache_parser/parser/inlines/
inline_footnotes.rs1use crate::syntax::SyntaxKind;
7use rowan::GreenNodeBuilder;
8
9use super::core::parse_inline_text;
10use crate::config::Config;
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(builder: &mut GreenNodeBuilder, content: &str, config: &Config) {
61 builder.start_node(SyntaxKind::INLINE_FOOTNOTE.into());
62
63 builder.token(SyntaxKind::INLINE_FOOTNOTE_START.into(), "^[");
65
66 parse_inline_text(builder, content, config, false);
68
69 builder.token(SyntaxKind::INLINE_FOOTNOTE_END.into(), "]");
71
72 builder.finish_node();
73}
74
75pub(crate) fn try_parse_footnote_reference(text: &str) -> Option<(usize, String)> {
78 let bytes = text.as_bytes();
79
80 if bytes.len() < 4 || bytes[0] != b'[' || bytes[1] != b'^' {
82 return None;
83 }
84
85 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
103pub(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 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 let input = "[^foo\r\nbar]";
181 let result = try_parse_footnote_reference(input);
182
183 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 let input = "[^foo\nbar]";
194 let result = try_parse_footnote_reference(input);
195
196 assert_eq!(
198 result, None,
199 "Should not parse footnote reference with LF in ID"
200 );
201 }
202}