graphitepdf-utils 0.4.0

Lightweight utility functions for GraphitePDF and related Rust workflows.
Documentation
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PercentMatch {
    pub value: f32,
    pub percent: f32,
}

pub fn upper_first(input: &str) -> String {
    let mut chars = input.chars();

    match chars.next() {
        Some(first) => {
            let mut result = String::new();
            result.extend(first.to_uppercase());
            result.push_str(chars.as_str());
            result
        }
        None => String::new(),
    }
}

pub fn capitalize(input: &str) -> String {
    let mut result = String::with_capacity(input.len());
    let mut should_capitalize = true;

    for ch in input.chars() {
        if ch.is_whitespace() {
            should_capitalize = true;
            result.push(ch);
            continue;
        }

        if should_capitalize {
            result.extend(ch.to_uppercase());
            should_capitalize = false;
        } else {
            result.push(ch);
        }
    }

    result
}

pub fn match_percent(input: &str) -> Option<PercentMatch> {
    let trimmed = input.trim();

    if !trimmed.ends_with('%') {
        return None;
    }

    let numeric = trimmed[..trimmed.len().saturating_sub(1)].trim();
    let value = numeric.parse::<f32>().ok()?;

    Some(PercentMatch {
        value,
        percent: value / 100.0,
    })
}

pub fn parse_float(input: &str) -> Option<f32> {
    let trimmed = input.trim();
    let mut consumed = 0_usize;
    let mut seen_digit = false;
    let mut seen_decimal = false;

    for (index, ch) in trimmed.char_indices() {
        let allowed = if index == 0 && (ch == '+' || ch == '-') {
            true
        } else if ch.is_ascii_digit() {
            seen_digit = true;
            true
        } else if ch == '.' && !seen_decimal {
            seen_decimal = true;
            true
        } else {
            false
        };

        if !allowed {
            break;
        }

        consumed = index + ch.len_utf8();
    }

    if !seen_digit || consumed == 0 {
        return None;
    }

    trimmed[..consumed].parse::<f32>().ok()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn capitalizes_words_and_first_letter() {
        assert_eq!(capitalize("hello world"), "Hello World");
        assert_eq!(upper_first("hello"), "Hello");
    }

    #[test]
    fn parses_percentages() {
        assert_eq!(
            match_percent("50%"),
            Some(PercentMatch {
                value: 50.0,
                percent: 0.5,
            })
        );
        assert_eq!(match_percent("abc"), None);
    }

    #[test]
    fn parses_leading_floats() {
        assert_eq!(parse_float("3.14"), Some(314.0_f32 / 100.0));
        assert_eq!(parse_float("10px"), Some(10.0));
        assert_eq!(parse_float("abc"), None);
    }
}