use super::combinators::ws;
use super::error::ParseError;
use super::types::ParseResult;
use nom::{
branch::alt,
bytes::complete::{tag, take_while, take_while1},
character::complete::{char, digit1},
combinator::{cut, map, map_res, opt, recognize, value as nom_value},
error::VerboseError,
multi::{many0, separated_list0},
number::complete::double,
sequence::{delimited, pair, separated_pair, tuple},
};
use pattern_core::{RangeValue, Value};
pub fn identifier(input: &str) -> ParseResult<'_, String> {
alt((
backtick_quoted_string,
map(integer, |n| n.to_string()),
unquoted_identifier,
))(input)
}
pub fn key_name(input: &str) -> ParseResult<'_, String> {
alt((
backtick_quoted_string,
double_quoted_string,
unquoted_identifier,
))(input)
}
pub fn unquoted_identifier(input: &str) -> ParseResult<'_, String> {
map(
recognize(pair(
take_while1(|c: char| c.is_ascii_alphabetic() || c == '_'),
take_while(|c: char| {
c.is_alphanumeric() || c == '_' || c == '-' || c == '@' || c == '.'
}),
)),
|s: &str| s.to_string(),
)(input)
}
pub fn string_value(input: &str) -> ParseResult<'_, String> {
alt((
fenced_string, double_quoted_string, single_quoted_string, backtick_quoted_string, ))(input)
}
fn double_quoted_string(input: &str) -> ParseResult<'_, String> {
delimited(
char('"'),
map(
recognize(many0(alt((
nom_value((), tag("\\\\")), nom_value((), tag("\\\"")), nom_value((), tag("\\\'")), nom_value((), tag("\\`")), nom_value((), tag("\\/")), nom_value((), tag("\\n")), nom_value((), tag("\\r")), nom_value((), tag("\\t")), nom_value((), tag("\\b")), nom_value((), tag("\\f")), nom_value((), take_while1(|c| c != '\\' && c != '"')),
)))),
|s: &str| unescape_string(s),
),
cut(char('"')),
)(input)
}
fn single_quoted_string(input: &str) -> ParseResult<'_, String> {
delimited(
char('\''),
map(
recognize(many0(alt((
nom_value((), tag("\\\\")), nom_value((), tag("\\\"")), nom_value((), tag("\\\'")), nom_value((), tag("\\`")), nom_value((), tag("\\/")), nom_value((), tag("\\n")), nom_value((), tag("\\r")), nom_value((), tag("\\t")), nom_value((), tag("\\b")), nom_value((), tag("\\f")), nom_value((), take_while1(|c| c != '\\' && c != '\'')),
)))),
|s: &str| unescape_string(s),
),
cut(char('\'')),
)(input)
}
fn backtick_quoted_string(input: &str) -> ParseResult<'_, String> {
delimited(
char('`'),
map(
recognize(many0(alt((
nom_value((), tag("\\\\")), nom_value((), tag("\\\"")), nom_value((), tag("\\\'")), nom_value((), tag("\\`")), nom_value((), tag("\\/")), nom_value((), tag("\\n")), nom_value((), tag("\\r")), nom_value((), tag("\\t")), nom_value((), tag("\\b")), nom_value((), tag("\\f")), nom_value((), take_while1(|c| c != '\\' && c != '`')),
)))),
|s: &str| unescape_string(s),
),
cut(char('`')),
)(input)
}
fn fenced_string(input: &str) -> ParseResult<'_, String> {
let (input, _) = tag("```")(input)?;
let (input, has_tag_and_newline) =
opt(pair(take_while(|c: char| c.is_alphanumeric()), char('\n')))(input)?;
let input = if has_tag_and_newline.is_some() {
input
} else {
let (input, _) = opt(char('\n'))(input)?;
input
};
let mut content = String::new();
let mut remaining = input;
loop {
if remaining.starts_with("```") {
let (rest, _) = tag("```")(remaining)?;
return Ok((rest, content));
}
if let Some(c) = remaining.chars().next() {
content.push(c);
remaining = &remaining[c.len_utf8()..];
} else {
return Err(nom::Err::Error(VerboseError {
errors: vec![(
input,
nom::error::VerboseErrorKind::Context("Unclosed fenced string"),
)],
}));
}
}
}
fn unescape_string(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => result.push('\n'),
Some('r') => result.push('\r'),
Some('t') => result.push('\t'),
Some('\\') => result.push('\\'),
Some('"') => result.push('"'),
Some('\'') => result.push('\''),
Some('`') => result.push('`'),
Some('/') => result.push('/'),
Some('b') => result.push('\u{0008}'),
Some('f') => result.push('\u{000C}'),
Some(c) => {
result.push('\\');
result.push(c);
}
None => result.push('\\'),
}
} else {
result.push(c);
}
}
result
}
pub fn integer(input: &str) -> ParseResult<'_, i64> {
alt((
map_res(
recognize(tuple((
opt(char('-')),
tag("0x"),
take_while1(|c: char| c.is_ascii_hexdigit()),
))),
|s: &str| {
let s = s
.strip_prefix('-')
.map(|rest| (true, rest))
.unwrap_or((false, s));
let hex_part = s.1.strip_prefix("0x").unwrap();
i64::from_str_radix(hex_part, 16).map(|n| if s.0 { -n } else { n })
},
),
map_res(recognize(pair(opt(char('-')), digit1)), |s: &str| {
s.parse::<i64>()
}),
))(input)
}
#[allow(dead_code)]
pub fn decimal(input: &str) -> ParseResult<'_, f64> {
double(input)
}
fn measurement(input: &str) -> ParseResult<'_, Value> {
map(
pair(
alt((
map(
recognize(tuple((opt(char('-')), digit1, char('.'), digit1))),
|s: &str| s.parse::<f64>().unwrap(),
),
map(integer, |i| i as f64),
)),
take_while1(|c: char| c.is_alphabetic()),
),
|(value, unit): (f64, &str)| Value::VMeasurement {
unit: unit.to_string(),
value,
},
)(input)
}
fn number(input: &str) -> ParseResult<'_, Value> {
alt((
map(
recognize(tuple((opt(char('-')), digit1, char('.'), digit1))),
|s: &str| Value::VDecimal(s.parse().unwrap()),
),
map(integer, Value::VInteger),
))(input)
}
pub fn boolean(input: &str) -> ParseResult<'_, bool> {
alt((nom_value(true, tag("true")), nom_value(false, tag("false"))))(input)
}
pub fn array(input: &str) -> ParseResult<'_, Vec<Value>> {
delimited(
char('['),
delimited(
ws,
separated_list0(delimited(ws, char(','), ws), value_parser),
ws,
),
cut(char(']')),
)(input)
}
pub fn range(input: &str) -> ParseResult<'_, RangeValue> {
alt((
map(
separated_pair(integer, alt((tag("..."), tag(".."))), integer),
|(lower, upper)| RangeValue {
lower: Some(lower as f64),
upper: Some(upper as f64),
},
),
map(
tuple((integer, alt((tag("..."), tag(".."))))),
|(lower, _)| RangeValue {
lower: Some(lower as f64),
upper: None,
},
),
map(
tuple((alt((tag("..."), tag(".."))), integer)),
|(_, upper)| RangeValue {
lower: None,
upper: Some(upper as f64),
},
),
map(alt((tag("..."), tag(".."))), |_| RangeValue {
lower: None,
upper: None,
}),
))(input)
}
pub fn tagged_string(input: &str) -> ParseResult<'_, (String, String)> {
map(
pair(
unquoted_identifier, backtick_quoted_string, ),
|(tag, content)| (tag, content),
)(input)
}
#[allow(dead_code)]
fn tagged_fenced_string(input: &str) -> ParseResult<'_, (String, String)> {
let (input, _) = tag("```")(input)?;
let (input, tag_str) = take_while1(|c: char| c.is_alphanumeric())(input)?;
let (input, _) = char('\n')(input)?;
let mut content = String::new();
let mut remaining = input;
loop {
if remaining.starts_with("```") {
let (rest, _) = tag("```")(remaining)?;
return Ok((rest, (tag_str.to_string(), content)));
}
if let Some(c) = remaining.chars().next() {
content.push(c);
remaining = &remaining[c.len_utf8()..];
} else {
return Err(nom::Err::Error(VerboseError {
errors: vec![(
input,
nom::error::VerboseErrorKind::Context("Unclosed tagged fenced string"),
)],
}));
}
}
}
#[allow(dead_code)]
fn tagged_triple_quoted(input: &str) -> ParseResult<'_, (String, String)> {
let (input, _) = tag(r#"""""#)(input)?;
let content_end = input.find(r#"""""#).ok_or_else(|| {
nom::Err::Error(VerboseError {
errors: vec![(
input,
nom::error::VerboseErrorKind::Context("unclosed tagged string"),
)],
})
})?;
let content_str = &input[..content_end];
let remaining = &input[content_end + 3..];
let trimmed = content_str.trim_start();
let (tag, content) = if let Some(pos) = trimmed.find(|c: char| c.is_whitespace()) {
let maybe_tag = &trimmed[..pos];
if !maybe_tag.is_empty()
&& maybe_tag
.chars()
.all(|c: char| c.is_alphanumeric() || c == '_')
{
(
maybe_tag.to_string(),
trimmed[pos..].trim_start().to_string(),
)
} else {
(String::new(), content_str.to_string())
}
} else {
(String::new(), trimmed.to_string())
};
Ok((remaining, (tag, content)))
}
fn map_value(input: &str) -> ParseResult<'_, Value> {
map(
delimited(
char('{'),
delimited(
ws,
separated_list0(
delimited(ws, char(','), ws),
separated_pair(
delimited(ws, key_name, ws),
char(':'),
value_parser, ),
),
ws,
),
char('}'),
),
|pairs| {
let mut map = std::collections::HashMap::new();
for (key, value) in pairs {
map.insert(key, value);
}
Value::VMap(map)
},
)(input)
}
pub fn value_parser(input: &str) -> ParseResult<'_, Value> {
delimited(
ws,
alt((
map(tagged_string, |(tag, content)| Value::VTaggedString {
tag,
content,
}),
map(string_value, Value::VString),
map_value,
map(range, Value::VRange),
measurement,
number,
map(boolean, Value::VBoolean),
map(array, Value::VArray),
map(unquoted_identifier, Value::VSymbol),
)),
ws,
)(input)
}
#[allow(dead_code)]
pub fn value_parse_error(
input: &str,
error: nom::Err<nom::error::VerboseError<&str>>,
) -> ParseError {
ParseError::from_nom_error(input, error)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unquoted_identifier() {
let (remaining, id) = unquoted_identifier("hello").unwrap();
assert_eq!(id, "hello");
assert_eq!(remaining, "");
let (remaining, id) = unquoted_identifier("hello_world-123").unwrap();
assert_eq!(id, "hello_world-123");
assert_eq!(remaining, "");
let (remaining, id) = unquoted_identifier("_private").unwrap();
assert_eq!(id, "_private");
assert_eq!(remaining, "");
}
#[test]
fn test_identifier_backtick() {
let (remaining, id) = identifier("`+1`").unwrap();
assert_eq!(id, "+1");
assert_eq!(remaining, "");
let (remaining, id) = identifier("`Role Label`").unwrap();
assert_eq!(id, "Role Label");
assert_eq!(remaining, "");
}
#[test]
fn test_identifier_rejects_double_quoted() {
assert!(identifier(r#""hello world""#).is_err());
}
#[test]
fn test_identifier_rejects_single_quoted() {
assert!(identifier("'hello'").is_err());
}
#[test]
fn test_key_name_symbol() {
let (remaining, key) = key_name("name").unwrap();
assert_eq!(key, "name");
assert_eq!(remaining, "");
}
#[test]
fn test_key_name_backtick() {
let (remaining, key) = key_name("`title name`").unwrap();
assert_eq!(key, "title name");
assert_eq!(remaining, "");
}
#[test]
fn test_key_name_double_quoted() {
let (remaining, key) = key_name(r#""display title""#).unwrap();
assert_eq!(key, "display title");
assert_eq!(remaining, "");
}
#[test]
fn test_key_name_rejects_single_quoted() {
assert!(key_name("'key'").is_err());
}
#[test]
fn test_string_value() {
let (remaining, s) = string_value(r#""hello""#).unwrap();
assert_eq!(s, "hello");
assert_eq!(remaining, "");
let (remaining, s) = string_value(r#""hello world""#).unwrap();
assert_eq!(s, "hello world");
assert_eq!(remaining, "");
}
#[test]
fn test_integer() {
let (remaining, n) = integer("42").unwrap();
assert_eq!(n, 42);
assert_eq!(remaining, "");
let (remaining, n) = integer("-10").unwrap();
assert_eq!(n, -10);
assert_eq!(remaining, "");
}
#[test]
fn test_decimal() {
let (remaining, n) = decimal("3.14").unwrap();
assert_eq!(n, 3.14);
assert_eq!(remaining, "");
let (remaining, n) = decimal("-2.5").unwrap();
assert_eq!(n, -2.5);
assert_eq!(remaining, "");
}
#[test]
fn test_boolean() {
let (remaining, b) = boolean("true").unwrap();
assert_eq!(b, true);
assert_eq!(remaining, "");
let (remaining, b) = boolean("false").unwrap();
assert_eq!(b, false);
assert_eq!(remaining, "");
}
#[test]
fn test_array() {
let (remaining, arr) = array(r#"["hello", 42, true]"#).unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(remaining, "");
match &arr[0] {
Value::VString(s) => assert_eq!(s, "hello"),
_ => panic!("Expected string"),
}
match &arr[1] {
Value::VInteger(n) => assert_eq!(*n, 42),
_ => panic!("Expected integer"),
}
match &arr[2] {
Value::VBoolean(b) => assert_eq!(*b, true),
_ => panic!("Expected boolean"),
}
}
#[test]
fn test_range() {
let (remaining, range_val) = range("1..10").unwrap();
assert_eq!(range_val.lower, Some(1.0));
assert_eq!(range_val.upper, Some(10.0));
assert_eq!(remaining, "");
}
#[test]
fn test_range_from() {
let (remaining, range_val) = range("5..").unwrap();
assert_eq!(range_val.lower, Some(5.0));
assert_eq!(range_val.upper, None);
assert_eq!(remaining, "");
}
#[test]
fn test_range_to() {
let (remaining, range_val) = range("..10").unwrap();
assert_eq!(range_val.lower, None);
assert_eq!(range_val.upper, Some(10.0));
assert_eq!(remaining, "");
}
#[test]
fn test_range_unbounded() {
let (remaining, range_val) = range("..").unwrap();
assert_eq!(range_val.lower, None);
assert_eq!(range_val.upper, None);
assert_eq!(remaining, "");
}
#[test]
fn test_tagged_string() {
}
#[test]
fn test_value_parser_string() {
let (remaining, val) = value_parser(r#""hello""#).unwrap();
match val {
Value::VString(s) => assert_eq!(s, "hello"),
_ => panic!("Expected string"),
}
assert_eq!(remaining, "");
}
#[test]
fn test_value_parser_integer() {
let (remaining, val) = value_parser("42").unwrap();
match val {
Value::VInteger(n) => assert_eq!(n, 42),
_ => panic!("Expected integer"),
}
assert_eq!(remaining, "");
}
#[test]
fn test_value_parser_boolean() {
let (remaining, val) = value_parser("true").unwrap();
match val {
Value::VBoolean(b) => assert_eq!(b, true),
_ => panic!("Expected boolean"),
}
assert_eq!(remaining, "");
}
#[test]
fn test_value_parser_array() {
let (remaining, val) = value_parser(r#"[1, 2, 3]"#).unwrap();
match val {
Value::VArray(arr) => assert_eq!(arr.len(), 3),
_ => panic!("Expected array"),
}
assert_eq!(remaining, "");
}
#[test]
fn test_value_parser_range() {
let (remaining, val) = value_parser("0..100").unwrap();
match val {
Value::VRange(range) => {
assert_eq!(range.lower, Some(0.0));
assert_eq!(range.upper, Some(100.0));
}
_ => panic!("Expected range"),
}
assert_eq!(remaining, "");
}
}