Skip to main content

textlane_core/models/
smartypants.rs

1use serde::Deserialize;
2use serde::de::{Deserializer, Error};
3
4/// The quote characters to use for double and single quotes.
5#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7pub struct QuoteCharacters {
8    pub double: String,
9    pub single: String,
10}
11
12/// How smartypants treats backticks: `false`, `true` or `'all'`.
13#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
14pub enum Backticks {
15    Off,
16    #[default]
17    On,
18    All,
19}
20
21/// How smartypants treats dashes: `false`, `true`, `'oldschool'` or `'inverted'`.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum Dashes {
24    Off,
25    #[default]
26    On,
27    Oldschool,
28    Inverted,
29}
30
31/// How smartypants treats dots: `false`, `true`, `'spaced'` or `'unspaced'`.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
33pub enum Ellipses {
34    Off,
35    #[default]
36    On,
37    Spaced,
38    Unspaced,
39}
40
41#[derive(Deserialize)]
42#[serde(untagged)]
43enum Switch {
44    Flag(bool),
45    Name(String),
46}
47
48// `null` and a missing option both mean the default, as upstream's `!== false` checks do
49fn read_mode<'de, D: Deserializer<'de>>(
50    deserializer: D,
51    names: &[&str],
52) -> Result<Option<Result<bool, usize>>, D::Error> {
53    match Option::<Switch>::deserialize(deserializer)? {
54        None => Ok(None),
55        Some(Switch::Flag(flag)) => Ok(Some(Ok(flag))),
56        Some(Switch::Name(name)) => match names.iter().position(|known| *known == name) {
57            Some(index) => Ok(Some(Err(index))),
58            None => Err(D::Error::custom(format!(
59                "unknown variant `{name}`, expected a boolean or one of {names:?}"
60            ))),
61        },
62    }
63}
64
65/// A switch upstream reads as `!== false`: `null` and a missing option mean `true`.
66pub fn read_switch<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {
67    Ok(Option::<bool>::deserialize(deserializer)? != Some(false))
68}
69
70impl<'de> Deserialize<'de> for Backticks {
71    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
72        Ok(match read_mode(deserializer, &["all"])? {
73            Some(Ok(false)) => Backticks::Off,
74            Some(Err(_)) => Backticks::All,
75            _ => Backticks::On,
76        })
77    }
78}
79
80impl<'de> Deserialize<'de> for Dashes {
81    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
82        Ok(match read_mode(deserializer, &["oldschool", "inverted"])? {
83            Some(Ok(false)) => Dashes::Off,
84            Some(Err(0)) => Dashes::Oldschool,
85            Some(Err(_)) => Dashes::Inverted,
86            _ => Dashes::On,
87        })
88    }
89}
90
91impl<'de> Deserialize<'de> for Ellipses {
92    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
93        Ok(match read_mode(deserializer, &["spaced", "unspaced"])? {
94            Some(Ok(false)) => Ellipses::Off,
95            Some(Err(0)) => Ellipses::Spaced,
96            Some(Err(_)) => Ellipses::Unspaced,
97            _ => Ellipses::On,
98        })
99    }
100}
101
102#[cfg(feature = "schema")]
103fn write_mode(names: &[&str]) -> schemars::Schema {
104    schemars::json_schema!({ "anyOf": [{ "type": "boolean" }, { "enum": names }] })
105}
106
107#[cfg(feature = "schema")]
108impl schemars::JsonSchema for Backticks {
109    fn schema_name() -> std::borrow::Cow<'static, str> {
110        "Backticks".into()
111    }
112
113    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
114        write_mode(&["all"])
115    }
116}
117
118#[cfg(feature = "schema")]
119impl schemars::JsonSchema for Dashes {
120    fn schema_name() -> std::borrow::Cow<'static, str> {
121        "Dashes".into()
122    }
123
124    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
125        write_mode(&["oldschool", "inverted"])
126    }
127}
128
129#[cfg(feature = "schema")]
130impl schemars::JsonSchema for Ellipses {
131    fn schema_name() -> std::borrow::Cow<'static, str> {
132        "Ellipses".into()
133    }
134
135    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
136        write_mode(&["spaced", "unspaced"])
137    }
138}
139
140/// What a `Typographer` replaces, as retext-smartypants' options name it.
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct Typography {
143    pub quotes: bool,
144    pub ellipses: Ellipses,
145    pub backticks: Backticks,
146    pub dashes: Dashes,
147    pub opening_quotes: Option<QuoteCharacters>,
148    pub closing_quotes: Option<QuoteCharacters>,
149}