use-yaml 0.1.0

Lightweight YAML marker and key-value helpers for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

/// A discovered YAML mapping entry.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct YamlKeyValue {
    pub key: String,
    pub value: String,
    pub line: usize,
    pub indent: usize,
}

/// Returns `true` when the input contains YAML-like markers, list items, or mappings.
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())
    })
}

/// Returns `true` when a line is a YAML document start marker.
pub fn is_yaml_document_start(line: &str) -> bool {
    line.trim() == "---"
}

/// Returns `true` when a line is a YAML document end marker.
pub fn is_yaml_document_end(line: &str) -> bool {
    line.trim() == "..."
}

/// Strips leading and trailing YAML document markers when they are present.
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]
}

/// Returns `true` when a line is a YAML list item.
pub fn is_yaml_list_item(line: &str) -> bool {
    let trimmed = line.trim_start();
    trimmed == "-" || trimmed.starts_with("- ")
}

/// Counts leading space indentation on a YAML line.
pub fn yaml_indent(line: &str) -> usize {
    line.chars().take_while(|ch| *ch == ' ').count()
}

/// Splits a simple YAML mapping line on the first `:` outside quotes.
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
}

/// Extracts simple YAML mapping entries from the input.
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
}

/// Quotes a string as a single-quoted YAML scalar.
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
}