#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct YamlKeyValue {
pub key: String,
pub value: String,
pub line: usize,
pub indent: usize,
}
pub fn looks_like_yaml(input: &str) -> bool {
input.lines().any(|line| {
let trimmed = line.trim();
!trimmed.is_empty()
&& !trimmed.starts_with('#')
&& (is_yaml_document_start(trimmed)
|| is_yaml_document_end(trimmed)
|| is_yaml_list_item(line)
|| split_yaml_key_value(line).is_some())
})
}
pub fn is_yaml_document_start(line: &str) -> bool {
line.trim() == "---"
}
pub fn is_yaml_document_end(line: &str) -> bool {
line.trim() == "..."
}
pub fn strip_yaml_document_markers(input: &str) -> &str {
let mut start = 0;
if !input.is_empty() {
let first_line_end = input.find('\n').unwrap_or(input.len());
let first_line = input[..first_line_end].trim_end_matches('\r');
if is_yaml_document_start(first_line) {
start = first_line_end.min(input.len());
if start < input.len() && input.as_bytes()[start] == b'\n' {
start += 1;
}
}
}
let remainder = &input[start..];
let mut body_end = remainder.len();
while body_end > 0 && matches!(remainder.as_bytes()[body_end - 1], b'\n' | b'\r') {
body_end -= 1;
}
if body_end == 0 {
return "";
}
let last_line_start = remainder[..body_end]
.rfind('\n')
.map_or(0, |index| index + 1);
let last_line = remainder[last_line_start..body_end].trim_end_matches('\r');
let end = if is_yaml_document_end(last_line) {
let mut marker_start = last_line_start;
if marker_start > 0 && remainder.as_bytes()[marker_start - 1] == b'\n' {
marker_start -= 1;
}
if marker_start > 0 && remainder.as_bytes()[marker_start - 1] == b'\r' {
marker_start -= 1;
}
marker_start
} else {
remainder.len()
};
&remainder[..end]
}
pub fn is_yaml_list_item(line: &str) -> bool {
let trimmed = line.trim_start();
trimmed == "-" || trimmed.starts_with("- ")
}
pub fn yaml_indent(line: &str) -> usize {
line.chars().take_while(|ch| *ch == ' ').count()
}
pub fn split_yaml_key_value(line: &str) -> Option<(String, String)> {
let content = strip_yaml_comment(line).trim();
if content.is_empty() || is_yaml_document_start(content) || is_yaml_document_end(content) {
return None;
}
if is_yaml_list_item(content) {
return None;
}
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
let mut chars = content.char_indices().peekable();
while let Some((index, ch)) = chars.next() {
if in_single {
if ch == '\'' {
in_single = false;
}
continue;
}
if in_double {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_double = false;
}
continue;
}
match ch {
'\'' => in_single = true,
'"' => in_double = true,
':' => {
let next_is_value = chars
.peek()
.map(|(_, next)| next.is_whitespace())
.unwrap_or(true);
if !next_is_value {
continue;
}
let key = content[..index].trim();
if key.is_empty() {
return None;
}
let value = content[index + 1..].trim().to_string();
return Some((key.to_string(), value));
}
_ => {}
}
}
None
}
pub fn extract_yaml_key_values(input: &str) -> Vec<YamlKeyValue> {
let mut pairs = Vec::new();
for (line_index, line) in input.lines().enumerate() {
if let Some((key, value)) = split_yaml_key_value(line) {
pairs.push(YamlKeyValue {
key,
value,
line: line_index + 1,
indent: yaml_indent(line),
});
}
}
pairs
}
pub fn quote_yaml_string(input: &str) -> String {
format!("'{}'", input.replace('\'', "''"))
}
fn strip_yaml_comment(line: &str) -> &str {
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
for (index, ch) in line.char_indices() {
if in_single {
if ch == '\'' {
in_single = false;
}
continue;
}
if in_double {
if escaped {
escaped = false;
} else if ch == '\\' {
escaped = true;
} else if ch == '"' {
in_double = false;
}
continue;
}
match ch {
'\'' => in_single = true,
'"' => in_double = true,
'#' => return &line[..index],
_ => {}
}
}
line
}