Skip to main content

css_lexer/
source_cursor.rs

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/// Wraps [Cursor] with a [str] that represents the underlying character data for this cursor.
16#[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			// It is important to manually write out quotes for 2 reasons:
39			//  1. The quote style can be mutated from the source string (such as the case of normalising/switching quotes.
40			//  2. Some strings may not have the closing quote, which should be corrected.
41			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				// Strings must always be quoted!
53				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	/// Returns a new `SourceCursor` with the `should_compact` flag set.
133	///
134	/// With the `should_compact` flag set, the cursor will format with optimised displays of:
135	/// - Numbers: Remove leading zeros (`0.8` -> `.8`), trailing zeros (`1.0` -> `1`), redundant `+` sign
136	/// - Idents/Functions/AtKeywords: Write UTF-8 instead of escape codes
137	/// - Dimensions: Same as numbers for the number part, and same as Idents for the unit part
138	/// - Whitespace: Normalize to a single space
139	///
140	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	/// Returns a new `SourceCursor` with the `should_expand` flag set.
151	///
152	/// With the `should_expand` flag set, the cursor will format with verbose displays of:
153	/// - Numbers: Padded with leading zeros and trailing decimal places (`1` -> `000001.00000000`)
154	/// - Idents/Functions/AtKeywords: Each character as `\XXXXXX ` escape codes
155	/// - Dimensions: Same as numbers for the number part, and same as Idents for the unit part
156	/// - Whitespace: Expanded to multiple characters
157	///
158	#[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	/// Checks if calling `compact().fmt(..)` _might_ produce different output than `fmt(..)`.
164	///
165	/// This can be used to check, rather than a full allocation & display, e.g. `format!("{}", sc.compact())`.
166	///
167	/// - Whitespace: returns `true` if len > 1
168	/// - Ident/Function/AtKeyword/Hash: returns `true` if contains escape chars
169	/// - Number: returns `true` if the number representation could be shortened
170	/// - Dimension: combines number and unit checks
171	#[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	/// Check if the numeric value could be compacted.
187	#[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				// Check if the decoded character is valid at this position unescaped.
289				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					// A trailing space is needed if the next character is a hex digit
303					// or whitespace, to prevent it from being consumed as part of the escape.
304					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		// Decode escape sequences
346		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					// Trailing space needed if next char is a hex digit.
361					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		// Use the opposite quote style to maximize escaping opportunity
475		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				// Escape the quote character
484				write!(f, "\\{:06x} ", c as u32)?;
485			} else if c.is_ascii() && !c.is_ascii_control() {
486				// Escape all printable ASCII as hex
487				write!(f, "\\{:06x} ", c as u32)?;
488			} else {
489				// Non-ASCII or control chars: write as-is or escape
490				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				// String has special rules
528				// https://drafts.csswg.org/css-syntax-3/#consume-string-token
529				if self.token().kind_bits() == Kind::String as u8 {
530					// When the token is a string, escaped EOF points are not consumed
531					// U+005C REVERSE SOLIDUS (\)
532					//   If the next input code point is EOF, do nothing.
533					//   Otherwise, if the next input code point is a newline, consume it.
534					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	/// Parse the cursor's content using any allocator that implements the Allocator trait.
566	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				// String has special rules
606				// https://drafts.csswg.org/css-syntax-3/#consume-string-cursor
607				if self.token().kind_bits() == Kind::String as u8 {
608					// When the token is a string, escaped EOF points are not consumed
609					// U+005C REVERSE SOLIDUS (\)
610					//   If the next input code point is EOF, do nothing.
611					//   Otherwise, if the next input code point is a newline, consume it.
612					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				// SAFETY: The source is valid UTF-8, so the slice is valid UTF-8
647				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	/// Parse the cursor's content to ASCII lowercase using any allocator that implements the Allocator trait.
654	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				// String has special rules
672				// https://drafts.csswg.org/css-syntax-3/#consume-string-cursor
673				if self.token().kind_bits() == Kind::String as u8 {
674					// When the token is a string, escaped EOF points are not consumed
675					// U+005C REVERSE SOLIDUS (\)
676					//   If the next input code point is EOF, do nothing.
677					//   Otherwise, if the next input code point is a newline, consume it.
678					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		// SAFETY: The source is valid UTF-8, so the slice is valid UTF-8
710		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		// Test that Bumpalo's Bump can be used as an allocator
832		let bump = Bump::new();
833		let c = Cursor::new(SourceOffset(0), Token::new_ident(true, false, false, 0, 3));
834
835		// Test that the old interface still works
836		assert_eq!(SourceCursor::from(c, "FoO").parse(&bump), "FoO");
837		assert_eq!(SourceCursor::from(c, "FoO").parse_ascii_lower(&bump), "foo");
838
839		// Test that the new interface works with Bumpalo too
840		assert_eq!(&*SourceCursor::from(c, "FoO").parse(&bump), "FoO");
841		assert_eq!(&*SourceCursor::from(c, "FoO").parse_ascii_lower(&bump), "foo");
842
843		// Test with escape sequences
844		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}