1use 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, PartialEq, Eq, Hash)]
135pub struct ChunkClass(SyntaxNode);
136
137impl AstNode for ChunkClass {
138 type Language = PanacheLanguage;
139
140 fn can_cast(kind: SyntaxKind) -> bool {
141 kind == SyntaxKind::ATTR_CLASS
142 }
143
144 fn cast(node: SyntaxNode) -> Option<Self> {
145 Self::can_cast(node.kind()).then(|| ChunkClass(node))
146 }
147
148 fn syntax(&self) -> &SyntaxNode {
149 &self.0
150 }
151}
152
153impl ChunkClass {
154 pub fn text(&self) -> String {
155 self.0.text().to_string()
156 }
157
158 pub fn range(&self) -> rowan::TextRange {
159 self.0.text_range()
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Hash)]
166pub struct ChunkId(SyntaxNode);
167
168impl AstNode for ChunkId {
169 type Language = PanacheLanguage;
170
171 fn can_cast(kind: SyntaxKind) -> bool {
172 kind == SyntaxKind::ATTR_ID
173 }
174
175 fn cast(node: SyntaxNode) -> Option<Self> {
176 Self::can_cast(node.kind()).then(|| ChunkId(node))
177 }
178
179 fn syntax(&self) -> &SyntaxNode {
180 &self.0
181 }
182}
183
184impl ChunkId {
185 pub fn text(&self) -> String {
186 self.0.text().to_string()
187 }
188
189 pub fn range(&self) -> rowan::TextRange {
190 self.0.text_range()
191 }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
195pub enum ChunkOptionSource {
196 InlineInfo,
197 HashpipeYaml,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Hash)]
206pub struct ChunkOptionEntry {
207 key: Option<String>,
208 value: Option<String>,
209 key_range: Option<rowan::TextRange>,
210 value_range: Option<rowan::TextRange>,
211 is_quoted: bool,
212 declaration_range: rowan::TextRange,
213 source: ChunkOptionSource,
214}
215
216impl ChunkOptionEntry {
217 #[allow(clippy::too_many_arguments)]
218 pub fn new(
219 key: Option<String>,
220 value: Option<String>,
221 key_range: Option<rowan::TextRange>,
222 value_range: Option<rowan::TextRange>,
223 is_quoted: bool,
224 declaration_range: rowan::TextRange,
225 source: ChunkOptionSource,
226 ) -> Self {
227 Self {
228 key,
229 value,
230 key_range,
231 value_range,
232 is_quoted,
233 declaration_range,
234 source,
235 }
236 }
237
238 pub fn from_inline_option(option: &ChunkOption, source: ChunkOptionSource) -> Self {
240 Self::new(
241 option.key(),
242 option.value(),
243 option.key_range(),
244 option.value_range(),
245 option.is_quoted(),
246 option.syntax().text_range(),
247 source,
248 )
249 }
250
251 pub fn source(&self) -> ChunkOptionSource {
252 self.source
253 }
254
255 pub fn key(&self) -> Option<String> {
256 self.key.clone()
257 }
258
259 pub fn key_range(&self) -> Option<rowan::TextRange> {
260 self.key_range
261 }
262
263 pub fn value(&self) -> Option<String> {
264 self.value.clone()
265 }
266
267 pub fn value_range(&self) -> Option<rowan::TextRange> {
268 self.value_range
269 }
270
271 pub fn is_quoted(&self) -> bool {
272 self.is_quoted
273 }
274
275 pub fn declaration_range(&self) -> rowan::TextRange {
278 self.declaration_range
279 }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
283pub enum ChunkLabelSource {
284 InlineLabel,
285 LabelOption,
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Hash)]
289pub struct ChunkLabelEntry {
290 value: String,
291 declaration_range: rowan::TextRange,
292 value_range: rowan::TextRange,
293 source: ChunkLabelSource,
294}
295
296impl ChunkLabelEntry {
297 pub fn new(
298 value: String,
299 declaration_range: rowan::TextRange,
300 value_range: rowan::TextRange,
301 source: ChunkLabelSource,
302 ) -> Self {
303 Self {
304 value,
305 declaration_range,
306 value_range,
307 source,
308 }
309 }
310
311 pub fn value(&self) -> &str {
312 &self.value
313 }
314
315 pub fn source(&self) -> ChunkLabelSource {
316 self.source
317 }
318
319 pub fn declaration_range(&self) -> rowan::TextRange {
320 self.declaration_range
321 }
322
323 pub fn value_range(&self) -> rowan::TextRange {
324 self.value_range
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Hash)]
329pub enum ChunkInfoItem {
330 Label(ChunkLabel),
331 Class(ChunkClass),
332 Id(ChunkId),
333 Option(ChunkOption),
334}
335
336pub struct ChunkOptions(SyntaxNode);
337
338impl AstNode for ChunkOptions {
339 type Language = PanacheLanguage;
340
341 fn can_cast(kind: SyntaxKind) -> bool {
342 kind == SyntaxKind::CHUNK_OPTIONS
343 }
344
345 fn cast(syntax: SyntaxNode) -> Option<Self> {
346 if Self::can_cast(syntax.kind()) {
347 Some(Self(syntax))
348 } else {
349 None
350 }
351 }
352
353 fn syntax(&self) -> &SyntaxNode {
354 &self.0
355 }
356}
357
358impl ChunkOptions {
359 pub fn options(&self) -> impl Iterator<Item = ChunkOption> {
360 support::children(&self.0)
361 }
362
363 pub fn labels(&self) -> impl Iterator<Item = ChunkLabel> {
364 self.0.children().filter_map(ChunkLabel::cast)
365 }
366
367 pub fn items(&self) -> impl Iterator<Item = ChunkInfoItem> {
368 self.0.children().filter_map(|child| {
369 if let Some(label) = ChunkLabel::cast(child.clone()) {
370 return Some(ChunkInfoItem::Label(label));
371 }
372 if let Some(class) = ChunkClass::cast(child.clone()) {
373 return Some(ChunkInfoItem::Class(class));
374 }
375 if let Some(id) = ChunkId::cast(child.clone()) {
376 return Some(ChunkInfoItem::Id(id));
377 }
378 ChunkOption::cast(child).map(ChunkInfoItem::Option)
379 })
380 }
381
382 pub fn option_entries(&self, source: ChunkOptionSource) -> Vec<ChunkOptionEntry> {
383 self.options()
384 .map(|option| ChunkOptionEntry::from_inline_option(&option, source))
385 .collect()
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use crate::options::{Flavor, ParserOptions};
393 use crate::parse;
394
395 #[test]
396 fn test_chunk_option_quoted() {
397 let config = ParserOptions {
398 flavor: Flavor::Quarto,
399 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
400 ..Default::default()
401 };
402 let tree = parse(
403 r#"```{r, fig.cap="A nice plot"}
404x <- 1
405```"#,
406 Some(config),
407 );
408
409 let option = tree
410 .descendants()
411 .find_map(ChunkOption::cast)
412 .expect("Should find chunk option");
413
414 assert_eq!(option.key(), Some("fig.cap".to_string()));
415 assert_eq!(option.value(), Some("A nice plot".to_string()));
416 assert!(option.key_range().is_some());
417 assert!(option.value_range().is_some());
418 assert!(option.is_quoted());
419 assert_eq!(option.quote_char(), Some('"'));
420 }
421
422 #[test]
423 fn test_chunk_option_unquoted() {
424 let config = ParserOptions {
425 flavor: Flavor::Quarto,
426 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
427 ..Default::default()
428 };
429 let tree = parse("```{r, echo=TRUE}\nx <- 1\n```", Some(config));
430
431 let option = tree
432 .descendants()
433 .find_map(ChunkOption::cast)
434 .expect("Should find chunk option");
435
436 assert_eq!(option.key(), Some("echo".to_string()));
437 assert_eq!(option.value(), Some("TRUE".to_string()));
438 assert!(option.key_range().is_some());
439 assert!(option.value_range().is_some());
440 assert!(!option.is_quoted());
441 }
442
443 #[test]
444 fn test_chunk_label() {
445 let config = ParserOptions {
446 flavor: Flavor::Quarto,
447 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
448 ..Default::default()
449 };
450 let tree = parse("```{r mylabel}\nx <- 1\n```", Some(config));
451
452 let label = tree
453 .descendants()
454 .find_map(ChunkLabel::cast)
455 .expect("Should find chunk label");
456
457 assert_eq!(label.text(), "mylabel");
458 assert!(!label.range().is_empty());
459 }
460}