1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6use use_js_identifier::{JsIdentifier, JsIdentifierError};
7
8#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct LitElementName(String);
11
12impl LitElementName {
13 pub fn new(input: &str) -> Result<Self, LitNameError> {
19 let trimmed = input.trim();
20 if trimmed.is_empty() {
21 return Err(LitNameError::Empty);
22 }
23 if trimmed.chars().any(char::is_whitespace) {
24 return Err(LitNameError::ContainsWhitespace);
25 }
26 if !is_custom_element_name(trimmed) {
27 return Err(LitNameError::InvalidElementName);
28 }
29 Ok(Self(trimmed.to_string()))
30 }
31
32 #[must_use]
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37}
38
39impl fmt::Display for LitElementName {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 formatter.write_str(self.as_str())
42 }
43}
44
45impl FromStr for LitElementName {
46 type Err = LitNameError;
47
48 fn from_str(input: &str) -> Result<Self, Self::Err> {
49 Self::new(input)
50 }
51}
52
53impl TryFrom<&str> for LitElementName {
54 type Error = LitNameError;
55
56 fn try_from(value: &str) -> Result<Self, Self::Error> {
57 Self::new(value)
58 }
59}
60
61#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
63pub struct LitPropertyName(String);
64
65impl LitPropertyName {
66 pub fn new(input: &str) -> Result<Self, LitNameError> {
72 let identifier = JsIdentifier::new(input).map_err(LitNameError::Identifier)?;
73 Ok(Self(identifier.as_str().to_string()))
74 }
75
76 #[must_use]
78 pub fn as_str(&self) -> &str {
79 &self.0
80 }
81}
82
83impl fmt::Display for LitPropertyName {
84 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85 formatter.write_str(self.as_str())
86 }
87}
88
89impl FromStr for LitPropertyName {
90 type Err = LitNameError;
91
92 fn from_str(input: &str) -> Result<Self, Self::Err> {
93 Self::new(input)
94 }
95}
96
97impl TryFrom<&str> for LitPropertyName {
98 type Error = LitNameError;
99
100 fn try_from(value: &str) -> Result<Self, Self::Error> {
101 Self::new(value)
102 }
103}
104
105#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
107pub struct LitDecoratorName(String);
108
109impl LitDecoratorName {
110 pub fn new(input: &str) -> Result<Self, LitNameError> {
116 let trimmed = input.trim();
117 let decorator = trimmed.strip_prefix('@').unwrap_or(trimmed);
118 if decorator.is_empty() {
119 return Err(LitNameError::Empty);
120 }
121 if decorator.chars().any(char::is_whitespace) {
122 return Err(LitNameError::ContainsWhitespace);
123 }
124 if !decorator.chars().all(is_decorator_character) {
125 return Err(LitNameError::InvalidDecoratorName);
126 }
127 Ok(Self(decorator.to_string()))
128 }
129
130 #[must_use]
132 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135}
136
137impl fmt::Display for LitDecoratorName {
138 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
139 formatter.write_str(self.as_str())
140 }
141}
142
143impl FromStr for LitDecoratorName {
144 type Err = LitNameError;
145
146 fn from_str(input: &str) -> Result<Self, Self::Err> {
147 Self::new(input)
148 }
149}
150
151impl TryFrom<&str> for LitDecoratorName {
152 type Error = LitNameError;
153
154 fn try_from(value: &str) -> Result<Self, Self::Error> {
155 Self::new(value)
156 }
157}
158
159#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
161pub enum LitFileKind {
162 Element,
163 Template,
164 Styles,
165 Controller,
166 Directive,
167 Decorator,
168}
169
170impl LitFileKind {
171 #[must_use]
173 pub const fn as_str(self) -> &'static str {
174 match self {
175 Self::Element => "element",
176 Self::Template => "template",
177 Self::Styles => "styles",
178 Self::Controller => "controller",
179 Self::Directive => "directive",
180 Self::Decorator => "decorator",
181 }
182 }
183}
184
185impl fmt::Display for LitFileKind {
186 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
187 formatter.write_str(self.as_str())
188 }
189}
190
191impl FromStr for LitFileKind {
192 type Err = LitNameError;
193
194 fn from_str(input: &str) -> Result<Self, Self::Err> {
195 match normalized_label(input)?.as_str() {
196 "element" => Ok(Self::Element),
197 "template" => Ok(Self::Template),
198 "styles" | "style" => Ok(Self::Styles),
199 "controller" => Ok(Self::Controller),
200 "directive" => Ok(Self::Directive),
201 "decorator" => Ok(Self::Decorator),
202 _ => Err(LitNameError::UnknownLabel),
203 }
204 }
205}
206
207#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
209pub enum LitTemplateKind {
210 Html,
211 Svg,
212 Css,
213 StaticHtml,
214}
215
216impl LitTemplateKind {
217 #[must_use]
219 pub const fn as_str(self) -> &'static str {
220 match self {
221 Self::Html => "html",
222 Self::Svg => "svg",
223 Self::Css => "css",
224 Self::StaticHtml => "static-html",
225 }
226 }
227}
228
229impl fmt::Display for LitTemplateKind {
230 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231 formatter.write_str(self.as_str())
232 }
233}
234
235impl FromStr for LitTemplateKind {
236 type Err = LitNameError;
237
238 fn from_str(input: &str) -> Result<Self, Self::Err> {
239 match normalized_label(input)?.as_str() {
240 "html" => Ok(Self::Html),
241 "svg" => Ok(Self::Svg),
242 "css" => Ok(Self::Css),
243 "statichtml" => Ok(Self::StaticHtml),
244 _ => Err(LitNameError::UnknownLabel),
245 }
246 }
247}
248
249#[derive(Clone, Debug, Eq, PartialEq)]
251pub enum LitNameError {
252 Empty,
253 ContainsWhitespace,
254 Identifier(JsIdentifierError),
255 InvalidElementName,
256 InvalidDecoratorName,
257 UnknownLabel,
258}
259
260impl fmt::Display for LitNameError {
261 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
262 match self {
263 Self::Empty => formatter.write_str("Lit metadata text cannot be empty"),
264 Self::ContainsWhitespace => {
265 formatter.write_str("Lit metadata text cannot contain whitespace")
266 }
267 Self::Identifier(error) => write!(formatter, "{error}"),
268 Self::InvalidElementName => formatter.write_str("invalid Lit custom element name"),
269 Self::InvalidDecoratorName => formatter.write_str("invalid Lit decorator name"),
270 Self::UnknownLabel => formatter.write_str("unknown Lit metadata label"),
271 }
272 }
273}
274
275impl Error for LitNameError {
276 fn source(&self) -> Option<&(dyn Error + 'static)> {
277 match self {
278 Self::Identifier(error) => Some(error),
279 Self::Empty
280 | Self::ContainsWhitespace
281 | Self::InvalidElementName
282 | Self::InvalidDecoratorName
283 | Self::UnknownLabel => None,
284 }
285 }
286}
287
288fn is_custom_element_name(input: &str) -> bool {
289 input.contains('-')
290 && input.split('-').all(|segment| !segment.is_empty())
291 && input.chars().all(|character| {
292 character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
293 })
294}
295
296const fn is_decorator_character(character: char) -> bool {
297 character.is_ascii_alphanumeric() || character == '_'
298}
299
300fn normalized_label(input: &str) -> Result<String, LitNameError> {
301 let trimmed = input.trim();
302 if trimmed.is_empty() {
303 return Err(LitNameError::Empty);
304 }
305 Ok(trimmed
306 .chars()
307 .filter(|character| !matches!(character, '-' | '_' | ' '))
308 .flat_map(char::to_lowercase)
309 .collect())
310}
311
312#[cfg(test)]
313mod tests {
314 use super::{
315 LitDecoratorName, LitElementName, LitFileKind, LitNameError, LitPropertyName,
316 LitTemplateKind,
317 };
318
319 #[test]
320 fn validates_element_names() -> Result<(), LitNameError> {
321 let element = LitElementName::new("app-shell")?;
322 assert_eq!(element.as_str(), "app-shell");
323 assert_eq!(
324 LitElementName::new("app"),
325 Err(LitNameError::InvalidElementName)
326 );
327 assert_eq!(
328 LitElementName::new("AppShell"),
329 Err(LitNameError::InvalidElementName)
330 );
331 assert_eq!(
332 LitElementName::new("app shell"),
333 Err(LitNameError::ContainsWhitespace)
334 );
335 Ok(())
336 }
337
338 #[test]
339 fn validates_property_and_decorator_names() -> Result<(), LitNameError> {
340 assert_eq!(LitPropertyName::new("isOpen")?.as_str(), "isOpen");
341 assert_eq!(LitDecoratorName::new("@property")?.as_str(), "property");
342 assert!(LitPropertyName::new("is-open").is_err());
343 assert_eq!(
344 LitDecoratorName::new("@custom-element"),
345 Err(LitNameError::InvalidDecoratorName)
346 );
347 Ok(())
348 }
349
350 #[test]
351 fn parses_labels() -> Result<(), LitNameError> {
352 assert_eq!(
353 "controller".parse::<LitFileKind>()?,
354 LitFileKind::Controller
355 );
356 assert_eq!(
357 "static-html".parse::<LitTemplateKind>()?,
358 LitTemplateKind::StaticHtml
359 );
360 assert_eq!(LitTemplateKind::Html.to_string(), "html");
361 Ok(())
362 }
363}