1use std::ops::Range;
2
3use crate::parser::utils::yaml_regions::hashpipe_language_and_prefix;
4use crate::parser::yaml::YamlDiagnostic;
5use crate::syntax::{
6 AstNode, PanacheLanguage, SyntaxKind, SyntaxNode, YamlDocument, YamlScalarStyle,
7};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct YamlFrontmatterRegion {
11 pub id: String,
12 pub host_range: Range<usize>,
13 pub content_range: Range<usize>,
14 pub content: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum YamlRegionKind {
19 Frontmatter,
20 Hashpipe,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct YamlRegion {
25 pub id: String,
26 pub kind: YamlRegionKind,
27 pub host_range: Range<usize>,
28 pub region_range: Range<usize>,
29 pub content_range: Range<usize>,
30 pub content: String,
31}
32
33#[derive(Debug, Clone)]
34pub struct ParsedYamlRegion {
35 region: YamlRegion,
36 embedded: Option<SyntaxNode>,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ParsedYamlRegionSnapshot {
45 region: YamlRegion,
46 parse_ok: bool,
47 document_shape_summary: Option<String>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum YamlEmbeddingHostKind {
52 FrontmatterMetadata,
53 HashpipePreamble,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57pub struct YamlMetadata(SyntaxNode);
58
59impl AstNode for YamlMetadata {
60 type Language = PanacheLanguage;
61
62 fn can_cast(kind: SyntaxKind) -> bool {
63 kind == SyntaxKind::YAML_METADATA
64 }
65
66 fn cast(node: SyntaxNode) -> Option<Self> {
67 Self::can_cast(node.kind()).then(|| Self(node))
68 }
69
70 fn syntax(&self) -> &SyntaxNode {
71 &self.0
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Hash)]
76pub struct HashpipeYamlPreamble(SyntaxNode);
77
78impl AstNode for HashpipeYamlPreamble {
79 type Language = PanacheLanguage;
80
81 fn can_cast(kind: SyntaxKind) -> bool {
82 kind == SyntaxKind::HASHPIPE_YAML_PREAMBLE
83 }
84
85 fn cast(node: SyntaxNode) -> Option<Self> {
86 Self::can_cast(node.kind()).then(|| Self(node))
87 }
88
89 fn syntax(&self) -> &SyntaxNode {
90 &self.0
91 }
92}
93
94#[derive(Debug, Clone)]
95pub enum YamlEmbeddingHost {
96 FrontmatterMetadata(YamlMetadata),
97 HashpipePreamble(HashpipeYamlPreamble),
98}
99
100#[derive(Debug, Clone)]
101pub struct YamlEmbeddedCst {
102 host: YamlEmbeddingHost,
103 parsed: ParsedYamlRegion,
104}
105
106impl YamlEmbeddedCst {
107 pub fn host_kind(&self) -> YamlEmbeddingHostKind {
108 match self.host {
109 YamlEmbeddingHost::FrontmatterMetadata(_) => YamlEmbeddingHostKind::FrontmatterMetadata,
110 YamlEmbeddingHost::HashpipePreamble(_) => YamlEmbeddingHostKind::HashpipePreamble,
111 }
112 }
113
114 pub fn host_node(&self) -> &SyntaxNode {
115 match &self.host {
116 YamlEmbeddingHost::FrontmatterMetadata(host) => host.syntax(),
117 YamlEmbeddingHost::HashpipePreamble(host) => host.syntax(),
118 }
119 }
120
121 pub fn frontmatter_host(&self) -> Option<&YamlMetadata> {
122 match &self.host {
123 YamlEmbeddingHost::FrontmatterMetadata(host) => Some(host),
124 _ => None,
125 }
126 }
127
128 pub fn hashpipe_host(&self) -> Option<&HashpipeYamlPreamble> {
129 match &self.host {
130 YamlEmbeddingHost::HashpipePreamble(host) => Some(host),
131 _ => None,
132 }
133 }
134
135 pub fn parsed(&self) -> &ParsedYamlRegion {
136 &self.parsed
137 }
138
139 pub fn yaml_content(&self) -> &str {
140 self.parsed.content()
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum YamlAstRootKind {
146 Root,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum YamlDocumentKind {
151 BlockMap,
152 BlockSeq,
153 BlockScalar,
154 Flow,
155 Empty,
156}
157
158#[derive(Debug, Clone, Copy)]
159pub struct YamlAstRoot<'a> {
160 node: &'a SyntaxNode,
161}
162
163impl YamlAstRoot<'_> {
164 pub fn kind(&self) -> YamlAstRootKind {
165 YamlAstRootKind::Root
166 }
167
168 pub fn document_count(&self) -> usize {
169 self.documents().count()
170 }
171
172 pub fn first_document_kind(&self) -> Option<YamlDocumentKind> {
173 let doc = self.documents().next()?;
174 if doc.block_map().is_some() {
175 return Some(YamlDocumentKind::BlockMap);
176 }
177 if doc.block_sequence().is_some() {
178 return Some(YamlDocumentKind::BlockSeq);
179 }
180 if let Some(scalar) = doc.scalar() {
181 return Some(match scalar.style() {
182 YamlScalarStyle::Literal | YamlScalarStyle::Folded => YamlDocumentKind::BlockScalar,
183 _ => YamlDocumentKind::Flow,
184 });
185 }
186 if doc.flow_map().is_some() || doc.flow_sequence().is_some() {
187 return Some(YamlDocumentKind::Flow);
188 }
189 Some(YamlDocumentKind::Empty)
190 }
191
192 fn documents(&self) -> impl Iterator<Item = YamlDocument> + '_ {
193 self.node.descendants().filter_map(YamlDocument::cast)
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct YamlParseError {
199 offset: usize,
200 message: String,
201}
202
203impl YamlParseError {
204 pub fn offset(&self) -> usize {
205 self.offset
206 }
207
208 pub fn message(&self) -> &str {
209 &self.message
210 }
211
212 fn from_diagnostic(diag: &YamlDiagnostic) -> Self {
213 Self {
214 offset: diag.byte_start,
215 message: diag.message.to_string(),
216 }
217 }
218}
219
220impl ParsedYamlRegion {
221 pub fn id(&self) -> &str {
222 &self.region.id
223 }
224
225 pub fn kind(&self) -> &YamlRegionKind {
226 &self.region.kind
227 }
228
229 pub fn is_frontmatter(&self) -> bool {
230 matches!(self.region.kind, YamlRegionKind::Frontmatter)
231 }
232
233 pub fn is_hashpipe(&self) -> bool {
234 matches!(self.region.kind, YamlRegionKind::Hashpipe)
235 }
236
237 pub fn root(&self) -> Option<YamlAstRoot<'_>> {
241 self.embedded.as_ref().map(|node| YamlAstRoot { node })
242 }
243
244 pub fn root_kind(&self) -> Option<YamlAstRootKind> {
245 self.root().map(|root| root.kind())
246 }
247
248 pub fn is_valid(&self) -> bool {
252 self.embedded.is_some()
253 }
254
255 pub fn host_range(&self) -> Range<usize> {
256 self.region.host_range.clone()
257 }
258
259 pub fn content_range(&self) -> Range<usize> {
260 self.region.content_range.clone()
261 }
262
263 pub fn region_range(&self) -> Range<usize> {
264 self.region.region_range.clone()
265 }
266
267 pub fn to_region(&self) -> YamlRegion {
268 self.region.clone()
269 }
270
271 pub fn content(&self) -> &str {
272 &self.region.content
273 }
274
275 pub fn document_shape_summary(&self) -> Option<String> {
276 let root = self.root()?;
277 let doc_count = root.document_count();
278 let first_kind = root.first_document_kind();
279 Some(match first_kind {
280 Some(kind) => format!("{:?} docs={} first={:?}", root.kind(), doc_count, kind),
281 None => format!("{:?} docs={}", root.kind(), doc_count),
282 })
283 }
284
285 pub fn to_snapshot(&self) -> ParsedYamlRegionSnapshot {
286 ParsedYamlRegionSnapshot {
287 region: self.region.clone(),
288 parse_ok: self.is_valid(),
289 document_shape_summary: self.document_shape_summary(),
290 }
291 }
292}
293
294impl ParsedYamlRegionSnapshot {
295 pub fn id(&self) -> &str {
296 &self.region.id
297 }
298
299 pub fn is_frontmatter(&self) -> bool {
300 matches!(self.region.kind, YamlRegionKind::Frontmatter)
301 }
302
303 pub fn is_hashpipe(&self) -> bool {
304 matches!(self.region.kind, YamlRegionKind::Hashpipe)
305 }
306
307 pub fn is_valid(&self) -> bool {
308 self.parse_ok
309 }
310
311 pub fn host_range(&self) -> Range<usize> {
312 self.region.host_range.clone()
313 }
314
315 pub fn document_shape_summary(&self) -> Option<&str> {
316 self.document_shape_summary.as_deref()
317 }
318
319 pub fn to_region(&self) -> YamlRegion {
320 self.region.clone()
321 }
322}
323
324pub fn collect_frontmatter_region(tree: &SyntaxNode) -> Option<YamlFrontmatterRegion> {
325 let metadata = tree
326 .descendants()
327 .find(|node| node.kind() == SyntaxKind::YAML_METADATA)?;
328 let content_node = metadata
329 .children()
330 .find(|child| child.kind() == SyntaxKind::YAML_METADATA_CONTENT)?;
331
332 let host_start: usize = metadata.text_range().start().into();
333 let host_end: usize = metadata.text_range().end().into();
334 let content_start: usize = content_node.text_range().start().into();
335 let content_end: usize = content_node.text_range().end().into();
336
337 Some(YamlFrontmatterRegion {
338 id: format!("frontmatter:{}:{}", content_start, content_end),
339 host_range: host_start..host_end,
340 content_range: content_start..content_end,
341 content: content_node.text().to_string(),
342 })
343}
344
345pub fn collect_frontmatter_yaml_region(tree: &SyntaxNode) -> Option<YamlRegion> {
346 let frontmatter = collect_frontmatter_region(tree)?;
347 let content_range = frontmatter.content_range.clone();
348 Some(YamlRegion {
349 id: frontmatter.id,
350 kind: YamlRegionKind::Frontmatter,
351 host_range: frontmatter.host_range.clone(),
352 region_range: frontmatter.host_range,
353 content_range,
354 content: frontmatter.content,
355 })
356}
357
358pub fn collect_hashpipe_regions(tree: &SyntaxNode) -> Vec<YamlRegion> {
359 let mut regions = Vec::new();
360 for node in tree
361 .descendants()
362 .filter(|n| n.kind() == SyntaxKind::CODE_BLOCK)
363 {
364 let mut info_text: Option<String> = None;
365 let mut content_node: Option<SyntaxNode> = None;
366 for child in node.children() {
367 match child.kind() {
368 SyntaxKind::CODE_FENCE_OPEN => {
369 for nested in child.children() {
370 if nested.kind() == SyntaxKind::CODE_INFO {
371 info_text = Some(nested.text().to_string());
372 }
373 }
374 }
375 SyntaxKind::CODE_CONTENT => content_node = Some(child),
376 _ => {}
377 }
378 }
379 let (Some(info_text), Some(content_node)) = (info_text, content_node) else {
380 continue;
381 };
382 let Some((language, prefix)) = hashpipe_language_and_prefix(&info_text) else {
383 continue;
384 };
385
386 let host_start: usize = node.text_range().start().into();
387 let host_end: usize = node.text_range().end().into();
388 let Some(preamble) = content_node
389 .children()
390 .find(|n| n.kind() == SyntaxKind::HASHPIPE_YAML_PREAMBLE)
391 else {
392 continue;
393 };
394 let Some(preamble_content) = preamble
395 .children()
396 .find(|n| n.kind() == SyntaxKind::HASHPIPE_YAML_CONTENT)
397 else {
398 continue;
399 };
400 let preamble_text = preamble_content.text().to_string();
401 let preamble_start: usize = preamble_content.text_range().start().into();
402 if let Some(region) = extract_hashpipe_region(
403 &preamble_text,
404 host_start,
405 host_end,
406 preamble_start,
407 prefix,
408 language.as_str(),
409 ) {
410 regions.push(region);
411 }
412 }
413 regions
414}
415
416pub fn collect_yaml_regions(tree: &SyntaxNode) -> Vec<YamlRegion> {
417 let mut regions = Vec::new();
418 if let Some(frontmatter) = collect_frontmatter_yaml_region(tree) {
419 regions.push(frontmatter);
420 }
421 regions.extend(collect_hashpipe_regions(tree));
422 regions
423}
424
425pub fn collect_parsed_yaml_regions(tree: &SyntaxNode) -> Vec<ParsedYamlRegion> {
426 let embedded_frontmatter = embedded_frontmatter_stream(tree);
427 collect_yaml_regions(tree)
428 .into_iter()
429 .map(|region| {
430 let embedded = match ®ion.kind {
435 YamlRegionKind::Frontmatter => embedded_frontmatter.clone(),
436 YamlRegionKind::Hashpipe => embedded_hashpipe_stream(tree, ®ion.region_range),
437 };
438 ParsedYamlRegion { embedded, region }
439 })
440 .collect()
441}
442
443fn embedded_frontmatter_stream(tree: &SyntaxNode) -> Option<SyntaxNode> {
451 let metadata = tree
452 .descendants()
453 .find(|node| node.kind() == SyntaxKind::YAML_METADATA)?;
454 let content_node = metadata
455 .children()
456 .find(|child| child.kind() == SyntaxKind::YAML_METADATA_CONTENT)?;
457 (!is_opaque_yaml_fallback(&content_node)).then_some(content_node)
458}
459
460fn embedded_hashpipe_stream(tree: &SyntaxNode, region_range: &Range<usize>) -> Option<SyntaxNode> {
466 tree.descendants()
467 .filter(|node| node.kind() == SyntaxKind::HASHPIPE_YAML_CONTENT)
468 .find(|node| {
469 let start: usize = node.text_range().start().into();
470 let end: usize = node.text_range().end().into();
471 start == region_range.start && end == region_range.end
472 })
473 .filter(|node| !is_opaque_yaml_fallback(node))
474}
475
476fn is_opaque_yaml_fallback(content_node: &SyntaxNode) -> bool {
483 content_node
484 .children_with_tokens()
485 .any(|element| element.kind() == SyntaxKind::TEXT)
486}
487
488pub fn collect_parsed_frontmatter_region(tree: &SyntaxNode) -> Option<ParsedYamlRegion> {
489 collect_parsed_yaml_regions(tree)
490 .into_iter()
491 .find(|region| region.is_frontmatter())
492}
493
494pub fn collect_parsed_yaml_region_snapshots(tree: &SyntaxNode) -> Vec<ParsedYamlRegionSnapshot> {
495 collect_parsed_yaml_regions(tree)
496 .iter()
497 .map(ParsedYamlRegion::to_snapshot)
498 .collect()
499}
500
501pub fn validate_yaml_text(input: &str) -> Result<(), YamlParseError> {
502 match crate::parser::yaml::parse_yaml_report(input)
503 .diagnostics
504 .first()
505 {
506 Some(diag) => Err(YamlParseError::from_diagnostic(diag)),
507 None => Ok(()),
508 }
509}
510
511pub fn validate_yaml_text_with_context(
518 input: &str,
519 ctx: crate::parser::yaml::YamlValidationContext,
520) -> Result<(), YamlParseError> {
521 match crate::parser::yaml::locate_yaml_diagnostic_ctx(input, "", ctx) {
522 Some((diag, _start, _end)) => Err(YamlParseError::from_diagnostic(&diag)),
523 None => Ok(()),
524 }
525}
526
527pub fn collect_embedded_yaml_cst(tree: &SyntaxNode) -> Vec<YamlEmbeddedCst> {
528 let parsed_regions = collect_parsed_yaml_regions(tree);
529 let frontmatter_node = tree.descendants().find_map(YamlMetadata::cast);
530 let hashpipe_preambles: Vec<HashpipeYamlPreamble> = tree
531 .descendants()
532 .filter_map(HashpipeYamlPreamble::cast)
533 .collect();
534
535 let mut embedded = Vec::new();
536 for parsed in parsed_regions {
537 match parsed.kind() {
538 YamlRegionKind::Frontmatter => {
539 if let Some(node) = frontmatter_node.clone() {
540 embedded.push(YamlEmbeddedCst {
541 host: YamlEmbeddingHost::FrontmatterMetadata(node),
542 parsed,
543 });
544 }
545 }
546 YamlRegionKind::Hashpipe => {
547 if let Some(node) = hashpipe_preambles.iter().find(|node| {
548 let range: Range<usize> = node.syntax().text_range().start().into()
549 ..node.syntax().text_range().end().into();
550 range == parsed.region_range()
551 }) {
552 embedded.push(YamlEmbeddedCst {
553 host: YamlEmbeddingHost::HashpipePreamble(node.clone()),
554 parsed,
555 });
556 }
557 }
558 }
559 }
560 embedded
561}
562
563pub fn collect_embedded_frontmatter_yaml_cst(tree: &SyntaxNode) -> Option<YamlEmbeddedCst> {
564 collect_embedded_yaml_cst(tree)
565 .into_iter()
566 .find(|embedded| embedded.frontmatter_host().is_some())
567}
568
569fn extract_hashpipe_region(
570 content: &str,
571 host_start: usize,
572 host_end: usize,
573 content_start: usize,
574 prefix: &str,
575 language: &str,
576) -> Option<YamlRegion> {
577 let lines: Vec<&str> = content.split_inclusive('\n').collect();
578 if lines.is_empty() {
579 return None;
580 }
581 let mut collected = String::new();
586 let mut offset = 0usize;
587 for line in &lines {
588 let line = *line;
589 let line_core = line.strip_suffix('\n').unwrap_or(line);
590 let line_core = line_core.strip_suffix('\r').unwrap_or(line_core);
591 let eol = &line[line_core.len()..];
592 let indent_len = line_core
593 .chars()
594 .take_while(|ch| *ch == ' ' || *ch == '\t')
595 .map(char::len_utf8)
596 .sum::<usize>();
597 let trimmed = &line_core[indent_len..];
598 let after_prefix = trimmed.strip_prefix(prefix)?;
599 let payload = after_prefix
600 .strip_prefix(' ')
601 .or_else(|| after_prefix.strip_prefix('\t'))
602 .unwrap_or(after_prefix);
603 collected.push_str(payload);
604 collected.push_str(eol);
605 offset += line.len();
606 }
607 let start = content_start;
608 let region_end = content_start + offset;
609 let id = format!("hashpipe:{}:{}:{}", language, host_start, start);
610 Some(YamlRegion {
611 id,
612 kind: YamlRegionKind::Hashpipe,
613 host_range: host_start..host_end,
614 region_range: start..region_end,
615 content_range: start..region_end,
616 content: collected,
617 })
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn parsed_yaml_regions_include_frontmatter_and_hashpipe_cst_roots() {
626 let input = "---\ntitle: Test\n---\n\n```{r}\n#| echo: false\n1 + 1\n```\n";
627 let config = crate::options::ParserOptions {
628 flavor: crate::options::Flavor::Quarto,
629 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
630 ..Default::default()
631 };
632 let tree = crate::parser::parse(input, Some(config));
633 let regions = collect_parsed_yaml_regions(&tree);
634 assert_eq!(regions.len(), 2);
635 assert!(regions.iter().any(|parsed| {
636 parsed.is_frontmatter() && parsed.root_kind() == Some(YamlAstRootKind::Root)
637 }));
638 assert!(regions.iter().any(|parsed| {
639 parsed.is_hashpipe() && parsed.root_kind() == Some(YamlAstRootKind::Root)
640 }));
641 }
642
643 #[test]
644 fn parsed_hashpipe_region_validity_derives_from_embedded_subtree() {
645 let config = crate::options::ParserOptions {
646 flavor: crate::options::Flavor::Quarto,
647 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
648 ..Default::default()
649 };
650 let bad = crate::parser::parse("```{r}\n#| echo: [\n1 + 1\n```\n", Some(config.clone()));
652 let bad_region = collect_parsed_yaml_regions(&bad)
653 .into_iter()
654 .find(|region| region.is_hashpipe())
655 .expect("hashpipe region");
656 assert!(!bad_region.is_valid());
657 assert!(bad_region.root().is_none());
658
659 let good = crate::parser::parse("```{r}\n#| echo: false\n1 + 1\n```\n", Some(config));
661 let good_region = collect_parsed_yaml_regions(&good)
662 .into_iter()
663 .find(|region| region.is_hashpipe())
664 .expect("hashpipe region");
665 assert!(good_region.is_valid());
666 assert!(good_region.root().is_some());
667 }
668
669 #[test]
670 fn empty_frontmatter_is_valid() {
671 let tree = crate::parser::parse("---\n---\n\nbody\n", None);
674 let parsed = collect_parsed_frontmatter_region(&tree).expect("frontmatter");
675 assert!(parsed.is_valid());
676 }
677
678 #[test]
679 fn malformed_frontmatter_is_invalid() {
680 let tree = crate::parser::parse("---\ntitle: [\n---\n", None);
681 let parsed = collect_parsed_frontmatter_region(&tree).expect("frontmatter");
682 assert!(!parsed.is_valid());
683 }
684
685 #[test]
686 fn yaml_ast_root_reports_document_shape() {
687 let input = "---\ntitle: Test\n---\n";
688 let tree = crate::parser::parse(input, None);
689 let parsed = collect_parsed_frontmatter_region(&tree).expect("frontmatter");
690 let root = parsed.root().expect("yaml root");
691 assert_eq!(root.document_count(), 1);
692 assert_eq!(root.first_document_kind(), Some(YamlDocumentKind::BlockMap));
693 }
694
695 #[test]
696 fn embedded_yaml_cst_attaches_to_frontmatter_and_hashpipe_hosts() {
697 let input = "---\ntitle: Test\n---\n\n```{r}\n#| echo: false\nx <- 1\n```\n";
698 let config = crate::options::ParserOptions {
699 flavor: crate::options::Flavor::Quarto,
700 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
701 ..Default::default()
702 };
703 let tree = crate::parser::parse(input, Some(config));
704 let embedded = collect_embedded_yaml_cst(&tree);
705 assert_eq!(embedded.len(), 2);
706 assert!(
707 embedded
708 .iter()
709 .any(|item| item.frontmatter_host().is_some())
710 );
711 assert!(embedded.iter().any(|item| item.hashpipe_host().is_some()));
712 }
713
714 #[test]
715 fn embedded_yaml_cst_exposes_frontmatter_and_hashpipe_payloads() {
716 let input = "---\ntitle: Test\n---\n\n```{r}\n#| fig-cap: |\n#| A caption\nx <- 1\n```\n";
717 let config = crate::options::ParserOptions {
718 flavor: crate::options::Flavor::Quarto,
719 extensions: crate::options::Extensions::for_flavor(crate::options::Flavor::Quarto),
720 ..Default::default()
721 };
722 let tree = crate::parser::parse(input, Some(config));
723 let embedded = collect_embedded_yaml_cst(&tree);
724 assert_eq!(embedded.len(), 2);
725
726 let frontmatter = embedded
727 .iter()
728 .find(|item| item.frontmatter_host().is_some())
729 .expect("frontmatter embedding");
730 assert!(frontmatter.parsed().is_valid());
731 assert_eq!(
732 frontmatter.parsed().document_shape_summary().as_deref(),
733 Some("Root docs=1 first=BlockMap")
734 );
735
736 let hashpipe = embedded
737 .iter()
738 .find(|item| item.hashpipe_host().is_some())
739 .expect("hashpipe embedding");
740 assert!(hashpipe.parsed().is_valid());
741 assert!(hashpipe.parsed().to_region().content.contains("fig-cap: |"));
742 }
743
744 #[test]
745 fn embedded_frontmatter_query_returns_typed_host_wrapper() {
746 let input = "---\ntitle: Test\n---\n\nBody\n";
747 let tree = crate::parser::parse(input, None);
748 let embedded = collect_embedded_frontmatter_yaml_cst(&tree).expect("frontmatter embedding");
749 let _host = embedded.frontmatter_host().expect("frontmatter host");
750 assert!(embedded.hashpipe_host().is_none());
751 }
752}