#![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 SvelteComponentName(String);
impl SvelteComponentName {
pub fn new(input: &str) -> Result<Self, SvelteNameError> {
validate_pascal_case(input).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SvelteComponentName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for SvelteComponentName {
type Err = SvelteNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for SvelteComponentName {
type Error = SvelteNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SvelteDirectiveName(String);
impl SvelteDirectiveName {
pub fn new(input: &str) -> Result<Self, SvelteNameError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(SvelteNameError::Empty);
}
if trimmed.chars().any(char::is_whitespace) {
return Err(SvelteNameError::ContainsWhitespace);
}
if !trimmed.chars().all(is_directive_character) || trimmed.split(':').any(str::is_empty) {
return Err(SvelteNameError::InvalidDirective);
}
Ok(Self(trimmed.to_string()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SvelteDirectiveName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for SvelteDirectiveName {
type Err = SvelteNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for SvelteDirectiveName {
type Error = SvelteNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SvelteFileKind {
Component,
Page,
Layout,
Error,
Server,
Config,
}
impl SvelteFileKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Component => "component",
Self::Page => "page",
Self::Layout => "layout",
Self::Error => "error",
Self::Server => "server",
Self::Config => "config",
}
}
}
impl fmt::Display for SvelteFileKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for SvelteFileKind {
type Err = SvelteNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"component" => Ok(Self::Component),
"page" => Ok(Self::Page),
"layout" => Ok(Self::Layout),
"error" => Ok(Self::Error),
"server" => Ok(Self::Server),
"config" => Ok(Self::Config),
_ => Err(SvelteNameError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SvelteKitDirectoryKind {
Routes,
Lib,
Static,
Params,
Hooks,
Server,
}
impl SvelteKitDirectoryKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Routes => "routes",
Self::Lib => "lib",
Self::Static => "static",
Self::Params => "params",
Self::Hooks => "hooks",
Self::Server => "server",
}
}
}
impl fmt::Display for SvelteKitDirectoryKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for SvelteKitDirectoryKind {
type Err = SvelteNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"routes" => Ok(Self::Routes),
"lib" => Ok(Self::Lib),
"static" => Ok(Self::Static),
"params" => Ok(Self::Params),
"hooks" => Ok(Self::Hooks),
"server" => Ok(Self::Server),
_ => Err(SvelteNameError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SvelteKitRenderingMode {
Ssr,
Spa,
Static,
Hybrid,
}
impl SvelteKitRenderingMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Ssr => "ssr",
Self::Spa => "spa",
Self::Static => "static",
Self::Hybrid => "hybrid",
}
}
}
impl fmt::Display for SvelteKitRenderingMode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for SvelteKitRenderingMode {
type Err = SvelteNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"ssr" => Ok(Self::Ssr),
"spa" => Ok(Self::Spa),
"static" => Ok(Self::Static),
"hybrid" => Ok(Self::Hybrid),
_ => Err(SvelteNameError::UnknownLabel),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SvelteConfigFile {
SvelteConfigJs,
SvelteConfigTs,
}
impl SvelteConfigFile {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::SvelteConfigJs => "svelte.config.js",
Self::SvelteConfigTs => "svelte.config.ts",
}
}
}
impl fmt::Display for SvelteConfigFile {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for SvelteConfigFile {
type Err = SvelteNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match normalized_label(input)?.as_str() {
"svelteconfigjs" | "svelte.config.js" => Ok(Self::SvelteConfigJs),
"svelteconfigts" | "svelte.config.ts" => Ok(Self::SvelteConfigTs),
_ => Err(SvelteNameError::UnknownLabel),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SvelteNameError {
Empty,
ContainsWhitespace,
Identifier(JsIdentifierError),
NotPascalCase,
InvalidDirective,
UnknownLabel,
}
impl fmt::Display for SvelteNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("Svelte metadata text cannot be empty"),
Self::ContainsWhitespace => {
formatter.write_str("Svelte metadata text cannot contain whitespace")
}
Self::Identifier(error) => write!(formatter, "{error}"),
Self::NotPascalCase => {
formatter.write_str("Svelte component name must be `PascalCase`-shaped")
}
Self::InvalidDirective => formatter.write_str("invalid Svelte directive name"),
Self::UnknownLabel => formatter.write_str("unknown Svelte metadata label"),
}
}
}
impl Error for SvelteNameError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Identifier(error) => Some(error),
Self::Empty
| Self::ContainsWhitespace
| Self::NotPascalCase
| Self::InvalidDirective
| Self::UnknownLabel => None,
}
}
}
fn validate_pascal_case(input: &str) -> Result<String, SvelteNameError> {
let identifier = JsIdentifier::new(input).map_err(SvelteNameError::Identifier)?;
if !identifier
.as_str()
.chars()
.next()
.is_some_and(|character| character.is_ascii_uppercase())
{
return Err(SvelteNameError::NotPascalCase);
}
Ok(identifier.as_str().to_string())
}
const fn is_directive_character(character: char) -> bool {
character.is_ascii_alphanumeric() || matches!(character, ':' | '_' | '-')
}
fn normalized_label(input: &str) -> Result<String, SvelteNameError> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(SvelteNameError::Empty);
}
Ok(trimmed
.chars()
.filter(|character| !matches!(character, '-' | '_' | ' '))
.flat_map(char::to_lowercase)
.collect())
}
#[cfg(test)]
mod tests {
use super::{
SvelteComponentName, SvelteConfigFile, SvelteDirectiveName, SvelteFileKind,
SvelteKitDirectoryKind, SvelteKitRenderingMode, SvelteNameError,
};
#[test]
fn validates_component_names() -> Result<(), SvelteNameError> {
let component = SvelteComponentName::new("AppShell")?;
assert_eq!(component.as_str(), "AppShell");
assert_eq!(
SvelteComponentName::new("appShell"),
Err(SvelteNameError::NotPascalCase)
);
assert!(SvelteComponentName::new("app-shell").is_err());
Ok(())
}
#[test]
fn validates_directive_names() -> Result<(), SvelteNameError> {
let directive = SvelteDirectiveName::new("on:click")?;
assert_eq!(directive.as_str(), "on:click");
assert_eq!(SvelteDirectiveName::new(""), Err(SvelteNameError::Empty));
assert_eq!(
SvelteDirectiveName::new("on click"),
Err(SvelteNameError::ContainsWhitespace)
);
assert_eq!(
SvelteDirectiveName::new("on:"),
Err(SvelteNameError::InvalidDirective)
);
Ok(())
}
#[test]
fn parses_labels() -> Result<(), SvelteNameError> {
assert_eq!("page".parse::<SvelteFileKind>()?, SvelteFileKind::Page);
assert_eq!(
"routes".parse::<SvelteKitDirectoryKind>()?,
SvelteKitDirectoryKind::Routes
);
assert_eq!(
"ssr".parse::<SvelteKitRenderingMode>()?,
SvelteKitRenderingMode::Ssr
);
assert_eq!(
"svelte.config.ts".parse::<SvelteConfigFile>()?,
SvelteConfigFile::SvelteConfigTs
);
assert_eq!(SvelteKitRenderingMode::Hybrid.to_string(), "hybrid");
Ok(())
}
}