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