1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
9#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub enum EcmaScriptEdition {
11 Edition5,
12 Edition6,
13 Edition7,
14 Edition8,
15 Edition9,
16 Edition10,
17 Edition11,
18 Edition12,
19 Edition13,
20 Edition14,
21 Edition15,
22}
23
24impl EcmaScriptEdition {
25 #[must_use]
27 pub const fn number(self) -> u8 {
28 match self {
29 Self::Edition5 => 5,
30 Self::Edition6 => 6,
31 Self::Edition7 => 7,
32 Self::Edition8 => 8,
33 Self::Edition9 => 9,
34 Self::Edition10 => 10,
35 Self::Edition11 => 11,
36 Self::Edition12 => 12,
37 Self::Edition13 => 13,
38 Self::Edition14 => 14,
39 Self::Edition15 => 15,
40 }
41 }
42}
43
44#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
46#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
47pub struct EcmaScriptYear(u16);
48
49impl EcmaScriptYear {
50 pub const fn new(year: u16) -> Result<Self, EcmaScriptParseError> {
56 if year >= 2015 && year <= 2024 {
57 Ok(Self(year))
58 } else {
59 Err(EcmaScriptParseError::UnsupportedYear)
60 }
61 }
62
63 #[must_use]
65 pub const fn get(self) -> u16 {
66 self.0
67 }
68}
69
70impl fmt::Display for EcmaScriptYear {
71 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(formatter, "{}", self.0)
73 }
74}
75
76impl FromStr for EcmaScriptYear {
77 type Err = EcmaScriptParseError;
78
79 fn from_str(input: &str) -> Result<Self, Self::Err> {
80 let trimmed = input.trim();
81 if trimmed.is_empty() {
82 return Err(EcmaScriptParseError::Empty);
83 }
84
85 let year = trimmed
86 .parse::<u16>()
87 .map_err(|_error| EcmaScriptParseError::UnknownTarget)?;
88 Self::new(year)
89 }
90}
91
92#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
94#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
95pub enum EcmaScriptTarget {
96 Es5,
97 Es2015,
98 Es2016,
99 Es2017,
100 Es2018,
101 Es2019,
102 Es2020,
103 Es2021,
104 Es2022,
105 Es2023,
106 Es2024,
107 EsNext,
108}
109
110pub const ES5: EcmaScriptTarget = EcmaScriptTarget::Es5;
111pub const ES2015: EcmaScriptTarget = EcmaScriptTarget::Es2015;
112pub const ES2016: EcmaScriptTarget = EcmaScriptTarget::Es2016;
113pub const ES2017: EcmaScriptTarget = EcmaScriptTarget::Es2017;
114pub const ES2018: EcmaScriptTarget = EcmaScriptTarget::Es2018;
115pub const ES2019: EcmaScriptTarget = EcmaScriptTarget::Es2019;
116pub const ES2020: EcmaScriptTarget = EcmaScriptTarget::Es2020;
117pub const ES2021: EcmaScriptTarget = EcmaScriptTarget::Es2021;
118pub const ES2022: EcmaScriptTarget = EcmaScriptTarget::Es2022;
119pub const ES2023: EcmaScriptTarget = EcmaScriptTarget::Es2023;
120pub const ES2024: EcmaScriptTarget = EcmaScriptTarget::Es2024;
121pub const ESNEXT: EcmaScriptTarget = EcmaScriptTarget::EsNext;
122
123impl EcmaScriptTarget {
124 #[must_use]
126 pub const fn year(self) -> Option<EcmaScriptYear> {
127 match self {
128 Self::Es5 | Self::EsNext => None,
129 Self::Es2015 => Some(EcmaScriptYear(2015)),
130 Self::Es2016 => Some(EcmaScriptYear(2016)),
131 Self::Es2017 => Some(EcmaScriptYear(2017)),
132 Self::Es2018 => Some(EcmaScriptYear(2018)),
133 Self::Es2019 => Some(EcmaScriptYear(2019)),
134 Self::Es2020 => Some(EcmaScriptYear(2020)),
135 Self::Es2021 => Some(EcmaScriptYear(2021)),
136 Self::Es2022 => Some(EcmaScriptYear(2022)),
137 Self::Es2023 => Some(EcmaScriptYear(2023)),
138 Self::Es2024 => Some(EcmaScriptYear(2024)),
139 }
140 }
141
142 #[must_use]
144 pub const fn edition(self) -> Option<EcmaScriptEdition> {
145 match self {
146 Self::Es5 => Some(EcmaScriptEdition::Edition5),
147 Self::Es2015 => Some(EcmaScriptEdition::Edition6),
148 Self::Es2016 => Some(EcmaScriptEdition::Edition7),
149 Self::Es2017 => Some(EcmaScriptEdition::Edition8),
150 Self::Es2018 => Some(EcmaScriptEdition::Edition9),
151 Self::Es2019 => Some(EcmaScriptEdition::Edition10),
152 Self::Es2020 => Some(EcmaScriptEdition::Edition11),
153 Self::Es2021 => Some(EcmaScriptEdition::Edition12),
154 Self::Es2022 => Some(EcmaScriptEdition::Edition13),
155 Self::Es2023 => Some(EcmaScriptEdition::Edition14),
156 Self::Es2024 => Some(EcmaScriptEdition::Edition15),
157 Self::EsNext => None,
158 }
159 }
160}
161
162impl fmt::Display for EcmaScriptTarget {
163 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164 formatter.write_str(match self {
165 Self::Es5 => "ES5",
166 Self::Es2015 => "ES2015",
167 Self::Es2016 => "ES2016",
168 Self::Es2017 => "ES2017",
169 Self::Es2018 => "ES2018",
170 Self::Es2019 => "ES2019",
171 Self::Es2020 => "ES2020",
172 Self::Es2021 => "ES2021",
173 Self::Es2022 => "ES2022",
174 Self::Es2023 => "ES2023",
175 Self::Es2024 => "ES2024",
176 Self::EsNext => "ESNext",
177 })
178 }
179}
180
181impl FromStr for EcmaScriptTarget {
182 type Err = EcmaScriptParseError;
183
184 fn from_str(input: &str) -> Result<Self, Self::Err> {
185 let trimmed = input.trim();
186 if trimmed.is_empty() {
187 return Err(EcmaScriptParseError::Empty);
188 }
189
190 let normalized = normalize_target(trimmed);
191 match normalized.as_str() {
192 "es5" | "ecmascript5" => Ok(Self::Es5),
193 "es6" | "es2015" | "ecmascript2015" => Ok(Self::Es2015),
194 "es7" | "es2016" | "ecmascript2016" => Ok(Self::Es2016),
195 "es8" | "es2017" | "ecmascript2017" => Ok(Self::Es2017),
196 "es9" | "es2018" | "ecmascript2018" => Ok(Self::Es2018),
197 "es10" | "es2019" | "ecmascript2019" => Ok(Self::Es2019),
198 "es11" | "es2020" | "ecmascript2020" => Ok(Self::Es2020),
199 "es12" | "es2021" | "ecmascript2021" => Ok(Self::Es2021),
200 "es13" | "es2022" | "ecmascript2022" => Ok(Self::Es2022),
201 "es14" | "es2023" | "ecmascript2023" => Ok(Self::Es2023),
202 "es15" | "es2024" | "ecmascript2024" => Ok(Self::Es2024),
203 "esnext" | "next" => Ok(Self::EsNext),
204 _ => Err(EcmaScriptParseError::UnknownTarget),
205 }
206 }
207}
208
209#[derive(Clone, Copy, Debug, Eq, PartialEq)]
211pub enum EcmaScriptParseError {
212 Empty,
213 UnsupportedYear,
214 UnknownTarget,
215}
216
217impl fmt::Display for EcmaScriptParseError {
218 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219 match self {
220 Self::Empty => formatter.write_str("ECMAScript target cannot be empty"),
221 Self::UnsupportedYear => formatter.write_str("unsupported ECMAScript edition year"),
222 Self::UnknownTarget => formatter.write_str("unknown ECMAScript target"),
223 }
224 }
225}
226
227impl Error for EcmaScriptParseError {}
228
229fn normalize_target(input: &str) -> String {
230 input
231 .chars()
232 .filter(|character| !matches!(character, '-' | '_' | ' '))
233 .flat_map(char::to_lowercase)
234 .collect()
235}
236
237#[cfg(test)]
238mod tests {
239 use super::{ES2020, ESNEXT, EcmaScriptParseError, EcmaScriptTarget, EcmaScriptYear};
240
241 #[test]
242 fn parses_common_targets() -> Result<(), EcmaScriptParseError> {
243 assert_eq!("es2020".parse::<EcmaScriptTarget>()?, ES2020);
244 assert_eq!("ES2020".parse::<EcmaScriptTarget>()?, ES2020);
245 assert_eq!("es-next".parse::<EcmaScriptTarget>()?, ESNEXT);
246 assert_eq!(ES2020.to_string(), "ES2020");
247 Ok(())
248 }
249
250 #[test]
251 fn validates_years() {
252 assert_eq!(EcmaScriptYear::new(2024).map(EcmaScriptYear::get), Ok(2024));
253 assert_eq!(
254 EcmaScriptYear::new(2014),
255 Err(EcmaScriptParseError::UnsupportedYear)
256 );
257 }
258}