loess 0.2.5

Small proc macro grammar- and parser-generator providing great UX.
Documentation
use proc_macro2::{Ident, Punct, Spacing, Span, TokenStream, TokenTree};

use crate::{Error, ErrorPriority, Errors, Input, IntoTokens, PeekFrom, PopFrom, SimpleSpanned};

/// `->`
#[derive(Clone)]
pub struct RArrow(pub Punct, pub Punct);

impl Default for RArrow {
	fn default() -> Self {
		Self(
			Punct::new('-', Spacing::Joint).with_span(Span::mixed_site()),
			Punct::new('>', Spacing::Alone).with_span(Span::mixed_site()),
		)
	}
}

/// `->`
impl PeekFrom for RArrow {
	fn peek_from(input: &Input) -> bool {
		input.peek(|tts, _| {
			matches!(tts, [TokenTree::Punct(minus), TokenTree::Punct(gt)]
			if minus.as_char() == '-' && minus.spacing() == Spacing::Joint && gt.as_char() == '>')
		})
	}
}

impl PopFrom for RArrow {
	fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()> {
		input
			.pop_or_replace(|tts, _| match tts {
				[TokenTree::Punct(minus), TokenTree::Punct(gt)]
					if minus.as_char() == '-'
						&& minus.spacing() == Spacing::Joint
						&& gt.as_char() == '>' =>
				{
					Ok(Self(minus, gt))
				}
				other => Err(other),
			})
			.map_err(|spans| {
				errors.push(Error::new(ErrorPriority::GRAMMAR, "Expected `->`.", spans))
			})
	}
}

impl IntoTokens for RArrow {
	fn into_tokens(self, _root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
		let Self(minus, gt) = self;
		tokens.extend([minus.into(), gt.into()])
	}
}

/// `..`
#[derive(Clone)]
pub struct DotDot(pub Punct, pub Punct);

impl Default for DotDot {
	fn default() -> Self {
		Self(
			Punct::new('.', Spacing::Joint).with_span(Span::mixed_site()),
			Punct::new('.', Spacing::Alone).with_span(Span::mixed_site()),
		)
	}
}

/// `..`
impl PeekFrom for DotDot {
	fn peek_from(input: &Input) -> bool {
		input.peek(|tts, mut rest| {
			matches!(tts, [TokenTree::Punct(dot0), TokenTree::Punct(dot1)]
			if dot0.as_char() == '.'
				&& dot0.spacing() == Spacing::Joint
				&& dot1.as_char() == '.'
				&& (dot1.spacing() == Spacing::Alone || !matches!(rest.next(), Some(TokenTree::Punct(next_punct)) if matches!(next_punct.as_char(), '.' | '='))))
		})
	}
}

impl PopFrom for DotDot {
	fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()> {
		input
			.pop_or_replace(|tts, rest| match tts {
				[TokenTree::Punct(dot0), TokenTree::Punct(dot1)]
					if dot0.as_char() == '.'
						&& dot0.spacing() == Spacing::Joint
						&& dot1.as_char() == '.'
						&& (dot1.spacing() == Spacing::Alone || !matches!(rest.front(), Some(TokenTree::Punct(next_punct)) if matches!(next_punct.as_char(), '.' | '=')))
					=> { Ok(Self(dot0, dot1)) }
				other => Err(other),
			})
			.map_err(|spans| {
				errors.push(Error::new(ErrorPriority::GRAMMAR, "Expected `..`.", spans))
			})
	}
}

impl IntoTokens for DotDot {
	fn into_tokens(self, _root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
		let Self(dot0, dot1) = self;
		tokens.extend([dot0.into(), dot1.into()])
	}
}

/// `;`
#[derive(Clone)]
pub struct Semi(pub Punct);

impl Default for Semi {
	fn default() -> Self {
		Self(Punct::new(';', Spacing::Alone).with_span(Span::mixed_site()))
	}
}

/// `;`
impl PeekFrom for Semi {
	fn peek_from(input: &Input) -> bool {
		matches!(
			input.front(),
			Some(TokenTree::Punct(semi)) if semi.as_char() == ';',
		)
	}
}

impl PopFrom for Semi {
	fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()> {
		input
			.pop_or_replace(|tts, _| match tts {
				[TokenTree::Punct(semi)] if semi.as_char() == ';' => Ok(Self(semi)),
				other => Err(other),
			})
			.map_err(|spans| {
				errors.push(Error::new(ErrorPriority::GRAMMAR, "Expected `;`.", spans))
			})
	}
}

impl IntoTokens for Semi {
	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
		self.0.into_tokens(root, tokens)
	}
}

/// `,`
#[derive(Clone)]
pub struct Comma(pub Punct);

impl Default for Comma {
	fn default() -> Self {
		Self(Punct::new(',', Spacing::Alone).with_span(Span::mixed_site()))
	}
}

/// `,`
impl PeekFrom for Comma {
	fn peek_from(input: &Input) -> bool {
		matches!(
			input.front(),
			Some(TokenTree::Punct(comma)) if comma.as_char() == ',',
		)
	}
}

impl PopFrom for Comma {
	fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()> {
		input
			.pop_or_replace(|tts, _| match tts {
				[TokenTree::Punct(comma)] if comma.as_char() == ',' => Ok(Self(comma)),
				other => Err(other),
			})
			.map_err(|spans| {
				errors.push(Error::new(ErrorPriority::GRAMMAR, "Expected `,`.", spans))
			})
	}
}

impl IntoTokens for Comma {
	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
		self.0.into_tokens(root, tokens)
	}
}

/// `|`
#[derive(Clone)]
pub struct Or(pub Punct);

impl Default for Or {
	fn default() -> Self {
		Self(Punct::new('|', Spacing::Alone).with_span(Span::mixed_site()))
	}
}

/// `|`
impl PeekFrom for Or {
	fn peek_from(input: &Input) -> bool {
		matches!(
			input.front(),
			Some(TokenTree::Punct(or)) if or.as_char() == '|' && or.spacing() == Spacing::Alone,
		)
	}
}

impl PopFrom for Or {
	fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()> {
		input
			.pop_or_replace(|tts, _| match tts {
				[TokenTree::Punct(or)] if or.as_char() == '|' && or.spacing() == Spacing::Alone => {
					Ok(Self(or))
				}
				other => Err(other),
			})
			.map_err(|spans| {
				errors.push(Error::new(ErrorPriority::GRAMMAR, "Expected `|`.", spans))
			})
	}
}

impl IntoTokens for Or {
	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
		self.0.into_tokens(root, tokens)
	}
}

/// `.`
#[derive(Clone)]
pub struct Dot(pub Punct);

impl Default for Dot {
	fn default() -> Self {
		Self(Punct::new('.', Spacing::Alone).with_span(Span::mixed_site()))
	}
}

/// `.`
impl PeekFrom for Dot {
	fn peek_from(input: &Input) -> bool {
		matches!(
			input.front(),
			Some(TokenTree::Punct(dot)) if dot.as_char() == '.' && dot.spacing() == Spacing::Alone,
		)
	}
}

impl PopFrom for Dot {
	fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()> {
		input
			.pop_or_replace(|tts, _| match tts {
				[TokenTree::Punct(dot)]
					if dot.as_char() == '.' && dot.spacing() == Spacing::Alone =>
				{
					Ok(Self(dot))
				}
				other => Err(other),
			})
			.map_err(|spans| {
				errors.push(Error::new(ErrorPriority::GRAMMAR, "Expected `.`.", spans))
			})
	}
}

impl IntoTokens for Dot {
	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
		self.0.into_tokens(root, tokens)
	}
}

/// `:`
#[derive(Clone)]
pub struct Colon(pub Punct);

impl Default for Colon {
	fn default() -> Self {
		Self(Punct::new(':', Spacing::Alone).with_span(Span::mixed_site()))
	}
}

/// `:`
impl PeekFrom for Colon {
	fn peek_from(input: &Input) -> bool {
		matches!(
			input.front(),
			Some(TokenTree::Punct(colon)) if colon.as_char() == ':' && colon.spacing() == Spacing::Alone,
		)
	}
}

impl PopFrom for Colon {
	fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()> {
		input
			.pop_or_replace(|tts, _| match tts {
				[TokenTree::Punct(colon)]
					if colon.as_char() == ':' && colon.spacing() == Spacing::Alone =>
				{
					Ok(Self(colon))
				}
				other => Err(other),
			})
			.map_err(|spans| {
				errors.push(Error::new(ErrorPriority::GRAMMAR, "Expected `:`.", spans))
			})
	}
}

impl IntoTokens for Colon {
	fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
		self.0.into_tokens(root, tokens)
	}
}

macro_rules! ident_token {
	($name:ident = $str:literal) => {
		#[doc = concat!("`", $str, "`")]
		#[derive(Clone)]
		pub struct $name(pub Ident);

		#[doc = concat!("`", $str, "`")]
		impl PeekFrom for $name {
			fn peek_from(input: &Input) -> bool {
				matches!(input.front(), Some(TokenTree::Ident(ident)) if ident == $str)
			}
		}

		impl PopFrom for $name {
			fn pop_from(input: &mut Input, errors: &mut Errors) -> Result<Self, ()>
			where
				Self: Sized,
			{
				input
					.pop_or_replace(|t, _| match t {
						[TokenTree::Ident(ident)] if ident == $str => Ok(Self(ident)),
						other => Err(other),
					})
					.map_err(|spans| {
						errors.push(Error::new(ErrorPriority::TOKEN, concat!("Expected `", $str, "`."), spans))
					})
			}
		}

		impl IntoTokens for $name {
			fn into_tokens(self, root: &TokenStream, tokens: &mut impl Extend<TokenTree>) {
				self.0.into_tokens(root, tokens)
			}
		}
	};
}

ident_token!(As = "as");
ident_token!(Async = "async");
ident_token!(Await = "await");
ident_token!(Box = "box");
ident_token!(Const = "const");
ident_token!(For = "for");
ident_token!(In = "in");
ident_token!(Let = "let");
ident_token!(Pub = "pub");
ident_token!(SelfLowercase = "self");
ident_token!(Struct = "struct");