@@ -103,19 +103,49 @@ impl Token {
103
103
}
104
104
}
105
105
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
+
106
136
fn lex ( stream : & mut Stream < ' _ > , delims : & [ Cow < ' _ , str > ] ) -> Option < Token > {
107
137
if stream. is_empty ( ) {
108
138
return None ;
109
139
}
110
140
111
141
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 ( ) ;
114
144
115
- stream. take_until ( |b| b == b'"' ) ;
145
+ stream. take_until_char ( |c| kind . is_ending_quote ( c ) ) ;
116
146
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 ( ) ;
119
149
120
150
let end = stream. offset ( ) ;
121
151
@@ -150,12 +180,35 @@ fn lex(stream: &mut Stream<'_>, delims: &[Cow<'_, str>]) -> Option<Token> {
150
180
Some ( Token :: new ( TokenKind :: Argument , start, end) )
151
181
}
152
182
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
+
153
201
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;
156
208
}
157
209
158
- s
210
+ // Refer to `QuoteKind` why we check for Unicode quote characters.
211
+ strip ( s, '\u{201C}' , '\u{201D}' ) . unwrap_or ( s)
159
212
}
160
213
161
214
#[ derive( Debug , Clone , Copy ) ]
@@ -299,11 +352,8 @@ impl Args {
299
352
. collect :: < Vec < _ > > ( ) ;
300
353
301
354
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 } ;
307
357
308
358
// If there are no delimiters, then the only possible argument is the whole message.
309
359
vec ! [ Token :: new( kind, 0 , message. len( ) ) ]
0 commit comments