use crate::ast::Inline;
use nom::IResult;
pub(crate) const MAX_ENV_VAR_LEN: usize = 50;
const IS_WORD_BYTE: [bool; 256] = {
let mut t = [false; 256];
let mut b = 0usize;
while b < 256 {
t[b] = (b as u8).is_ascii_alphanumeric() || b as u8 == b'_';
b += 1;
}
t
};
fn scan_word_within(rest: &str, limit: usize) -> (usize, Option<usize>) {
let bytes = rest.as_bytes();
let limit = bytes.len().min(limit);
let mut underscore = None;
let mut i = 0;
while i < limit && IS_WORD_BYTE[bytes[i] as usize] {
if bytes[i] == b'_' && underscore.is_none() {
underscore = Some(i);
}
i += 1;
}
(i, underscore)
}
pub(crate) fn scan_word(rest: &str) -> (usize, Option<usize>) {
let (len, underscore) = scan_word_within(rest, MAX_ENV_VAR_LEN + 1);
if len > MAX_ENV_VAR_LEN {
return (len, None);
}
(len, underscore)
}
pub(crate) fn scan_word_full(rest: &str) -> (usize, Option<usize>) {
scan_word_within(rest, usize::MAX)
}
pub(crate) fn fits(len: usize) -> bool {
len <= MAX_ENV_VAR_LEN
}
pub(crate) fn environment_variable(input: &str) -> IResult<&str, Inline> {
let fail = || nom::Err::Error(nom::error::Error::new(input, nom::error::ErrorKind::Verify));
if !input
.as_bytes()
.first()
.is_some_and(u8::is_ascii_alphabetic)
{
return Err(fail());
}
let (len, underscore) = scan_word(input);
if !fits(len) || underscore.is_none() || !is_likely_env_var(&input[..len]) {
return Err(fail());
}
Ok((&input[len..], Inline::Text(input[..len].to_string())))
}
pub(crate) fn is_likely_env_var(s: &str) -> bool {
if !s.contains('_') {
return false;
}
if s.starts_with('_') || s.ends_with('_') {
return false;
}
if s.contains("__") {
return false;
}
if s.len() < 3 || s.len() > 50 {
return false;
}
true
}