use ecow::EcoString;
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{
array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str,
};
use crate::layout::Dir;
use crate::syntax::is_newline;
use crate::text::{Lang, Region};
#[elem(name = "smartquote", PlainText)]
pub struct SmartQuoteElem {
#[default(true)]
pub double: bool,
#[default(true)]
pub enabled: bool,
#[default(false)]
pub alternative: bool,
#[borrowed]
pub quotes: Smart<SmartQuoteDict>,
}
impl PlainText for Packed<SmartQuoteElem> {
fn plain_text(&self, text: &mut EcoString) {
if self.double.unwrap_or(true) {
text.push_str("\"");
} else {
text.push_str("'");
}
}
}
#[derive(Debug, Clone)]
pub struct SmartQuoter {
depth: u8,
kinds: u32,
}
impl SmartQuoter {
pub fn new() -> Self {
Self { depth: 0, kinds: 0 }
}
pub fn quote<'a>(
&mut self,
before: Option<char>,
quotes: &SmartQuotes<'a>,
double: bool,
) -> &'a str {
let opened = self.top();
let before = before.unwrap_or(' ');
if before.is_numeric() && opened != Some(double) {
return if double { "″" } else { "′" };
}
if !double
&& opened != Some(false)
&& (before.is_alphabetic() || before == '\u{FFFC}')
{
return "’";
}
if opened == Some(double)
&& !before.is_whitespace()
&& !is_newline(before)
&& !is_opening_bracket(before)
{
self.pop();
return quotes.close(double);
}
self.push(double);
quotes.open(double)
}
fn top(&self) -> Option<bool> {
self.depth.checked_sub(1).map(|i| (self.kinds >> i) & 1 == 1)
}
fn push(&mut self, double: bool) {
if self.depth < 32 {
self.kinds |= (double as u32) << self.depth;
self.depth += 1;
}
}
fn pop(&mut self) {
self.depth -= 1;
self.kinds &= (1 << self.depth) - 1;
}
}
impl Default for SmartQuoter {
fn default() -> Self {
Self::new()
}
}
fn is_opening_bracket(c: char) -> bool {
matches!(c, '(' | '{' | '[')
}
pub struct SmartQuotes<'s> {
pub single_open: &'s str,
pub single_close: &'s str,
pub double_open: &'s str,
pub double_close: &'s str,
}
impl<'s> SmartQuotes<'s> {
pub fn get(
quotes: &'s Smart<SmartQuoteDict>,
lang: Lang,
region: Option<Region>,
alternative: bool,
) -> Self {
let region = region.as_ref().map(Region::as_str);
let default = ("‘", "’", "“", "”");
let low_high = ("‚", "‘", "„", "“");
let (single_open, single_close, double_open, double_close) = match lang.as_str() {
"de" if matches!(region, Some("CH" | "LI")) => match alternative {
false => ("‹", "›", "«", "»"),
true => low_high,
},
"fr" if matches!(region, Some("CH")) => match alternative {
false => ("‹\u{202F}", "\u{202F}›", "«\u{202F}", "\u{202F}»"),
true => default,
},
"cs" | "da" | "de" | "sk" | "sl" if alternative => ("›", "‹", "»", "«"),
"cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high,
"da" => ("‘", "’", "“", "”"),
"fr" | "ru" if alternative => default,
"fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"),
"fi" | "sv" if alternative => ("’", "’", "»", "»"),
"bs" | "fi" | "sv" => ("’", "’", "”", "”"),
"it" if alternative => default,
"la" if alternative => ("“", "”", "«\u{202F}", "\u{202F}»"),
"it" | "la" => ("“", "”", "«", "»"),
"es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"),
"hu" | "pl" | "ro" => ("’", "’", "„", "”"),
"no" | "nb" | "nn" if alternative => low_high,
"ru" | "no" | "nb" | "nn" | "ua" => ("’", "’", "«", "»"),
"gr" => ("‘", "’", "«", "»"),
"he" => ("’", "’", "”", "”"),
_ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
_ => default,
};
fn inner_or_default<'s>(
quotes: Smart<&'s SmartQuoteDict>,
f: impl FnOnce(&'s SmartQuoteDict) -> Smart<&'s SmartQuoteSet>,
default: [&'s str; 2],
) -> [&'s str; 2] {
match quotes.and_then(f) {
Smart::Auto => default,
Smart::Custom(SmartQuoteSet { open, close }) => {
[open, close].map(|s| s.as_str())
}
}
}
let quotes = quotes.as_ref();
let [single_open, single_close] =
inner_or_default(quotes, |q| q.single.as_ref(), [single_open, single_close]);
let [double_open, double_close] =
inner_or_default(quotes, |q| q.double.as_ref(), [double_open, double_close]);
Self {
single_open,
single_close,
double_open,
double_close,
}
}
pub fn open(&self, double: bool) -> &'s str {
if double {
self.double_open
} else {
self.single_open
}
}
pub fn close(&self, double: bool) -> &'s str {
if double {
self.double_close
} else {
self.single_close
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct SmartQuoteSet {
open: EcoString,
close: EcoString,
}
cast! {
SmartQuoteSet,
self => array![self.open, self.close].into_value(),
value: Array => {
let [open, close] = array_to_set(value)?;
Self { open, close }
},
value: Str => {
let [open, close] = str_to_set(value.as_str())?;
Self { open, close }
},
}
fn str_to_set(value: &str) -> StrResult<[EcoString; 2]> {
let mut iter = value.graphemes(true);
match (iter.next(), iter.next(), iter.next()) {
(Some(open), Some(close), None) => Ok([open.into(), close.into()]),
_ => {
let count = value.graphemes(true).count();
bail!(
"expected 2 characters, found {count} character{}",
if count > 1 { "s" } else { "" }
);
}
}
}
fn array_to_set(value: Array) -> HintedStrResult<[EcoString; 2]> {
let value = value.as_slice();
if value.len() != 2 {
bail!(
"expected 2 quotes, found {} quote{}",
value.len(),
if value.len() > 1 { "s" } else { "" }
);
}
let open: EcoString = value[0].clone().cast()?;
let close: EcoString = value[1].clone().cast()?;
Ok([open, close])
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct SmartQuoteDict {
double: Smart<SmartQuoteSet>,
single: Smart<SmartQuoteSet>,
}
cast! {
SmartQuoteDict,
self => dict! { "double" => self.double, "single" => self.single }.into_value(),
mut value: Dict => {
let keys = ["double", "single"];
let double = value
.take("double")
.ok()
.map(FromValue::from_value)
.transpose()?
.unwrap_or(Smart::Auto);
let single = value
.take("single")
.ok()
.map(FromValue::from_value)
.transpose()?
.unwrap_or(Smart::Auto);
value.finish(&keys)?;
Self { single, double }
},
value: SmartQuoteSet => Self {
double: Smart::Custom(value),
single: Smart::Auto,
},
}