use std::collections::HashMap;
use crate::syntax::{SyntaxKind, SyntaxNode};
use super::parser::parse_yaml_tree;
type TagHandles = HashMap<String, String>;
fn default_tag_handles() -> TagHandles {
let mut handles = HashMap::new();
handles.insert("!!".to_string(), "tag:yaml.org,2002:".to_string());
handles
}
fn collect_tag_handles(doc: &SyntaxNode) -> TagHandles {
let mut handles = default_tag_handles();
for tok in doc
.descendants_with_tokens()
.filter_map(|el| el.into_token())
{
if tok.kind() != SyntaxKind::YAML_SCALAR {
continue;
}
let line = tok.text().trim_start();
let Some(rest) = line.strip_prefix("%TAG") else {
continue;
};
let mut parts = rest.split_whitespace();
let Some(handle) = parts.next() else { continue };
let Some(prefix) = parts.next() else { continue };
handles.insert(handle.to_string(), prefix.to_string());
}
handles
}
fn resolve_long_tag(tag: &str, handles: &TagHandles) -> Option<String> {
let mut best: Option<(&str, &String)> = None;
for (h, p) in handles {
if tag.starts_with(h)
&& best.is_none_or(|(b_handle, _): (&str, _)| h.len() > b_handle.len())
{
best = Some((h.as_str(), p));
}
}
if let Some((handle, prefix)) = best {
let suffix = &tag[handle.len()..];
let resolved = format!("{prefix}{suffix}");
return Some(format!("<{}>", percent_decode_tag(&resolved)));
}
long_tag_builtin(tag)
}
fn percent_decode_tag(tag: &str) -> String {
let bytes = tag.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%'
&& i + 2 < bytes.len()
&& let (Some(hi), Some(lo)) =
(hex_digit_value(bytes[i + 1]), hex_digit_value(bytes[i + 2]))
{
out.push(hi * 16 + lo);
i += 3;
continue;
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8(out).unwrap_or_else(|_| tag.to_string())
}
fn hex_digit_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
pub fn project_events(input: &str) -> Vec<String> {
let Some(tree) = parse_yaml_tree(input) else {
return Vec::new();
};
let mut events = vec!["+STR".to_string()];
let stream = tree
.descendants()
.find(|n| n.kind() == SyntaxKind::YAML_STREAM);
if let Some(stream) = stream {
for doc in stream
.children()
.filter(|n| n.kind() == SyntaxKind::YAML_DOCUMENT)
{
project_document(&doc, &mut events);
}
}
events.push("-STR".to_string());
events
}
fn project_document(doc: &SyntaxNode, out: &mut Vec<String>) {
let has_doc_start = doc
.children_with_tokens()
.filter_map(|el| el.into_token())
.any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_START);
let has_doc_end = doc
.children_with_tokens()
.filter_map(|el| el.into_token())
.any(|tok| tok.kind() == SyntaxKind::YAML_DOCUMENT_END);
out.push(if has_doc_start {
"+DOC ---".to_string()
} else {
"+DOC".to_string()
});
let handles = collect_tag_handles(doc);
if let Some(seq_node) = doc
.descendants()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
{
out.push("+SEQ".to_string());
project_block_sequence_items(&seq_node, &handles, out);
out.push("-SEQ".to_string());
} else if let Some(root_map) = doc
.descendants()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
{
let mut values = Vec::new();
project_block_map_entries(&root_map, &handles, &mut values);
if !values.is_empty() {
out.push("+MAP".to_string());
out.append(&mut values);
out.push("-MAP".to_string());
} else if let Some(flow_map) = doc
.descendants()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
{
let mut flow_values = Vec::new();
project_flow_map_entries(&flow_map, &handles, &mut flow_values);
out.push("+MAP {}".to_string());
out.append(&mut flow_values);
out.push("-MAP".to_string());
} else if let Some(flow_seq) = doc
.descendants()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
&& let Some(items) = simple_flow_sequence_items(&flow_seq.text().to_string())
{
out.push("+SEQ []".to_string());
for item in items {
project_flow_seq_item(&item, &handles, out);
}
out.push("-SEQ".to_string());
} else if let Some(scalar) = scalar_document_value(doc, &handles) {
out.push(scalar);
} else {
out.push("=VAL :".to_string());
}
} else if let Some(flow_map) = doc
.descendants()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
{
out.push("+MAP {}".to_string());
project_flow_map_entries(&flow_map, &handles, out);
out.push("-MAP".to_string());
} else if let Some(flow_seq) = doc
.descendants()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
&& let Some(items) = simple_flow_sequence_items(&flow_seq.text().to_string())
{
out.push("+SEQ []".to_string());
for item in items {
project_flow_seq_item(&item, &handles, out);
}
out.push("-SEQ".to_string());
} else if let Some(scalar) = scalar_document_value(doc, &handles) {
out.push(scalar);
} else {
out.push("=VAL :".to_string());
}
out.push(if has_doc_end {
"-DOC ...".to_string()
} else {
"-DOC".to_string()
});
}
fn scalar_document_value(doc: &SyntaxNode, handles: &TagHandles) -> Option<String> {
let text = doc
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
.filter(|tok| !tok.text().trim_start().starts_with('%'))
.map(|tok| tok.text().to_string())
.collect::<Vec<_>>()
.join("");
let trimmed_text = text.trim();
if trimmed_text.is_empty() {
let tag_only = doc
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.find(|tok| tok.kind() == SyntaxKind::YAML_TAG)
.map(|tok| tok.text().to_string());
if let Some(tag) = tag_only
&& let Some(long) = resolve_long_tag(&tag, handles)
{
return Some(format!("=VAL {long} :"));
}
return None;
}
let tag_text = doc
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.find(|tok| tok.kind() == SyntaxKind::YAML_TAG)
.map(|tok| tok.text().to_string());
let event = if let Some(tag) = tag_text
&& let Some(long) = resolve_long_tag(&tag, handles)
{
if trimmed_text.starts_with('"') || trimmed_text.starts_with('\'') {
let quoted = quoted_val_event(trimmed_text);
quoted.replacen("=VAL ", &format!("=VAL {long} "), 1)
} else {
format!("=VAL {long} :{trimmed_text}")
}
} else if trimmed_text.starts_with('"') || trimmed_text.starts_with('\'') {
quoted_val_event(&text)
} else {
plain_val_event(&text)
};
Some(event)
}
fn plain_val_event(text: &str) -> String {
format!("=VAL :{}", text.replace('\\', "\\\\"))
}
fn flow_scalar_event(text: &str, handles: &TagHandles) -> String {
let trimmed = text.trim();
if trimmed.starts_with('"') || trimmed.starts_with('\'') {
return quoted_val_event(trimmed);
}
let (anchor, long_tag, body) = decompose_scalar(trimmed, handles);
if anchor.is_some() || long_tag.is_some() {
return scalar_event(anchor, long_tag.as_deref(), body);
}
plain_val_event(&fold_plain_scalar(text))
}
fn split_leading_tag(text: &str) -> Option<(&str, &str)> {
let rest = text.strip_prefix('!')?;
let mut i = 0usize;
let mut bangs = 0usize;
for (idx, ch) in rest.char_indices() {
if ch == '!' {
bangs += 1;
if bangs > 1 {
return None;
}
i = idx + 1;
continue;
}
if matches!(ch, ' ' | '\t' | '\n' | ',' | '}' | ']') {
i = idx;
break;
}
i = idx + ch.len_utf8();
}
let tag_len = 1 + i;
let (tag, remainder) = text.split_at(tag_len);
Some((tag, remainder))
}
fn flow_kv_split(item: &str) -> Option<(usize, usize)> {
let bytes = item.as_bytes();
let mut in_single = false;
let mut in_double = false;
let mut escaped_double = false;
for (idx, ch) in item.char_indices() {
if in_double {
if escaped_double {
escaped_double = false;
continue;
}
match ch {
'\\' => escaped_double = true,
'"' => in_double = false,
_ => {}
}
continue;
}
if in_single {
if ch == '\'' {
in_single = false;
}
continue;
}
match ch {
'\'' => in_single = true,
'"' => in_double = true,
':' => {
let next_off = idx + ch.len_utf8();
let after_is_break = next_off >= bytes.len()
|| matches!(bytes[next_off], b' ' | b'\t' | b'\n' | b'\r');
if after_is_break {
return Some((idx, next_off));
}
}
_ => {}
}
}
None
}
fn project_flow_seq_item(item: &str, handles: &TagHandles, out: &mut Vec<String>) {
if let Some((colon, after)) = flow_kv_split(item) {
let raw_key_full = item[..colon].trim();
let raw_key = strip_explicit_key_indicator(raw_key_full);
let raw_value = item[after..].trim();
out.push("+MAP {}".to_string());
if raw_key.is_empty() {
out.push("=VAL :".to_string());
} else {
out.push(flow_scalar_event(raw_key, handles));
}
if raw_value.is_empty() {
out.push("=VAL :".to_string());
} else {
out.push(flow_scalar_event(raw_value, handles));
}
out.push("-MAP".to_string());
} else if item.trim_start().starts_with('"') || item.trim_start().starts_with('\'') {
out.push(quoted_val_event(item.trim()));
} else {
out.push(plain_val_event(&fold_plain_scalar(item)));
}
}
fn strip_explicit_key_indicator(key: &str) -> &str {
let trimmed = key.trim_start();
if let Some(rest) = trimmed.strip_prefix('?')
&& (rest.is_empty() || rest.starts_with([' ', '\t', '\n']))
{
return rest.trim_start();
}
key
}
fn quoted_val_event(text: &str) -> String {
if text.starts_with('\'') {
let inner = decode_single_quoted(text);
format!("=VAL '{}", escape_for_event(&inner))
} else {
let inner = decode_double_quoted(text);
format!("=VAL \"{}", escape_for_event(&inner))
}
}
fn decode_single_quoted(text: &str) -> String {
let body = text.strip_prefix('\'').unwrap_or(text);
let body = body.strip_suffix('\'').unwrap_or(body);
body.replace("''", "'")
}
fn decode_double_quoted(text: &str) -> String {
let body = text.strip_prefix('"').unwrap_or(text);
let mut out = String::with_capacity(body.len());
let mut chars = body.chars();
while let Some(ch) = chars.next() {
if ch == '"' {
break;
}
if ch != '\\' {
out.push(ch);
continue;
}
let Some(next) = chars.next() else {
out.push('\\');
break;
};
match next {
'0' => out.push('\0'),
'a' => out.push('\u{07}'),
'b' => out.push('\u{08}'),
't' | '\t' => out.push('\t'),
'n' => out.push('\n'),
'v' => out.push('\u{0B}'),
'f' => out.push('\u{0C}'),
'r' => out.push('\r'),
'e' => out.push('\u{1B}'),
' ' => out.push(' '),
'"' => out.push('"'),
'/' => out.push('/'),
'\\' => out.push('\\'),
'N' => out.push('\u{85}'),
'_' => out.push('\u{A0}'),
'L' => out.push('\u{2028}'),
'P' => out.push('\u{2029}'),
'x' => {
if let Some(c) = take_hex_char(&mut chars, 2) {
out.push(c);
}
}
'u' => {
if let Some(c) = take_hex_char(&mut chars, 4) {
out.push(c);
}
}
'U' => {
if let Some(c) = take_hex_char(&mut chars, 8) {
out.push(c);
}
}
other => {
out.push('\\');
out.push(other);
}
}
}
out
}
fn take_hex_char(chars: &mut std::str::Chars<'_>, n: usize) -> Option<char> {
let hex: String = chars.take(n).collect();
if hex.len() != n {
return None;
}
u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
}
fn escape_for_event(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\u{07}' => out.push_str("\\a"),
'\u{08}' => out.push_str("\\b"),
'\u{0B}' => out.push_str("\\v"),
'\u{0C}' => out.push_str("\\f"),
'\u{1B}' => out.push_str("\\e"),
'\0' => out.push_str("\\0"),
other => out.push(other),
}
}
out
}
fn long_tag_builtin(tag: &str) -> Option<String> {
if tag == "!" {
return Some("<!>".to_string());
}
if let Some(rest) = tag.strip_prefix('!')
&& !rest.contains('!')
{
return Some(format!("<!{rest}>"));
}
None
}
fn simple_flow_sequence_items(text: &str) -> Option<Vec<String>> {
let trimmed = text.trim();
let inner = trimmed.strip_prefix('[')?.strip_suffix(']')?;
let inner = inner.trim();
if inner.is_empty() {
return Some(Vec::new());
}
let mut items = Vec::new();
let mut start = 0usize;
let mut in_single = false;
let mut in_double = false;
let mut escaped_double = false;
for (idx, ch) in inner.char_indices() {
if in_double {
if escaped_double {
escaped_double = false;
continue;
}
match ch {
'\\' => escaped_double = true,
'"' => in_double = false,
_ => {}
}
continue;
}
if in_single {
if ch == '\'' {
in_single = false;
}
continue;
}
match ch {
'\'' => in_single = true,
'"' => in_double = true,
',' => {
let item = inner[start..idx].trim();
if item.is_empty() {
return None;
}
items.push(item.to_string());
start = idx + 1;
}
_ => {}
}
}
let last = inner[start..].trim();
if !last.is_empty() {
items.push(last.to_string());
}
Some(items)
}
fn escape_block_scalar_text(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
other => out.push(other),
}
}
out
}
fn extract_block_scalar_body(value_node: &SyntaxNode) -> Option<(char, String)> {
let tokens: Vec<_> = value_node
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| matches!(tok.kind(), SyntaxKind::YAML_SCALAR | SyntaxKind::NEWLINE))
.collect();
let first = tokens.first()?;
if first.kind() != SyntaxKind::YAML_SCALAR {
return None;
}
let indicator = match first.text() {
"|" => '|',
">" => '>',
_ => return None,
};
let mut raw = String::new();
let mut seen_header = false;
let mut skipped_header_newline = false;
for tok in tokens.iter().skip(1) {
if !seen_header && !skipped_header_newline && tok.kind() == SyntaxKind::NEWLINE {
skipped_header_newline = true;
seen_header = true;
continue;
}
raw.push_str(tok.text());
}
let mut lines: Vec<&str> = raw.split('\n').collect();
if lines.last().is_some_and(|s| s.is_empty()) {
lines.pop();
}
let content_indent = lines
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| l.chars().take_while(|c| *c == ' ').count())
.min()
.unwrap_or(0);
let stripped: Vec<String> = lines
.iter()
.map(|l| {
if l.len() >= content_indent {
l[content_indent..].to_string()
} else {
String::new()
}
})
.collect();
let folded = match indicator {
'|' => stripped.join("\n"),
'>' => {
let mut result = String::new();
let mut last_blank = false;
for (idx, line) in stripped.iter().enumerate() {
if line.is_empty() {
result.push('\n');
last_blank = true;
} else {
if idx > 0 && !last_blank {
result.push(' ');
}
result.push_str(line);
last_blank = false;
}
}
result
}
_ => unreachable!(),
};
let trimmed = folded.trim_end_matches('\n');
let body = if trimmed.is_empty() {
String::new()
} else {
format!("{trimmed}\n")
};
Some((indicator, body))
}
fn fold_plain_scalar(text: &str) -> String {
let mut pieces = Vec::new();
for line in text.split('\n') {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
pieces.push(trimmed.to_string());
}
if pieces.is_empty() {
return String::new();
}
pieces.join(" ")
}
fn project_flow_map_entries(flow_map: &SyntaxNode, handles: &TagHandles, out: &mut Vec<String>) {
for entry in flow_map
.children()
.filter(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_ENTRY)
{
let key_node = entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_KEY)
.expect("flow map key");
let value_node = entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP_VALUE)
.expect("flow map value");
let has_explicit_colon = key_node
.children_with_tokens()
.filter_map(|el| el.into_token())
.any(|tok| tok.kind() == SyntaxKind::YAML_COLON);
let raw_key = key_node
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| matches!(tok.kind(), SyntaxKind::YAML_SCALAR | SyntaxKind::YAML_KEY))
.map(|tok| tok.text().to_string())
.collect::<Vec<_>>()
.join("");
if has_explicit_colon {
let stripped_key = strip_explicit_key_indicator(raw_key.trim());
if stripped_key.is_empty() {
out.push("=VAL :".to_string());
} else {
out.push(flow_scalar_event(stripped_key, handles));
}
project_flow_map_value(&value_node, handles, out);
} else {
let raw_value = value_node
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
.map(|tok| tok.text().to_string())
.collect::<Vec<_>>()
.join("");
let combined = format!("{raw_key}{raw_value}");
let folded = fold_plain_scalar(&combined);
let stripped = strip_explicit_key_indicator(&folded);
if stripped.is_empty() {
out.push("=VAL :".to_string());
} else {
out.push(plain_val_event(stripped));
}
out.push("=VAL :".to_string());
}
}
}
fn project_flow_map_value(value_node: &SyntaxNode, handles: &TagHandles, out: &mut Vec<String>) {
if let Some(flow_seq) = value_node
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
{
out.push("+SEQ []".to_string());
project_flow_sequence_items_cst(&flow_seq, handles, out);
out.push("-SEQ".to_string());
return;
}
if let Some(nested_map) = value_node
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
{
out.push("+MAP {}".to_string());
project_flow_map_entries(&nested_map, handles, out);
out.push("-MAP".to_string());
return;
}
let raw_value = value_node
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
.map(|tok| tok.text().to_string())
.collect::<Vec<_>>()
.join("");
out.push(flow_scalar_event(&raw_value, handles));
}
fn project_flow_sequence_items_cst(
flow_seq: &SyntaxNode,
handles: &TagHandles,
out: &mut Vec<String>,
) {
for item in flow_seq
.children()
.filter(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE_ITEM)
{
if let Some(nested_seq) = item
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
{
out.push("+SEQ []".to_string());
project_flow_sequence_items_cst(&nested_seq, handles, out);
out.push("-SEQ".to_string());
continue;
}
if let Some(nested_map) = item
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
{
out.push("+MAP {}".to_string());
project_flow_map_entries(&nested_map, handles, out);
out.push("-MAP".to_string());
continue;
}
let item_text: String = item
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| matches!(tok.kind(), SyntaxKind::YAML_SCALAR | SyntaxKind::YAML_KEY))
.map(|tok| tok.text().to_string())
.collect();
project_flow_seq_item(&item_text, handles, out);
}
}
fn project_block_sequence_items(
seq_node: &SyntaxNode,
handles: &TagHandles,
out: &mut Vec<String>,
) {
for item in seq_node
.children()
.filter(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE_ITEM)
{
if let Some(nested_seq) = item
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_SEQUENCE)
{
out.push("+SEQ".to_string());
project_block_sequence_items(&nested_seq, handles, out);
out.push("-SEQ".to_string());
continue;
}
if let Some(nested_map) = item
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
{
out.push("+MAP".to_string());
project_block_map_entries(&nested_map, handles, out);
out.push("-MAP".to_string());
continue;
}
if let Some(flow_seq) = item
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_SEQUENCE)
{
let flow_text = flow_seq.text().to_string();
if let Some(flow_items) = simple_flow_sequence_items(&flow_text) {
out.push("+SEQ []".to_string());
for value in flow_items {
project_flow_seq_item(&value, handles, out);
}
out.push("-SEQ".to_string());
continue;
}
}
if let Some(flow_map) = item
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
{
out.push("+MAP {}".to_string());
project_flow_map_entries(&flow_map, handles, out);
out.push("-MAP".to_string());
continue;
}
let item_tag = item
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.find(|tok| tok.kind() == SyntaxKind::YAML_TAG)
.map(|tok| tok.text().to_string());
let scalar_text = item
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
.map(|tok| tok.text().to_string())
.collect::<Vec<_>>()
.join("");
let scalar_trimmed = scalar_text.trim();
let event = if scalar_trimmed.starts_with('*') {
format!("=ALI {scalar_trimmed}")
} else {
let item_long_tag = item_tag
.as_deref()
.and_then(|t| resolve_long_tag(t, handles));
let (anchor, body_tag, body) = decompose_scalar(scalar_trimmed, handles);
let long_tag = item_long_tag.or(body_tag);
scalar_event(anchor, long_tag.as_deref(), body)
};
out.push(event);
}
}
fn decompose_scalar<'a>(
text: &'a str,
handles: &TagHandles,
) -> (Option<&'a str>, Option<String>, &'a str) {
let mut anchor: Option<&str> = None;
let mut long_tag: Option<String> = None;
let mut rest = text.trim();
loop {
if anchor.is_none()
&& let Some(after) = rest.strip_prefix('&')
{
let end = after
.find(|c: char| c.is_whitespace() || matches!(c, ',' | '}' | ']'))
.unwrap_or(after.len());
let (name, tail) = after.split_at(end);
anchor = Some(name);
rest = tail.trim_start();
continue;
}
if long_tag.is_none()
&& let Some((tag, tail)) = split_leading_tag(rest)
&& let Some(long) = resolve_long_tag(tag, handles)
{
long_tag = Some(long);
rest = tail.trim_start();
continue;
}
break;
}
(anchor, long_tag, rest)
}
fn scalar_event(anchor: Option<&str>, long_tag: Option<&str>, body: &str) -> String {
let mut prefix = String::new();
if let Some(a) = anchor {
prefix.push_str(&format!("&{a} "));
}
if let Some(t) = long_tag {
prefix.push_str(t);
prefix.push(' ');
}
let body = body.trim();
if body.is_empty() {
return format!("=VAL {prefix}:");
}
if body.starts_with('"') || body.starts_with('\'') {
let quoted = quoted_val_event(body);
return quoted.replacen("=VAL ", &format!("=VAL {prefix}"), 1);
}
format!("=VAL {prefix}:{body}")
}
fn project_block_map_entries(map_node: &SyntaxNode, handles: &TagHandles, out: &mut Vec<String>) {
for child in map_node.children_with_tokens() {
match child {
rowan::NodeOrToken::Token(tok)
if tok.kind() == SyntaxKind::YAML_SCALAR
&& tok.text().trim_start().starts_with("? ") =>
{
let body = tok.text().trim_start().trim_start_matches("? ").trim();
if body.is_empty() {
out.push("=VAL :".to_string());
} else {
let (anchor, body_tag, rest) = decompose_scalar(body, handles);
out.push(scalar_event(anchor, body_tag.as_deref(), rest));
}
out.push("=VAL :".to_string());
}
rowan::NodeOrToken::Node(entry) if entry.kind() == SyntaxKind::YAML_BLOCK_MAP_ENTRY => {
project_block_map_entry(&entry, handles, out);
}
_ => {}
}
}
}
fn project_block_map_entry(entry: &SyntaxNode, handles: &TagHandles, out: &mut Vec<String>) {
let key_node = entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_KEY)
.expect("key node");
let value_node = entry
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP_VALUE)
.expect("value node");
let key_tag = key_node
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|tok| tok.kind() == SyntaxKind::YAML_TAG)
.map(|tok| tok.text().to_string());
let key_text = key_node
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|tok| tok.kind() == SyntaxKind::YAML_KEY)
.map(|tok| tok.text().trim_end().to_string())
.expect("key token");
let key_event = if key_text.starts_with('*') {
format!("=ALI {}", key_text.trim_end())
} else {
let key_long_tag = key_tag
.as_deref()
.and_then(|t| resolve_long_tag(t, handles));
let (anchor, body_tag, body) = decompose_scalar(key_text.trim(), handles);
let long_tag = key_long_tag.or(body_tag);
scalar_event(anchor, long_tag.as_deref(), body)
};
out.push(key_event);
if let Some(nested_map) = value_node
.children()
.find(|n| n.kind() == SyntaxKind::YAML_BLOCK_MAP)
{
out.push("+MAP".to_string());
project_block_map_entries(&nested_map, handles, out);
out.push("-MAP".to_string());
return;
}
if let Some(flow_map) = value_node
.children()
.find(|n| n.kind() == SyntaxKind::YAML_FLOW_MAP)
{
out.push("+MAP {}".to_string());
project_flow_map_entries(&flow_map, handles, out);
out.push("-MAP".to_string());
return;
}
if let Some((indicator, body)) = extract_block_scalar_body(&value_node) {
let escaped = escape_block_scalar_text(&body);
out.push(format!("=VAL {indicator}{escaped}"));
return;
}
let value_tag = value_node
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|tok| tok.kind() == SyntaxKind::YAML_TAG)
.map(|tok| tok.text().to_string());
let value_text = value_node
.descendants_with_tokens()
.filter_map(|el| el.into_token())
.filter(|tok| tok.kind() == SyntaxKind::YAML_SCALAR)
.map(|tok| tok.text().to_string())
.collect::<Vec<_>>()
.join("");
if value_tag.is_none()
&& let Some(items) = simple_flow_sequence_items(&value_text)
{
out.push("+SEQ []".to_string());
for item in items {
project_flow_seq_item(&item, handles, out);
}
out.push("-SEQ".to_string());
} else if value_text.trim().is_empty() {
if let Some(tag) = value_tag
&& let Some(long) = resolve_long_tag(&tag, handles)
{
out.push(format!("=VAL {long} :"));
} else {
out.push("=VAL :".to_string());
}
} else if value_text.trim_start().starts_with('*') {
out.push(format!("=ALI {}", value_text.trim()));
} else {
let value_long_tag = value_tag
.as_deref()
.and_then(|t| resolve_long_tag(t, handles));
let (anchor, body_tag, body) = decompose_scalar(value_text.trim(), handles);
let long_tag = value_long_tag.or(body_tag);
out.push(scalar_event(anchor, long_tag.as_deref(), body));
}
}