Skip to content

Commit 9bfc1e7

Browse files
authored
Handle Unicode double-quote characters when parsing arguments (#1553)
Closes #1545
1 parent c0463c0 commit 9bfc1e7

File tree

1 file changed

+63
-13
lines changed

1 file changed

+63
-13
lines changed

src/framework/standard/args.rs

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,49 @@ impl Token {
103103
}
104104
}
105105

106+
// A utility enum to handle an edge case with Apple OSs.
107+
//
108+
// By default, a feature called "Smart Quotes" is enabled on MacOS and iOS devices. This feature
109+
// automatically substitutes the lame, but simple `"` ASCII character for quotation with the cool
110+
// `”` Unicode character. It can be disabled, but users may not want to do that as it is a global
111+
// setting (i.e. they might not want to disable it just for properly invoking commands of bots on
112+
// Discord).
113+
#[derive(Clone, Copy)]
114+
enum QuoteKind {
115+
Ascii,
116+
Apple,
117+
}
118+
119+
impl QuoteKind {
120+
fn new(c: char) -> Option<Self> {
121+
match c {
122+
'"' => Some(QuoteKind::Ascii),
123+
'\u{201C}' => Some(QuoteKind::Apple),
124+
_ => None,
125+
}
126+
}
127+
128+
fn is_ending_quote(self, c: char) -> bool {
129+
match self {
130+
QuoteKind::Ascii => c == '"',
131+
QuoteKind::Apple => c == '\u{201D}',
132+
}
133+
}
134+
}
135+
106136
fn lex(stream: &mut Stream<'_>, delims: &[Cow<'_, str>]) -> Option<Token> {
107137
if stream.is_empty() {
108138
return None;
109139
}
110140

111141
let start = stream.offset();
112-
if stream.current()? == b'"' {
113-
stream.next();
142+
if let Some(kind) = QuoteKind::new(stream.current_char()?) {
143+
stream.next_char();
114144

115-
stream.take_until(|b| b == b'"');
145+
stream.take_until_char(|c| kind.is_ending_quote(c));
116146

117-
let is_quote = stream.current().map_or(false, |b| b == b'"');
118-
stream.next();
147+
let is_quote = stream.current_char().map_or(false, |c| kind.is_ending_quote(c));
148+
stream.next_char();
119149

120150
let end = stream.offset();
121151

@@ -150,12 +180,35 @@ fn lex(stream: &mut Stream<'_>, delims: &[Cow<'_, str>]) -> Option<Token> {
150180
Some(Token::new(TokenKind::Argument, start, end))
151181
}
152182

183+
fn is_surrounded_with(s: &str, begin: char, end: char) -> bool {
184+
s.starts_with(begin) && s.ends_with(end)
185+
}
186+
187+
fn is_quoted(s: &str) -> bool {
188+
if s.len() < 2 {
189+
return false;
190+
}
191+
192+
// Refer to `QuoteKind` why we check for Unicode quote characters.
193+
is_surrounded_with(s, '"', '"') || is_surrounded_with(s, '\u{201C}', '\u{201D}')
194+
}
195+
196+
fn strip(s: &str, begin: char, end: char) -> Option<&str> {
197+
let s = s.strip_prefix(begin)?;
198+
s.strip_suffix(end)
199+
}
200+
153201
fn remove_quotes(s: &str) -> &str {
154-
if s.starts_with('"') && s.ends_with('"') {
155-
return &s[1..s.len() - 1];
202+
if s.len() < 2 {
203+
return s;
204+
}
205+
206+
if let Some(s) = strip(s, '"', '"') {
207+
return s;
156208
}
157209

158-
s
210+
// Refer to `QuoteKind` why we check for Unicode quote characters.
211+
strip(s, '\u{201C}', '\u{201D}').unwrap_or(s)
159212
}
160213

161214
#[derive(Debug, Clone, Copy)]
@@ -299,11 +352,8 @@ impl Args {
299352
.collect::<Vec<_>>();
300353

301354
let args = if delims.is_empty() && !message.is_empty() {
302-
let kind = if message.starts_with('"') && message.ends_with('"') {
303-
TokenKind::QuotedArgument
304-
} else {
305-
TokenKind::Argument
306-
};
355+
let kind =
356+
if is_quoted(message) { TokenKind::QuotedArgument } else { TokenKind::Argument };
307357

308358
// If there are no delimiters, then the only possible argument is the whole message.
309359
vec![Token::new(kind, 0, message.len())]

0 commit comments

Comments
 (0)