#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum EcmaScriptEdition {
Edition5,
Edition6,
Edition7,
Edition8,
Edition9,
Edition10,
Edition11,
Edition12,
Edition13,
Edition14,
Edition15,
}
impl EcmaScriptEdition {
#[must_use]
pub const fn number(self) -> u8 {
match self {
Self::Edition5 => 5,
Self::Edition6 => 6,
Self::Edition7 => 7,
Self::Edition8 => 8,
Self::Edition9 => 9,
Self::Edition10 => 10,
Self::Edition11 => 11,
Self::Edition12 => 12,
Self::Edition13 => 13,
Self::Edition14 => 14,
Self::Edition15 => 15,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EcmaScriptYear(u16);
impl EcmaScriptYear {
pub const fn new(year: u16) -> Result<Self, EcmaScriptParseError> {
if year >= 2015 && year <= 2024 {
Ok(Self(year))
} else {
Err(EcmaScriptParseError::UnsupportedYear)
}
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
}
impl fmt::Display for EcmaScriptYear {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
impl FromStr for EcmaScriptYear {
type Err = EcmaScriptParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(EcmaScriptParseError::Empty);
}
let year = trimmed
.parse::<u16>()
.map_err(|_error| EcmaScriptParseError::UnknownTarget)?;
Self::new(year)
}
}
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum EcmaScriptTarget {
Es5,
Es2015,
Es2016,
Es2017,
Es2018,
Es2019,
Es2020,
Es2021,
Es2022,
Es2023,
Es2024,
EsNext,
}
pub const ES5: EcmaScriptTarget = EcmaScriptTarget::Es5;
pub const ES2015: EcmaScriptTarget = EcmaScriptTarget::Es2015;
pub const ES2016: EcmaScriptTarget = EcmaScriptTarget::Es2016;
pub const ES2017: EcmaScriptTarget = EcmaScriptTarget::Es2017;
pub const ES2018: EcmaScriptTarget = EcmaScriptTarget::Es2018;
pub const ES2019: EcmaScriptTarget = EcmaScriptTarget::Es2019;
pub const ES2020: EcmaScriptTarget = EcmaScriptTarget::Es2020;
pub const ES2021: EcmaScriptTarget = EcmaScriptTarget::Es2021;
pub const ES2022: EcmaScriptTarget = EcmaScriptTarget::Es2022;
pub const ES2023: EcmaScriptTarget = EcmaScriptTarget::Es2023;
pub const ES2024: EcmaScriptTarget = EcmaScriptTarget::Es2024;
pub const ESNEXT: EcmaScriptTarget = EcmaScriptTarget::EsNext;
impl EcmaScriptTarget {
#[must_use]
pub const fn year(self) -> Option<EcmaScriptYear> {
match self {
Self::Es5 | Self::EsNext => None,
Self::Es2015 => Some(EcmaScriptYear(2015)),
Self::Es2016 => Some(EcmaScriptYear(2016)),
Self::Es2017 => Some(EcmaScriptYear(2017)),
Self::Es2018 => Some(EcmaScriptYear(2018)),
Self::Es2019 => Some(EcmaScriptYear(2019)),
Self::Es2020 => Some(EcmaScriptYear(2020)),
Self::Es2021 => Some(EcmaScriptYear(2021)),
Self::Es2022 => Some(EcmaScriptYear(2022)),
Self::Es2023 => Some(EcmaScriptYear(2023)),
Self::Es2024 => Some(EcmaScriptYear(2024)),
}
}
#[must_use]
pub const fn edition(self) -> Option<EcmaScriptEdition> {
match self {
Self::Es5 => Some(EcmaScriptEdition::Edition5),
Self::Es2015 => Some(EcmaScriptEdition::Edition6),
Self::Es2016 => Some(EcmaScriptEdition::Edition7),
Self::Es2017 => Some(EcmaScriptEdition::Edition8),
Self::Es2018 => Some(EcmaScriptEdition::Edition9),
Self::Es2019 => Some(EcmaScriptEdition::Edition10),
Self::Es2020 => Some(EcmaScriptEdition::Edition11),
Self::Es2021 => Some(EcmaScriptEdition::Edition12),
Self::Es2022 => Some(EcmaScriptEdition::Edition13),
Self::Es2023 => Some(EcmaScriptEdition::Edition14),
Self::Es2024 => Some(EcmaScriptEdition::Edition15),
Self::EsNext => None,
}
}
}
impl fmt::Display for EcmaScriptTarget {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Es5 => "ES5",
Self::Es2015 => "ES2015",
Self::Es2016 => "ES2016",
Self::Es2017 => "ES2017",
Self::Es2018 => "ES2018",
Self::Es2019 => "ES2019",
Self::Es2020 => "ES2020",
Self::Es2021 => "ES2021",
Self::Es2022 => "ES2022",
Self::Es2023 => "ES2023",
Self::Es2024 => "ES2024",
Self::EsNext => "ESNext",
})
}
}
impl FromStr for EcmaScriptTarget {
type Err = EcmaScriptParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(EcmaScriptParseError::Empty);
}
let normalized = normalize_target(trimmed);
match normalized.as_str() {
"es5" | "ecmascript5" => Ok(Self::Es5),
"es6" | "es2015" | "ecmascript2015" => Ok(Self::Es2015),
"es7" | "es2016" | "ecmascript2016" => Ok(Self::Es2016),
"es8" | "es2017" | "ecmascript2017" => Ok(Self::Es2017),
"es9" | "es2018" | "ecmascript2018" => Ok(Self::Es2018),
"es10" | "es2019" | "ecmascript2019" => Ok(Self::Es2019),
"es11" | "es2020" | "ecmascript2020" => Ok(Self::Es2020),
"es12" | "es2021" | "ecmascript2021" => Ok(Self::Es2021),
"es13" | "es2022" | "ecmascript2022" => Ok(Self::Es2022),
"es14" | "es2023" | "ecmascript2023" => Ok(Self::Es2023),
"es15" | "es2024" | "ecmascript2024" => Ok(Self::Es2024),
"esnext" | "next" => Ok(Self::EsNext),
_ => Err(EcmaScriptParseError::UnknownTarget),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EcmaScriptParseError {
Empty,
UnsupportedYear,
UnknownTarget,
}
impl fmt::Display for EcmaScriptParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("ECMAScript target cannot be empty"),
Self::UnsupportedYear => formatter.write_str("unsupported ECMAScript edition year"),
Self::UnknownTarget => formatter.write_str("unknown ECMAScript target"),
}
}
}
impl Error for EcmaScriptParseError {}
fn normalize_target(input: &str) -> String {
input
.chars()
.filter(|character| !matches!(character, '-' | '_' | ' '))
.flat_map(char::to_lowercase)
.collect()
}
#[cfg(test)]
mod tests {
use super::{ES2020, ESNEXT, EcmaScriptParseError, EcmaScriptTarget, EcmaScriptYear};
#[test]
fn parses_common_targets() -> Result<(), EcmaScriptParseError> {
assert_eq!("es2020".parse::<EcmaScriptTarget>()?, ES2020);
assert_eq!("ES2020".parse::<EcmaScriptTarget>()?, ES2020);
assert_eq!("es-next".parse::<EcmaScriptTarget>()?, ESNEXT);
assert_eq!(ES2020.to_string(), "ES2020");
Ok(())
}
#[test]
fn validates_years() {
assert_eq!(EcmaScriptYear::new(2024).map(EcmaScriptYear::get), Ok(2024));
assert_eq!(
EcmaScriptYear::new(2014),
Err(EcmaScriptParseError::UnsupportedYear)
);
}
}