diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index 9656fafbf..9472e207a 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -745,3 +745,13 @@ impl Fold for SmallVec<[T; N]> { self } } + +/// A type that accumulates depth when folded. +#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)] +pub struct Depth(pub usize); + +impl Fold for Depth { + fn fold(self, outer: Self) -> Self { + Self(outer.0 + self.0) + } +} diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 3e3b7a76a..17407bfae 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -464,15 +464,12 @@ fn collect<'a>( Segment::Text(c.len_utf8()) } else if let Some(elem) = child.to_packed::() { let prev = full.len(); - if SmartQuoteElem::enabled_in(styles) { - let quotes = SmartQuoteElem::quotes_in(styles); - let lang = TextElem::lang_in(styles); - let region = TextElem::region_in(styles); + if elem.enabled(styles) { let quotes = SmartQuotes::new( - quotes, - lang, - region, - SmartQuoteElem::alternative_in(styles), + elem.quotes(styles), + TextElem::lang_in(styles), + TextElem::region_in(styles), + elem.alternative(styles), ); let peeked = iter.peek().and_then(|&child| { let child = if let Some(styled) = child.to_packed::() { diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs index ebd733e32..6de78bbd5 100644 --- a/crates/typst/src/model/list.rs +++ b/crates/typst/src/model/list.rs @@ -1,7 +1,7 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, Fold, Func, Packed, Smart, StyleChain, Value, + cast, elem, scope, Array, Content, Depth, Func, Packed, Smart, StyleChain, Value, }; use crate::layout::{ Axes, BlockElem, Cell, CellGrid, Em, Fragment, GridLayouter, HAlignment, @@ -236,12 +236,3 @@ cast! { }, v: Func => Self::Func(v), } - -#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)] -struct Depth(usize); - -impl Fold for Depth { - fn fold(self, outer: Self) -> Self { - Self(outer.0 + self.0) - } -} diff --git a/crates/typst/src/model/quote.rs b/crates/typst/src/model/quote.rs index 883f876e9..d02208b6d 100644 --- a/crates/typst/src/model/quote.rs +++ b/crates/typst/src/model/quote.rs @@ -1,12 +1,12 @@ use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Label, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, - Styles, + cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, + StyleChain, Styles, }; use crate::layout::{Alignment, BlockElem, Em, HElem, PadElem, Spacing, VElem}; use crate::model::{CitationForm, CiteElem}; -use crate::text::{SmartQuoteElem, SpaceElem, TextElem}; +use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// Displays a quote alongside an optional attribution. /// @@ -126,6 +126,12 @@ pub struct QuoteElem { /// The quote. #[required] body: Content, + + /// The nesting depth. + #[internal] + #[fold] + #[ghost] + depth: Depth, } /// Attribution for a [quote](QuoteElem). @@ -152,11 +158,27 @@ impl Show for Packed { let block = self.block(styles); if self.quotes(styles) == Smart::Custom(true) || !block { + let quotes = SmartQuotes::new( + SmartQuoteElem::quotes_in(styles), + TextElem::lang_in(styles), + TextElem::region_in(styles), + SmartQuoteElem::alternative_in(styles), + ); + + // Alternate between single and double quotes. + let Depth(depth) = QuoteElem::depth_in(styles); + let double = depth % 2 == 0; + // Add zero-width weak spacing to make the quotes "sticky". let hole = HElem::hole().pack(); - let quote = SmartQuoteElem::new().with_double(true).pack(); - realized = - Content::sequence([quote.clone(), hole.clone(), realized, hole, quote]); + realized = Content::sequence([ + TextElem::packed(quotes.open(double)), + hole.clone(), + realized, + hole, + TextElem::packed(quotes.close(double)), + ]) + .styled(QuoteElem::set_depth(Depth(1))); } if block { diff --git a/crates/typst/src/text/smartquote.rs b/crates/typst/src/text/smartquote.rs index 9e62bea35..0c435f112 100644 --- a/crates/typst/src/text/smartquote.rs +++ b/crates/typst/src/text/smartquote.rs @@ -251,7 +251,7 @@ impl<'s> SmartQuotes<'s> { } /// The opening quote. - fn open(&self, double: bool) -> &'s str { + pub fn open(&self, double: bool) -> &'s str { if double { self.double_open } else { @@ -260,7 +260,7 @@ impl<'s> SmartQuotes<'s> { } /// The closing quote. - fn close(&self, double: bool) -> &'s str { + pub fn close(&self, double: bool) -> &'s str { if double { self.double_close } else { @@ -269,7 +269,7 @@ impl<'s> SmartQuotes<'s> { } /// Which character should be used as a prime. - fn prime(&self, double: bool) -> &'static str { + pub fn prime(&self, double: bool) -> &'static str { if double { "″" } else { @@ -278,7 +278,7 @@ impl<'s> SmartQuotes<'s> { } /// Which character should be used as a fallback quote. - fn fallback(&self, double: bool) -> &'static str { + pub fn fallback(&self, double: bool) -> &'static str { if double { "\"" } else { diff --git a/tests/ref/text/quote-nesting.png b/tests/ref/text/quote-nesting.png new file mode 100644 index 000000000..fb16002de Binary files /dev/null and b/tests/ref/text/quote-nesting.png differ diff --git a/tests/typ/text/quote-nesting.typ b/tests/typ/text/quote-nesting.typ new file mode 100644 index 000000000..381aaa569 --- /dev/null +++ b/tests/typ/text/quote-nesting.typ @@ -0,0 +1,27 @@ +// Test quote nesting. + +--- +// Test quote selection. +#set page(width: auto) +#set text(lang: "en") +=== EN +#quote[An apostroph'] \ +#quote[A #quote[nested] quote] \ +#quote[A #quote[very #quote[nested]] quote] + +#set text(lang: "de") +=== DE +#quote[Satz mit Apostroph'] \ +#quote[Satz mit #quote[Zitat]] \ +#quote[A #quote[very #quote[nested]] quote] + +#set smartquote(alternative: true) +=== DE Alternative +#quote[Satz mit Apostroph'] \ +#quote[Satz mit #quote[Zitat]] \ +#quote[A #quote[very #quote[nested]] quote] + +--- +// With custom quotes. +#set smartquote(quotes: (single: ("<", ">"), double: ("(", ")"))) +#quote[A #quote[nested] quote]