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