panache_parser/parser/inlines/
code_spans.rs1use super::sink::InlineSink;
2use crate::syntax::SyntaxKind;
4
5use crate::parser::utils::attributes::{
7 AttributeBlock, emit_attribute_node, try_parse_trailing_attributes,
8};
9
10#[allow(clippy::type_complexity)]
16pub fn try_parse_code_span(
17 text: &str,
18) -> Option<(usize, &str, usize, Option<(AttributeBlock, &str)>)> {
19 let opening_backticks = text.bytes().take_while(|&b| b == b'`').count();
21 if opening_backticks == 0 {
22 return None;
23 }
24
25 let rest = &text[opening_backticks..];
26 let rest_bytes = rest.as_bytes();
27
28 let mut pos = 0;
34 while pos < rest_bytes.len() {
35 let next_tick = match rest_bytes[pos..].iter().position(|&b| b == b'`') {
36 Some(off) => pos + off,
37 None => break,
38 };
39 let mut closing_backticks = 0;
41 while next_tick + closing_backticks < rest_bytes.len()
42 && rest_bytes[next_tick + closing_backticks] == b'`'
43 {
44 closing_backticks += 1;
45 }
46
47 if closing_backticks == opening_backticks {
48 let code_content = &rest[..next_tick];
50 let after_close = opening_backticks + next_tick + closing_backticks;
51
52 let remaining = &text[after_close..];
54 if remaining.starts_with('{') {
55 if let Some(close_brace_pos) = remaining.find('}') {
57 let attr_text = &remaining[..=close_brace_pos];
58 if let Some((attrs, _)) = try_parse_trailing_attributes(attr_text) {
60 let total_len = after_close + close_brace_pos + 1;
61 return Some((
62 total_len,
63 code_content,
64 opening_backticks,
65 Some((attrs, attr_text)),
66 ));
67 }
68 }
69 }
70
71 return Some((after_close, code_content, opening_backticks, None));
73 }
74 pos = next_tick + closing_backticks;
76 }
77
78 None
80}
81
82pub fn emit_code_span(
84 builder: &mut impl InlineSink,
85 content: &str,
86 backtick_count: usize,
87 attr_text: Option<&str>,
88) {
89 builder.start_node(SyntaxKind::INLINE_CODE.into());
90
91 builder.token(
93 SyntaxKind::INLINE_CODE_MARKER.into(),
94 &"`".repeat(backtick_count),
95 );
96
97 builder.token(SyntaxKind::INLINE_CODE_CONTENT.into(), content);
99
100 builder.token(
102 SyntaxKind::INLINE_CODE_MARKER.into(),
103 &"`".repeat(backtick_count),
104 );
105
106 if let Some(raw) = attr_text {
108 emit_attribute_node(builder, raw);
109 }
110
111 builder.finish_node();
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_parse_simple_code_span() {
120 let result = try_parse_code_span("`code`");
121 assert_eq!(result, Some((6, "code", 1, None)));
122 }
123
124 #[test]
125 fn test_parse_code_span_with_backticks() {
126 let result = try_parse_code_span("`` `backtick` ``");
127 assert_eq!(result, Some((16, " `backtick` ", 2, None)));
128 }
129
130 #[test]
131 fn test_parse_code_span_triple_backticks() {
132 let result = try_parse_code_span("``` `` ```");
133 assert_eq!(result, Some((10, " `` ", 3, None)));
134 }
135
136 #[test]
137 fn test_parse_code_span_no_close() {
138 let result = try_parse_code_span("`no close");
139 assert_eq!(result, None);
140 }
141
142 #[test]
143 fn test_parse_code_span_mismatched_close() {
144 let result = try_parse_code_span("`single``");
145 assert_eq!(result, None);
146 }
147
148 #[test]
149 fn test_not_code_span() {
150 let result = try_parse_code_span("no backticks");
151 assert_eq!(result, None);
152 }
153
154 #[test]
155 fn test_code_span_with_trailing_text() {
156 let result = try_parse_code_span("`code` and more");
157 assert_eq!(result, Some((6, "code", 1, None)));
158 }
159
160 #[test]
161 fn test_code_span_with_simple_class() {
162 let result = try_parse_code_span("`code`{.python}");
163 let (len, content, backticks, attrs) = result.unwrap();
164 assert_eq!(len, 15);
165 assert_eq!(content, "code");
166 assert_eq!(backticks, 1);
167 assert!(attrs.is_some());
168 let (attrs, raw) = attrs.unwrap();
169 assert_eq!(attrs.classes, vec!["python"]);
170 assert_eq!(raw, "{.python}");
171 }
172
173 #[test]
174 fn test_code_span_with_id() {
175 let result = try_parse_code_span("`code`{#mycode}");
176 let (len, content, backticks, attrs) = result.unwrap();
177 assert_eq!(len, 15);
178 assert_eq!(content, "code");
179 assert_eq!(backticks, 1);
180 assert!(attrs.is_some());
181 let (attrs, _raw) = attrs.unwrap();
182 assert_eq!(attrs.identifier, Some("mycode".to_string()));
183 }
184
185 #[test]
186 fn test_code_span_with_full_attributes() {
187 let result = try_parse_code_span("`x + y`{#calc .haskell .eval}");
188 let (len, content, backticks, attrs) = result.unwrap();
189 assert_eq!(len, 29);
190 assert_eq!(content, "x + y");
191 assert_eq!(backticks, 1);
192 assert!(attrs.is_some());
193 let (attrs, _raw) = attrs.unwrap();
194 assert_eq!(attrs.identifier, Some("calc".to_string()));
195 assert_eq!(attrs.classes, vec!["haskell", "eval"]);
196 }
197
198 #[test]
199 fn test_code_span_attributes_must_be_adjacent() {
200 let result = try_parse_code_span("`code` {.python}");
202 assert_eq!(result, Some((6, "code", 1, None)));
203 }
204}