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 source_tools::SourceCursor as GenericSourceCursor;
13use std::char::REPLACEMENT_CHARACTER;
14use std::fmt::{Display, Formatter, Result, Write};
15
16/// Wraps [Cursor] with a [str] that represents the underlying character data for this cursor.
17#[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			// 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 = &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				// 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.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	/// Returns a new `SourceCursor` with the `should_compact` flag set.
142	///
143	/// With the `should_compact` flag set, the cursor will format with optimised displays of:
144	/// - Numbers: Remove leading zeros (`0.8` -> `.8`), trailing zeros (`1.0` -> `1`), redundant `+` sign
145	/// - Idents/Functions/AtKeywords: Write UTF-8 instead of escape codes
146	/// - Dimensions: Same as numbers for the number part, and same as Idents for the unit part
147	/// - Whitespace: Normalize to a single space
148	///
149	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	/// Returns a new `SourceCursor` with the `should_expand` flag set.
159	///
160	/// With the `should_expand` flag set, the cursor will format with verbose displays of:
161	/// - Numbers: Padded with leading zeros and trailing decimal places (`1` -> `000001.00000000`)
162	/// - Idents/Functions/AtKeywords: Each character as `\XXXXXX ` escape codes
163	/// - Dimensions: Same as numbers for the number part, and same as Idents for the unit part
164	/// - Whitespace: Expanded to multiple characters
165	///
166	#[cfg(feature = "egg")]
167	pub fn expand(&self) -> SourceCursor<'a> {
168		Self { inner: self.inner, should_compact: false, should_expand: true }
169	}
170
171	/// Checks if calling `compact().fmt(..)` _might_ produce different output than `fmt(..)`.
172	///
173	/// This can be used to check, rather than a full allocation & display, e.g. `format!("{}", sc.compact())`.
174	///
175	/// - Whitespace: returns `true` if len > 1
176	/// - Ident/Function/AtKeyword/Hash: returns `true` if contains escape chars
177	/// - Number: returns `true` if the number representation could be shortened
178	/// - Dimension: combines number and unit checks
179	#[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	/// Check if the numeric value could be compacted.
197	#[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				// Check if the decoded character is valid at this position unescaped.
299				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					// A trailing space is needed if the next character is a hex digit
313					// or whitespace, to prevent it from being consumed as part of the escape.
314					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		// Decode escape sequences
356		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					// Trailing space needed if next char is a hex digit.
371					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		// Use the opposite quote style to maximize escaping opportunity
485		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				// Escape the quote character
494				write!(f, "\\{:06x} ", c as u32)?;
495			} else if c.is_ascii() && !c.is_ascii_control() {
496				// Escape all printable ASCII as hex
497				write!(f, "\\{:06x} ", c as u32)?;
498			} else {
499				// Non-ASCII or control chars: write as-is or escape
500				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				// String has special rules
538				// https://drafts.csswg.org/css-syntax-3/#consume-string-token
539				if self.token().kind_bits() == Kind::String as u8 {
540					// When the token is a string, escaped EOF points are not consumed
541					// U+005C REVERSE SOLIDUS (\)
542					//   If the next input code point is EOF, do nothing.
543					//   Otherwise, if the next input code point is a newline, consume it.
544					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	/// Parse the cursor's content using any allocator that implements the Allocator trait.
576	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				// String has special rules
616				// https://drafts.csswg.org/css-syntax-3/#consume-string-cursor
617				if self.token().kind_bits() == Kind::String as u8 {
618					// When the token is a string, escaped EOF points are not consumed
619					// U+005C REVERSE SOLIDUS (\)
620					//   If the next input code point is EOF, do nothing.
621					//   Otherwise, if the next input code point is a newline, consume it.
622					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				// SAFETY: The source is valid UTF-8, so the slice is valid UTF-8
657				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	/// Parse the cursor's content to ASCII lowercase using any allocator that implements the Allocator trait.
664	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				// String has special rules
682				// https://drafts.csswg.org/css-syntax-3/#consume-string-cursor
683				if self.token().kind_bits() == Kind::String as u8 {
684					// When the token is a string, escaped EOF points are not consumed
685					// U+005C REVERSE SOLIDUS (\)
686					//   If the next input code point is EOF, do nothing.
687					//   Otherwise, if the next input code point is a newline, consume it.
688					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		// SAFETY: The source is valid UTF-8, so the slice is valid UTF-8
720		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}