Move parse errors, add helper func for parse fail
This commit is contained in:
parent
b89381ac95
commit
447ff6931c
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::i18n::{tr_args, tr_strs, I18n, TR};
|
use crate::i18n::{tr_args, tr_strs, I18n, TR};
|
||||||
pub use failure::{Error, Fail};
|
pub use failure::{Error, Fail};
|
||||||
|
use nom::error::{ErrorKind as NomErrorKind, ParseError as NomParseError};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use std::{io, str::Utf8Error};
|
use std::{io, str::Utf8Error};
|
||||||
|
|
||||||
@ -327,3 +328,46 @@ pub enum DBErrorKind {
|
|||||||
Utf8,
|
Utf8,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ParseError<'a> {
|
||||||
|
Anki(&'a str, ParseErrorKind),
|
||||||
|
Nom(&'a str, NomErrorKind),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum ParseErrorKind {
|
||||||
|
MisplacedAnd,
|
||||||
|
MisplacedOr,
|
||||||
|
EmptyGroup,
|
||||||
|
EmptyQuote,
|
||||||
|
UnclosedQuote,
|
||||||
|
MissingKey,
|
||||||
|
UnknownEscape(String),
|
||||||
|
InvalidIdList,
|
||||||
|
InvalidState,
|
||||||
|
InvalidFlag,
|
||||||
|
InvalidAdded,
|
||||||
|
InvalidEdited,
|
||||||
|
InvalidRatedDays,
|
||||||
|
InvalidRatedEase,
|
||||||
|
InvalidDupesMid,
|
||||||
|
InvalidDupesText,
|
||||||
|
InvalidPropProperty,
|
||||||
|
InvalidPropOperator,
|
||||||
|
InvalidPropFloat,
|
||||||
|
InvalidPropInteger,
|
||||||
|
InvalidPropUnsigned,
|
||||||
|
InvalidDid,
|
||||||
|
InvalidMid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> NomParseError<&'a str> for ParseError<'a> {
|
||||||
|
fn from_error_kind(input: &'a str, kind: NomErrorKind) -> Self {
|
||||||
|
ParseError::Nom(input, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append(_: &str, _: NomErrorKind, other: Self) -> Self {
|
||||||
|
other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
decks::DeckID,
|
decks::DeckID,
|
||||||
err::{AnkiError, Result},
|
err::{AnkiError, ParseError, ParseErrorKind, Result},
|
||||||
notetype::NoteTypeID,
|
notetype::NoteTypeID,
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
@ -12,58 +12,19 @@ use nom::{
|
|||||||
bytes::complete::{escaped, is_not, tag},
|
bytes::complete::{escaped, is_not, tag},
|
||||||
character::complete::{anychar, char, none_of, one_of},
|
character::complete::{anychar, char, none_of, one_of},
|
||||||
combinator::{all_consuming, map, verify},
|
combinator::{all_consuming, map, verify},
|
||||||
error::{ErrorKind as NomErrorKind, ParseError as NomParseError},
|
error::ErrorKind as NomErrorKind,
|
||||||
multi::many0,
|
multi::many0,
|
||||||
sequence::{delimited, preceded, separated_pair},
|
sequence::{delimited, preceded, separated_pair},
|
||||||
Err::{Error, Failure},
|
|
||||||
};
|
};
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use ParseErrorKind::*;
|
||||||
#[derive(Debug)]
|
|
||||||
enum ParseError<'a> {
|
|
||||||
Anki(&'a str, ErrorKind),
|
|
||||||
Nom(&'a str, NomErrorKind),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum ErrorKind {
|
|
||||||
MisplacedAnd,
|
|
||||||
MisplacedOr,
|
|
||||||
EmptyGroup,
|
|
||||||
EmptyQuote,
|
|
||||||
UnclosedQuote,
|
|
||||||
MissingKey,
|
|
||||||
UnknownEscape(String),
|
|
||||||
InvalidIdList,
|
|
||||||
InvalidState,
|
|
||||||
InvalidFlag,
|
|
||||||
InvalidAdded,
|
|
||||||
InvalidEdited,
|
|
||||||
InvalidRatedDays,
|
|
||||||
InvalidRatedEase,
|
|
||||||
InvalidDupesMid,
|
|
||||||
InvalidDupesText,
|
|
||||||
InvalidPropProperty,
|
|
||||||
InvalidPropOperator,
|
|
||||||
InvalidPropFloat,
|
|
||||||
InvalidPropInteger,
|
|
||||||
InvalidPropUnsigned,
|
|
||||||
InvalidDid,
|
|
||||||
InvalidMid,
|
|
||||||
}
|
|
||||||
|
|
||||||
type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err<ParseError<'a>>>;
|
type IResult<'a, O> = std::result::Result<(&'a str, O), nom::Err<ParseError<'a>>>;
|
||||||
type ParseResult<'a, O> = std::result::Result<O, nom::Err<ParseError<'a>>>;
|
type ParseResult<'a, O> = std::result::Result<O, nom::Err<ParseError<'a>>>;
|
||||||
|
|
||||||
impl<'a> NomParseError<&'a str> for ParseError<'a> {
|
fn parse_failure(input: &str, kind: ParseErrorKind) -> nom::Err<ParseError<'_>> {
|
||||||
fn from_error_kind(input: &'a str, kind: NomErrorKind) -> Self {
|
nom::Err::Failure(ParseError::Anki(input, kind))
|
||||||
ParseError::Nom(input, kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn append(_: &str, _: NomErrorKind, other: Self) -> Self {
|
|
||||||
other
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@ -171,9 +132,9 @@ fn group_inner(input: &str) -> IResult<Vec<Node>> {
|
|||||||
// before adding the node, if the length is even then the node
|
// before adding the node, if the length is even then the node
|
||||||
// must not be a boolean
|
// must not be a boolean
|
||||||
if node == Node::And {
|
if node == Node::And {
|
||||||
return Err(Failure(ParseError::Anki(input, ErrorKind::MisplacedAnd)));
|
return Err(parse_failure(input, MisplacedAnd));
|
||||||
} else if node == Node::Or {
|
} else if node == Node::Or {
|
||||||
return Err(Failure(ParseError::Anki(input, ErrorKind::MisplacedOr)));
|
return Err(parse_failure(input, MisplacedOr));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if the length is odd, the next item must be a boolean. if it's
|
// if the length is odd, the next item must be a boolean. if it's
|
||||||
@ -193,11 +154,11 @@ fn group_inner(input: &str) -> IResult<Vec<Node>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nodes.is_empty() {
|
if nodes.is_empty() {
|
||||||
Err(Failure(ParseError::Anki(input, ErrorKind::EmptyGroup)))
|
Err(parse_failure(input, EmptyGroup))
|
||||||
} else if nodes.last().unwrap() == &Node::And {
|
} else if nodes.last().unwrap() == &Node::And {
|
||||||
Err(Failure(ParseError::Anki(input, ErrorKind::MisplacedAnd)))
|
Err(parse_failure(input, MisplacedAnd))
|
||||||
} else if nodes.last().unwrap() == &Node::Or {
|
} else if nodes.last().unwrap() == &Node::Or {
|
||||||
Err(Failure(ParseError::Anki(input, ErrorKind::MisplacedOr)))
|
Err(parse_failure(input, MisplacedOr))
|
||||||
} else {
|
} else {
|
||||||
// chomp any trailing whitespace
|
// chomp any trailing whitespace
|
||||||
let (remaining, _) = whitespace0(remaining)?;
|
let (remaining, _) = whitespace0(remaining)?;
|
||||||
@ -245,10 +206,7 @@ fn partially_quoted_term(s: &str) -> IResult<Node> {
|
|||||||
quoted_term_str,
|
quoted_term_str,
|
||||||
)(s)?;
|
)(s)?;
|
||||||
if key.is_empty() {
|
if key.is_empty() {
|
||||||
Err(nom::Err::Failure(ParseError::Anki(
|
Err(parse_failure(s, MissingKey))
|
||||||
s,
|
|
||||||
ErrorKind::MissingKey,
|
|
||||||
)))
|
|
||||||
} else {
|
} else {
|
||||||
Ok((
|
Ok((
|
||||||
remaining,
|
remaining,
|
||||||
@ -266,10 +224,7 @@ fn unquoted_term(s: &str) -> IResult<Node> {
|
|||||||
{
|
{
|
||||||
if tail.starts_with('\\') {
|
if tail.starts_with('\\') {
|
||||||
let escaped = (if tail.len() > 1 { &tail[0..2] } else { "" }).to_string();
|
let escaped = (if tail.len() > 1 { &tail[0..2] } else { "" }).to_string();
|
||||||
Err(Failure(ParseError::Anki(
|
Err(parse_failure(s, UnknownEscape(format!("\\{}", escaped))))
|
||||||
s,
|
|
||||||
ErrorKind::UnknownEscape(format!("\\{}", escaped)),
|
|
||||||
)))
|
|
||||||
} else if term.eq_ignore_ascii_case("and") {
|
} else if term.eq_ignore_ascii_case("and") {
|
||||||
Ok((tail, Node::And))
|
Ok((tail, Node::And))
|
||||||
} else if term.eq_ignore_ascii_case("or") {
|
} else if term.eq_ignore_ascii_case("or") {
|
||||||
@ -279,12 +234,9 @@ fn unquoted_term(s: &str) -> IResult<Node> {
|
|||||||
}
|
}
|
||||||
} else if s.starts_with('\\') {
|
} else if s.starts_with('\\') {
|
||||||
let escaped = (if s.len() > 1 { &s[0..2] } else { "" }).to_string();
|
let escaped = (if s.len() > 1 { &s[0..2] } else { "" }).to_string();
|
||||||
Err(Failure(ParseError::Anki(
|
Err(parse_failure(s, UnknownEscape(format!("\\{}", escaped))))
|
||||||
s,
|
|
||||||
ErrorKind::UnknownEscape(format!("\\{}", escaped)),
|
|
||||||
)))
|
|
||||||
} else {
|
} else {
|
||||||
Err(Error(ParseError::Nom(s, NomErrorKind::Verify)))
|
Err(nom::Err::Error(ParseError::Nom(s, NomErrorKind::Verify)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,17 +247,17 @@ fn quoted_term_str(s: &str) -> IResult<&str> {
|
|||||||
escaped::<_, ParseError, _, _, _, _>(is_not(r#""\"#), '\\', anychar)(opened)
|
escaped::<_, ParseError, _, _, _, _>(is_not(r#""\"#), '\\', anychar)(opened)
|
||||||
{
|
{
|
||||||
if tail.is_empty() {
|
if tail.is_empty() {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::UnclosedQuote)))
|
Err(parse_failure(s, UnclosedQuote))
|
||||||
} else if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) {
|
} else if let Ok((remaining, _)) = char::<_, ParseError>('"')(tail) {
|
||||||
Ok((remaining, inner))
|
Ok((remaining, inner))
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::UnclosedQuote)))
|
Err(parse_failure(s, UnclosedQuote))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match opened.chars().next().unwrap() {
|
match opened.chars().next().unwrap() {
|
||||||
'"' => Err(Failure(ParseError::Anki(s, ErrorKind::EmptyQuote))),
|
'"' => Err(parse_failure(s, EmptyQuote)),
|
||||||
// '\' followed by nothing
|
// '\' followed by nothing
|
||||||
'\\' => Err(Failure(ParseError::Anki(s, ErrorKind::UnclosedQuote))),
|
'\\' => Err(parse_failure(s, UnclosedQuote)),
|
||||||
// everything else is accepted by escaped
|
// everything else is accepted by escaped
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
@ -324,7 +276,7 @@ fn search_node_for_text(s: &str) -> ParseResult<SearchNode> {
|
|||||||
} else {
|
} else {
|
||||||
// escaped only fails on "\" and leading ':'
|
// escaped only fails on "\" and leading ':'
|
||||||
// "\" cannot be passed as an argument by a calling parser
|
// "\" cannot be passed as an argument by a calling parser
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::MissingKey)))
|
Err(parse_failure(s, MissingKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -368,12 +320,12 @@ fn parse_template(s: &str) -> ParseResult<SearchNode> {
|
|||||||
fn parse_flag(s: &str) -> ParseResult<SearchNode> {
|
fn parse_flag(s: &str) -> ParseResult<SearchNode> {
|
||||||
if let Ok(flag) = s.parse::<u8>() {
|
if let Ok(flag) = s.parse::<u8>() {
|
||||||
if flag > 4 {
|
if flag > 4 {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidFlag)))
|
Err(parse_failure(s, InvalidFlag))
|
||||||
} else {
|
} else {
|
||||||
Ok(SearchNode::Flag(flag))
|
Ok(SearchNode::Flag(flag))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidEdited)))
|
Err(parse_failure(s, InvalidEdited))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,7 +339,7 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> {
|
|||||||
tag("ease"),
|
tag("ease"),
|
||||||
tag("pos"),
|
tag("pos"),
|
||||||
))(s)
|
))(s)
|
||||||
.map_err(|_| Failure(ParseError::Anki(s, ErrorKind::InvalidPropProperty)))?;
|
.map_err(|_| parse_failure(s, InvalidPropProperty))?;
|
||||||
|
|
||||||
let (num, operator) = alt::<&str, &str, ParseError, _>((
|
let (num, operator) = alt::<&str, &str, ParseError, _>((
|
||||||
tag("<="),
|
tag("<="),
|
||||||
@ -397,19 +349,19 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> {
|
|||||||
tag("<"),
|
tag("<"),
|
||||||
tag(">"),
|
tag(">"),
|
||||||
))(tail)
|
))(tail)
|
||||||
.map_err(|_| Failure(ParseError::Anki(s, ErrorKind::InvalidPropOperator)))?;
|
.map_err(|_| parse_failure(s, InvalidPropOperator))?;
|
||||||
|
|
||||||
let kind = if prop == "ease" {
|
let kind = if prop == "ease" {
|
||||||
if let Ok(f) = num.parse::<f32>() {
|
if let Ok(f) = num.parse::<f32>() {
|
||||||
PropertyKind::Ease(f)
|
PropertyKind::Ease(f)
|
||||||
} else {
|
} else {
|
||||||
return Err(Failure(ParseError::Anki(s, ErrorKind::InvalidPropFloat)));
|
return Err(parse_failure(s, InvalidPropFloat));
|
||||||
}
|
}
|
||||||
} else if prop == "due" {
|
} else if prop == "due" {
|
||||||
if let Ok(i) = num.parse::<i32>() {
|
if let Ok(i) = num.parse::<i32>() {
|
||||||
PropertyKind::Due(i)
|
PropertyKind::Due(i)
|
||||||
} else {
|
} else {
|
||||||
return Err(Failure(ParseError::Anki(s, ErrorKind::InvalidPropInteger)));
|
return Err(parse_failure(s, InvalidPropInteger));
|
||||||
}
|
}
|
||||||
} else if let Ok(u) = num.parse::<u32>() {
|
} else if let Ok(u) = num.parse::<u32>() {
|
||||||
match prop {
|
match prop {
|
||||||
@ -420,7 +372,7 @@ fn parse_prop(s: &str) -> ParseResult<SearchNode<'static>> {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(Failure(ParseError::Anki(s, ErrorKind::InvalidPropUnsigned)));
|
return Err(parse_failure(s, InvalidPropUnsigned));
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(SearchNode::Property {
|
Ok(SearchNode::Property {
|
||||||
@ -434,7 +386,7 @@ fn parse_added(s: &str) -> ParseResult<SearchNode> {
|
|||||||
if let Ok(days) = s.parse::<u32>() {
|
if let Ok(days) = s.parse::<u32>() {
|
||||||
Ok(SearchNode::AddedInDays(days.max(1)))
|
Ok(SearchNode::AddedInDays(days.max(1)))
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidAdded)))
|
Err(parse_failure(s, InvalidAdded))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,7 +395,7 @@ fn parse_edited(s: &str) -> ParseResult<SearchNode> {
|
|||||||
if let Ok(days) = s.parse::<u32>() {
|
if let Ok(days) = s.parse::<u32>() {
|
||||||
Ok(SearchNode::EditedInDays(days.max(1)))
|
Ok(SearchNode::EditedInDays(days.max(1)))
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidEdited)))
|
Err(parse_failure(s, InvalidEdited))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,17 +410,17 @@ fn parse_rated(s: &str) -> ParseResult<SearchNode> {
|
|||||||
if u < 5 {
|
if u < 5 {
|
||||||
Some(u)
|
Some(u)
|
||||||
} else {
|
} else {
|
||||||
return Err(Failure(ParseError::Anki(s, ErrorKind::InvalidRatedEase)));
|
return Err(parse_failure(s, InvalidRatedEase));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(Failure(ParseError::Anki(s, ErrorKind::InvalidRatedEase)));
|
return Err(parse_failure(s, InvalidRatedEase));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
Ok(SearchNode::Rated { days, ease })
|
Ok(SearchNode::Rated { days, ease })
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidRatedDays)))
|
Err(parse_failure(s, InvalidRatedDays))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,7 +436,7 @@ fn parse_state(s: &str) -> ParseResult<SearchNode> {
|
|||||||
"buried-manually" => UserBuried,
|
"buried-manually" => UserBuried,
|
||||||
"buried-sibling" => SchedBuried,
|
"buried-sibling" => SchedBuried,
|
||||||
"suspended" => Suspended,
|
"suspended" => Suspended,
|
||||||
_ => return Err(Failure(ParseError::Anki(s, ErrorKind::InvalidState))),
|
_ => return Err(parse_failure(s, InvalidState)),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,7 +444,7 @@ fn parse_did(s: &str) -> ParseResult<SearchNode> {
|
|||||||
if let Ok(did) = s.parse() {
|
if let Ok(did) = s.parse() {
|
||||||
Ok(SearchNode::DeckID(did))
|
Ok(SearchNode::DeckID(did))
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidDid)))
|
Err(parse_failure(s, InvalidDid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -500,7 +452,7 @@ fn parse_mid(s: &str) -> ParseResult<SearchNode> {
|
|||||||
if let Ok(mid) = s.parse() {
|
if let Ok(mid) = s.parse() {
|
||||||
Ok(SearchNode::NoteTypeID(mid))
|
Ok(SearchNode::NoteTypeID(mid))
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidMid)))
|
Err(parse_failure(s, InvalidMid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -513,7 +465,7 @@ fn check_id_list(s: &str) -> ParseResult<&str> {
|
|||||||
if RE.is_match(s) {
|
if RE.is_match(s) {
|
||||||
Ok(s)
|
Ok(s)
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidIdList)))
|
Err(parse_failure(s, InvalidIdList))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,10 +479,10 @@ fn parse_dupes(s: &str) -> ParseResult<SearchNode> {
|
|||||||
text: unescape_quotes(text),
|
text: unescape_quotes(text),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidDupesText)))
|
Err(parse_failure(s, InvalidDupesText))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(Failure(ParseError::Anki(s, ErrorKind::InvalidDupesMid)))
|
Err(parse_failure(s, InvalidDupesMid))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,10 +514,7 @@ fn unescape_quotes(s: &str) -> Cow<str> {
|
|||||||
/// Unescape chars with special meaning to the parser.
|
/// Unescape chars with special meaning to the parser.
|
||||||
fn unescape(txt: &str) -> ParseResult<Cow<str>> {
|
fn unescape(txt: &str) -> ParseResult<Cow<str>> {
|
||||||
if let Some(seq) = invalid_escape_sequence(txt) {
|
if let Some(seq) = invalid_escape_sequence(txt) {
|
||||||
Err(Failure(ParseError::Anki(
|
Err(parse_failure(txt, UnknownEscape(seq)))
|
||||||
txt,
|
|
||||||
ErrorKind::UnknownEscape(seq),
|
|
||||||
)))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(if is_parser_escape(txt) {
|
Ok(if is_parser_escape(txt) {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
Loading…
Reference in New Issue
Block a user