1use ecow::EcoString;
2use typst_syntax::is_newline;
3use unicode_segmentation::UnicodeSegmentation;
4
5use crate::diag::{bail, HintedStrResult, StrResult};
6use crate::foundations::{
7 array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str,
8};
9use crate::layout::Dir;
10use crate::text::{Lang, Region};
11
12#[elem(name = "smartquote", PlainText)]
32pub struct SmartQuoteElem {
33 #[default(true)]
35 pub double: bool,
36
37 #[default(true)]
48 pub enabled: bool,
49
50 #[default(false)]
62 pub alternative: bool,
63
64 #[borrowed]
87 pub quotes: Smart<SmartQuoteDict>,
88}
89
90impl PlainText for Packed<SmartQuoteElem> {
91 fn plain_text(&self, text: &mut EcoString) {
92 if self.double.unwrap_or(true) {
93 text.push_str("\"");
94 } else {
95 text.push_str("'");
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct SmartQuoter {
103 depth: u8,
105 kinds: u32,
108}
109
110impl SmartQuoter {
111 pub fn new() -> Self {
113 Self { depth: 0, kinds: 0 }
114 }
115
116 pub fn quote<'a>(
119 &mut self,
120 before: Option<char>,
121 quotes: &SmartQuotes<'a>,
122 double: bool,
123 ) -> &'a str {
124 let opened = self.top();
125 let before = before.unwrap_or(' ');
126
127 if before.is_numeric() && opened != Some(double) {
130 return if double { "″" } else { "′" };
131 }
132
133 if !double
137 && opened != Some(false)
138 && (before.is_alphabetic() || before == '\u{FFFC}')
139 {
140 return "’";
141 }
142
143 if opened == Some(double)
146 && !before.is_whitespace()
147 && !is_newline(before)
148 && !is_opening_bracket(before)
149 {
150 self.pop();
151 return quotes.close(double);
152 }
153
154 self.push(double);
156 quotes.open(double)
157 }
158
159 fn top(&self) -> Option<bool> {
162 self.depth.checked_sub(1).map(|i| (self.kinds >> i) & 1 == 1)
163 }
164
165 fn push(&mut self, double: bool) {
167 if self.depth < 32 {
168 self.kinds |= (double as u32) << self.depth;
169 self.depth += 1;
170 }
171 }
172
173 fn pop(&mut self) {
175 self.depth -= 1;
176 self.kinds &= (1 << self.depth) - 1;
177 }
178}
179
180impl Default for SmartQuoter {
181 fn default() -> Self {
182 Self::new()
183 }
184}
185
186fn is_opening_bracket(c: char) -> bool {
188 matches!(c, '(' | '{' | '[')
189}
190
191pub struct SmartQuotes<'s> {
193 pub single_open: &'s str,
195 pub single_close: &'s str,
197 pub double_open: &'s str,
199 pub double_close: &'s str,
201}
202
203impl<'s> SmartQuotes<'s> {
204 pub fn get(
218 quotes: &'s Smart<SmartQuoteDict>,
219 lang: Lang,
220 region: Option<Region>,
221 alternative: bool,
222 ) -> Self {
223 let region = region.as_ref().map(Region::as_str);
224
225 let default = ("‘", "’", "“", "”");
226 let low_high = ("‚", "‘", "„", "“");
227
228 let (single_open, single_close, double_open, double_close) = match lang.as_str() {
229 "de" if matches!(region, Some("CH" | "LI")) => match alternative {
230 false => ("‹", "›", "«", "»"),
231 true => low_high,
232 },
233 "fr" if matches!(region, Some("CH")) => match alternative {
234 false => ("‹\u{202F}", "\u{202F}›", "«\u{202F}", "\u{202F}»"),
235 true => default,
236 },
237 "cs" | "da" | "de" | "sk" | "sl" if alternative => ("›", "‹", "»", "«"),
238 "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high,
239 "da" => ("‘", "’", "“", "”"),
240 "fr" | "ru" if alternative => default,
241 "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"),
242 "fi" | "sv" if alternative => ("’", "’", "»", "»"),
243 "bs" | "fi" | "sv" => ("’", "’", "”", "”"),
244 "it" if alternative => default,
245 "la" if alternative => ("“", "”", "«\u{202F}", "\u{202F}»"),
246 "it" | "la" => ("“", "”", "«", "»"),
247 "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"),
248 "hu" | "pl" | "ro" => ("’", "’", "„", "”"),
249 "no" | "nb" | "nn" if alternative => low_high,
250 "ru" | "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"),
251 "el" => ("‘", "’", "«", "»"),
252 "he" => ("’", "’", "”", "”"),
253 "hr" => ("‘", "’", "„", "”"),
254 "bg" => ("’", "’", "„", "“"),
255 _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
256 _ => default,
257 };
258
259 fn inner_or_default<'s>(
260 quotes: Smart<&'s SmartQuoteDict>,
261 f: impl FnOnce(&'s SmartQuoteDict) -> Smart<&'s SmartQuoteSet>,
262 default: [&'s str; 2],
263 ) -> [&'s str; 2] {
264 match quotes.and_then(f) {
265 Smart::Auto => default,
266 Smart::Custom(SmartQuoteSet { open, close }) => {
267 [open, close].map(|s| s.as_str())
268 }
269 }
270 }
271
272 let quotes = quotes.as_ref();
273 let [single_open, single_close] =
274 inner_or_default(quotes, |q| q.single.as_ref(), [single_open, single_close]);
275 let [double_open, double_close] =
276 inner_or_default(quotes, |q| q.double.as_ref(), [double_open, double_close]);
277
278 Self {
279 single_open,
280 single_close,
281 double_open,
282 double_close,
283 }
284 }
285
286 pub fn open(&self, double: bool) -> &'s str {
288 if double {
289 self.double_open
290 } else {
291 self.single_open
292 }
293 }
294
295 pub fn close(&self, double: bool) -> &'s str {
297 if double {
298 self.double_close
299 } else {
300 self.single_close
301 }
302 }
303}
304
305#[derive(Debug, Clone, Eq, PartialEq, Hash)]
307pub struct SmartQuoteSet {
308 open: EcoString,
309 close: EcoString,
310}
311
312cast! {
313 SmartQuoteSet,
314 self => array![self.open, self.close].into_value(),
315 value: Array => {
316 let [open, close] = array_to_set(value)?;
317 Self { open, close }
318 },
319 value: Str => {
320 let [open, close] = str_to_set(value.as_str())?;
321 Self { open, close }
322 },
323}
324
325fn str_to_set(value: &str) -> StrResult<[EcoString; 2]> {
326 let mut iter = value.graphemes(true);
327 match (iter.next(), iter.next(), iter.next()) {
328 (Some(open), Some(close), None) => Ok([open.into(), close.into()]),
329 _ => {
330 let count = value.graphemes(true).count();
331 bail!(
332 "expected 2 characters, found {count} character{}",
333 if count > 1 { "s" } else { "" }
334 );
335 }
336 }
337}
338
339fn array_to_set(value: Array) -> HintedStrResult<[EcoString; 2]> {
340 let value = value.as_slice();
341 if value.len() != 2 {
342 bail!(
343 "expected 2 quotes, found {} quote{}",
344 value.len(),
345 if value.len() > 1 { "s" } else { "" }
346 );
347 }
348
349 let open: EcoString = value[0].clone().cast()?;
350 let close: EcoString = value[1].clone().cast()?;
351
352 Ok([open, close])
353}
354
355#[derive(Debug, Clone, Eq, PartialEq, Hash)]
357pub struct SmartQuoteDict {
358 double: Smart<SmartQuoteSet>,
359 single: Smart<SmartQuoteSet>,
360}
361
362cast! {
363 SmartQuoteDict,
364 self => dict! { "double" => self.double, "single" => self.single }.into_value(),
365 mut value: Dict => {
366 let keys = ["double", "single"];
367
368 let double = value
369 .take("double")
370 .ok()
371 .map(FromValue::from_value)
372 .transpose()?
373 .unwrap_or(Smart::Auto);
374 let single = value
375 .take("single")
376 .ok()
377 .map(FromValue::from_value)
378 .transpose()?
379 .unwrap_or(Smart::Auto);
380
381 value.finish(&keys)?;
382
383 Self { single, double }
384 },
385 value: SmartQuoteSet => Self {
386 double: Smart::Custom(value),
387 single: Smart::Auto,
388 },
389}