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