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)]
201pub struct ChunkOptionEntry {
202 option: ChunkOption,
203 source: ChunkOptionSource,
204}
205
206impl ChunkOptionEntry {
207 pub fn new(option: ChunkOption, source: ChunkOptionSource) -> Self {
208 Self { option, source }
209 }
210
211 pub fn option(&self) -> &ChunkOption {
212 &self.option
213 }
214
215 pub fn into_option(self) -> ChunkOption {
216 self.option
217 }
218
219 pub fn source(&self) -> ChunkOptionSource {
220 self.source
221 }
222
223 pub fn key(&self) -> Option<String> {
224 self.option.key()
225 }
226
227 pub fn key_range(&self) -> Option<rowan::TextRange> {
228 self.option.key_range()
229 }
230
231 pub fn value(&self) -> Option<String> {
232 self.option.value()
233 }
234
235 pub fn value_range(&self) -> Option<rowan::TextRange> {
236 self.option.value_range()
237 }
238
239 pub fn is_quoted(&self) -> bool {
240 self.option.is_quoted()
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
245pub enum ChunkLabelSource {
246 InlineLabel,
247 LabelOption,
248}
249
250#[derive(Debug, Clone, PartialEq, Eq, Hash)]
251pub struct ChunkLabelEntry {
252 value: String,
253 declaration_range: rowan::TextRange,
254 value_range: rowan::TextRange,
255 source: ChunkLabelSource,
256}
257
258impl ChunkLabelEntry {
259 pub fn new(
260 value: String,
261 declaration_range: rowan::TextRange,
262 value_range: rowan::TextRange,
263 source: ChunkLabelSource,
264 ) -> Self {
265 Self {
266 value,
267 declaration_range,
268 value_range,
269 source,
270 }
271 }
272
273 pub fn value(&self) -> &str {
274 &self.value
275 }
276
277 pub fn source(&self) -> ChunkLabelSource {
278 self.source
279 }
280
281 pub fn declaration_range(&self) -> rowan::TextRange {
282 self.declaration_range
283 }
284
285 pub fn value_range(&self) -> rowan::TextRange {
286 self.value_range
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Hash)]
291pub enum ChunkInfoItem {
292 Label(ChunkLabel),
293 Class(ChunkClass),
294 Id(ChunkId),
295 Option(ChunkOption),
296}
297
298pub struct ChunkOptions(SyntaxNode);
299
300impl AstNode for ChunkOptions {
301 type Language = PanacheLanguage;
302
303 fn can_cast(kind: SyntaxKind) -> bool {
304 kind == SyntaxKind::CHUNK_OPTIONS
305 }
306
307 fn cast(syntax: SyntaxNode) -> Option<Self> {
308 if Self::can_cast(syntax.kind()) {
309 Some(Self(syntax))
310 } else {
311 None
312 }
313 }
314
315 fn syntax(&self) -> &SyntaxNode {
316 &self.0
317 }
318}
319
320impl ChunkOptions {
321 pub fn options(&self) -> impl Iterator<Item = ChunkOption> {
322 support::children(&self.0)
323 }
324
325 pub fn labels(&self) -> impl Iterator<Item = ChunkLabel> {
326 self.0.children().filter_map(ChunkLabel::cast)
327 }
328
329 pub fn items(&self) -> impl Iterator<Item = ChunkInfoItem> {
330 self.0.children().filter_map(|child| {
331 if let Some(label) = ChunkLabel::cast(child.clone()) {
332 return Some(ChunkInfoItem::Label(label));
333 }
334 if let Some(class) = ChunkClass::cast(child.clone()) {
335 return Some(ChunkInfoItem::Class(class));
336 }
337 if let Some(id) = ChunkId::cast(child.clone()) {
338 return Some(ChunkInfoItem::Id(id));
339 }
340 ChunkOption::cast(child).map(ChunkInfoItem::Option)
341 })
342 }
343
344 pub fn option_entries(&self, source: ChunkOptionSource) -> Vec<ChunkOptionEntry> {
345 self.options()
346 .map(|option| ChunkOptionEntry::new(option, source))
347 .collect()
348 }
349}
350
351pub fn collect_option_entries_from_descendants(
352 root: &SyntaxNode,
353 source: ChunkOptionSource,
354) -> Vec<ChunkOptionEntry> {
355 root.descendants()
356 .filter_map(ChunkOption::cast)
357 .map(|option| ChunkOptionEntry::new(option, source))
358 .collect()
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::options::{Flavor, ParserOptions};
365 use crate::parse;
366
367 #[test]
368 fn test_chunk_option_quoted() {
369 let config = ParserOptions {
370 flavor: Flavor::Quarto,
371 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
372 ..Default::default()
373 };
374 let tree = parse(
375 r#"```{r, fig.cap="A nice plot"}
376x <- 1
377```"#,
378 Some(config),
379 );
380
381 let option = tree
382 .descendants()
383 .find_map(ChunkOption::cast)
384 .expect("Should find chunk option");
385
386 assert_eq!(option.key(), Some("fig.cap".to_string()));
387 assert_eq!(option.value(), Some("A nice plot".to_string()));
388 assert!(option.key_range().is_some());
389 assert!(option.value_range().is_some());
390 assert!(option.is_quoted());
391 assert_eq!(option.quote_char(), Some('"'));
392 }
393
394 #[test]
395 fn test_chunk_option_unquoted() {
396 let config = ParserOptions {
397 flavor: Flavor::Quarto,
398 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
399 ..Default::default()
400 };
401 let tree = parse("```{r, echo=TRUE}\nx <- 1\n```", Some(config));
402
403 let option = tree
404 .descendants()
405 .find_map(ChunkOption::cast)
406 .expect("Should find chunk option");
407
408 assert_eq!(option.key(), Some("echo".to_string()));
409 assert_eq!(option.value(), Some("TRUE".to_string()));
410 assert!(option.key_range().is_some());
411 assert!(option.value_range().is_some());
412 assert!(!option.is_quoted());
413 }
414
415 #[test]
416 fn test_chunk_label() {
417 let config = ParserOptions {
418 flavor: Flavor::Quarto,
419 extensions: crate::options::Extensions::for_flavor(Flavor::Quarto),
420 ..Default::default()
421 };
422 let tree = parse("```{r mylabel}\nx <- 1\n```", Some(config));
423
424 let label = tree
425 .descendants()
426 .find_map(ChunkLabel::cast)
427 .expect("Should find chunk label");
428
429 assert_eq!(label.text(), "mylabel");
430 assert!(!label.range().is_empty());
431 }
432}