omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use omena_value_lattice::parse_whole_function_value_arguments;

use super::{
    parse_static_scss_list_index, static_scss_quoted_value_end,
    static_scss_top_level_separator_index, strip_static_scss_quotes,
};

pub(super) fn parse_static_scss_quote_value(value: &str) -> Option<String> {
    parse_static_scss_quote_value_with_name(value, "quote")
}

pub(super) fn parse_static_scss_string_quote_value(value: &str) -> Option<String> {
    parse_static_scss_quote_value_with_name(value, "string.quote")
}

pub(super) fn parse_static_scss_unquote_value(value: &str) -> Option<String> {
    parse_static_scss_unquote_value_with_name(value, "unquote")
}

pub(super) fn parse_static_scss_string_unquote_value(value: &str) -> Option<String> {
    parse_static_scss_unquote_value_with_name(value, "string.unquote")
}

pub(super) fn parse_static_scss_str_length_value(value: &str) -> Option<String> {
    parse_static_scss_string_length_value_with_name(value, "str-length")
}

pub(super) fn parse_static_scss_string_length_value(value: &str) -> Option<String> {
    parse_static_scss_string_length_value_with_name(value, "string.length")
}

pub(super) fn parse_static_scss_str_index_value(value: &str) -> Option<String> {
    parse_static_scss_string_index_value_with_name(value, "str-index")
}

pub(super) fn parse_static_scss_string_index_value(value: &str) -> Option<String> {
    parse_static_scss_string_index_value_with_name(value, "string.index")
}

pub(super) fn parse_static_scss_str_insert_value(value: &str) -> Option<String> {
    parse_static_scss_string_insert_value_with_name(value, "str-insert")
}

pub(super) fn parse_static_scss_string_insert_value(value: &str) -> Option<String> {
    parse_static_scss_string_insert_value_with_name(value, "string.insert")
}

pub(super) fn parse_static_scss_str_slice_value(value: &str) -> Option<String> {
    parse_static_scss_string_slice_value_with_name(value, "str-slice")
}

pub(super) fn parse_static_scss_string_slice_value(value: &str) -> Option<String> {
    parse_static_scss_string_slice_value_with_name(value, "string.slice")
}

pub(super) fn parse_static_scss_to_upper_case_value(value: &str) -> Option<String> {
    parse_static_scss_string_case_value_with_name(
        value,
        "to-upper-case",
        StaticScssStringCase::Upper,
    )
}

pub(super) fn parse_static_scss_string_to_upper_case_value(value: &str) -> Option<String> {
    parse_static_scss_string_case_value_with_name(
        value,
        "string.to-upper-case",
        StaticScssStringCase::Upper,
    )
}

pub(super) fn parse_static_scss_to_lower_case_value(value: &str) -> Option<String> {
    parse_static_scss_string_case_value_with_name(
        value,
        "to-lower-case",
        StaticScssStringCase::Lower,
    )
}

pub(super) fn parse_static_scss_string_to_lower_case_value(value: &str) -> Option<String> {
    parse_static_scss_string_case_value_with_name(
        value,
        "string.to-lower-case",
        StaticScssStringCase::Lower,
    )
}

fn parse_static_scss_quote_value_with_name(value: &str, function_name: &str) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [string] = arguments.as_slice() else {
        return None;
    };
    static_scss_quote_string(parse_static_scss_string_argument(string)?.text.as_str())
}

fn parse_static_scss_unquote_value_with_name(value: &str, function_name: &str) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [string] = arguments.as_slice() else {
        return None;
    };
    Some(parse_static_scss_string_argument(string)?.text)
}

fn parse_static_scss_string_length_value_with_name(
    value: &str,
    function_name: &str,
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [string] = arguments.as_slice() else {
        return None;
    };
    Some(
        parse_static_scss_string_argument(string)?
            .text
            .chars()
            .count()
            .to_string(),
    )
}

fn parse_static_scss_string_index_value_with_name(
    value: &str,
    function_name: &str,
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [string, substring] = arguments.as_slice() else {
        return None;
    };
    let string = parse_static_scss_string_argument(string)?;
    let substring = parse_static_scss_string_argument(substring)?;
    if substring.text.is_empty() {
        return None;
    }
    Some(
        string
            .text
            .find(substring.text.as_str())
            .and_then(|byte_index| {
                string
                    .text
                    .get(..byte_index)
                    .map(|prefix| prefix.chars().count() + 1)
            })
            .map(|index| index.to_string())
            .unwrap_or_else(|| "null".to_string()),
    )
}

fn parse_static_scss_string_insert_value_with_name(
    value: &str,
    function_name: &str,
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [string, insert, index] = arguments.as_slice() else {
        return None;
    };
    let string = parse_static_scss_string_argument(string)?;
    let insert = parse_static_scss_string_argument(insert)?;
    let index = parse_static_scss_list_index(index)?;
    let chars = string.text.chars().collect::<Vec<_>>();
    let offset = static_scss_string_insert_offset(index, chars.len())?;
    let output = chars
        .iter()
        .take(offset)
        .copied()
        .chain(insert.text.chars())
        .chain(chars.iter().skip(offset).copied())
        .collect::<String>();
    static_scss_render_string_value(output.as_str(), string.quoted)
}

fn parse_static_scss_string_slice_value_with_name(
    value: &str,
    function_name: &str,
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [string, start, end @ ..] = arguments.as_slice() else {
        return None;
    };
    if end.len() > 1 {
        return None;
    }
    let string = parse_static_scss_string_argument(string)?;
    let start = parse_static_scss_list_index(start)?;
    let end = match end {
        [] => -1,
        [end] => parse_static_scss_list_index(end)?,
        _ => return None,
    };
    let chars = string.text.chars().collect::<Vec<_>>();
    let start_offset = static_scss_string_slice_start_offset(start, chars.len())?;
    let end_offset = static_scss_string_slice_end_offset(end, chars.len())?;
    let output = if start_offset >= end_offset {
        String::new()
    } else {
        chars[start_offset..end_offset].iter().collect::<String>()
    };
    static_scss_render_string_value(output.as_str(), string.quoted)
}

fn parse_static_scss_string_case_value_with_name(
    value: &str,
    function_name: &str,
    case: StaticScssStringCase,
) -> Option<String> {
    let arguments = parse_whole_function_value_arguments(value, function_name)?;
    let [string] = arguments.as_slice() else {
        return None;
    };
    let string = parse_static_scss_string_argument(string)?;
    let output = match case {
        StaticScssStringCase::Upper => string.text.to_ascii_uppercase(),
        StaticScssStringCase::Lower => string.text.to_ascii_lowercase(),
    };
    static_scss_render_string_value(output.as_str(), string.quoted)
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct StaticScssStringValue {
    pub(super) text: String,
    pub(super) quoted: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StaticScssStringCase {
    Upper,
    Lower,
}

pub(super) fn parse_static_scss_string_argument(value: &str) -> Option<StaticScssStringValue> {
    let value = value.trim();
    if value.contains('$') {
        return None;
    }
    if let Some(text) = static_scss_quoted_string_text(value) {
        return Some(StaticScssStringValue {
            text: text.to_string(),
            quoted: true,
        });
    }
    if value.is_empty()
        || value.contains('(')
        || value.contains(')')
        || value.contains('[')
        || value.contains(']')
        || static_scss_top_level_separator_index(value, ',')?.is_some()
    {
        return None;
    }
    Some(StaticScssStringValue {
        text: value.to_string(),
        quoted: false,
    })
}

pub(super) fn static_scss_quoted_string_text(value: &str) -> Option<&str> {
    let quote = value.chars().next()?;
    if !matches!(quote, '"' | '\'') || static_scss_quoted_value_end(value, 0, quote)? != value.len()
    {
        return None;
    }
    strip_static_scss_quotes(value)
}

fn static_scss_render_string_value(value: &str, quoted: bool) -> Option<String> {
    if quoted {
        static_scss_quote_string(value)
    } else {
        Some(value.to_string())
    }
}

pub(super) fn static_scss_quote_string(value: &str) -> Option<String> {
    let mut output = String::with_capacity(value.len() + 2);
    output.push('"');
    for ch in value.chars() {
        match ch {
            '"' | '\\' => {
                output.push('\\');
                output.push(ch);
            }
            _ if ch.is_control() => return None,
            _ => output.push(ch),
        }
    }
    output.push('"');
    Some(output)
}

fn static_scss_string_insert_offset(index: isize, len: usize) -> Option<usize> {
    let len = isize::try_from(len).ok()?;
    Some(if index > 0 {
        (index - 1).clamp(0, len) as usize
    } else {
        (len + index + 1).clamp(0, len) as usize
    })
}

fn static_scss_string_slice_start_offset(index: isize, len: usize) -> Option<usize> {
    let len = isize::try_from(len).ok()?;
    Some(if index > 0 {
        (index - 1).clamp(0, len) as usize
    } else {
        (len + index).clamp(0, len) as usize
    })
}

fn static_scss_string_slice_end_offset(index: isize, len: usize) -> Option<usize> {
    let len = isize::try_from(len).ok()?;
    Some(if index > 0 {
        index.clamp(0, len) as usize
    } else {
        (len + index + 1).clamp(0, len) as usize
    })
}