Skip to main content

css_lexer/
lib.rs

1//! An implementation of the [CSS Syntax Level 3 tokenization algorithm][1]. It is intended as a low-level building
2//! block for buidling parsers for CSS or CSS-alike languages (for example SASS).
3//!
4//! This crate provides the [Lexer] struct, which borrows `&str` and can incrementally produce [Tokens][Token]. The
5//! encoding of the `&str` is assumed to be utf-8.
6//!
7//! The [Lexer] _may_ be configured with additional [Features][Feature] to allow for lexing tokens in ways which diverge
8//! from the CSS specification (such as tokenizing comments using `//`). With no additional features this lexer is fully
9//! spec compliant.
10//!
11//! [Tokens][Token] are _untyped_ (there are no super-classes like `Ident`); but they have a [Kind] which can be used to
12//! determine their type. Tokens do not store the underlying character data, nor do they store their offsets. They just
13//! provide "facts" about the underlying data. In order to re-build a string, each [Token] will need to be wrapped in a
14//! [Cursor] and consult the original `&str` to get the character data. This design allows Tokens live in the stack,
15//! avoiding heap allocation as they are always `size_of` `8`. Likewise [Cursors][Cursor] are always a `size_of` `12`.
16//!
17//! # Limitations
18//!
19//! The [Lexer] has limitations around document sizes and token sizes, in order to keep [Token], [SourceOffset] and
20//! [Cursor] small. It's very unlikely the average document will run into these limitations, but they're listed here
21//! for completeness:
22//!
23//! - Documents are limited to ~4gb in size. [SourceOffset] is a [u32] so cannot represent larger offsets. Attempting to
24//!   lex larger documents is considrered [undefined behaviour][2].
25//!
26//! - [Tokens][Token] are limited to ~4gb in length. A [Token's][Token] is a [u32] so cannot represent larger lengths.
27//!   If the lexer encounters a token with  larger length this is considered [undefined behaviour][2].
28//!
29//! - Number [Tokens][Token] are limited to 16,777,216 characters in length. For example encountering a number with
30//!   17MM `0`s is considered [undefined behaviour][2]. This is not the same as the number value, which is an [f32].
31//!   (Please note that the CSS spec dictates numbers are f32, CSS does not have larger numbers).
32//!
33//! - Dimension [Tokens][Token] are limited to 4,096 numeric characters in length and 4,096 ident characters in length.
34//!   For example encountering a dimension with 4,097 `0`s is considered [undefined behaviour][2].
35//!
36//! # General usage
37//!
38//! A parser can be implemented on top of the [Lexer] by instantiating a [Lexer] with [Lexer::new()] or
39//! [Lexer::new_with_features()] if you wish to opt-into non-spec-compliant features. The [Lexer] needs to be given a
40//! `&str` which it will reference to produce Tokens.
41//!
42//! Repeatedly calling [Lexer::advance()] will move the Lexer's internal position one [Token] forward, and return the
43//! newly lexed [Token], once the end of `&str` is reached [Lexer::advance()] will repeatedly return [Token::EOF].
44//!
45//! # Example
46//!
47//! ```
48//! use css_lexer::*;
49//! let mut lexer = Lexer::new(&EmptyAtomSet::ATOMS, "width: 1px");
50//! assert_eq!(lexer.offset(), 0);
51//! {
52//!     let token = lexer.advance();
53//!     assert_eq!(token, Kind::Ident);
54//!     let cursor = token.with_cursor(SourceOffset(0));
55//!     assert_eq!(cursor.str_slice(lexer.source()), "width");
56//! }
57//! {
58//!     let token = lexer.advance();
59//!     assert_eq!(token, Kind::Colon);
60//!     assert_eq!(token, ':');
61//! }
62//! {
63//!     let token = lexer.advance();
64//!     assert_eq!(token, Kind::Whitespace);
65//! }
66//! {
67//!     let token = lexer.advance();
68//!     assert_eq!(token, Kind::Dimension);
69//! }
70//! ```
71//!
72//! [1]: https://drafts.csswg.org/css-syntax/#tokenization
73//! [2]: https://en.wikipedia.org/wiki/Undefined_behavior
74
75mod associated_whitespace_rules;
76mod atom_set;
77mod comment_style;
78mod constants;
79mod cow;
80mod cursor;
81#[cfg(feature = "dynamic-atoms")]
82mod dyn_atom_registry;
83mod empty_atom_set;
84mod feature;
85mod kind;
86mod kindset;
87mod pairwise;
88mod private;
89mod quote_style;
90mod simd;
91mod small_str_buf;
92mod source_cursor;
93mod syntax;
94mod token;
95mod whitespace_style;
96
97/// A convenience alias for the most common use case - a Lexer
98pub type BasicLexer<'a> = Lexer<'a>;
99
100pub use associated_whitespace_rules::AssociatedWhitespaceRules;
101pub use atom_set::{AtomSet, DynAtomSet};
102pub use comment_style::CommentStyle;
103pub use cow::CowStr;
104pub type Cursor = source_tools::Cursor<Token>;
105#[cfg(feature = "dynamic-atoms")]
106pub use dyn_atom_registry::{Atom, DynAtomRegistry, RegisteredAtomSet};
107pub use empty_atom_set::EmptyAtomSet;
108pub use feature::Feature;
109pub use kind::Kind;
110pub use kindset::KindSet;
111pub use pairwise::PairWise;
112pub use quote_style::QuoteStyle;
113pub use source_cursor::SourceCursor;
114pub use source_tools::{LineIndex, SourceOffset, Span, ToSpan};
115pub use token::Token;
116pub use whitespace_style::Whitespace;
117
118/// The [Lexer] struct - the core of the library - borrows `&str` and can incrementally produce [Tokens][Token].
119///
120/// The encoding of the `&str` is assumed to be utf-8. Other sources should be re-encoded into utf-8 prior to ingesting
121/// into the [Lexer].
122///
123/// The [Lexer] _may_ be configured with additional [Features][Feature] to allow for lexing tokens in ways which diverge
124/// from the CSS specification (such as tokenizing comments using `//`). With no additional features this lexer is fully
125/// spec compliant.
126///
127/// [Tokens][Token] are _untyped_ (there are no super-classes like `Ident`); but they have a [Kind] which can be used to
128/// determine their type. Tokens do not store the underlying character data, nor do they store their offsets. They just
129/// provide "facts" about the underlying data. In order to re-build a string, each [Token] will need to be wrapped in a
130/// [Cursor] and consult the original `&str` to get the character data. This design allows Tokens live in the stack,
131/// avoiding heap allocation as they are always `size_of` `8`. Likewise [Cursors][Cursor] are always a `size_of` `12`.
132///
133/// # Limitations
134///
135/// The [Lexer] has limitations around document sizes and token sizes, in order to keep [Token], [SourceOffset] and
136/// [Cursor] small.
137///
138/// - Documents are limited to ~4gb in size. [SourceOffset] is a [u32] so cannot represent larger offsets. Attempting to
139///   lex larger documents is considrered [undefined behaviour][2].
140///
141/// - [Tokens][Token] are limited to ~4gb in length. A [Token's][Token] is a [u32] so cannot represent larger lengths.
142///   If the lexer encounters a token with  larger length this is considered [undefined behaviour][2].
143///
144/// - Number [Tokens][Token] are limited to 16,777,216 characters in length. For example encountering a number with
145///   17MM `0`s is considered [undefined behaviour][2]. This is not the same as the number value, which is an [f32].
146///   (Please note that the CSS spec dictates numbers are f32, CSS does not have larger numbers).
147///
148/// - Dimension [Tokens][Token] are limited to 4,096 numeric characters in length and 4,096 ident characters in length.
149///   For example encountering a dimension with 4,097 `0` is considered [undefined behaviour][2].
150///
151/// # General usage
152///
153/// A parser can be implemented on top of the [Lexer] by instantiating a [Lexer] with [Lexer::new()] or
154/// [Lexer::new_with_features()] if you wish to opt-into non-spec-compliant features. The [Lexer] needs to be given a
155/// `&str` which it will reference to produce Tokens.
156///
157/// Repeatedly calling [Lexer::advance()] will move the Lexer's internal position one [Token] forward, and return the
158/// newly lexed [Token], once the end of `&str` is reached [Lexer::advance()] will repeatedly return [Token::EOF].
159///
160/// # Example
161///
162/// ```
163/// use css_lexer::*;
164/// let mut lexer = Lexer::new(&EmptyAtomSet::ATOMS, "width: 1px");
165/// assert_eq!(lexer.offset(), 0);
166/// {
167///     let token = lexer.advance();
168///     assert_eq!(token, Kind::Ident);
169///     let cursor = token.with_cursor(SourceOffset(0));
170///     assert_eq!(cursor.str_slice(lexer.source()), "width");
171/// }
172/// {
173///     let token = lexer.advance();
174///     assert_eq!(token, Kind::Colon);
175///     assert_eq!(token, ':');
176/// }
177/// {
178///     let token = lexer.advance();
179///     assert_eq!(token, Kind::Whitespace);
180/// }
181/// {
182///     let token = lexer.advance();
183///     assert_eq!(token, Kind::Dimension);
184/// }
185/// ```
186///
187/// [1]: https://drafts.csswg.org/css-syntax/#tokenization
188/// [2]: https://en.wikipedia.org/wiki/Undefined_behavior
189#[derive(Debug, Clone)]
190pub struct Lexer<'a> {
191	source: &'a str,
192	offset: SourceOffset,
193	token: Token,
194	features: Feature,
195	atoms: &'static dyn DynAtomSet,
196}
197
198impl<'a> Lexer<'a> {
199	#[inline]
200	pub fn new(atoms: &'static dyn DynAtomSet, source: &'a str) -> Self {
201		Self { source, offset: SourceOffset::default(), token: Token::default(), features: Feature::default(), atoms }
202	}
203
204	#[inline]
205	pub fn new_with_features(atoms: &'static dyn DynAtomSet, source: &'a str, features: Feature) -> Self {
206		Self { source, features, offset: SourceOffset::default(), token: Token::default(), atoms }
207	}
208
209	#[inline(always)]
210	pub fn source(&self) -> &'a str {
211		self.source
212	}
213
214	/// Is the lexer at the last token
215	pub fn at_end(&self) -> bool {
216		self.offset.0 as usize == self.source.len()
217	}
218
219	/// Current position in file
220	#[inline(always)]
221	pub const fn offset(&self) -> SourceOffset {
222		self.offset
223	}
224
225	#[inline(always)]
226	pub fn checkpoint(&self) -> Cursor {
227		Cursor::new(self.offset(), self.token)
228	}
229
230	/// Rewinds the lexer back to the given checkpoint
231	pub fn rewind(&mut self, cursor: Cursor) {
232		debug_assert!(cursor.offset() <= self.offset());
233		self.offset = cursor.offset();
234		self.token = cursor.token();
235	}
236
237	/// Advances the lexer to the end of the given token
238	pub fn hop(&mut self, cursor: Cursor) {
239		debug_assert!(cursor.offset().0 as usize >= (self.offset.0 + self.token.len()) as usize);
240		self.offset = cursor.offset();
241		self.token = cursor.token();
242	}
243
244	/// Moves the lexer one token forward, returning that token
245	pub fn advance(&mut self) -> Token {
246		self.token = self.read_next_token(self.offset.0);
247		self.offset.0 += self.token.len();
248		self.token
249	}
250}
251
252impl<'a> Iterator for Lexer<'a> {
253	type Item = Cursor;
254
255	#[inline]
256	fn next(&mut self) -> Option<Self::Item> {
257		if self.offset.0 as usize >= self.source.len() {
258			return None;
259		}
260		let offset = self.offset;
261		let token = self.advance();
262		if token.kind() == Kind::Eof { None } else { Some(token.with_cursor(offset)) }
263	}
264}
265
266#[test]
267fn size_test() {
268	assert_eq!(::std::mem::size_of::<Lexer>(), 48);
269}
270
271#[test]
272fn test_smallstr_buf_overflow() {
273	let mut source = String::from("a");
274	source.extend(std::iter::repeat_n('\0', 86));
275	let lexer = Lexer::new(&EmptyAtomSet::ATOMS, &source);
276	for cursor in lexer {
277		let _ = cursor;
278	}
279}
280
281#[cfg(test)]
282mod iterator_tests {
283	use super::*;
284
285	#[test]
286	fn test_lexer_iterator_basic() {
287		let lexer = Lexer::new(&EmptyAtomSet::ATOMS, "foo bar");
288		let cursors: Vec<_> = lexer.collect();
289		assert_eq!(cursors.len(), 3); // ident, whitespace, ident
290		assert_eq!(cursors[0], Kind::Ident);
291		assert_eq!(cursors[1], Kind::Whitespace);
292		assert_eq!(cursors[2], Kind::Ident);
293	}
294
295	#[test]
296	fn test_lexer_iterator_empty() {
297		let lexer = Lexer::new(&EmptyAtomSet::ATOMS, "");
298		let cursors: Vec<_> = lexer.collect();
299		assert_eq!(cursors.len(), 0);
300	}
301
302	#[test]
303	fn test_lexer_iterator_equivalence() {
304		let source = "width: 1px";
305
306		let lexer = Lexer::new(&EmptyAtomSet::ATOMS, source);
307		let cursors: Vec<_> = lexer.collect();
308
309		let mut lexer = Lexer::new(&EmptyAtomSet::ATOMS, source);
310		let mut manual_cursors = Vec::new();
311		while !lexer.at_end() {
312			let offset = lexer.offset();
313			let token = lexer.advance();
314			if token.kind() != Kind::Eof {
315				manual_cursors.push(token.with_cursor(offset));
316			}
317		}
318
319		assert_eq!(cursors.len(), manual_cursors.len());
320		for (c1, c2) in cursors.iter().zip(manual_cursors.iter()) {
321			assert_eq!(c1.token().kind(), c2.token().kind());
322			assert_eq!(c1.offset(), c2.offset());
323		}
324	}
325
326	#[test]
327	fn test_lexer_iterator_clone() {
328		let source = "foo bar baz";
329		let mut lexer = Lexer::new(&EmptyAtomSet::ATOMS, source);
330
331		let first = lexer.next();
332		assert!(first.is_some());
333		assert_eq!(first.unwrap(), Kind::Ident);
334
335		let lexer_clone = lexer.clone();
336
337		let cursors1: Vec<_> = lexer.collect();
338		let cursors2: Vec<_> = lexer_clone.collect();
339
340		assert_eq!(cursors1.len(), cursors2.len());
341		for (c1, c2) in cursors1.iter().zip(cursors2.iter()) {
342			assert_eq!(c1.token().kind(), c2.token().kind());
343			assert_eq!(c1.offset(), c2.offset());
344		}
345	}
346}