use rowan::TextRange;
use super::ast::{AstChildren, AstNode, support};
use super::{PanacheLanguage, SyntaxKind, SyntaxNode, SyntaxToken};
use crate::parser::yaml::{ScalarStyle, cook, parse_yaml_tree};
pub fn parse_yaml_document(input: &str) -> Option<YamlDocument> {
first_document(&parse_yaml_tree(input)?)
}
pub fn parse_yaml_documents(input: &str) -> Vec<YamlDocument> {
let Some(tree) = parse_yaml_tree(input) else {
return Vec::new();
};
stream_container(&tree)
.map(|stream| stream.children().filter_map(YamlDocument::cast).collect())
.unwrap_or_default()
}
pub(crate) fn is_stream_equivalent(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::YAML_STREAM
| SyntaxKind::YAML_METADATA_CONTENT
| SyntaxKind::HASHPIPE_YAML_CONTENT
)
}
fn stream_container(tree: &SyntaxNode) -> Option<SyntaxNode> {
tree.descendants().find(|n| is_stream_equivalent(n.kind()))
}
fn first_document(tree: &SyntaxNode) -> Option<YamlDocument> {
stream_container(tree)?
.children()
.find_map(YamlDocument::cast)
}
#[derive(Debug, Clone)]
pub enum YamlNode {
BlockMap(YamlBlockMap),
BlockSequence(YamlBlockSequence),
FlowMap(YamlFlowMap),
FlowSequence(YamlFlowSequence),
Scalar(YamlScalar),
}
fn node_child(parent: &SyntaxNode) -> Option<YamlNode> {
for child in parent.children() {
match child.kind() {
SyntaxKind::YAML_BLOCK_MAP => return YamlBlockMap::cast(child).map(YamlNode::BlockMap),
SyntaxKind::YAML_BLOCK_SEQUENCE => {
return YamlBlockSequence::cast(child).map(YamlNode::BlockSequence);
}
SyntaxKind::YAML_FLOW_MAP => return YamlFlowMap::cast(child).map(YamlNode::FlowMap),
SyntaxKind::YAML_FLOW_SEQUENCE => {
return YamlFlowSequence::cast(child).map(YamlNode::FlowSequence);
}
_ => {}
}
}
scalar_token(parent).map(YamlNode::Scalar)
}
fn scalar_token(parent: &SyntaxNode) -> Option<YamlScalar> {
parent
.children()
.find(|n| n.kind() == SyntaxKind::YAML_SCALAR)
.and_then(YamlScalar::cast)
}
fn token_of(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
parent
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == kind)
}
macro_rules! node_projections {
() => {
pub fn as_node(&self) -> Option<YamlNode> {
node_child(&self.0)
}
pub fn as_scalar(&self) -> Option<YamlScalar> {
match self.as_node()? {
YamlNode::Scalar(s) => Some(s),
_ => None,
}
}
pub fn as_block_map(&self) -> Option<YamlBlockMap> {
match self.as_node()? {
YamlNode::BlockMap(m) => Some(m),
_ => None,
}
}
pub fn as_block_sequence(&self) -> Option<YamlBlockSequence> {
match self.as_node()? {
YamlNode::BlockSequence(s) => Some(s),
_ => None,
}
}
pub fn as_flow_map(&self) -> Option<YamlFlowMap> {
match self.as_node()? {
YamlNode::FlowMap(m) => Some(m),
_ => None,
}
}
pub fn as_flow_sequence(&self) -> Option<YamlFlowSequence> {
match self.as_node()? {
YamlNode::FlowSequence(s) => Some(s),
_ => None,
}
}
pub fn is_empty(&self) -> bool {
self.as_node().is_none()
}
pub fn tag(&self) -> Option<SyntaxToken> {
token_of(&self.0, SyntaxKind::YAML_TAG)
}
};
}
macro_rules! ast_node {
($(#[$meta:meta])* $name:ident, $kind:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct $name(SyntaxNode);
impl AstNode for $name {
type Language = PanacheLanguage;
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::$kind
}
fn cast(syntax: SyntaxNode) -> Option<Self> {
Self::can_cast(syntax.kind()).then_some(Self(syntax))
}
fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
};
}
ast_node!(
YamlDocument, YAML_DOCUMENT
);
impl YamlDocument {
pub fn block_map(&self) -> Option<YamlBlockMap> {
support::child(&self.0)
}
pub fn block_sequence(&self) -> Option<YamlBlockSequence> {
support::child(&self.0)
}
pub fn flow_map(&self) -> Option<YamlFlowMap> {
support::child(&self.0)
}
pub fn flow_sequence(&self) -> Option<YamlFlowSequence> {
support::child(&self.0)
}
pub fn scalar(&self) -> Option<YamlScalar> {
scalar_token(&self.0)
}
pub fn as_node(&self) -> Option<YamlNode> {
node_child(&self.0)
}
}
ast_node!(
YamlBlockMap, YAML_BLOCK_MAP
);
impl YamlBlockMap {
pub fn entries(&self) -> AstChildren<YamlBlockMapEntry> {
support::children(&self.0)
}
pub fn entry(&self, key: &str) -> Option<YamlBlockMapEntry> {
self.entries()
.find(|entry| entry.key_text().as_deref() == Some(key))
}
pub fn value_of(&self, key: &str) -> Option<YamlBlockMapValue> {
self.entry(key)?.value()
}
}
ast_node!(
YamlBlockMapEntry, YAML_BLOCK_MAP_ENTRY
);
impl YamlBlockMapEntry {
pub fn key(&self) -> Option<YamlBlockMapKey> {
support::child(&self.0)
}
pub fn key_text(&self) -> Option<String> {
self.key()?.scalar().map(|s| s.value())
}
pub fn value(&self) -> Option<YamlBlockMapValue> {
support::child(&self.0)
}
}
ast_node!(
YamlBlockMapKey, YAML_BLOCK_MAP_KEY
);
impl YamlBlockMapKey {
pub fn scalar(&self) -> Option<YamlScalar> {
scalar_token(&self.0)
}
}
ast_node!(
YamlBlockMapValue, YAML_BLOCK_MAP_VALUE
);
impl YamlBlockMapValue {
node_projections!();
}
ast_node!(
YamlBlockSequence, YAML_BLOCK_SEQUENCE
);
impl YamlBlockSequence {
pub fn items(&self) -> AstChildren<YamlBlockSequenceItem> {
support::children(&self.0)
}
}
ast_node!(
YamlBlockSequenceItem, YAML_BLOCK_SEQUENCE_ITEM
);
impl YamlBlockSequenceItem {
node_projections!();
}
ast_node!(
YamlFlowSequence, YAML_FLOW_SEQUENCE
);
impl YamlFlowSequence {
pub fn items(&self) -> AstChildren<YamlFlowSequenceItem> {
support::children(&self.0)
}
}
ast_node!(
YamlFlowSequenceItem, YAML_FLOW_SEQUENCE_ITEM
);
impl YamlFlowSequenceItem {
node_projections!();
}
ast_node!(
YamlFlowMap, YAML_FLOW_MAP
);
impl YamlFlowMap {
pub fn entries(&self) -> AstChildren<YamlFlowMapEntry> {
support::children(&self.0)
}
pub fn entry(&self, key: &str) -> Option<YamlFlowMapEntry> {
self.entries()
.find(|entry| entry.key_text().as_deref() == Some(key))
}
pub fn value_of(&self, key: &str) -> Option<YamlFlowMapValue> {
self.entry(key)?.value()
}
}
ast_node!(
YamlFlowMapEntry, YAML_FLOW_MAP_ENTRY
);
impl YamlFlowMapEntry {
pub fn key(&self) -> Option<YamlFlowMapKey> {
support::child(&self.0)
}
pub fn key_text(&self) -> Option<String> {
self.key()?.scalar().map(|s| s.value())
}
pub fn value(&self) -> Option<YamlFlowMapValue> {
support::child(&self.0)
}
}
ast_node!(
YamlFlowMapKey, YAML_FLOW_MAP_KEY
);
impl YamlFlowMapKey {
pub fn scalar(&self) -> Option<YamlScalar> {
scalar_token(&self.0)
}
}
ast_node!(
YamlFlowMapValue, YAML_FLOW_MAP_VALUE
);
impl YamlFlowMapValue {
node_projections!();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum YamlScalarStyle {
Plain,
SingleQuoted,
DoubleQuoted,
Literal,
Folded,
}
impl YamlScalarStyle {
fn to_cook_style(self) -> ScalarStyle {
match self {
YamlScalarStyle::Plain => ScalarStyle::Plain,
YamlScalarStyle::SingleQuoted => ScalarStyle::SingleQuoted,
YamlScalarStyle::DoubleQuoted => ScalarStyle::DoubleQuoted,
YamlScalarStyle::Literal => ScalarStyle::Literal,
YamlScalarStyle::Folded => ScalarStyle::Folded,
}
}
}
fn detect_style(raw: &str) -> YamlScalarStyle {
match raw.trim_start().as_bytes().first() {
Some(b'\'') => YamlScalarStyle::SingleQuoted,
Some(b'"') => YamlScalarStyle::DoubleQuoted,
Some(b'|') => YamlScalarStyle::Literal,
Some(b'>') => YamlScalarStyle::Folded,
_ => YamlScalarStyle::Plain,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct YamlScalar(SyntaxNode);
impl YamlScalar {
pub fn cast(node: SyntaxNode) -> Option<Self> {
(node.kind() == SyntaxKind::YAML_SCALAR).then_some(Self(node))
}
pub fn raw(&self) -> String {
self.0.text().to_string()
}
pub fn style(&self) -> YamlScalarStyle {
detect_style(&self.raw())
}
pub fn value(&self) -> String {
let source = self.prefix_stripped_source();
cook(detect_style(&source).to_cook_style(), &source)
}
fn prefix_stripped_source(&self) -> String {
self.0
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| t.kind() != SyntaxKind::YAML_LINE_PREFIX)
.map(|t| t.text().to_string())
.collect()
}
pub fn text_range(&self) -> TextRange {
self.0.text_range()
}
pub fn syntax(&self) -> &SyntaxNode {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_yaml_document_descends_envelope() {
let doc = parse_yaml_document("title: x\n").expect("document");
let map = doc.block_map().expect("block map");
assert_eq!(map.entries().count(), 1);
}
#[test]
fn key_text_strips_colon() {
let doc = parse_yaml_document("key: value\n").expect("document");
let entry = doc.block_map().unwrap().entries().next().unwrap();
assert_eq!(entry.key_text().as_deref(), Some("key"));
}
#[test]
fn value_is_cooked() {
let doc = parse_yaml_document("k: 'it''s'\n").expect("document");
let value = doc.block_map().unwrap().value_of("k").unwrap();
assert_eq!(value.as_scalar().unwrap().value(), "it's");
let doc = parse_yaml_document("k: \"a\\nb\"\n").expect("document");
let value = doc.block_map().unwrap().value_of("k").unwrap();
assert_eq!(value.as_scalar().unwrap().value(), "a\nb");
}
#[test]
fn raw_preserves_quotes() {
let doc = parse_yaml_document("k: 'it''s'\n").expect("document");
let scalar = doc
.block_map()
.unwrap()
.value_of("k")
.unwrap()
.as_scalar()
.unwrap();
assert_eq!(scalar.raw(), "'it''s'");
assert_eq!(scalar.style(), YamlScalarStyle::SingleQuoted);
}
#[test]
fn value_skips_embedded_line_prefix() {
use crate::parser::yaml::parse_stream_with_prefix;
let tree = parse_stream_with_prefix("#| key: \"foo\n#| bar\"\n", "#|");
let scalar = first_document(&tree)
.and_then(|d| d.block_map())
.and_then(|m| m.value_of("key"))
.and_then(|v| v.as_scalar())
.expect("scalar value");
let value = scalar.value();
assert!(!value.contains("#|"), "prefix leaked into value: {value:?}");
assert_eq!(value, "foo bar");
assert!(
scalar.raw().contains("#|"),
"raw() must retain the prefix leaf: {:?}",
scalar.raw()
);
}
#[test]
fn scalar_text_range_is_content_relative() {
let input = "k: value\n";
let doc = parse_yaml_document(input).expect("document");
let scalar = doc
.block_map()
.unwrap()
.value_of("k")
.unwrap()
.as_scalar()
.unwrap();
let range = scalar.text_range();
let start: usize = range.start().into();
let end: usize = range.end().into();
assert_eq!(&input[start..end], "value");
}
#[test]
fn empty_value_has_no_scalar() {
let doc = parse_yaml_document("k:\n").expect("document");
let value = doc.block_map().unwrap().value_of("k").unwrap();
assert!(value.is_empty());
assert!(value.as_scalar().is_none());
}
#[test]
fn block_sequence_items_yield_scalars() {
let doc = parse_yaml_document("k:\n - a\n - b\n").expect("document");
let seq = doc
.block_map()
.unwrap()
.value_of("k")
.unwrap()
.as_block_sequence()
.expect("block sequence");
let items: Vec<String> = seq
.items()
.filter_map(|item| item.as_scalar().map(|s| s.value()))
.collect();
assert_eq!(items, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn flow_sequence_items_yield_scalars() {
let doc = parse_yaml_document("k: [a, b]\n").expect("document");
let seq = doc
.block_map()
.unwrap()
.value_of("k")
.unwrap()
.as_flow_sequence()
.expect("flow sequence");
let items: Vec<String> = seq
.items()
.filter_map(|item| item.as_scalar().map(|s| s.value()))
.collect();
assert_eq!(items, vec!["a".to_string(), "b".to_string()]);
}
#[test]
fn tag_token_is_exposed_and_scalar_ignores_it() {
let doc = parse_yaml_document("k: !expr foo\n").expect("document");
let value = doc.block_map().unwrap().value_of("k").unwrap();
assert_eq!(
value.tag().map(|t| t.text().to_string()),
Some("!expr".to_string())
);
assert_eq!(value.as_scalar().unwrap().raw(), "foo");
}
#[test]
fn quoted_key_with_colon_round_trips() {
let doc = parse_yaml_document("\"foo:bar\": 1\n").expect("document");
let entry = doc.block_map().unwrap().entries().next().unwrap();
assert_eq!(entry.key_text().as_deref(), Some("foo:bar"));
assert_eq!(entry.key().unwrap().scalar().unwrap().raw(), "\"foo:bar\"");
}
#[test]
fn parse_yaml_documents_returns_all_documents() {
let docs = parse_yaml_documents("a: 1\n---\nb: 2\n");
assert_eq!(docs.len(), 2);
}
#[test]
fn invalid_yaml_yields_no_document() {
assert!(parse_yaml_document("k: [\n").is_none());
}
}