panache_parser/syntax/
chunk_options.rs1use crate::syntax::ast::support;
4use crate::syntax::{AstNode, PanacheLanguage, SyntaxKind, SyntaxNode};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct ChunkOption(SyntaxNode);
9
10impl AstNode for ChunkOption {
11 type Language = PanacheLanguage;
12
13 fn can_cast(kind: SyntaxKind) -> bool {
14 kind == SyntaxKind::CHUNK_OPTION
15 }
16
17 fn cast(node: SyntaxNode) -> Option<Self> {
18 Self::can_cast(node.kind()).then(|| ChunkOption(node))
19 }
20
21 fn syntax(&self) -> &SyntaxNode {
22 &self.0
23 }
24}
25
26impl ChunkOption {
27 pub fn key(&self) -> Option<String> {
29 self.0.children_with_tokens().find_map(|child| {
30 if let rowan::NodeOrToken::Token(token) = child
31 && token.kind() == SyntaxKind::CHUNK_OPTION_KEY
32 {
33 return Some(token.text().to_string());
34 }
35 None
36 })
37 }
38
39 pub fn value(&self) -> Option<String> {
42 self.0.children_with_tokens().find_map(|child| {
43 if let rowan::NodeOrToken::Token(token) = child
44 && token.kind() == SyntaxKind::CHUNK_OPTION_VALUE
45 {
46 return Some(token.text().to_string());
47 }
48 None
49 })
50 }
51
52 pub fn value_range(&self) -> Option<rowan::TextRange> {
54 self.0.children_with_tokens().find_map(|child| {
55 if let rowan::NodeOrToken::Token(token) = child
56 && token.kind() == SyntaxKind::CHUNK_OPTION_VALUE
57 {
58 return Some(token.text_range());
59 }
60 None
61 })
62 }
63
64 pub fn key_range(&self) -> Option<rowan::TextRange> {
66 self.0.children_with_tokens().find_map(|child| {
67 if let rowan::NodeOrToken::Token(token) = child
68 && token.kind() == SyntaxKind::CHUNK_OPTION_KEY
69 {
70 return Some(token.text_range());
71 }
72 None
73 })
74 }
75
76 pub fn is_quoted(&self) -> bool {
78 self.0.children_with_tokens().any(|child| {
79 if let rowan::NodeOrToken::Token(token) = child {
80 token.kind() == SyntaxKind::CHUNK_OPTION_QUOTE
81 } else {
82 false
83 }
84 })
85 }
86
87 pub fn quote_char(&self) -> Option<char> {
89 self.0.children_with_tokens().find_map(|child| {
90 if let rowan::NodeOrToken::Token(token) = child
91 && token.kind() == SyntaxKind::CHUNK_OPTION_QUOTE
92 {
93 return token.text().chars().next();
94 }
95 None
96 })
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct ChunkLabel(SyntaxNode);
103
104impl AstNode for ChunkLabel {
105 type Language = PanacheLanguage;
106
107 fn can_cast(kind: SyntaxKind) -> bool {
108 kind == SyntaxKind::CHUNK_LABEL
109 }
110
111 fn cast(node: SyntaxNode) -> Option<Self> {
112 Self::can_cast(node.kind()).then(|| ChunkLabel(node))
113 }
114
115 fn syntax(&self) -> &SyntaxNode {
116 &self.0
117 }
118}
119
120impl ChunkLabel {
121 pub fn text(&self) -> String {
123 self.0.text().to_string()
124 }
125
126 pub fn range(&self) -> rowan::TextRange {
128 self.0.text_range()
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133pub enum ChunkOptionSource {
134 InlineInfo,
135 HashpipeYaml,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Hash)]
139pub struct ChunkOptionEntry {
140 option: ChunkOption,
141 source: ChunkOptionSource,
142}
143
144impl ChunkOptionEntry {
145 pub fn new(option: ChunkOption, source: ChunkOptionSource) -> Self {
146 Self { option, source }
147 }
148
149 pub fn option(&self) -> &ChunkOption {
150 &self.option
151 }
152
153 pub fn into_option(self) -> ChunkOption {
154 self.option
155 }
156
157 pub fn source(&self) -> ChunkOptionSource {
158 self.source
159 }
160
161 pub fn key(&self) -> Option<String> {
162 self.option.key()
163 }
164
165 pub fn key_range(&self) -> Option<rowan::TextRange> {
166 self.option.key_range()
167 }
168
169 pub fn value(&self) -> Option<String> {
170 self.option.value()
171 }
172
173 pub fn value_range(&self) -> Option<rowan::TextRange> {
174 self.option.value_range()
175 }
176
177 pub fn is_quoted(&self) -> bool {
178 self.option.is_quoted()
179 }
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub enum ChunkLabelSource {
184 InlineLabel,
185 LabelOption,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Hash)]
189pub struct ChunkLabelEntry {
190 value: String,
191 declaration_range: rowan::TextRange,
192 value_range: rowan::TextRange,
193 source: ChunkLabelSource,
194}
195
196impl ChunkLabelEntry {
197 pub fn new(
198 value: String,
199 declaration_range: rowan::TextRange,
200 value_range: rowan::TextRange,
201 source: ChunkLabelSource,
202 ) -> Self {
203 Self {
204 value,
205 declaration_range,
206 value_range,
207 source,
208 }
209 }
210
211 pub fn value(&self) -> &str {
212 &self.value
213 }
214
215 pub fn source(&self) -> ChunkLabelSource {
216 self.source
217 }
218
219 pub fn declaration_range(&self) -> rowan::TextRange {
220 self.declaration_range
221 }
222
223 pub fn value_range(&self) -> rowan::TextRange {
224 self.value_range
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub enum ChunkInfoItem {
230 Label(ChunkLabel),
231 Option(ChunkOption),
232}
233
234pub struct ChunkOptions(SyntaxNode);
235
236impl AstNode for ChunkOptions {
237 type Language = PanacheLanguage;
238
239 fn can_cast(kind: SyntaxKind) -> bool {
240 kind == SyntaxKind::CHUNK_OPTIONS
241 }
242
243 fn cast(syntax: SyntaxNode) -> Option<Self> {
244 if Self::can_cast(syntax.kind()) {
245 Some(Self(syntax))
246 } else {
247 None
248 }
249 }
250
251 fn syntax(&self) -> &SyntaxNode {
252 &self.0
253 }
254}
255
256impl ChunkOptions {
257 pub fn options(&self) -> impl Iterator<Item = ChunkOption> {
258 support::children(&self.0)
259 }
260
261 pub fn labels(&self) -> impl Iterator<Item = ChunkLabel> {
262 self.0.children().filter_map(ChunkLabel::cast)
263 }
264
265 pub fn items(&self) -> impl Iterator<Item = ChunkInfoItem> {
266 self.0.children().filter_map(|child| {
267 if let Some(label) = ChunkLabel::cast(child.clone()) {
268 return Some(ChunkInfoItem::Label(label));
269 }
270 ChunkOption::cast(child).map(ChunkInfoItem::Option)
271 })
272 }
273
274 pub fn option_entries(&self, source: ChunkOptionSource) -> Vec<ChunkOptionEntry> {
275 self.options()
276 .map(|option| ChunkOptionEntry::new(option, source))
277 .collect()
278 }
279}
280
281pub fn collect_option_entries_from_descendants(
282 root: &SyntaxNode,
283 source: ChunkOptionSource,
284) -> Vec<ChunkOptionEntry> {
285 root.descendants()
286 .filter_map(ChunkOption::cast)
287 .map(|option| ChunkOptionEntry::new(option, source))
288 .collect()
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::options::{Flavor, ParserOptions};
295 use crate::parse;
296
297 #[test]
298 fn test_chunk_option_quoted() {
299 let config = ParserOptions {
300 flavor: Flavor::Quarto,
301 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
302 ..Default::default()
303 };
304 let tree = parse(
305 r#"```{r, fig.cap="A nice plot"}
306x <- 1
307```"#,
308 Some(config),
309 );
310
311 let option = tree
312 .descendants()
313 .find_map(ChunkOption::cast)
314 .expect("Should find chunk option");
315
316 assert_eq!(option.key(), Some("fig.cap".to_string()));
317 assert_eq!(option.value(), Some("A nice plot".to_string()));
318 assert!(option.key_range().is_some());
319 assert!(option.value_range().is_some());
320 assert!(option.is_quoted());
321 assert_eq!(option.quote_char(), Some('"'));
322 }
323
324 #[test]
325 fn test_chunk_option_unquoted() {
326 let config = ParserOptions {
327 flavor: Flavor::Quarto,
328 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
329 ..Default::default()
330 };
331 let tree = parse("```{r, echo=TRUE}\nx <- 1\n```", Some(config));
332
333 let option = tree
334 .descendants()
335 .find_map(ChunkOption::cast)
336 .expect("Should find chunk option");
337
338 assert_eq!(option.key(), Some("echo".to_string()));
339 assert_eq!(option.value(), Some("TRUE".to_string()));
340 assert!(option.key_range().is_some());
341 assert!(option.value_range().is_some());
342 assert!(!option.is_quoted());
343 }
344
345 #[test]
346 fn test_chunk_label() {
347 let config = ParserOptions {
348 flavor: Flavor::Quarto,
349 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
350 ..Default::default()
351 };
352 let tree = parse("```{r mylabel}\nx <- 1\n```", Some(config));
353
354 let label = tree
355 .descendants()
356 .find_map(ChunkLabel::cast)
357 .expect("Should find chunk label");
358
359 assert_eq!(label.text(), "mylabel");
360 assert!(!label.range().is_empty());
361 }
362}