use crate::value::{FieldKind, FieldType, Value};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct Slug(String);
impl Slug {
pub const MAX_LEN: usize = 80;
pub fn from_title(title: &str) -> Slug {
let mut out = String::with_capacity(title.len().min(Self::MAX_LEN));
let mut gap = false;
for c in title.chars() {
if c == 'İ' {
push(&mut out, &mut gap, "i");
continue;
}
for c in c.to_lowercase() {
if c.is_ascii_alphanumeric() {
let mut buf = [0u8; 4];
push(&mut out, &mut gap, c.encode_utf8(&mut buf));
} else if c.is_whitespace() || c.is_ascii_punctuation() {
gap = true;
} else if let Some(folded) = fold(c) {
push(&mut out, &mut gap, folded);
}
}
if out.len() > Self::MAX_LEN {
break;
}
}
if out.len() > Self::MAX_LEN {
let cut = out[..=Self::MAX_LEN]
.rfind('-')
.unwrap_or(Self::MAX_LEN);
out.truncate(cut);
}
while out.ends_with('-') {
out.pop();
}
Slug(out)
}
pub fn parse(s: &str) -> Option<Slug> {
let slug = Slug::from_title(s);
(!slug.0.is_empty() && slug.0 == s).then_some(slug)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
fn push(out: &mut String, gap: &mut bool, part: &str) {
if *gap && !out.is_empty() {
out.push('-');
}
*gap = false;
out.push_str(part);
}
fn fold(c: char) -> Option<&'static str> {
Some(match c {
'ı' => "i",
'ş' | 'ș' | 'ś' | 'š' => "s",
'ğ' => "g",
'ç' | 'ć' | 'č' => "c",
'ö' | 'ò' | 'ó' | 'ô' | 'õ' | 'ø' | 'ō' | 'ő' => "o",
'ü' | 'ù' | 'ú' | 'û' | 'ū' | 'ů' | 'ű' | 'ų' => "u",
'à' | 'á' | 'â' | 'ã' | 'ä' | 'å' | 'ā' | 'ă' | 'ą' => "a",
'è' | 'é' | 'ê' | 'ë' | 'ē' | 'ė' | 'ę' | 'ě' => "e",
'ì' | 'í' | 'î' | 'ï' | 'ī' | 'į' => "i",
'ñ' | 'ń' | 'ň' => "n",
'ý' | 'ÿ' => "y",
'ź' | 'ż' | 'ž' => "z",
'ď' | 'đ' | 'ð' => "d",
'ť' | 'ţ' | 'ț' => "t",
'ł' => "l",
'ř' => "r",
'ß' => "ss",
'æ' => "ae",
'œ' => "oe",
'þ' => "th",
_ => return None,
})
}
impl fmt::Display for Slug {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Slug {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&str> for Slug {
fn from(title: &str) -> Self {
Slug::from_title(title)
}
}
impl<'de> Deserialize<'de> for Slug {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Ok(Slug::from_title(&String::deserialize(d)?))
}
}
impl FieldType for Slug {
const KIND: FieldKind = FieldKind::Text;
fn to_value(&self) -> Value {
Value::Text(self.0.clone())
}
fn __is_empty(&self) -> bool {
self.0.is_empty()
}
fn __text(&self) -> Option<&str> {
Some(&self.0)
}
}
impl crate::value::SupportsRequired for Slug {}
impl crate::value::SupportsText for Slug {}
impl crate::screen::PathValue for Slug {
fn from_segment(raw: &str) -> Option<Self> {
Slug::parse(raw)
}
}