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}