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