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