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