#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
use use_js_identifier::{JsIdentifier, JsIdentifierError};
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LitElementName(String);
impl LitElementName {
pub fn new(input: &str) -> Result<Self, LitNameError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(LitNameError::Empty);
}
if trimmed.chars().any(char::is_whitespace) {
return Err(LitNameError::ContainsWhitespace);
}
if !is_custom_element_name(trimmed) {
return Err(LitNameError::InvalidElementName);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for LitElementName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LitElementName {
type Err = LitNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for LitElementName {
type Error = LitNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LitPropertyName(String);
impl LitPropertyName {
pub fn new(input: &str) -> Result<Self, LitNameError> {
let identifier = JsIdentifier::new(input).map_err(LitNameError::Identifier)?;
Ok(Self(identifier.as_str().to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for LitPropertyName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LitPropertyName {
type Err = LitNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for LitPropertyName {
type Error = LitNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LitDecoratorName(String);
impl LitDecoratorName {
pub fn new(input: &str) -> Result<Self, LitNameError> {
let trimmed = input.trim();
let decorator = trimmed.strip_prefix('@').unwrap_or(trimmed);
if decorator.is_empty() {
return Err(LitNameError::Empty);
}
if decorator.chars().any(char::is_whitespace) {
return Err(LitNameError::ContainsWhitespace);
}
if !decorator.chars().all(is_decorator_character) {
return Err(LitNameError::InvalidDecoratorName);
}
Ok(Self(decorator.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for LitDecoratorName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LitDecoratorName {
type Err = LitNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for LitDecoratorName {
type Error = LitNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum LitFileKind {
Element,
Template,
Styles,
Controller,
Directive,
Decorator,
}
impl LitFileKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Element => "element",
Self::Template => "template",
Self::Styles => "styles",
Self::Controller => "controller",
Self::Directive => "directive",
Self::Decorator => "decorator",
}
}
}
impl fmt::Display for LitFileKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LitFileKind {
type Err = LitNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"element" => Ok(Self::Element),
"template" => Ok(Self::Template),
"styles" | "style" => Ok(Self::Styles),
"controller" => Ok(Self::Controller),
"directive" => Ok(Self::Directive),
"decorator" => Ok(Self::Decorator),
_ => Err(LitNameError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum LitTemplateKind {
Html,
Svg,
Css,
StaticHtml,
}
impl LitTemplateKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Html => "html",
Self::Svg => "svg",
Self::Css => "css",
Self::StaticHtml => "static-html",
}
}
}
impl fmt::Display for LitTemplateKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LitTemplateKind {
type Err = LitNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"html" => Ok(Self::Html),
"svg" => Ok(Self::Svg),
"css" => Ok(Self::Css),
"statichtml" => Ok(Self::StaticHtml),
_ => Err(LitNameError::UnknownLabel),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum LitNameError {
Empty,
ContainsWhitespace,
Identifier(JsIdentifierError),
InvalidElementName,
InvalidDecoratorName,
UnknownLabel,
}
impl fmt::Display for LitNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Lit metadata text cannot be empty"),
Self::ContainsWhitespace => {
formatter.write_str("Lit metadata text cannot contain whitespace")
}
Self::Identifier(error) => write!(formatter, "{error}"),
Self::InvalidElementName => formatter.write_str("invalid Lit custom element name"),
Self::InvalidDecoratorName => formatter.write_str("invalid Lit decorator name"),
Self::UnknownLabel => formatter.write_str("unknown Lit metadata label"),
}
}
}
impl Error for LitNameError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Identifier(error) => Some(error),
Self::Empty
| Self::ContainsWhitespace
| Self::InvalidElementName
| Self::InvalidDecoratorName
| Self::UnknownLabel => None,
}
}
}
fn is_custom_element_name(input: &str) -> bool {
input.contains('-')
&& input.split('-').all(|segment| !segment.is_empty())
&& input.chars().all(|character| {
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '-'
})
}
const fn is_decorator_character(character: char) -> bool {
character.is_ascii_alphanumeric() || character == '_'
}
fn normalized_label(input: &str) -> Result<String, LitNameError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(LitNameError::Empty);
}
Ok(trimmed
.chars()
.filter(|character| !matches!(character, '-' | '_' | ' '))
.flat_map(char::to_lowercase)
.collect())
}
#[cfg(test)]
mod tests {
use super::{
LitDecoratorName, LitElementName, LitFileKind, LitNameError, LitPropertyName,
LitTemplateKind,
};
#[test]
fn validates_element_names() -> Result<(), LitNameError> {
let element = LitElementName::new("app-shell")?;
assert_eq!(element.as_str(), "app-shell");
assert_eq!(
LitElementName::new("app"),
Err(LitNameError::InvalidElementName)
);
assert_eq!(
LitElementName::new("AppShell"),
Err(LitNameError::InvalidElementName)
);
assert_eq!(
LitElementName::new("app shell"),
Err(LitNameError::ContainsWhitespace)
);
Ok(())
}
#[test]
fn validates_property_and_decorator_names() -> Result<(), LitNameError> {
assert_eq!(LitPropertyName::new("isOpen")?.as_str(), "isOpen");
assert_eq!(LitDecoratorName::new("@property")?.as_str(), "property");
assert!(LitPropertyName::new("is-open").is_err());
assert_eq!(
LitDecoratorName::new("@custom-element"),
Err(LitNameError::InvalidDecoratorName)
);
Ok(())
}
#[test]
fn parses_labels() -> Result<(), LitNameError> {
assert_eq!(
"controller".parse::<LitFileKind>()?,
LitFileKind::Controller
);
assert_eq!(
"static-html".parse::<LitTemplateKind>()?,
LitTemplateKind::StaticHtml
);
assert_eq!(LitTemplateKind::Html.to_string(), "html");
Ok(())
}
}