mirror of https://github.com/astral-sh/ruff
1087 lines
33 KiB
Rust
1087 lines
33 KiB
Rust
use itertools::{Itertools, PeekingNext};
|
|
|
|
use std::error::Error;
|
|
use std::str::FromStr;
|
|
|
|
use crate::Case;
|
|
|
|
trait FormatParse {
|
|
fn parse(text: &str) -> (Option<Self>, &str)
|
|
where
|
|
Self: Sized;
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
pub enum FormatConversion {
|
|
Str,
|
|
Repr,
|
|
Ascii,
|
|
Bytes,
|
|
}
|
|
|
|
impl FormatParse for FormatConversion {
|
|
fn parse(text: &str) -> (Option<Self>, &str) {
|
|
let Some(conversion) = Self::from_string(text) else {
|
|
return (None, text);
|
|
};
|
|
let mut chars = text.chars();
|
|
chars.next(); // Consume the bang
|
|
chars.next(); // Consume one r,s,a char
|
|
(Some(conversion), chars.as_str())
|
|
}
|
|
}
|
|
|
|
impl FormatConversion {
|
|
pub fn from_char(c: char) -> Option<FormatConversion> {
|
|
match c {
|
|
's' => Some(FormatConversion::Str),
|
|
'r' => Some(FormatConversion::Repr),
|
|
'a' => Some(FormatConversion::Ascii),
|
|
'b' => Some(FormatConversion::Bytes),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn from_string(text: &str) -> Option<FormatConversion> {
|
|
let mut chars = text.chars();
|
|
if chars.next() != Some('!') {
|
|
return None;
|
|
}
|
|
|
|
FormatConversion::from_char(chars.next()?)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
pub enum FormatAlign {
|
|
Left,
|
|
Right,
|
|
AfterSign,
|
|
Center,
|
|
}
|
|
|
|
impl FormatAlign {
|
|
fn from_char(c: char) -> Option<FormatAlign> {
|
|
match c {
|
|
'<' => Some(FormatAlign::Left),
|
|
'>' => Some(FormatAlign::Right),
|
|
'=' => Some(FormatAlign::AfterSign),
|
|
'^' => Some(FormatAlign::Center),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FormatParse for FormatAlign {
|
|
fn parse(text: &str) -> (Option<Self>, &str) {
|
|
let mut chars = text.chars();
|
|
if let Some(maybe_align) = chars.next().and_then(Self::from_char) {
|
|
(Some(maybe_align), chars.as_str())
|
|
} else {
|
|
(None, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
|
pub enum FormatSign {
|
|
Plus,
|
|
Minus,
|
|
MinusOrSpace,
|
|
}
|
|
|
|
impl FormatParse for FormatSign {
|
|
fn parse(text: &str) -> (Option<Self>, &str) {
|
|
let mut chars = text.chars();
|
|
match chars.next() {
|
|
Some('-') => (Some(Self::Minus), chars.as_str()),
|
|
Some('+') => (Some(Self::Plus), chars.as_str()),
|
|
Some(' ') => (Some(Self::MinusOrSpace), chars.as_str()),
|
|
_ => (None, text),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FormatGrouping {
|
|
Comma,
|
|
Underscore,
|
|
}
|
|
|
|
impl FormatParse for FormatGrouping {
|
|
fn parse(text: &str) -> (Option<Self>, &str) {
|
|
let mut chars = text.chars();
|
|
match chars.next() {
|
|
Some('_') => (Some(Self::Underscore), chars.as_str()),
|
|
Some(',') => (Some(Self::Comma), chars.as_str()),
|
|
_ => (None, text),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
pub enum FormatType {
|
|
String,
|
|
Binary,
|
|
Character,
|
|
Decimal,
|
|
Octal,
|
|
Number(Case),
|
|
Hex(Case),
|
|
Exponent(Case),
|
|
GeneralFormat(Case),
|
|
FixedPoint(Case),
|
|
Percentage,
|
|
}
|
|
|
|
impl From<&FormatType> for char {
|
|
fn from(from: &FormatType) -> char {
|
|
match from {
|
|
FormatType::String => 's',
|
|
FormatType::Binary => 'b',
|
|
FormatType::Character => 'c',
|
|
FormatType::Decimal => 'd',
|
|
FormatType::Octal => 'o',
|
|
FormatType::Number(Case::Lower) => 'n',
|
|
FormatType::Number(Case::Upper) => 'N',
|
|
FormatType::Hex(Case::Lower) => 'x',
|
|
FormatType::Hex(Case::Upper) => 'X',
|
|
FormatType::Exponent(Case::Lower) => 'e',
|
|
FormatType::Exponent(Case::Upper) => 'E',
|
|
FormatType::GeneralFormat(Case::Lower) => 'g',
|
|
FormatType::GeneralFormat(Case::Upper) => 'G',
|
|
FormatType::FixedPoint(Case::Lower) => 'f',
|
|
FormatType::FixedPoint(Case::Upper) => 'F',
|
|
FormatType::Percentage => '%',
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FormatParse for FormatType {
|
|
fn parse(text: &str) -> (Option<Self>, &str) {
|
|
let mut chars = text.chars();
|
|
match chars.next() {
|
|
Some('s') => (Some(Self::String), chars.as_str()),
|
|
Some('b') => (Some(Self::Binary), chars.as_str()),
|
|
Some('c') => (Some(Self::Character), chars.as_str()),
|
|
Some('d') => (Some(Self::Decimal), chars.as_str()),
|
|
Some('o') => (Some(Self::Octal), chars.as_str()),
|
|
Some('n') => (Some(Self::Number(Case::Lower)), chars.as_str()),
|
|
Some('N') => (Some(Self::Number(Case::Upper)), chars.as_str()),
|
|
Some('x') => (Some(Self::Hex(Case::Lower)), chars.as_str()),
|
|
Some('X') => (Some(Self::Hex(Case::Upper)), chars.as_str()),
|
|
Some('e') => (Some(Self::Exponent(Case::Lower)), chars.as_str()),
|
|
Some('E') => (Some(Self::Exponent(Case::Upper)), chars.as_str()),
|
|
Some('f') => (Some(Self::FixedPoint(Case::Lower)), chars.as_str()),
|
|
Some('F') => (Some(Self::FixedPoint(Case::Upper)), chars.as_str()),
|
|
Some('g') => (Some(Self::GeneralFormat(Case::Lower)), chars.as_str()),
|
|
Some('G') => (Some(Self::GeneralFormat(Case::Upper)), chars.as_str()),
|
|
Some('%') => (Some(Self::Percentage), chars.as_str()),
|
|
Some(_) => (None, chars.as_str()),
|
|
_ => (None, text),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The format specification component of a format field
|
|
///
|
|
/// For example the content would be parsed from `<20` in:
|
|
/// ```python
|
|
/// "hello {name:<20}".format(name="test")
|
|
/// ```
|
|
///
|
|
/// Format specifications allow nested placeholders for dynamic formatting.
|
|
/// For example, the following statements are equivalent:
|
|
/// ```python
|
|
/// "hello {name:{fmt}}".format(name="test", fmt="<20")
|
|
/// "hello {name:{align}{width}}".format(name="test", align="<", width="20")
|
|
/// "hello {name:<20{empty}>}".format(name="test", empty="")
|
|
/// ```
|
|
///
|
|
/// Nested placeholders can include additional format specifiers.
|
|
/// ```python
|
|
/// "hello {name:{fmt:*>}}".format(name="test", fmt="<20")
|
|
/// ```
|
|
///
|
|
/// However, placeholders can only be singly nested (preserving our sanity).
|
|
/// A [`FormatSpecError::PlaceholderRecursionExceeded`] will be raised while parsing in this case.
|
|
/// ```python
|
|
/// "hello {name:{fmt:{not_allowed}}}".format(name="test", fmt="<20") # Syntax error
|
|
/// ```
|
|
///
|
|
/// When placeholders are present in a format specification, parsing will return a [`DynamicFormatSpec`]
|
|
/// and avoid attempting to parse any of the clauses. Otherwise, a [`StaticFormatSpec`] will be used.
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FormatSpec {
|
|
Static(StaticFormatSpec),
|
|
Dynamic(DynamicFormatSpec),
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct StaticFormatSpec {
|
|
// Ex) `!s` in `'{!s}'`
|
|
conversion: Option<FormatConversion>,
|
|
// Ex) `*` in `'{:*^30}'`
|
|
fill: Option<char>,
|
|
// Ex) `<` in `'{:<30}'`
|
|
align: Option<FormatAlign>,
|
|
// Ex) `+` in `'{:+f}'`
|
|
sign: Option<FormatSign>,
|
|
// Ex) `#` in `'{:#x}'`
|
|
alternate_form: bool,
|
|
// Ex) `30` in `'{:<30}'`
|
|
width: Option<usize>,
|
|
// Ex) `,` in `'{:,}'`
|
|
grouping_option: Option<FormatGrouping>,
|
|
// Ex) `2` in `'{:.2}'`
|
|
precision: Option<usize>,
|
|
// Ex) `f` in `'{:+f}'`
|
|
format_type: Option<FormatType>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct DynamicFormatSpec {
|
|
// Ex) `x` and `y` in `'{:*{x},{y}b}'`
|
|
pub placeholders: Vec<FormatPart>,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Default)]
|
|
pub enum AllowPlaceholderNesting {
|
|
#[default]
|
|
Yes,
|
|
No,
|
|
AllowPlaceholderNesting,
|
|
}
|
|
|
|
fn get_num_digits(text: &str) -> usize {
|
|
for (index, character) in text.char_indices() {
|
|
if !character.is_ascii_digit() {
|
|
return index;
|
|
}
|
|
}
|
|
text.len()
|
|
}
|
|
|
|
fn parse_fill_and_align(text: &str) -> (Option<char>, Option<FormatAlign>, &str) {
|
|
let char_indices: Vec<(usize, char)> = text.char_indices().take(3).collect();
|
|
if char_indices.is_empty() {
|
|
(None, None, text)
|
|
} else if char_indices.len() == 1 {
|
|
let (maybe_align, remaining) = FormatAlign::parse(text);
|
|
(None, maybe_align, remaining)
|
|
} else {
|
|
let (maybe_align, remaining) = FormatAlign::parse(&text[char_indices[1].0..]);
|
|
if maybe_align.is_some() {
|
|
(Some(char_indices[0].1), maybe_align, remaining)
|
|
} else {
|
|
let (only_align, only_align_remaining) = FormatAlign::parse(text);
|
|
(None, only_align, only_align_remaining)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parse_number(text: &str) -> Result<(Option<usize>, &str), FormatSpecError> {
|
|
let num_digits: usize = get_num_digits(text);
|
|
if num_digits == 0 {
|
|
return Ok((None, text));
|
|
}
|
|
if let Ok(num) = text[..num_digits].parse::<usize>() {
|
|
Ok((Some(num), &text[num_digits..]))
|
|
} else {
|
|
// NOTE: this condition is different from CPython
|
|
Err(FormatSpecError::DecimalDigitsTooMany)
|
|
}
|
|
}
|
|
|
|
fn parse_alternate_form(text: &str) -> (bool, &str) {
|
|
let mut chars = text.chars();
|
|
match chars.next() {
|
|
Some('#') => (true, chars.as_str()),
|
|
_ => (false, text),
|
|
}
|
|
}
|
|
|
|
fn parse_zero(text: &str) -> (bool, &str) {
|
|
let mut chars = text.chars();
|
|
match chars.next() {
|
|
Some('0') => (true, chars.as_str()),
|
|
_ => (false, text),
|
|
}
|
|
}
|
|
|
|
fn parse_precision(text: &str) -> Result<(Option<usize>, &str), FormatSpecError> {
|
|
let mut chars = text.chars();
|
|
Ok(match chars.next() {
|
|
Some('.') => {
|
|
let (size, remaining) = parse_number(chars.as_str())?;
|
|
if let Some(size) = size {
|
|
if size > i32::MAX as usize {
|
|
return Err(FormatSpecError::PrecisionTooBig);
|
|
}
|
|
(Some(size), remaining)
|
|
} else {
|
|
(None, text)
|
|
}
|
|
}
|
|
_ => (None, text),
|
|
})
|
|
}
|
|
|
|
/// Parses a placeholder format part within a format specification
|
|
fn parse_nested_placeholder(text: &str) -> Result<Option<(FormatPart, &str)>, FormatSpecError> {
|
|
match FormatString::parse_spec(text, AllowPlaceholderNesting::No) {
|
|
// Not a nested placeholder, OK
|
|
Err(FormatParseError::MissingStartBracket) => Ok(None),
|
|
Err(err) => Err(FormatSpecError::InvalidPlaceholder(err)),
|
|
Ok((format_part, text)) => Ok(Some((format_part, text))),
|
|
}
|
|
}
|
|
|
|
/// Parse all placeholders in a format specification
|
|
/// If no placeholders are present, an empty vector will be returned
|
|
fn parse_nested_placeholders(mut text: &str) -> Result<Vec<FormatPart>, FormatSpecError> {
|
|
let mut placeholders = vec![];
|
|
while let Some(bracket) = text.find('{') {
|
|
if let Some((format_part, rest)) = parse_nested_placeholder(&text[bracket..])? {
|
|
text = rest;
|
|
placeholders.push(format_part);
|
|
} else {
|
|
text = &text[bracket + 1..];
|
|
}
|
|
}
|
|
Ok(placeholders)
|
|
}
|
|
|
|
impl FormatSpec {
|
|
pub fn parse(text: &str) -> Result<Self, FormatSpecError> {
|
|
let placeholders = parse_nested_placeholders(text)?;
|
|
if !placeholders.is_empty() {
|
|
return Ok(FormatSpec::Dynamic(DynamicFormatSpec { placeholders }));
|
|
}
|
|
|
|
let (conversion, text) = FormatConversion::parse(text);
|
|
let (mut fill, mut align, text) = parse_fill_and_align(text);
|
|
let (sign, text) = FormatSign::parse(text);
|
|
let (alternate_form, text) = parse_alternate_form(text);
|
|
let (zero, text) = parse_zero(text);
|
|
let (width, text) = parse_number(text)?;
|
|
let (grouping_option, text) = FormatGrouping::parse(text);
|
|
let (precision, text) = parse_precision(text)?;
|
|
|
|
let (format_type, _text) = if text.is_empty() {
|
|
(None, text)
|
|
} else {
|
|
// If there's any remaining text, we should yield a valid format type and consume it
|
|
// all.
|
|
let (format_type, text) = FormatType::parse(text);
|
|
if format_type.is_none() {
|
|
return Err(FormatSpecError::InvalidFormatType);
|
|
}
|
|
if !text.is_empty() {
|
|
return Err(FormatSpecError::InvalidFormatSpecifier);
|
|
}
|
|
(format_type, text)
|
|
};
|
|
|
|
if zero && fill.is_none() {
|
|
fill.replace('0');
|
|
align = align.or(Some(FormatAlign::AfterSign));
|
|
}
|
|
|
|
Ok(FormatSpec::Static(StaticFormatSpec {
|
|
conversion,
|
|
fill,
|
|
align,
|
|
sign,
|
|
alternate_form,
|
|
width,
|
|
grouping_option,
|
|
precision,
|
|
format_type,
|
|
}))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FormatSpecError {
|
|
DecimalDigitsTooMany,
|
|
PrecisionTooBig,
|
|
InvalidFormatSpecifier,
|
|
InvalidFormatType,
|
|
InvalidPlaceholder(FormatParseError),
|
|
PlaceholderRecursionExceeded,
|
|
UnspecifiedFormat(char, char),
|
|
UnknownFormatCode(char, &'static str),
|
|
PrecisionNotAllowed,
|
|
NotAllowed(&'static str),
|
|
UnableToConvert,
|
|
CodeNotInRange,
|
|
NotImplemented(char, &'static str),
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FormatParseError {
|
|
UnmatchedBracket,
|
|
MissingStartBracket,
|
|
UnescapedStartBracketInLiteral,
|
|
PlaceholderRecursionExceeded,
|
|
UnknownConversion,
|
|
EmptyAttribute,
|
|
MissingRightBracket,
|
|
InvalidCharacterAfterRightBracket,
|
|
}
|
|
|
|
impl std::fmt::Display for FormatParseError {
|
|
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::UnmatchedBracket => {
|
|
std::write!(fmt, "unmatched bracket in format string")
|
|
}
|
|
Self::MissingStartBracket => {
|
|
std::write!(fmt, "missing start bracket in format string")
|
|
}
|
|
Self::UnescapedStartBracketInLiteral => {
|
|
std::write!(fmt, "unescaped start bracket in literal")
|
|
}
|
|
Self::PlaceholderRecursionExceeded => {
|
|
std::write!(fmt, "multiply nested placeholder not allowed")
|
|
}
|
|
Self::UnknownConversion => {
|
|
std::write!(fmt, "unknown conversion")
|
|
}
|
|
Self::EmptyAttribute => {
|
|
std::write!(fmt, "empty attribute")
|
|
}
|
|
Self::MissingRightBracket => {
|
|
std::write!(fmt, "missing right bracket")
|
|
}
|
|
Self::InvalidCharacterAfterRightBracket => {
|
|
std::write!(fmt, "invalid character after right bracket")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for FormatParseError {}
|
|
|
|
impl FromStr for FormatSpec {
|
|
type Err = FormatSpecError;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
FormatSpec::parse(s)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FieldNamePart {
|
|
Attribute(String),
|
|
Index(usize),
|
|
StringIndex(String),
|
|
}
|
|
|
|
impl FieldNamePart {
|
|
fn parse_part(
|
|
chars: &mut impl PeekingNext<Item = char>,
|
|
) -> Result<Option<FieldNamePart>, FormatParseError> {
|
|
chars
|
|
.next()
|
|
.map(|ch| match ch {
|
|
'.' => {
|
|
let mut attribute = String::new();
|
|
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
|
|
attribute.push(ch);
|
|
}
|
|
if attribute.is_empty() {
|
|
Err(FormatParseError::EmptyAttribute)
|
|
} else {
|
|
Ok(FieldNamePart::Attribute(attribute))
|
|
}
|
|
}
|
|
'[' => {
|
|
let mut index = String::new();
|
|
for ch in chars {
|
|
if ch == ']' {
|
|
return if index.is_empty() {
|
|
Err(FormatParseError::EmptyAttribute)
|
|
} else if let Ok(index) = index.parse::<usize>() {
|
|
Ok(FieldNamePart::Index(index))
|
|
} else {
|
|
Ok(FieldNamePart::StringIndex(index))
|
|
};
|
|
}
|
|
index.push(ch);
|
|
}
|
|
Err(FormatParseError::MissingRightBracket)
|
|
}
|
|
_ => Err(FormatParseError::InvalidCharacterAfterRightBracket),
|
|
})
|
|
.transpose()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FieldType {
|
|
Auto,
|
|
Index(usize),
|
|
Keyword(String),
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct FieldName {
|
|
pub field_type: FieldType,
|
|
pub parts: Vec<FieldNamePart>,
|
|
}
|
|
|
|
impl FieldName {
|
|
pub fn parse(text: &str) -> Result<FieldName, FormatParseError> {
|
|
let mut chars = text.chars().peekable();
|
|
let mut first = String::new();
|
|
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
|
|
first.push(ch);
|
|
}
|
|
|
|
let field_type = if first.is_empty() {
|
|
FieldType::Auto
|
|
} else if let Ok(index) = first.parse::<usize>() {
|
|
FieldType::Index(index)
|
|
} else {
|
|
FieldType::Keyword(first)
|
|
};
|
|
|
|
let mut parts = Vec::new();
|
|
while let Some(part) = FieldNamePart::parse_part(&mut chars)? {
|
|
parts.push(part);
|
|
}
|
|
|
|
Ok(FieldName { field_type, parts })
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum FormatPart {
|
|
Field {
|
|
field_name: String,
|
|
conversion_spec: Option<char>,
|
|
format_spec: String,
|
|
},
|
|
Literal(String),
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct FormatString {
|
|
pub format_parts: Vec<FormatPart>,
|
|
}
|
|
|
|
impl FormatString {
|
|
fn parse_literal_single(text: &str) -> Result<(char, &str), FormatParseError> {
|
|
let mut chars = text.chars();
|
|
// This should never be called with an empty str
|
|
let first_char = chars.next().unwrap();
|
|
// isn't this detectable only with bytes operation?
|
|
if first_char == '{' || first_char == '}' {
|
|
let maybe_next_char = chars.next();
|
|
// if we see a bracket, it has to be escaped by doubling up to be in a literal
|
|
return if maybe_next_char.is_none() || maybe_next_char.unwrap() != first_char {
|
|
Err(FormatParseError::UnescapedStartBracketInLiteral)
|
|
} else {
|
|
Ok((first_char, chars.as_str()))
|
|
};
|
|
}
|
|
Ok((first_char, chars.as_str()))
|
|
}
|
|
|
|
fn parse_literal(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
|
|
let mut cur_text = text;
|
|
let mut result_string = String::new();
|
|
let mut pending_escape = false;
|
|
while !cur_text.is_empty() {
|
|
if pending_escape
|
|
&& let Some((unicode_string, remaining)) =
|
|
FormatString::parse_escaped_unicode_string(cur_text)
|
|
{
|
|
result_string.push_str(unicode_string);
|
|
cur_text = remaining;
|
|
pending_escape = false;
|
|
continue;
|
|
}
|
|
|
|
match FormatString::parse_literal_single(cur_text) {
|
|
Ok((next_char, remaining)) => {
|
|
result_string.push(next_char);
|
|
cur_text = remaining;
|
|
pending_escape = next_char == '\\' && !pending_escape;
|
|
}
|
|
Err(err) => {
|
|
return if result_string.is_empty() {
|
|
Err(err)
|
|
} else {
|
|
Ok((FormatPart::Literal(result_string), cur_text))
|
|
};
|
|
}
|
|
}
|
|
}
|
|
Ok((FormatPart::Literal(result_string), ""))
|
|
}
|
|
|
|
fn parse_part_in_brackets(text: &str) -> Result<FormatPart, FormatParseError> {
|
|
let parts: Vec<&str> = text.splitn(2, ':').collect();
|
|
// before the comma is a keyword or arg index, after the comma is maybe a spec.
|
|
let arg_part = parts[0];
|
|
|
|
let format_spec = if parts.len() > 1 {
|
|
parts[1].to_owned()
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
// On parts[0] can still be the conversion (!r, !s, !a)
|
|
let parts: Vec<&str> = arg_part.splitn(2, '!').collect();
|
|
// before the bang is a keyword or arg index, after the comma is maybe a conversion spec.
|
|
let arg_part = parts[0];
|
|
|
|
let conversion_spec = parts
|
|
.get(1)
|
|
.map(|conversion| {
|
|
// conversions are only every one character
|
|
conversion
|
|
.chars()
|
|
.exactly_one()
|
|
.map_err(|_| FormatParseError::UnknownConversion)
|
|
})
|
|
.transpose()?;
|
|
|
|
Ok(FormatPart::Field {
|
|
field_name: arg_part.to_owned(),
|
|
conversion_spec,
|
|
format_spec,
|
|
})
|
|
}
|
|
|
|
fn parse_spec(
|
|
text: &str,
|
|
allow_nesting: AllowPlaceholderNesting,
|
|
) -> Result<(FormatPart, &str), FormatParseError> {
|
|
let Some(text) = text.strip_prefix('{') else {
|
|
return Err(FormatParseError::MissingStartBracket);
|
|
};
|
|
|
|
let mut nested = false;
|
|
let mut left = String::new();
|
|
|
|
for (idx, c) in text.char_indices() {
|
|
if c == '{' {
|
|
// There may be one layer nesting brackets in spec
|
|
if nested || allow_nesting == AllowPlaceholderNesting::No {
|
|
return Err(FormatParseError::PlaceholderRecursionExceeded);
|
|
}
|
|
nested = true;
|
|
left.push(c);
|
|
continue;
|
|
} else if c == '}' {
|
|
if nested {
|
|
nested = false;
|
|
left.push(c);
|
|
continue;
|
|
}
|
|
let (_, right) = text.split_at(idx + 1);
|
|
let format_part = FormatString::parse_part_in_brackets(&left)?;
|
|
return Ok((format_part, right));
|
|
}
|
|
left.push(c);
|
|
}
|
|
Err(FormatParseError::UnmatchedBracket)
|
|
}
|
|
|
|
fn parse_escaped_unicode_string(text: &str) -> Option<(&str, &str)> {
|
|
text.strip_prefix("N{")?.find('}').map(|idx| {
|
|
let end_idx = idx + 3; // 3 for "N{"
|
|
(&text[..end_idx], &text[end_idx..])
|
|
})
|
|
}
|
|
}
|
|
|
|
pub trait FromTemplate<'a>: Sized {
|
|
type Err;
|
|
fn from_str(s: &'a str) -> Result<Self, Self::Err>;
|
|
}
|
|
|
|
impl<'a> FromTemplate<'a> for FormatString {
|
|
type Err = FormatParseError;
|
|
|
|
fn from_str(text: &'a str) -> Result<Self, Self::Err> {
|
|
let mut cur_text: &str = text;
|
|
let mut parts: Vec<FormatPart> = Vec::new();
|
|
while !cur_text.is_empty() {
|
|
// Try to parse both literals and bracketed format parts until we
|
|
// run out of text
|
|
cur_text = FormatString::parse_literal(cur_text)
|
|
.or_else(|_| FormatString::parse_spec(cur_text, AllowPlaceholderNesting::Yes))
|
|
.map(|(part, new_text)| {
|
|
parts.push(part);
|
|
new_text
|
|
})?;
|
|
}
|
|
Ok(FormatString {
|
|
format_parts: parts,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_fill_and_align() {
|
|
assert_eq!(
|
|
parse_fill_and_align(" <"),
|
|
(Some(' '), Some(FormatAlign::Left), "")
|
|
);
|
|
assert_eq!(
|
|
parse_fill_and_align(" <22"),
|
|
(Some(' '), Some(FormatAlign::Left), "22")
|
|
);
|
|
assert_eq!(
|
|
parse_fill_and_align("<22"),
|
|
(None, Some(FormatAlign::Left), "22")
|
|
);
|
|
assert_eq!(
|
|
parse_fill_and_align(" ^^"),
|
|
(Some(' '), Some(FormatAlign::Center), "^")
|
|
);
|
|
assert_eq!(
|
|
parse_fill_and_align("==="),
|
|
(Some('='), Some(FormatAlign::AfterSign), "=")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_width_only() {
|
|
let expected = Ok(FormatSpec::Static(StaticFormatSpec {
|
|
conversion: None,
|
|
fill: None,
|
|
align: None,
|
|
sign: None,
|
|
alternate_form: false,
|
|
width: Some(33),
|
|
grouping_option: None,
|
|
precision: None,
|
|
format_type: None,
|
|
}));
|
|
assert_eq!(FormatSpec::parse("33"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fill_and_width() {
|
|
let expected = Ok(FormatSpec::Static(StaticFormatSpec {
|
|
conversion: None,
|
|
fill: Some('<'),
|
|
align: Some(FormatAlign::Right),
|
|
sign: None,
|
|
alternate_form: false,
|
|
width: Some(33),
|
|
grouping_option: None,
|
|
precision: None,
|
|
format_type: None,
|
|
}));
|
|
assert_eq!(FormatSpec::parse("<>33"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_part() {
|
|
let expected = Ok(FormatSpec::Dynamic(DynamicFormatSpec {
|
|
placeholders: vec![FormatPart::Field {
|
|
field_name: "x".to_string(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
}],
|
|
}));
|
|
assert_eq!(FormatSpec::parse("{x}"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dynamic_format_spec() {
|
|
let expected = Ok(FormatSpec::Dynamic(DynamicFormatSpec {
|
|
placeholders: vec![
|
|
FormatPart::Field {
|
|
field_name: "x".to_string(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
},
|
|
FormatPart::Field {
|
|
field_name: "y".to_string(),
|
|
conversion_spec: None,
|
|
format_spec: "<2".to_string(),
|
|
},
|
|
FormatPart::Field {
|
|
field_name: "z".to_string(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
},
|
|
],
|
|
}));
|
|
assert_eq!(FormatSpec::parse("{x}{y:<2}{z}"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_dynamic_format_spec_with_others() {
|
|
let expected = Ok(FormatSpec::Dynamic(DynamicFormatSpec {
|
|
placeholders: vec![FormatPart::Field {
|
|
field_name: "x".to_string(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
}],
|
|
}));
|
|
assert_eq!(FormatSpec::parse("<{x}20b"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_all() {
|
|
let expected = Ok(FormatSpec::Static(StaticFormatSpec {
|
|
conversion: None,
|
|
fill: Some('<'),
|
|
align: Some(FormatAlign::Right),
|
|
sign: Some(FormatSign::Minus),
|
|
alternate_form: true,
|
|
width: Some(23),
|
|
grouping_option: Some(FormatGrouping::Comma),
|
|
precision: Some(11),
|
|
format_type: Some(FormatType::Binary),
|
|
}));
|
|
assert_eq!(FormatSpec::parse("<>-#23,.11b"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_parse() {
|
|
let expected = Ok(FormatString {
|
|
format_parts: vec![
|
|
FormatPart::Literal("abcd".to_owned()),
|
|
FormatPart::Field {
|
|
field_name: "1".to_owned(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
},
|
|
FormatPart::Literal(":".to_owned()),
|
|
FormatPart::Field {
|
|
field_name: "key".to_owned(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
},
|
|
],
|
|
});
|
|
|
|
assert_eq!(FormatString::from_str("abcd{1}:{key}"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_parse_nested_placeholder() {
|
|
let expected = Ok(FormatString {
|
|
format_parts: vec![
|
|
FormatPart::Literal("abcd".to_owned()),
|
|
FormatPart::Field {
|
|
field_name: "1".to_owned(),
|
|
conversion_spec: None,
|
|
format_spec: "{a}".to_owned(),
|
|
},
|
|
],
|
|
});
|
|
|
|
assert_eq!(FormatString::from_str("abcd{1:{a}}"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_parse_multi_byte_char() {
|
|
assert!(FormatString::from_str("{a:%ЫйЯЧ}").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_parse_fail() {
|
|
assert_eq!(
|
|
FormatString::from_str("{s"),
|
|
Err(FormatParseError::UnmatchedBracket)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_parse_escape() {
|
|
let expected = Ok(FormatString {
|
|
format_parts: vec![
|
|
FormatPart::Literal("{".to_owned()),
|
|
FormatPart::Field {
|
|
field_name: "key".to_owned(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
},
|
|
FormatPart::Literal("}ddfe".to_owned()),
|
|
],
|
|
});
|
|
|
|
assert_eq!(FormatString::from_str("{{{key}}}ddfe"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_invalid_specification() {
|
|
assert_eq!(
|
|
FormatSpec::parse("%3"),
|
|
Err(FormatSpecError::InvalidFormatSpecifier)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse(".2fa"),
|
|
Err(FormatSpecError::InvalidFormatSpecifier)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("ds"),
|
|
Err(FormatSpecError::InvalidFormatSpecifier)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("x+"),
|
|
Err(FormatSpecError::InvalidFormatSpecifier)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("b4"),
|
|
Err(FormatSpecError::InvalidFormatSpecifier)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("o!"),
|
|
Err(FormatSpecError::InvalidFormatSpecifier)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("{"),
|
|
Err(FormatSpecError::InvalidPlaceholder(
|
|
FormatParseError::UnmatchedBracket
|
|
))
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("{x"),
|
|
Err(FormatSpecError::InvalidPlaceholder(
|
|
FormatParseError::UnmatchedBracket
|
|
))
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("}"),
|
|
Err(FormatSpecError::InvalidFormatType)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("{}}"),
|
|
// Note this should be an `InvalidFormatType` but we give up
|
|
// on all other parsing validation when we see a placeholder
|
|
Ok(FormatSpec::Dynamic(DynamicFormatSpec {
|
|
placeholders: vec![FormatPart::Field {
|
|
field_name: String::new(),
|
|
conversion_spec: None,
|
|
format_spec: String::new()
|
|
}]
|
|
}))
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("{{x}}"),
|
|
Err(FormatSpecError::InvalidPlaceholder(
|
|
FormatParseError::PlaceholderRecursionExceeded
|
|
))
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("d "),
|
|
Err(FormatSpecError::InvalidFormatSpecifier)
|
|
);
|
|
assert_eq!(
|
|
FormatSpec::parse("z"),
|
|
Err(FormatSpecError::InvalidFormatType)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_field_name() {
|
|
assert_eq!(
|
|
FieldName::parse(""),
|
|
Ok(FieldName {
|
|
field_type: FieldType::Auto,
|
|
parts: Vec::new(),
|
|
})
|
|
);
|
|
assert_eq!(
|
|
FieldName::parse("0"),
|
|
Ok(FieldName {
|
|
field_type: FieldType::Index(0),
|
|
parts: Vec::new(),
|
|
})
|
|
);
|
|
assert_eq!(
|
|
FieldName::parse("key"),
|
|
Ok(FieldName {
|
|
field_type: FieldType::Keyword("key".to_owned()),
|
|
parts: Vec::new(),
|
|
})
|
|
);
|
|
assert_eq!(
|
|
FieldName::parse("key.attr[0][string]"),
|
|
Ok(FieldName {
|
|
field_type: FieldType::Keyword("key".to_owned()),
|
|
parts: vec![
|
|
FieldNamePart::Attribute("attr".to_owned()),
|
|
FieldNamePart::Index(0),
|
|
FieldNamePart::StringIndex("string".to_owned())
|
|
],
|
|
})
|
|
);
|
|
assert_eq!(
|
|
FieldName::parse("key.."),
|
|
Err(FormatParseError::EmptyAttribute)
|
|
);
|
|
assert_eq!(
|
|
FieldName::parse("key[]"),
|
|
Err(FormatParseError::EmptyAttribute)
|
|
);
|
|
assert_eq!(
|
|
FieldName::parse("key["),
|
|
Err(FormatParseError::MissingRightBracket)
|
|
);
|
|
assert_eq!(
|
|
FieldName::parse("key[0]after"),
|
|
Err(FormatParseError::InvalidCharacterAfterRightBracket)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_unicode_escape() {
|
|
let expected = Ok(FormatString {
|
|
format_parts: vec![FormatPart::Literal("I am a \\N{snowman}".to_owned())],
|
|
});
|
|
|
|
assert_eq!(FormatString::from_str("I am a \\N{snowman}"), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_unicode_escape_with_field() {
|
|
let expected = Ok(FormatString {
|
|
format_parts: vec![
|
|
FormatPart::Literal("I am a \\N{snowman}".to_owned()),
|
|
FormatPart::Field {
|
|
field_name: "snowman".to_owned(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
},
|
|
],
|
|
});
|
|
|
|
assert_eq!(
|
|
FormatString::from_str("I am a \\N{snowman}{snowman}"),
|
|
expected
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_multiple_escape_with_field() {
|
|
let expected = Ok(FormatString {
|
|
format_parts: vec![
|
|
FormatPart::Literal("I am a \\\\N".to_owned()),
|
|
FormatPart::Field {
|
|
field_name: "snowman".to_owned(),
|
|
conversion_spec: None,
|
|
format_spec: String::new(),
|
|
},
|
|
],
|
|
});
|
|
|
|
assert_eq!(FormatString::from_str("I am a \\\\N{snowman}"), expected);
|
|
}
|
|
}
|