Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "handlebars"
|
||||
edition = "2024"
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
warpui.workspace = true
|
||||
@@ -0,0 +1,90 @@
|
||||
pub mod parser;
|
||||
|
||||
use parser::{ParsedArgumentResult, ParsedArgumentsIterator};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub fn get_arguments(template: &str) -> Vec<String> {
|
||||
let mut char_to_byte: Vec<usize> = template
|
||||
.char_indices()
|
||||
.map(|(byte_idx, _)| byte_idx)
|
||||
.collect();
|
||||
char_to_byte.push(template.len());
|
||||
|
||||
ParsedArgumentsIterator::new(template.chars())
|
||||
.filter_map(|parsed| {
|
||||
if let ParsedArgumentResult::Valid { .. } = parsed.result() {
|
||||
let name_range = parsed.chars_range();
|
||||
|
||||
if name_range.start >= 2 {
|
||||
let name_start_byte = char_to_byte[name_range.start];
|
||||
let name_end_byte = char_to_byte[name_range.end];
|
||||
let name = template[name_start_byte..name_end_byte].to_string();
|
||||
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn render_template(template: &str, context: &HashMap<String, String>) -> String {
|
||||
// Map char indices to byte indices for slicing.
|
||||
let mut char_to_byte: Vec<usize> = template
|
||||
.char_indices()
|
||||
.map(|(byte_idx, _)| byte_idx)
|
||||
.collect();
|
||||
char_to_byte.push(template.len());
|
||||
|
||||
let mut out = String::with_capacity(template.len());
|
||||
let mut cursor_byte = 0usize;
|
||||
|
||||
for parsed in ParsedArgumentsIterator::new(template.chars()) {
|
||||
if let ParsedArgumentResult::Valid { .. } = parsed.result() {
|
||||
let name_range = parsed.chars_range();
|
||||
// The iterator yields only valid unescaped args without whitespace; the
|
||||
// braces are not included in the returned range. For valid args, there
|
||||
// must be exactly two braces on each side.
|
||||
if name_range.start >= 2 {
|
||||
let placeholder_start_char = name_range.start - 2;
|
||||
let placeholder_end_char = name_range.end + 2; // exclusive
|
||||
|
||||
let start_byte = char_to_byte[placeholder_start_char];
|
||||
let name_start_byte = char_to_byte[name_range.start];
|
||||
let name_end_byte = char_to_byte[name_range.end];
|
||||
let end_byte = char_to_byte[placeholder_end_char];
|
||||
|
||||
// Append unchanged prefix
|
||||
if cursor_byte < start_byte {
|
||||
out.push_str(&template[cursor_byte..start_byte]);
|
||||
}
|
||||
|
||||
let var_name = &template[name_start_byte..name_end_byte];
|
||||
if let Some(value) = context.get(var_name) {
|
||||
out.push_str(value);
|
||||
} else {
|
||||
// If no value provided, keep original placeholder
|
||||
out.push_str(&template[start_byte..end_byte]);
|
||||
}
|
||||
|
||||
cursor_byte = end_byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append suffix
|
||||
if cursor_byte < template.len() {
|
||||
out.push_str(&template[cursor_byte..]);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "lib_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use super::{get_arguments, render_template};
|
||||
|
||||
fn create_map(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_simple_substitution() {
|
||||
let template = "Hello, {{name}}!".to_string();
|
||||
let context = create_map(&[("name", "Warp")]);
|
||||
|
||||
let args = get_arguments(&template);
|
||||
assert_eq!(args, vec!["name".to_string()]);
|
||||
|
||||
let out = render_template(&template, &context);
|
||||
assert_eq!(out, "Hello, Warp!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_unknown_placeholder_unchanged() {
|
||||
let template = "Hello, {{name}} and {{unknown}}!".to_string();
|
||||
let context = create_map(&[("name", "Warp")]);
|
||||
|
||||
let args = get_arguments(&template);
|
||||
assert_eq!(
|
||||
args.into_iter().collect::<HashSet<String>>(),
|
||||
HashSet::from(["name".to_string(), "unknown".to_string()])
|
||||
);
|
||||
|
||||
let out = render_template(&template, &context);
|
||||
assert_eq!(out, "Hello, Warp and {{unknown}}!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_and_repeated_arguments() {
|
||||
let template = "{{a}}-{{b}}-{{a}}";
|
||||
let context = create_map(&[("a", "X"), ("b", "Y")]);
|
||||
|
||||
let args = get_arguments(template);
|
||||
assert_eq!(
|
||||
args.into_iter().collect::<HashSet<String>>(),
|
||||
HashSet::from(["a".to_string(), "b".to_string()])
|
||||
);
|
||||
|
||||
let out = render_template(template, &context);
|
||||
assert_eq!(out, "X-Y-X");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_in_names_and_text() {
|
||||
// name contains multibyte chars
|
||||
let template = "前缀 {{ab東早}} 后缀";
|
||||
let context = create_map(&[("ab東早", "值")]);
|
||||
|
||||
let args = get_arguments(template);
|
||||
assert_eq!(args, vec!["ab東早".to_string()]);
|
||||
|
||||
let out = render_template(template, &context);
|
||||
assert_eq!(out, "前缀 值 后缀");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_escaped_triple_braces() {
|
||||
let template = "{{{name}}} {{name}}";
|
||||
let context = create_map(&[("name", "Warp")]);
|
||||
|
||||
let args = get_arguments(template);
|
||||
// Only the double-braced arg should be returned
|
||||
assert_eq!(args, vec!["name".to_string()]);
|
||||
|
||||
let out = render_template(template, &context);
|
||||
// Triple braces should not be substituted by our parser; double braces should.
|
||||
assert_eq!(out, "{{{name}}} Warp");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_replace_spaced_variants() {
|
||||
// Variants with spaces are considered invalid by the parser and should remain unchanged.
|
||||
let template = "A {{ name }} B {{name }} C {{ name}} D {{name}}";
|
||||
let context = create_map(&[("name", "ok")]);
|
||||
|
||||
let args = get_arguments(template);
|
||||
// Only the last {{name}} without spaces should be detected
|
||||
assert_eq!(args, vec!["name".to_string()]);
|
||||
|
||||
let out = render_template(template, &context);
|
||||
assert_eq!(out, "A {{ name }} B {{name }} C {{ name}} D ok");
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
use std::{
|
||||
iter::{Enumerate, Peekable},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ParsedArgument {
|
||||
chars_range: Range<usize>,
|
||||
result: ParsedArgumentResult,
|
||||
}
|
||||
|
||||
impl ParsedArgument {
|
||||
pub fn chars_range(&self) -> Range<usize> {
|
||||
self.chars_range.clone()
|
||||
}
|
||||
|
||||
pub fn result(&self) -> &ParsedArgumentResult {
|
||||
&self.result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ParsedArgumentResult {
|
||||
Valid { current_word_index: usize },
|
||||
Invalid,
|
||||
}
|
||||
|
||||
enum Symbol {
|
||||
DoubleOpeningBraces,
|
||||
DoubleClosingBraces,
|
||||
Whitespace,
|
||||
Character,
|
||||
}
|
||||
|
||||
fn parse_characters(character: char, next_character: Option<char>) -> Symbol {
|
||||
if double_opening_braces(character, next_character) {
|
||||
Symbol::DoubleOpeningBraces
|
||||
} else if double_closing_braces(character, next_character) {
|
||||
Symbol::DoubleClosingBraces
|
||||
} else if character.is_whitespace() {
|
||||
Symbol::Whitespace
|
||||
} else {
|
||||
Symbol::Character
|
||||
}
|
||||
}
|
||||
|
||||
enum ParserState {
|
||||
Word,
|
||||
/// Identifies when iterator is in a period of consecutive whitespace characters and
|
||||
/// argument is not open. (In an argument, this will get handled as an invalid character.)
|
||||
Whitespace,
|
||||
/// State when argument has started, as identified by double opening braces (`{{`).
|
||||
Argument {
|
||||
/// Start index, in characters. Points to first character of argument, not including braces.
|
||||
char_start_index: usize,
|
||||
/// `true` while only valid argument characters are seen.
|
||||
is_valid: bool,
|
||||
/// `true` if an argument begins with an escape sequence of three opening braces (`{{{`).
|
||||
is_escaped: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn valid_arg_character(character: char, is_first_char: bool) -> bool {
|
||||
if is_first_char {
|
||||
character.is_alphabetic() || character == '-' || character == '_'
|
||||
} else {
|
||||
character.is_alphanumeric() || character == '-' || character == '_'
|
||||
}
|
||||
}
|
||||
|
||||
fn double_opening_braces(character: char, next_character: Option<char>) -> bool {
|
||||
character == '{' && next_character == Some('{')
|
||||
}
|
||||
|
||||
fn double_closing_braces(character: char, next_character: Option<char>) -> bool {
|
||||
character == '}' && next_character == Some('}')
|
||||
}
|
||||
|
||||
fn current_is_first_char_in_argument(start_index: usize, character_index: usize) -> bool {
|
||||
start_index == character_index
|
||||
}
|
||||
|
||||
/// Iterator that parses through input chars, and returns the next `ParsedArgument` or None.
|
||||
/// The goal of this parser is to identify the locations of valid and invalid arguments
|
||||
/// by their start and end indexes, for a given command string. Valid arguments are also
|
||||
/// returned with their word index in the string, if needed for history diffs as in
|
||||
/// `ArgumentsState::from_string()`. Words are separated by whitespace, 2+ opening
|
||||
/// braces (`{`), or 2+ closing braces (`}`).
|
||||
///
|
||||
/// For an argument to be valid, it must start with `a-zA-Z_-` and be composed only of `a-zA-Z0-9_-`.
|
||||
/// (These rules are derived from POSIX naming conventions.)
|
||||
///
|
||||
/// The iterator implementation steps through each character and identifies if it is the start or end
|
||||
/// of an argument/word. Once an argument is closed (whether valid or invalid), it is returned with its
|
||||
/// start index, end index, and validity result (`ParsedArgumentResult` enum) in a `ParsedArgument` object.
|
||||
pub struct ParsedArgumentsIterator<I>
|
||||
where
|
||||
I: IntoIterator,
|
||||
{
|
||||
/// Iterator holding chars of passed command string (expects `Chars`).
|
||||
char_iter: Peekable<Enumerate<I::IntoIter>>,
|
||||
parser_state: ParserState,
|
||||
/// Number of words seen. Count includes valid/invalid arguments; increments when separator
|
||||
/// character is encountered (whitespace, 2+ opening braces (`{`), or 2+ closing braces (`}`)).
|
||||
word_count: usize,
|
||||
}
|
||||
|
||||
impl<I> ParsedArgumentsIterator<I>
|
||||
where
|
||||
I: IntoIterator<Item = char>,
|
||||
{
|
||||
pub fn new(string_chars: I) -> Self {
|
||||
Self {
|
||||
char_iter: string_chars.into_iter().enumerate().peekable(),
|
||||
parser_state: ParserState::Word,
|
||||
word_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_is_none_or_whitespace(&mut self) -> bool {
|
||||
match self.char_iter.peek() {
|
||||
Some((_, character)) => character.is_whitespace(),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_argument(&mut self, argument_name_start_index: usize, is_escaped: bool) {
|
||||
self.parser_state = ParserState::Argument {
|
||||
char_start_index: argument_name_start_index,
|
||||
is_valid: true,
|
||||
is_escaped,
|
||||
};
|
||||
}
|
||||
|
||||
fn parsed_argument(
|
||||
&self,
|
||||
start_index: usize,
|
||||
current_index: usize,
|
||||
is_valid: bool,
|
||||
) -> Option<ParsedArgument> {
|
||||
let argument_name_empty = current_is_first_char_in_argument(start_index, current_index);
|
||||
|
||||
if !argument_name_empty {
|
||||
let chars_range = start_index..current_index;
|
||||
|
||||
let result = if is_valid {
|
||||
ParsedArgumentResult::Valid {
|
||||
current_word_index: self.word_count - 1,
|
||||
}
|
||||
} else {
|
||||
ParsedArgumentResult::Invalid
|
||||
};
|
||||
|
||||
Some(ParsedArgument {
|
||||
chars_range,
|
||||
result,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls `next()` on `self.char_iter` until `peek()` returns None or not `repeating_character`
|
||||
fn iterate_until_next_is_not(&mut self, repeating_character: char) -> i32 {
|
||||
let mut chars_skipped = 0;
|
||||
while let Some(_) = self.char_iter.next() {
|
||||
chars_skipped += 1;
|
||||
if let Some((_, character)) = self.char_iter.peek()
|
||||
&& *character != repeating_character
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
chars_skipped
|
||||
}
|
||||
|
||||
pub fn word_count(&self) -> usize {
|
||||
self.word_count
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for ParsedArgumentsIterator<I>
|
||||
where
|
||||
I: IntoIterator<Item = char>,
|
||||
{
|
||||
type Item = ParsedArgument;
|
||||
|
||||
/// Returns next `ParsedArgument` item or None if no more can be found. An argument starts with
|
||||
/// two opening braces `{{` and finishes with two closing braces `}}`. If every character inside
|
||||
/// these braces is valid, the argument is deemed valid and its result is `ParsedArgument::Valid`.
|
||||
/// If not, the argument is invalid and its result is `ParsedArgument::Invalid`.
|
||||
///
|
||||
/// The implementation calls next() on the `char_iter` iterator to obtain the next character and its
|
||||
/// index in the string. This continues until an argument is found (or `char_iter` runs out of chars).
|
||||
/// The match statement handles:
|
||||
/// (1) DoubleOpeningBraces: start arguments
|
||||
/// (2) DoubleClosingBraces: ends arguments, will return an argument if found
|
||||
/// (3) Character: if argument in process, check for validity. Else, check if it was
|
||||
/// preceded by whitespace (and if so, increment word count).
|
||||
/// (4) Whitespace: invalid argument character
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let (character_index, character) = self.char_iter.next()?;
|
||||
let next_character = self.char_iter.peek().map(|(_, next_char)| *next_char);
|
||||
|
||||
let parsed_symbol = parse_characters(character, next_character);
|
||||
let whitespace_ending = matches!(self.parser_state, ParserState::Whitespace)
|
||||
&& !matches!(parsed_symbol, Symbol::Whitespace);
|
||||
|
||||
match parsed_symbol {
|
||||
Symbol::DoubleOpeningBraces => {
|
||||
self.word_count += 1;
|
||||
|
||||
// If we consumed more than one extra open bracket, we've entered a potential escape scenario (three brackets indicate arg escape)
|
||||
let is_escaped_open = self.iterate_until_next_is_not('{') > 1;
|
||||
|
||||
let argument_name_start_index = self.char_iter.peek().map(|(index, _)| *index);
|
||||
if let Some(start_index) = argument_name_start_index {
|
||||
self.start_argument(start_index, is_escaped_open);
|
||||
}
|
||||
}
|
||||
Symbol::DoubleClosingBraces => {
|
||||
let parsed_argument = match self.parser_state {
|
||||
ParserState::Argument {
|
||||
char_start_index,
|
||||
is_valid,
|
||||
..
|
||||
} => self.parsed_argument(char_start_index, character_index, is_valid),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// If we end by consuming more than one extra close bracket, and we started the argument with an escape open sequence, we should escape this argument
|
||||
let is_escape_closed = self.iterate_until_next_is_not('}') > 1;
|
||||
let is_escaped = match self.parser_state {
|
||||
ParserState::Argument {
|
||||
is_escaped: is_escaped_open,
|
||||
..
|
||||
} => is_escaped_open && is_escape_closed,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if self.peek_is_none_or_whitespace() {
|
||||
self.parser_state = ParserState::Whitespace;
|
||||
} else {
|
||||
// If encountered }}e, `e` starts a new word
|
||||
self.word_count += 1;
|
||||
self.parser_state = ParserState::Word;
|
||||
}
|
||||
|
||||
if parsed_argument.is_some() && !is_escaped {
|
||||
return parsed_argument;
|
||||
}
|
||||
}
|
||||
Symbol::Character => match self.parser_state {
|
||||
ParserState::Argument {
|
||||
char_start_index,
|
||||
is_escaped,
|
||||
..
|
||||
} => {
|
||||
if !valid_arg_character(
|
||||
character,
|
||||
current_is_first_char_in_argument(char_start_index, character_index),
|
||||
) {
|
||||
self.parser_state = ParserState::Argument {
|
||||
char_start_index,
|
||||
is_valid: false,
|
||||
is_escaped,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// At least one non-whitespace char guarantees that at least 1 word exists
|
||||
// If whitespace ending, means new word seen
|
||||
if self.word_count == 0 || whitespace_ending {
|
||||
self.word_count += 1;
|
||||
}
|
||||
self.parser_state = ParserState::Word;
|
||||
}
|
||||
},
|
||||
Symbol::Whitespace => match self.parser_state {
|
||||
ParserState::Argument {
|
||||
char_start_index,
|
||||
is_escaped,
|
||||
..
|
||||
} => {
|
||||
self.parser_state = ParserState::Argument {
|
||||
char_start_index,
|
||||
is_valid: false,
|
||||
is_escaped,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
self.parser_state = ParserState::Whitespace;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "parser_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,133 @@
|
||||
use warpui::App;
|
||||
|
||||
use crate::parser::{ParsedArgument, ParsedArgumentResult, ParsedArgumentsIterator};
|
||||
|
||||
#[test]
|
||||
fn test_parsed_arguments_iterator() {
|
||||
App::test((), |_app| async move {
|
||||
let mut args_iterator = ParsedArgumentsIterator::new(
|
||||
"one two{{three}} {{four}}{{ab東早}} \ne{{восибing}}}".chars(),
|
||||
);
|
||||
|
||||
let arg_three = args_iterator.next();
|
||||
assert_eq!(
|
||||
arg_three,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 9..14,
|
||||
result: ParsedArgumentResult::Valid {
|
||||
current_word_index: 2
|
||||
},
|
||||
})
|
||||
);
|
||||
let arg_four = args_iterator.next();
|
||||
assert_eq!(
|
||||
arg_four,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 19..23,
|
||||
result: ParsedArgumentResult::Valid {
|
||||
current_word_index: 3
|
||||
},
|
||||
})
|
||||
);
|
||||
let arg_ab = args_iterator.next();
|
||||
assert_eq!(
|
||||
arg_ab,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 27..31,
|
||||
result: ParsedArgumentResult::Valid {
|
||||
current_word_index: 5
|
||||
},
|
||||
})
|
||||
);
|
||||
let arg_vosibing = args_iterator.next();
|
||||
assert_eq!(
|
||||
arg_vosibing,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 38..46,
|
||||
result: ParsedArgumentResult::Valid {
|
||||
current_word_index: 7
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
let mut invalid_ranges_iter = ParsedArgumentsIterator::new(
|
||||
"one {{two}} {{inv\nal東旪!}} \n{{1malformed}} {{overlap{{bad }}".chars(),
|
||||
);
|
||||
|
||||
let arg_two = invalid_ranges_iter.next();
|
||||
assert_eq!(
|
||||
arg_two,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 6..9,
|
||||
result: ParsedArgumentResult::Valid {
|
||||
current_word_index: 1
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
let arg_invalid = invalid_ranges_iter.next();
|
||||
assert_eq!(
|
||||
arg_invalid,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 14..23,
|
||||
result: ParsedArgumentResult::Invalid,
|
||||
})
|
||||
);
|
||||
let arg_malformed = invalid_ranges_iter.next();
|
||||
assert_eq!(
|
||||
arg_malformed,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 29..39,
|
||||
result: ParsedArgumentResult::Invalid,
|
||||
})
|
||||
);
|
||||
let arg_bad = invalid_ranges_iter.next();
|
||||
assert_eq!(
|
||||
arg_bad,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 53..57,
|
||||
result: ParsedArgumentResult::Invalid,
|
||||
})
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parsed_arguments_iterator_with_escaped_args() {
|
||||
App::test((), |_app| async move {
|
||||
let mut escaped_ranges_iter = ParsedArgumentsIterator::new(
|
||||
"one {{TWO}} {{{.ID!}}} {{{{{not_3 an arg?}}} {{real_arg}} {{.invalid}}".chars(),
|
||||
);
|
||||
|
||||
let arg_two = escaped_ranges_iter.next();
|
||||
assert_eq!(
|
||||
arg_two,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 6..9,
|
||||
result: ParsedArgumentResult::Valid {
|
||||
current_word_index: 1
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let arg_real = escaped_ranges_iter.next();
|
||||
assert_eq!(
|
||||
arg_real,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 47..55,
|
||||
result: ParsedArgumentResult::Valid {
|
||||
current_word_index: 4
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let arg_invalid = escaped_ranges_iter.next();
|
||||
assert_eq!(
|
||||
arg_invalid,
|
||||
Some(ParsedArgument {
|
||||
chars_range: 60..68,
|
||||
result: ParsedArgumentResult::Invalid
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user