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