#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Token<'a> {
Literal(&'a [u8]),
Keyword(&'a [u8]),
Number(f64),
Str(&'a [u8]),
Delim(u8),
}
#[derive(Debug, Clone)]
pub struct Lexer<'a> {
bytes: &'a [u8],
at: usize,
}
impl<'a> Lexer<'a> {
#[must_use]
pub fn new(bytes: &'a [u8]) -> Self {
Self { bytes, at: 0 }
}
fn skip_blanks(&mut self) {
loop {
match self.bytes.get(self.at) {
Some(b) if b.is_ascii_whitespace() => self.at = self.at.saturating_add(1),
Some(b'%') => {
while !matches!(self.bytes.get(self.at), None | Some(b'\r' | b'\n')) {
self.at = self.at.saturating_add(1);
}
}
_ => return,
}
}
}
pub fn take_binary(&mut self, n: usize) -> Option<&'a [u8]> {
if self.bytes.get(self.at).is_some_and(u8::is_ascii_whitespace) {
self.at = self.at.saturating_add(1);
}
let end = self.at.checked_add(n)?;
let out = self.bytes.get(self.at..end)?;
self.at = end;
Some(out)
}
}
impl<'a> Iterator for Lexer<'a> {
type Item = Token<'a>;
fn next(&mut self) -> Option<Token<'a>> {
self.skip_blanks();
let first = self.bytes.get(self.at).copied()?;
match first {
b'/' => {
self.at = self.at.saturating_add(1);
let start = self.at;
self.advance_over_regular();
Some(Token::Literal(
self.bytes.get(start..self.at).unwrap_or_default(),
))
}
b'[' | b']' | b'{' | b'}' => {
self.at = self.at.saturating_add(1);
Some(Token::Delim(first))
}
b'<' | b'>' => {
self.at = self.at.saturating_add(1);
if self.bytes.get(self.at) == Some(&first) {
self.at = self.at.saturating_add(1);
}
Some(Token::Delim(first))
}
b'(' => {
self.at = self.at.saturating_add(1);
let start = self.at;
let mut depth = 1usize;
while depth > 0 {
match self.bytes.get(self.at).copied() {
None => break,
Some(b'\\') => self.at = self.at.saturating_add(1),
Some(b'(') => depth = depth.saturating_add(1),
Some(b')') => depth = depth.saturating_sub(1),
Some(_) => {}
}
self.at = self.at.saturating_add(1);
}
let end = self.at.saturating_sub(usize::from(depth == 0));
Some(Token::Str(
self.bytes.get(start..end.max(start)).unwrap_or_default(),
))
}
_ => {
let start = self.at;
self.advance_over_regular();
let word = self.bytes.get(start..self.at).unwrap_or_default();
if word.is_empty() {
self.at = self.at.saturating_add(1);
return self.next();
}
Some(number(word).map_or(Token::Keyword(word), Token::Number))
}
}
}
}
impl Lexer<'_> {
fn advance_over_regular(&mut self) {
while self
.bytes
.get(self.at)
.is_some_and(|b| !b.is_ascii_whitespace() && !is_delimiter(*b))
{
self.at = self.at.saturating_add(1);
}
}
}
fn is_delimiter(b: u8) -> bool {
matches!(
b,
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
)
}
fn number(word: &[u8]) -> Option<f64> {
if word.is_empty() {
return None;
}
let mut has_digit = false;
for (i, b) in word.iter().enumerate() {
match b {
b'0'..=b'9' => has_digit = true,
b'+' | b'-' => {
let prev = i.checked_sub(1).and_then(|p| word.get(p)).copied();
if !matches!(prev, None | Some(b'e' | b'E')) {
return None;
}
}
b'.' => {}
b'e' | b'E' if has_digit => {}
_ => return None,
}
}
if !has_digit {
return None;
}
core::str::from_utf8(word).ok()?.parse().ok()
}
#[cfg(test)]
mod tests {
use super::{Lexer, Token};
fn toks(s: &[u8]) -> Vec<Token<'_>> {
Lexer::new(s).collect()
}
#[test]
fn splits_a_typical_definition() {
assert_eq!(
toks(b"/FontMatrix [ 0.001 0 0 0.001 0 0 ] readonly def"),
vec![
Token::Literal(b"FontMatrix"),
Token::Delim(b'['),
Token::Number(0.001),
Token::Number(0.0),
Token::Number(0.0),
Token::Number(0.001),
Token::Number(0.0),
Token::Number(0.0),
Token::Delim(b']'),
Token::Keyword(b"readonly"),
Token::Keyword(b"def"),
]
);
}
#[test]
fn comments_and_strings() {
assert_eq!(
toks(b"% a /comment (with parens)\n/FullName (Chrome Sans MM) readonly def"),
vec![
Token::Literal(b"FullName"),
Token::Str(b"Chrome Sans MM"),
Token::Keyword(b"readonly"),
Token::Keyword(b"def"),
]
);
}
#[test]
fn a_slash_ends_the_previous_token() {
assert_eq!(
toks(b"/A/B"),
vec![Token::Literal(b"A"), Token::Literal(b"B")]
);
}
#[test]
fn binary_payload_is_taken_verbatim() {
let src = b"/g 5 RD \x00\x20/\x7dabc ND";
let mut lx = Lexer::new(src);
assert_eq!(lx.next(), Some(Token::Literal(b"g")));
assert_eq!(lx.next(), Some(Token::Number(5.0)));
assert_eq!(lx.next(), Some(Token::Keyword(b"RD")));
assert_eq!(lx.take_binary(5), Some(b"\x00\x20/\x7da".as_slice()));
assert_eq!(lx.next(), Some(Token::Keyword(b"bc")));
}
#[test]
fn keywords_that_look_like_numbers_are_not() {
assert_eq!(
toks(b"NaN inf -- 1e 1e3 -2.5"),
vec![
Token::Keyword(b"NaN"),
Token::Keyword(b"inf"),
Token::Keyword(b"--"),
Token::Keyword(b"1e"),
Token::Number(1000.0),
Token::Number(-2.5),
]
);
}
#[test]
fn unterminated_string_yields_the_rest() {
assert_eq!(toks(b"(no close"), vec![Token::Str(b"no close")]);
}
#[test]
fn a_stray_close_paren_does_not_stall() {
assert_eq!(toks(b") def"), vec![Token::Keyword(b"def")]);
}
}