summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-02-15 10:41:27 +0100
committerGitHub <noreply@github.com>2024-02-15 09:41:27 +0000
commitaabb4b5ecf67a5b51b0e550f0b06087465f4ba75 (patch)
treee7dd89965f75a234160e7c8f70b9a4478a8fbc5b
parent79e37ccbac080212dc42e996d760664c75d1a56f (diff)
Better quote selection (#3422)
-rw-r--r--crates/typst/src/foundations/styles.rs10
-rw-r--r--crates/typst/src/layout/inline/mod.rs13
-rw-r--r--crates/typst/src/model/list.rs11
-rw-r--r--crates/typst/src/model/quote.rs34
-rw-r--r--crates/typst/src/text/smartquote.rs8
-rw-r--r--tests/ref/text/quote-nesting.pngbin0 -> 14034 bytes
-rw-r--r--tests/typ/text/quote-nesting.typ27
7 files changed, 75 insertions, 28 deletions
diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs
index 9656fafb..9472e207 100644
--- a/crates/typst/src/foundations/styles.rs
+++ b/crates/typst/src/foundations/styles.rs
@@ -745,3 +745,13 @@ impl<T, const N: usize> 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 3e3b7a76..17407bfa 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::<SmartQuoteElem>() {
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::<StyledElem>() {
diff --git a/crates/typst/src/model/list.rs b/crates/typst/src/model/list.rs
index ebd733e3..6de78bbd 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 883f876e..d02208b6 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<QuoteElem> {
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 9e62bea3..0c435f11 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 00000000..fb16002d
--- /dev/null
+++ b/tests/ref/text/quote-nesting.png
Binary files differ
diff --git a/tests/typ/text/quote-nesting.typ b/tests/typ/text/quote-nesting.typ
new file mode 100644
index 00000000..381aaa56
--- /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]