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