use serde::{Deserialize, Serialize};
use super::HyphenationStyle;
use crate::String;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AttributeHyphenationConfig {
Style(HyphenationStyle),
Options(AttributeHyphenationOptions),
}
impl AttributeHyphenationConfig {
pub fn style(&self) -> HyphenationStyle {
match self {
Self::Style(style) => *style,
Self::Options(options) => options.style,
}
}
pub fn ignore(&self) -> Option<&[String]> {
match self {
Self::Style(_) => None,
Self::Options(options) => Some(&options.ignore),
}
}
pub fn ignore_tags(&self) -> Option<&[String]> {
match self {
Self::Style(_) => None,
Self::Options(options) => Some(&options.ignore_tags),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", try_from = "RawOptions")]
pub struct AttributeHyphenationOptions {
pub style: HyphenationStyle,
pub ignore: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ignore_tags: Vec<String>,
}
impl Default for AttributeHyphenationOptions {
fn default() -> Self {
Self {
style: HyphenationStyle::Always,
ignore: Vec::new(),
ignore_tags: Vec::new(),
}
}
}
impl AttributeHyphenationOptions {
pub fn validate(&self) -> Result<(), &'static str> {
for (index, entry) in self.ignore.iter().enumerate() {
if entry.chars().all(is_ecmascript_whitespace) {
return Err("ignore entries must not be blank");
}
if entry.ends_with(":exit") {
return Err("ignore entries must not end with :exit");
}
if self
.ignore
.iter()
.take(index)
.any(|previous| previous == entry)
{
return Err("ignore entries must be unique");
}
}
for (index, tag) in self.ignore_tags.iter().enumerate() {
if is_regex_tag_policy(tag) {
return Err(
"ignoreTags supports literal tag names only; regular-expression policies are unsupported",
);
}
if self
.ignore_tags
.iter()
.take(index)
.any(|previous| previous == tag)
{
return Err("ignoreTags entries must be unique");
}
}
Ok(())
}
}
#[derive(Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "camelCase")]
struct RawOptions {
style: HyphenationStyle,
ignore: Vec<String>,
ignore_tags: Vec<String>,
}
impl Default for RawOptions {
fn default() -> Self {
Self {
style: HyphenationStyle::Always,
ignore: Vec::new(),
ignore_tags: Vec::new(),
}
}
}
impl TryFrom<RawOptions> for AttributeHyphenationOptions {
type Error = &'static str;
fn try_from(raw: RawOptions) -> Result<Self, Self::Error> {
let options = Self {
style: raw.style,
ignore: raw.ignore,
ignore_tags: raw.ignore_tags,
};
options.validate()?;
Ok(options)
}
}
fn is_regex_tag_policy(value: &str) -> bool {
value.starts_with('/')
&& !value
.chars()
.any(|ch| matches!(ch, '\n' | '\r' | '\u{2028}' | '\u{2029}'))
&& value.rfind('/').is_some_and(|index| index > 1)
}
fn is_ecmascript_whitespace(character: char) -> bool {
matches!(
character,
'\u{0009}'..='\u{000d}'
| '\u{0020}'
| '\u{00a0}'
| '\u{1680}'
| '\u{2000}'..='\u{200a}'
| '\u{2028}'
| '\u{2029}'
| '\u{202f}'
| '\u{205f}'
| '\u{3000}'
| '\u{feff}'
)
}
#[cfg(test)]
mod tests;