1use crate::{
2 AssociatedWhitespaceRules, CommentStyle, CowStr, Cursor, Kind, KindSet, QuoteStyle, SourceOffset, Span, ToSpan,
3 Token,
4 small_str_buf::SmallStrBuf,
5 syntax::{ParseEscape, is_newline},
6};
7use allocator_api2::{alloc::Allocator, boxed::Box, vec::Vec};
8use std::char::REPLACEMENT_CHARACTER;
9use std::fmt::{Display, Formatter, Result, Write};
10
11#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct SourceCursor<'a> {
14 cursor: Cursor,
15 source: &'a str,
16 should_compact: bool,
17 #[cfg(feature = "egg")]
18 should_expand: bool,
19}
20
21impl<'a> ToSpan for SourceCursor<'a> {
22 fn to_span(&self) -> Span {
23 self.cursor.to_span()
24 }
25}
26
27impl<'a> Display for SourceCursor<'a> {
28 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
29 match self.token().kind() {
30 Kind::Eof => Ok(()),
31 #[cfg(feature = "egg")]
32 Kind::String if self.should_expand => self.fmt_expanded_string(f),
33 Kind::String if self.should_compact && self.token().contains_escape_chars() => self.fmt_compacted_string(f),
34 Kind::String => match self.token().quote_style() {
38 QuoteStyle::Single => {
39 let inner =
40 &self.source[1..(self.token().len() as usize) - self.token().has_close_quote() as usize];
41 write!(f, "'{inner}'")
42 }
43 QuoteStyle::Double => {
44 let inner =
45 &self.source[1..(self.token().len() as usize) - self.token().has_close_quote() as usize];
46 write!(f, "\"{inner}\"")
47 }
48 QuoteStyle::None => unreachable!(),
50 },
51 Kind::Delim
52 | Kind::Colon
53 | Kind::Semicolon
54 | Kind::Comma
55 | Kind::LeftSquare
56 | Kind::LeftParen
57 | Kind::RightSquare
58 | Kind::RightParen
59 | Kind::LeftCurly
60 | Kind::RightCurly => self.token().char().unwrap().fmt(f),
61 _ if self.should_compact => self.fmt_compacted(f),
62 #[cfg(feature = "egg")]
63 _ if self.should_expand => self.fmt_expanded(f),
64 _ => f.write_str(self.source),
65 }
66 }
67}
68
69impl<'a> SourceCursor<'a> {
70 pub const SPACE: SourceCursor<'static> = SourceCursor::from(Cursor::new(SourceOffset(0), Token::SPACE), " ");
71 pub const TAB: SourceCursor<'static> = SourceCursor::from(Cursor::new(SourceOffset(0), Token::TAB), "\t");
72 pub const NEWLINE: SourceCursor<'static> = SourceCursor::from(Cursor::new(SourceOffset(0), Token::NEWLINE), "\n");
73 pub const SEMICOLON: SourceCursor<'static> =
74 SourceCursor::from(Cursor::new(SourceOffset(0), Token::SEMICOLON), ";");
75
76 #[inline(always)]
77 pub const fn from(cursor: Cursor, source: &'a str) -> Self {
78 debug_assert!(
79 (cursor.len() as usize) == source.len(),
80 "A SourceCursor should be constructed with a source that matches the length of the cursor!"
81 );
82 Self {
83 cursor,
84 source,
85 should_compact: false,
86 #[cfg(feature = "egg")]
87 should_expand: false,
88 }
89 }
90
91 #[inline(always)]
92 pub const fn cursor(&self) -> Cursor {
93 self.cursor
94 }
95
96 #[inline(always)]
97 pub const fn token(&self) -> Token {
98 self.cursor.token()
99 }
100
101 #[inline(always)]
102 pub const fn source(&self) -> &'a str {
103 self.source
104 }
105
106 pub fn with_quotes(&self, quote_style: QuoteStyle) -> Self {
107 Self {
108 cursor: self.cursor.with_quotes(quote_style),
109 source: self.source,
110 should_compact: self.should_compact,
111 #[cfg(feature = "egg")]
112 should_expand: self.should_expand,
113 }
114 }
115
116 pub fn with_associated_whitespace(&self, rules: AssociatedWhitespaceRules) -> Self {
117 Self {
118 cursor: self.cursor.with_associated_whitespace(rules),
119 source: self.source,
120 should_compact: self.should_compact,
121 #[cfg(feature = "egg")]
122 should_expand: self.should_expand,
123 }
124 }
125
126 pub fn compact(&self) -> SourceCursor<'a> {
135 Self {
136 cursor: self.cursor,
137 source: self.source,
138 should_compact: true,
139 #[cfg(feature = "egg")]
140 should_expand: false,
141 }
142 }
143
144 #[cfg(feature = "egg")]
153 pub fn expand(&self) -> SourceCursor<'a> {
154 Self { cursor: self.cursor, source: self.source, should_compact: false, should_expand: true }
155 }
156
157 #[inline]
166 pub fn may_compact(&self) -> bool {
167 let token = self.token();
168 match token.kind() {
169 Kind::Whitespace => token.len() > 1,
170 Kind::Ident | Kind::Function | Kind::AtKeyword | Kind::Hash => token.contains_escape_chars(),
171 Kind::Number => self.can_compact_number(),
172 Kind::Dimension => {
173 self.can_compact_number()
174 || self.source[(self.token().numeric_len() as usize)..].bytes().any(|b| b == b'\\' || b == 0)
175 }
176 _ => false,
177 }
178 }
179
180 #[inline]
182 fn can_compact_number(&self) -> bool {
183 let token = self.token();
184 let value = token.value();
185 let num_len = token.numeric_len() as usize;
186 if value > -1.0 && value < 1.0 && value != 0.0 {
187 let bytes = self.source.as_bytes();
188 if bytes.first() == Some(&b'.') {
189 return false;
190 }
191 if value < 0.0 && bytes.get(1) == Some(&b'.') {
192 return false;
193 }
194 return true;
195 }
196 if token.has_sign() && value > 0.0 {
197 return true;
198 }
199 if token.is_float() && value.fract() == 0.0 {
200 return true;
201 }
202 if token.is_int() {
203 let abs_value = value.abs();
204 let digits = if abs_value == 0.0 { 1 } else { (abs_value.log10().floor() as usize) + 1 };
205 return num_len > (digits + (value < 0.0) as usize);
206 }
207 false
208 }
209
210 fn fmt_compacted(&self, f: &mut Formatter<'_>) -> Result {
211 let token = self.token();
212 match token.kind() {
213 Kind::Whitespace => f.write_str(" "),
214 Kind::Ident | Kind::Function | Kind::AtKeyword | Kind::Hash
215 if self.should_compact && token.contains_escape_chars() =>
216 {
217 self.fmt_compacted_ident(f)
218 }
219 Kind::Number => self.fmt_compacted_number(f),
220 Kind::Dimension => {
221 self.fmt_compacted_number(f)?;
222 self.fmt_compacted_ident(f)
223 }
224 Kind::Url => self.fmt_compacted_url(f),
225 _ => f.write_str(self.source),
226 }
227 }
228
229 fn fmt_compacted_number(&self, f: &mut Formatter<'_>) -> Result {
230 let value = self.token().value();
231 if value <= -1.0 || value >= 1.0 || value == 0.0 {
232 if value > 0.0 && self.token().kind() == Kind::Number && self.token().sign_is_required() {
233 f.write_str("+")?;
234 }
235 return value.fmt(f);
236 }
237
238 let mut small_str = SmallStrBuf::<255>::new();
239 write!(&mut small_str, "{}", value.abs())?;
240 if let Some(str) = small_str.as_str() {
241 if value < 0.0 {
242 f.write_str("-")?;
243 } else if value > 0.0 && self.token().kind() == Kind::Number && self.token().sign_is_required() {
244 f.write_str("+")?;
245 }
246 if let Some(rest) = str.strip_prefix("0.") {
247 f.write_str(".")?;
248 f.write_str(rest)
249 } else {
250 f.write_str(str)
251 }
252 } else {
253 value.fmt(f)
254 }
255 }
256
257 fn fmt_compacted_ident(&self, f: &mut Formatter<'_>) -> Result {
258 let token = self.token();
259 let start = token.leading_len() as usize;
260 let end = self.source.len() - token.trailing_len() as usize;
261 let source = &self.source[start..end];
262
263 match token.kind() {
264 Kind::AtKeyword => f.write_str("@")?,
265 Kind::Hash => f.write_str("#")?,
266 _ => {}
267 }
268
269 let mut chars = source.chars().peekable();
270 let mut i = 0;
271 while let Some(c) = chars.next() {
272 if c == '\0' {
273 write!(f, "{}", REPLACEMENT_CHARACTER)?;
274 i += 1;
275 } else if c == '\\' {
276 i += 1;
277 let (ch, n) = source[i..].chars().parse_escape_sequence();
278 write!(f, "{}", if ch == '\0' { REPLACEMENT_CHARACTER } else { ch })?;
279 i += n as usize;
280 chars = source[i..].chars().peekable();
281 } else {
282 write!(f, "{}", c)?;
283 i += c.len_utf8();
284 }
285 }
286
287 if token.kind() == Kind::Function {
288 f.write_str("(")?;
289 }
290
291 Ok(())
292 }
293
294 fn fmt_compacted_url(&self, f: &mut Formatter<'_>) -> Result {
295 let token = self.token();
296 let leading_len = token.leading_len() as usize;
297 let trailing_len = token.trailing_len() as usize;
298 f.write_str("url(")?;
299 let url_content = &self.source[leading_len..(self.source.len() - trailing_len)];
300 f.write_str(url_content.trim())?;
301 if token.url_has_closing_paren() {
302 f.write_str(")")?;
303 }
304 Ok(())
305 }
306
307 fn fmt_compacted_string(&self, f: &mut Formatter<'_>) -> Result {
308 let token = self.token();
309 let inner = &self.source[1..(token.len() as usize) - token.has_close_quote() as usize];
310 let quote = match token.quote_style() {
311 QuoteStyle::Single => '\'',
312 QuoteStyle::Double => '"',
313 QuoteStyle::None => unreachable!(),
314 };
315 f.write_char(quote)?;
316 let mut chars = inner.chars().peekable();
318 let mut i = 0;
319 while let Some(c) = chars.next() {
320 if c == '\0' {
321 write!(f, "{}", REPLACEMENT_CHARACTER)?;
322 i += 1;
323 } else if c == '\\' {
324 i += 1;
325 let (ch, n) = inner[i..].chars().parse_escape_sequence();
326 write!(f, "{}", if ch == '\0' { REPLACEMENT_CHARACTER } else { ch })?;
327 i += n as usize;
328 chars = inner[i..].chars().peekable();
329 } else {
330 write!(f, "{}", c)?;
331 i += c.len_utf8();
332 }
333 }
334 f.write_char(quote)?;
335 Ok(())
336 }
337
338 #[cfg(feature = "egg")]
339 fn fmt_expanded(&self, f: &mut Formatter<'_>) -> Result {
340 let token = self.token();
341 match token.kind() {
342 Kind::Whitespace => f.write_str(" "),
343 Kind::Ident | Kind::Function | Kind::AtKeyword | Kind::Hash => self.fmt_expanded_ident(f),
344 Kind::Number => self.fmt_expanded_number(f),
345 Kind::Dimension => {
346 self.fmt_expanded_number(f)?;
347 self.fmt_expanded_ident(f)
348 }
349 Kind::Url => self.fmt_expanded_url(f),
350 _ => f.write_str(self.source),
351 }
352 }
353
354 #[cfg(feature = "egg")]
355 fn fmt_expanded_number(&self, f: &mut Formatter<'_>) -> Result {
356 let value = self.token().value();
357 let is_negative = value < 0.0;
358 let abs_value = value.abs();
359 if is_negative {
360 f.write_str("-")?;
361 } else {
362 f.write_str("+")?;
363 }
364
365 if self.token().is_int() {
366 return write!(f, "{:010.0}", abs_value);
367 }
368 if value == 0.0 {
369 return f.write_str("0.00000000000000e+0000000000");
370 }
371 let exp = abs_value.log10().floor() as i32;
372 let mantissa = abs_value / 10_f32.powi(exp);
373 write!(f, "{:.14}e{:+011}", mantissa, exp)
374 }
375
376 #[cfg(feature = "egg")]
377 fn fmt_expanded_ident(&self, f: &mut Formatter<'_>) -> Result {
378 let token = self.token();
379 let start = token.leading_len() as usize;
380 let end = self.source.len() - token.trailing_len() as usize;
381 let source = &self.source[start..end];
382
383 match token.kind() {
384 Kind::AtKeyword => f.write_str("@")?,
385 Kind::Hash => f.write_str("#")?,
386 _ => {}
387 }
388
389 let mut chars = source.chars().peekable();
390 let mut i = 0;
391 while let Some(c) = chars.next() {
392 if c == '\0' {
393 write!(f, "\\{:06x} ", 0xFFFDu32)?;
394 i += 1;
395 } else if c == '\\' {
396 i += 1;
397 let (ch, n) = source[i..].chars().parse_escape_sequence();
398 write!(f, "\\{:06x} ", if ch == '\0' { REPLACEMENT_CHARACTER } else { ch } as u32)?;
399 i += n as usize;
400 chars = source[i..].chars().peekable();
401 } else {
402 write!(f, "\\{:06x} ", c as u32)?;
403 i += c.len_utf8();
404 }
405 }
406
407 if token.kind() == Kind::Function {
408 f.write_str("(")?;
409 }
410
411 Ok(())
412 }
413
414 #[cfg(feature = "egg")]
415 fn fmt_expanded_url(&self, f: &mut Formatter<'_>) -> Result {
416 let token = self.token();
417 let leading_len = token.leading_len() as usize;
418 let trailing_len = token.trailing_len() as usize;
419 let url_prefix = &self.source[..leading_len];
420 let url_content = &self.source[leading_len..(self.source.len() - trailing_len)];
421 f.write_str(url_prefix)?;
422 f.write_str(" ")?;
423 f.write_str(url_content.trim())?;
424 f.write_str(" ")?;
425 if token.url_has_closing_paren() {
426 f.write_str(")")?;
427 }
428 Ok(())
429 }
430
431 #[cfg(feature = "egg")]
432 fn fmt_expanded_string(&self, f: &mut Formatter<'_>) -> Result {
433 let token = self.token();
434 let inner = &self.source[1..(token.len() as usize) - token.has_close_quote() as usize];
435 let (open_quote, close_quote, escape_char) = match token.quote_style() {
437 QuoteStyle::Single => ('"', '"', '"'),
438 QuoteStyle::Double => ('\'', '\'', '\''),
439 QuoteStyle::None => unreachable!(),
440 };
441 f.write_char(open_quote)?;
442 for c in inner.chars() {
443 if c == escape_char {
444 write!(f, "\\{:06x} ", c as u32)?;
446 } else if c.is_ascii() && !c.is_ascii_control() {
447 write!(f, "\\{:06x} ", c as u32)?;
449 } else {
450 write!(f, "\\{:06x} ", c as u32)?;
452 }
453 }
454 f.write_char(close_quote)?;
455 Ok(())
456 }
457
458 pub fn eq_ignore_ascii_case(&self, other: &str) -> bool {
459 debug_assert!(self.token() != Kind::Delim && self.token() != Kind::Url);
460 debug_assert!(other.to_ascii_lowercase() == other);
461 let start = self.token().leading_len() as usize;
462 let end = self.source.len() - self.token().trailing_len() as usize;
463 if !self.token().contains_escape_chars() {
464 if end - start != other.len() {
465 return false;
466 }
467 if self.token().is_lower_case() {
468 debug_assert!(self.source[start..end].to_ascii_lowercase() == self.source[start..end]);
469 return &self.source[start..end] == other;
470 }
471 return self.source[start..end].eq_ignore_ascii_case(other);
472 }
473 let mut chars = self.source[start..end].chars().peekable();
474 let mut other_chars = other.chars();
475 let mut i = 0;
476 while let Some(c) = chars.next() {
477 let o = other_chars.next();
478 if o.is_none() {
479 return false;
480 }
481 let o = o.unwrap();
482 if c == '\0' {
483 if REPLACEMENT_CHARACTER != o {
484 return false;
485 }
486 i += 1;
487 } else if c == '\\' {
488 if self.token().kind_bits() == Kind::String as u8 {
491 let c = chars.peek();
496 if let Some(c) = c {
497 if is_newline(*c) {
498 chars.next();
499 if chars.peek() == Some(&'\n') {
500 i += 1;
501 }
502 i += 2;
503 chars = self.source[(start + i)..end].chars().peekable();
504 continue;
505 }
506 } else {
507 break;
508 }
509 }
510 i += 1;
511 let (ch, n) = self.source[(start + i)..].chars().parse_escape_sequence();
512 i += n as usize;
513 chars = self.source[(start + i)..end].chars().peekable();
514 if (ch == '\0' && REPLACEMENT_CHARACTER != o) || ch != o {
515 return false;
516 }
517 } else if c != o {
518 return false;
519 } else {
520 i += c.len_utf8();
521 }
522 }
523 other_chars.next().is_none()
524 }
525
526 pub fn parse<A: Allocator + Clone + 'a>(&self, allocator: A) -> CowStr<'a, A> {
528 debug_assert!(self.token() != Kind::Delim);
529 let start = self.token().leading_len() as usize;
530 let end = self.source.len() - self.token().trailing_len() as usize;
531 if !self.token().contains_escape_chars() {
532 return CowStr::<A>::Borrowed(&self.source[start..end]);
533 }
534 let mut chars = self.source[start..end].chars().peekable();
535 let mut i = 0;
536 let mut vec: Option<Vec<u8, A>> = None;
537 while let Some(c) = chars.next() {
538 if c == '\0' {
539 if vec.is_none() {
540 vec = if i == 0 {
541 Some(Vec::new_in(allocator.clone()))
542 } else {
543 Some({
544 let mut v = Vec::new_in(allocator.clone());
545 v.extend(self.source[start..(start + i)].bytes());
546 v
547 })
548 }
549 }
550 let mut buf = [0; 4];
551 let bytes = REPLACEMENT_CHARACTER.encode_utf8(&mut buf).as_bytes();
552 vec.as_mut().unwrap().extend_from_slice(bytes);
553 i += 1;
554 } else if c == '\\' {
555 if vec.is_none() {
556 vec = if i == 0 {
557 Some(Vec::new_in(allocator.clone()))
558 } else {
559 Some({
560 let mut v = Vec::new_in(allocator.clone());
561 v.extend(self.source[start..(start + i)].bytes());
562 v
563 })
564 }
565 }
566 if self.token().kind_bits() == Kind::String as u8 {
569 let c = chars.peek();
574 if let Some(c) = c {
575 if is_newline(*c) {
576 chars.next();
577 if chars.peek() == Some(&'\n') {
578 i += 1;
579 }
580 i += 2;
581 chars = self.source[(start + i)..end].chars().peekable();
582 continue;
583 }
584 } else {
585 break;
586 }
587 }
588 i += 1;
589 let (ch, n) = self.source[(start + i)..].chars().parse_escape_sequence();
590 let mut buf = [0; 4];
591 let bytes = if ch == '\0' { REPLACEMENT_CHARACTER } else { ch }.encode_utf8(&mut buf).as_bytes();
592 vec.as_mut().unwrap().extend_from_slice(bytes);
593 i += n as usize;
594 chars = self.source[(start + i)..end].chars().peekable();
595 } else {
596 if let Some(bytes) = &mut vec {
597 let mut buf = [0; 4];
598 let char_bytes = c.encode_utf8(&mut buf).as_bytes();
599 bytes.extend_from_slice(char_bytes);
600 }
601 i += c.len_utf8();
602 }
603 }
604 match vec {
605 Some(vec) => {
606 let boxed_slice = vec.into_boxed_slice();
607 unsafe { CowStr::Owned(Box::from_raw_in(Box::into_raw(boxed_slice) as *mut str, allocator)) }
609 }
610 None => CowStr::Borrowed(&self.source[start..start + i]),
611 }
612 }
613
614 pub fn parse_ascii_lower<A: Allocator + Clone + 'a>(&self, allocator: A) -> CowStr<'a, A> {
616 debug_assert!(self.token() != Kind::Delim);
617 let start = self.token().leading_len() as usize;
618 let end = self.source.len() - self.token().trailing_len() as usize;
619 if !self.token().contains_escape_chars() && self.token().is_lower_case() {
620 return CowStr::Borrowed(&self.source[start..end]);
621 }
622 let mut chars = self.source[start..end].chars().peekable();
623 let mut i = 0;
624 let mut vec: Vec<u8, A> = Vec::new_in(allocator.clone());
625 while let Some(c) = chars.next() {
626 if c == '\0' {
627 let mut buf = [0; 4];
628 let bytes = REPLACEMENT_CHARACTER.encode_utf8(&mut buf).as_bytes();
629 vec.extend_from_slice(bytes);
630 i += 1;
631 } else if c == '\\' {
632 if self.token().kind_bits() == Kind::String as u8 {
635 let c = chars.peek();
640 if let Some(c) = c {
641 if is_newline(*c) {
642 chars.next();
643 if chars.peek() == Some(&'\n') {
644 i += 1;
645 }
646 i += 2;
647 chars = self.source[(start + i)..end].chars().peekable();
648 continue;
649 }
650 } else {
651 break;
652 }
653 }
654 i += 1;
655 let (ch, n) = self.source[(start + i)..].chars().parse_escape_sequence();
656 let char_to_push = if ch == '\0' { REPLACEMENT_CHARACTER } else { ch.to_ascii_lowercase() };
657 let mut buf = [0; 4];
658 let bytes = char_to_push.encode_utf8(&mut buf).as_bytes();
659 vec.extend_from_slice(bytes);
660 i += n as usize;
661 chars = self.source[(start + i)..end].chars().peekable();
662 } else {
663 let mut buf = [0; 4];
664 let bytes = c.to_ascii_lowercase().encode_utf8(&mut buf).as_bytes();
665 vec.extend_from_slice(bytes);
666 i += c.len_utf8();
667 }
668 }
669 let boxed_slice = vec.into_boxed_slice();
670 unsafe { CowStr::Owned(Box::from_raw_in(Box::into_raw(boxed_slice) as *mut str, allocator)) }
672 }
673}
674
675impl PartialEq<Kind> for SourceCursor<'_> {
676 fn eq(&self, other: &Kind) -> bool {
677 self.token() == *other
678 }
679}
680
681impl PartialEq<CommentStyle> for SourceCursor<'_> {
682 fn eq(&self, other: &CommentStyle) -> bool {
683 self.token() == *other
684 }
685}
686
687impl From<SourceCursor<'_>> for KindSet {
688 fn from(cursor: SourceCursor<'_>) -> Self {
689 cursor.token().into()
690 }
691}
692
693impl PartialEq<KindSet> for SourceCursor<'_> {
694 fn eq(&self, other: &KindSet) -> bool {
695 self.token() == *other
696 }
697}
698
699#[cfg(test)]
700mod test {
701 use crate::{Cursor, QuoteStyle, SourceCursor, SourceOffset, Token, Whitespace};
702 use allocator_api2::alloc::Global;
703 use std::fmt::Write;
704
705 #[test]
706 fn parse_str_lower() {
707 let c = Cursor::new(SourceOffset(0), Token::new_ident(true, false, false, 0, 3));
708 assert_eq!(SourceCursor::from(c, "FoO").parse_ascii_lower(Global), "foo");
709 assert_eq!(SourceCursor::from(c, "FOO").parse_ascii_lower(Global), "foo");
710 assert_eq!(SourceCursor::from(c, "foo").parse_ascii_lower(Global), "foo");
711
712 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Single, true, false, 5));
713 assert_eq!(SourceCursor::from(c, "'FoO'").parse_ascii_lower(Global), "foo");
714 assert_eq!(SourceCursor::from(c, "'FOO'").parse_ascii_lower(Global), "foo");
715
716 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Single, false, false, 4));
717 assert_eq!(SourceCursor::from(c, "'FoO").parse_ascii_lower(Global), "foo");
718 assert_eq!(SourceCursor::from(c, "'FOO").parse_ascii_lower(Global), "foo");
719 assert_eq!(SourceCursor::from(c, "'foo").parse_ascii_lower(Global), "foo");
720
721 let c = Cursor::new(SourceOffset(0), Token::new_url(true, false, false, 4, 1, 6));
722 assert_eq!(SourceCursor::from(c, "url(a)").parse_ascii_lower(Global), "a");
723 assert_eq!(SourceCursor::from(c, "url(b)").parse_ascii_lower(Global), "b");
724
725 let c = Cursor::new(SourceOffset(0), Token::new_url(true, false, false, 6, 1, 8));
726 assert_eq!(SourceCursor::from(c, "\\75rl(A)").parse_ascii_lower(Global), "a");
727 assert_eq!(SourceCursor::from(c, "u\\52l(B)").parse_ascii_lower(Global), "b");
728 assert_eq!(SourceCursor::from(c, "ur\\6c(C)").parse_ascii_lower(Global), "c");
729
730 let c = Cursor::new(SourceOffset(0), Token::new_url(true, false, false, 8, 1, 10));
731 assert_eq!(SourceCursor::from(c, "\\75\\52l(A)").parse_ascii_lower(Global), "a");
732 assert_eq!(SourceCursor::from(c, "u\\52\\6c(B)").parse_ascii_lower(Global), "b");
733 assert_eq!(SourceCursor::from(c, "\\75r\\6c(C)").parse_ascii_lower(Global), "c");
734 }
735
736 #[test]
737 fn eq_ignore_ascii_case() {
738 let c = Cursor::new(SourceOffset(0), Token::new_ident(false, false, false, 0, 3));
739 assert!(SourceCursor::from(c, "foo").eq_ignore_ascii_case("foo"));
740 assert!(!SourceCursor::from(c, "foo").eq_ignore_ascii_case("bar"));
741 assert!(!SourceCursor::from(c, "fo ").eq_ignore_ascii_case("foo"));
742 assert!(!SourceCursor::from(c, "foo").eq_ignore_ascii_case("fooo"));
743 assert!(!SourceCursor::from(c, "foo").eq_ignore_ascii_case("ғоо"));
744
745 let c = Cursor::new(SourceOffset(0), Token::new_ident(true, false, false, 0, 3));
746 assert!(SourceCursor::from(c, "FoO").eq_ignore_ascii_case("foo"));
747 assert!(SourceCursor::from(c, "FOO").eq_ignore_ascii_case("foo"));
748 assert!(!SourceCursor::from(c, "foo").eq_ignore_ascii_case("bar"));
749 assert!(!SourceCursor::from(c, "fo ").eq_ignore_ascii_case("foo"));
750 assert!(!SourceCursor::from(c, "foo").eq_ignore_ascii_case("fooo"));
751 assert!(!SourceCursor::from(c, "foo").eq_ignore_ascii_case("ғоо"));
752
753 let c = Cursor::new(SourceOffset(3), Token::new_ident(false, false, false, 0, 3));
754 assert!(SourceCursor::from(c, "bar").eq_ignore_ascii_case("bar"));
755
756 let c = Cursor::new(SourceOffset(3), Token::new_ident(false, false, true, 0, 3));
757 assert!(SourceCursor::from(c, "bar").eq_ignore_ascii_case("bar"));
758
759 let c = Cursor::new(SourceOffset(3), Token::new_ident(false, false, true, 0, 5));
760 assert!(SourceCursor::from(c, "b\\61r").eq_ignore_ascii_case("bar"));
761
762 let c = Cursor::new(SourceOffset(3), Token::new_ident(false, false, true, 0, 7));
763 assert!(SourceCursor::from(c, "b\\61\\72").eq_ignore_ascii_case("bar"));
764 }
765
766 #[test]
767 fn write_str() {
768 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Double, true, false, 5));
769 let mut str = String::new();
770 write!(str, "{}", SourceCursor::from(c, "'foo'")).unwrap();
771 assert_eq!(c.token().quote_style(), QuoteStyle::Double);
772 assert_eq!(str, "\"foo\"");
773
774 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Double, false, false, 4));
775 let mut str = String::new();
776 write!(str, "{}", SourceCursor::from(c, "'foo")).unwrap();
777 assert_eq!(c.token().quote_style(), QuoteStyle::Double);
778 assert_eq!(str, "\"foo\"");
779
780 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Single, false, false, 4));
781 let mut str = String::new();
782 write!(str, "{}", SourceCursor::from(c, "\"foo")).unwrap();
783 assert_eq!(c.token().quote_style(), QuoteStyle::Single);
784 assert_eq!(str, "'foo'");
785 }
786
787 #[test]
788 #[cfg(feature = "bumpalo")]
789 fn test_bumpalo_compatibility() {
790 use bumpalo::Bump;
791
792 let bump = Bump::new();
794 let c = Cursor::new(SourceOffset(0), Token::new_ident(true, false, false, 0, 3));
795
796 assert_eq!(SourceCursor::from(c, "FoO").parse(&bump), "FoO");
798 assert_eq!(SourceCursor::from(c, "FoO").parse_ascii_lower(&bump), "foo");
799
800 assert_eq!(&*SourceCursor::from(c, "FoO").parse(&bump), "FoO");
802 assert_eq!(&*SourceCursor::from(c, "FoO").parse_ascii_lower(&bump), "foo");
803
804 let c = Cursor::new(SourceOffset(0), Token::new_ident(false, false, true, 0, 7));
806 assert_eq!(SourceCursor::from(c, "b\\61\\72").parse(&bump), "bar");
807 assert_eq!(&*SourceCursor::from(c, "b\\61\\72").parse(&bump), "bar");
808 }
809
810 #[test]
811 fn test_compact_ident_with_escapes() {
812 let c = Cursor::new(SourceOffset(0), Token::new_ident(false, false, true, 0, 5));
813 let sc = SourceCursor::from(c, r"\66oo");
814 assert_eq!(format!("{}", sc), r"\66oo");
815 assert_eq!(format!("{}", sc.compact()), "foo");
816 }
817
818 #[test]
819 fn test_compact_function_with_escapes() {
820 let c = Cursor::new(SourceOffset(0), Token::new_ident(false, false, true, 0, 6));
821 let sc = SourceCursor::from(c, r"\72gb(");
822 assert_eq!(format!("{}", sc), r"\72gb(");
823 assert_eq!(format!("{}", sc.compact()), "rgb(");
824 }
825
826 #[test]
827 fn test_compact_number() {
828 let c = Cursor::new(SourceOffset(0), Token::new_number(true, false, 3, 0.8));
829 let sc = SourceCursor::from(c, r"0.8");
830 assert_eq!(format!("{}", sc), r"0.8");
831 assert_eq!(format!("{}", sc.compact()), ".8");
832
833 let c = Cursor::new(SourceOffset(0), Token::new_number(false, false, 3, 1.0));
834 let sc = SourceCursor::from(c, r"001");
835 assert_eq!(format!("{}", sc), r"001");
836 assert_eq!(format!("{}", sc.compact()), "1");
837
838 let c = Cursor::new(SourceOffset(0), Token::new_number(true, true, 8, 1.0));
839 let sc = SourceCursor::from(c, r"+1.00000");
840 assert_eq!(format!("{}", sc), r"+1.00000");
841 assert_eq!(format!("{}", sc.compact()), "1");
842
843 let c = Cursor::new(SourceOffset(0), Token::new_number(true, true, 8, 1.0).with_sign_required());
844 let sc = SourceCursor::from(c, r"+1.00000");
845 assert_eq!(format!("{}", sc), r"+1.00000");
846 assert_eq!(format!("{}", sc.compact()), "+1");
847
848 let c = Cursor::new(SourceOffset(0), Token::new_number(true, false, 4, 0.01));
849 let sc = SourceCursor::from(c, r"0.01");
850 assert_eq!(format!("{}", sc), r"0.01");
851 assert_eq!(format!("{}", sc.compact()), ".01");
852
853 let c = Cursor::new(SourceOffset(0), Token::new_number(true, false, 5, -0.01));
854 let sc = SourceCursor::from(c, r"-0.01");
855 assert_eq!(format!("{}", sc), r"-0.01");
856 assert_eq!(format!("{}", sc.compact()), "-.01");
857
858 let c = Cursor::new(SourceOffset(0), Token::new_number(true, false, 4, 0.06));
859 let sc = SourceCursor::from(c, r"0.06");
860 assert_eq!(format!("{}", sc), r"0.06");
861 assert_eq!(format!("{}", sc.compact()), ".06");
862 }
863
864 #[test]
865 fn test_compact_dimension() {
866 let c = Cursor::new(SourceOffset(0), Token::new_dimension(true, false, 4, 4, 0.8, 0));
867 let sc = SourceCursor::from(c, r"+0.8\70x");
868 assert_eq!(format!("{}", sc), r"+0.8\70x");
869 assert_eq!(format!("{}", sc.compact()), ".8px");
870 }
871
872 #[test]
873 fn test_compact_whitespace() {
874 let c = Cursor::new(SourceOffset(0), Token::new_whitespace(Whitespace::Space, 3));
875 let sc = SourceCursor::from(c, " ");
876 assert_eq!(format!("{}", sc), r" ");
877 assert_eq!(format!("{}", sc.compact()), " ");
878
879 let c = Cursor::new(SourceOffset(0), Token::new_whitespace(Whitespace::Space, 7));
880 let sc = SourceCursor::from(c, r" \n\r");
881 assert_eq!(format!("{}", sc), r" \n\r");
882 assert_eq!(format!("{}", sc.compact()), " ");
883 }
884
885 #[test]
886 fn test_can_be_compacted_whitespace() {
887 let c = Cursor::new(SourceOffset(0), Token::new_whitespace(Whitespace::Space, 1));
888 assert!(!SourceCursor::from(c, " ").may_compact());
889
890 let c = Cursor::new(SourceOffset(0), Token::new_whitespace(Whitespace::Space, 3));
891 assert!(SourceCursor::from(c, " ").may_compact());
892
893 let c = Cursor::new(SourceOffset(0), Token::new_whitespace(Whitespace::Newline, 2));
894 assert!(SourceCursor::from(c, "\n\n").may_compact());
895 }
896
897 #[test]
898 fn test_can_be_compacted_ident() {
899 let c = Cursor::new(SourceOffset(0), Token::new_ident(false, false, false, 0, 3));
900 assert!(!SourceCursor::from(c, "foo").may_compact());
901
902 let c = Cursor::new(SourceOffset(0), Token::new_ident(false, false, true, 0, 5));
903 assert!(SourceCursor::from(c, r"\66oo").may_compact());
904 }
905
906 #[test]
907 fn test_can_be_compacted_number() {
908 let c = Cursor::new(SourceOffset(0), Token::new_number(true, false, 3, 0.8));
909 assert!(SourceCursor::from(c, "0.8").may_compact());
910
911 let c = Cursor::new(SourceOffset(0), Token::new_number(true, false, 4, -0.5));
912 assert!(SourceCursor::from(c, "-0.5").may_compact());
913
914 let c = Cursor::new(SourceOffset(0), Token::new_number(false, false, 1, 1.0));
915 assert!(!SourceCursor::from(c, "1").may_compact());
916
917 let c = Cursor::new(SourceOffset(0), Token::new_number(false, false, 3, 1.0));
918 assert!(SourceCursor::from(c, "001").may_compact());
919
920 let c = Cursor::new(SourceOffset(0), Token::new_number(false, false, 2, 1.0));
921 assert!(SourceCursor::from(c, "+1").may_compact());
922
923 let c = Cursor::new(SourceOffset(0), Token::new_number(true, false, 3, 1.0));
924 assert!(SourceCursor::from(c, "1.0").may_compact());
925 }
926
927 #[test]
928 fn test_can_be_compacted_dimension() {
929 let c = Cursor::new(SourceOffset(0), Token::new_dimension(true, true, 4, 4, 0.8, 0));
930 assert!(SourceCursor::from(c, r"+0.8\70x").may_compact());
931
932 let c = Cursor::new(SourceOffset(0), Token::new_dimension(false, false, 1, 2, 1.0, 0));
933 assert!(!SourceCursor::from(c, "1px").may_compact());
934
935 let c = Cursor::new(SourceOffset(0), Token::new_dimension(false, false, 2, 2, 1.0, 0));
936 assert!(SourceCursor::from(c, "01px").may_compact());
937
938 let c = Cursor::new(SourceOffset(0), Token::new_dimension(false, false, 2, 2, 1.0, 0));
939 assert!(SourceCursor::from(c, "+1px").may_compact());
940
941 let c = Cursor::new(SourceOffset(0), Token::new_dimension(true, false, 3, 2, 0.5, 0));
942 assert!(SourceCursor::from(c, "0.5px").may_compact());
943
944 let c = Cursor::new(SourceOffset(0), Token::new_dimension(true, false, 2, 2, 0.5, 0));
945 assert!(!SourceCursor::from(c, ".5px").may_compact());
946
947 let c = Cursor::new(SourceOffset(0), Token::new_dimension(false, false, 1, 4, 1.0, 0));
948 assert!(SourceCursor::from(c, r"1\70x").may_compact());
949 }
950
951 #[test]
952 fn test_compact_url() {
953 let c = Cursor::new(SourceOffset(0), Token::new_url(true, true, false, 7, 1, 15));
954 let sc = SourceCursor::from(c, "url( foo.png)");
955 assert_eq!(format!("{}", sc), "url( foo.png)");
956 assert_eq!(format!("{}", sc.compact()), "url(foo.png)");
957
958 let c = Cursor::new(SourceOffset(0), Token::new_url(true, false, false, 4, 4, 15));
959 let sc = SourceCursor::from(c, "url(foo.png )");
960 assert_eq!(format!("{}", sc), "url(foo.png )");
961 assert_eq!(format!("{}", sc.compact()), "url(foo.png)");
962
963 let c = Cursor::new(SourceOffset(0), Token::new_url(true, true, false, 6, 3, 16));
964 let sc = SourceCursor::from(c, "url( foo.png )");
965 assert_eq!(format!("{}", sc), "url( foo.png )");
966 assert_eq!(format!("{}", sc.compact()), "url(foo.png)");
967
968 let c = Cursor::new(SourceOffset(0), Token::new_url(false, false, false, 4, 0, 11));
969 let sc = SourceCursor::from(c, "url(foo.png");
970 assert_eq!(format!("{}", sc), "url(foo.png");
971 assert_eq!(format!("{}", sc.compact()), "url(foo.png");
972 }
973
974 #[test]
975 fn test_compact_string_with_escapes() {
976 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Double, true, true, 7));
977 let sc = SourceCursor::from(c, r#""\66oo""#);
978 assert_eq!(format!("{}", sc), r#""\66oo""#);
979 assert_eq!(format!("{}", sc.compact()), r#""foo""#);
980
981 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Single, true, true, 8));
982 let sc = SourceCursor::from(c, r"'\62 ar'");
983 assert_eq!(format!("{}", sc), r"'\62 ar'");
984 assert_eq!(format!("{}", sc.compact()), "'bar'");
985
986 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Double, true, true, 11));
987 let sc = SourceCursor::from(c, r#""\68\65llo""#);
988 assert_eq!(format!("{}", sc), r#""\68\65llo""#);
989 assert_eq!(format!("{}", sc.compact()), r#""hello""#);
990
991 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Double, true, true, 5));
992 let sc = SourceCursor::from(c, "\"\0oo\"");
993 assert_eq!(format!("{}", sc.compact()), "\"\u{FFFD}oo\"");
994
995 let c = Cursor::new(SourceOffset(0), Token::new_string(QuoteStyle::Double, true, true, 6));
996 let sc = SourceCursor::from(c, "\"\x5c0oo\"");
997 assert_eq!(format!("{}", sc.compact()), "\"\u{FFFD}oo\"");
998 }
999}