typst_library/model/
quote.rs

1use typst_syntax::Span;
2
3use crate::foundations::{
4    Content, Depth, Label, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles,
5    cast, elem,
6};
7use crate::introspection::{Locatable, Tagged};
8use crate::layout::{BlockElem, Em, PadElem};
9use crate::model::{CitationForm, CiteElem};
10use crate::text::{SmartQuotes, SpaceElem, TextElem};
11
12/// Displays a quote alongside an optional attribution.
13///
14/// # Example
15/// ```example
16/// Plato is often misquoted as the author of #quote[I know that I know
17/// nothing], however, this is a derivation form his original quote:
18///
19/// #set quote(block: true)
20///
21/// #quote(attribution: [Plato])[
22///   ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι
23///   ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
24/// ]
25/// #quote(attribution: [from the Henry Cary literal translation of 1897])[
26///   ... I seem, then, in just this little thing to be wiser than this man at
27///   any rate, that what I do not know I do not think I know either.
28/// ]
29/// ```
30///
31/// By default block quotes are padded left and right by `{1em}`, alignment and
32/// padding can be controlled with show rules:
33/// ```example
34/// #set quote(block: true)
35/// #show quote: set align(center)
36/// #show quote: set pad(x: 5em)
37///
38/// #quote[
39///   You cannot pass... I am a servant of the Secret Fire, wielder of the
40///   flame of Anor. You cannot pass. The dark fire will not avail you,
41///   flame of Udûn. Go back to the Shadow! You cannot pass.
42/// ]
43/// ```
44#[elem(Locatable, Tagged, ShowSet)]
45pub struct QuoteElem {
46    /// Whether this is a block quote.
47    ///
48    /// ```example
49    /// An inline citation would look like
50    /// this: #quote(
51    ///   attribution: [René Descartes]
52    /// )[
53    ///   cogito, ergo sum
54    /// ], and a block equation like this:
55    /// #quote(
56    ///   block: true,
57    ///   attribution: [JFK]
58    /// )[
59    ///   Ich bin ein Berliner.
60    /// ]
61    /// ```
62    pub block: bool,
63
64    /// Whether double quotes should be added around this quote.
65    ///
66    /// The double quotes used are inferred from the `quotes` property on
67    /// [smartquote], which is affected by the `lang` property on [text].
68    ///
69    /// - `{true}`: Wrap this quote in double quotes.
70    /// - `{false}`: Do not wrap this quote in double quotes.
71    /// - `{auto}`: Infer whether to wrap this quote in double quotes based on
72    ///   the `block` property. If `block` is `{false}`, double quotes are
73    ///   automatically added.
74    ///
75    /// ```example
76    /// #set text(lang: "de")
77    ///
78    /// Ein deutsch-sprechender Author
79    /// zitiert unter umständen JFK:
80    /// #quote[Ich bin ein Berliner.]
81    ///
82    /// #set text(lang: "en")
83    ///
84    /// And an english speaking one may
85    /// translate the quote:
86    /// #quote[I am a Berliner.]
87    /// ```
88    pub quotes: Smart<bool>,
89
90    /// The attribution of this quote, usually the author or source. Can be a
91    /// label pointing to a bibliography entry or any content. By default only
92    /// displayed for block quotes, but can be changed using a `{show}` rule.
93    ///
94    /// ```example
95    /// #quote(attribution: [René Descartes])[
96    ///   cogito, ergo sum
97    /// ]
98    ///
99    /// #show quote.where(block: false): it => {
100    ///   ["] + h(0pt, weak: true) + it.body + h(0pt, weak: true) + ["]
101    ///   if it.attribution != none [ (#it.attribution)]
102    /// }
103    ///
104    /// #quote(
105    ///   attribution: link("https://typst.app/home")[typst.app]
106    /// )[
107    ///   Compose papers faster
108    /// ]
109    ///
110    /// #set quote(block: true)
111    ///
112    /// #quote(attribution: <tolkien54>)[
113    ///   You cannot pass... I am a servant
114    ///   of the Secret Fire, wielder of the
115    ///   flame of Anor. You cannot pass. The
116    ///   dark fire will not avail you, flame
117    ///   of Udûn. Go back to the Shadow! You
118    ///   cannot pass.
119    /// ]
120    ///
121    /// #bibliography("works.bib", style: "apa")
122    /// ```
123    pub attribution: Option<Attribution>,
124
125    /// The quote.
126    #[required]
127    pub body: Content,
128
129    /// The nesting depth.
130    #[internal]
131    #[fold]
132    #[ghost]
133    pub depth: Depth,
134}
135
136impl QuoteElem {
137    /// Quotes the body content with the appropriate quotes based on the current
138    /// styles and surroundings.
139    pub fn quoted(body: Content, styles: StyleChain<'_>) -> Content {
140        let quotes = SmartQuotes::get_in(styles);
141
142        // Alternate between single and double quotes.
143        let Depth(depth) = styles.get(QuoteElem::depth);
144        let double = depth % 2 == 0;
145
146        Content::sequence([
147            TextElem::packed(quotes.open(double)),
148            body,
149            TextElem::packed(quotes.close(double)),
150        ])
151        .set(QuoteElem::depth, Depth(1))
152    }
153}
154
155/// Attribution for a [quote](QuoteElem).
156#[derive(Debug, Clone, PartialEq, Hash)]
157pub enum Attribution {
158    Content(Content),
159    Label(Label),
160}
161
162impl Attribution {
163    /// Realize as an em dash followed by text or a citation.
164    pub fn realize(&self, span: Span) -> Content {
165        Content::sequence([
166            TextElem::packed('—'),
167            SpaceElem::shared().clone(),
168            match self {
169                Attribution::Content(content) => content.clone(),
170                Attribution::Label(label) => CiteElem::new(*label)
171                    .with_form(Some(CitationForm::Prose))
172                    .pack()
173                    .spanned(span),
174            },
175        ])
176    }
177}
178
179cast! {
180    Attribution,
181    self => match self {
182        Self::Content(content) => content.into_value(),
183        Self::Label(label) => label.into_value(),
184    },
185    content: Content => Self::Content(content),
186    label: Label => Self::Label(label),
187}
188
189impl ShowSet for Packed<QuoteElem> {
190    fn show_set(&self, styles: StyleChain) -> Styles {
191        let mut out = Styles::new();
192        if self.block.get(styles) {
193            out.set(PadElem::left, Em::new(1.0).into());
194            out.set(PadElem::right, Em::new(1.0).into());
195            out.set(BlockElem::above, Smart::Custom(Em::new(2.4).into()));
196            out.set(BlockElem::below, Smart::Custom(Em::new(1.8).into()));
197        }
198        out
199    }
200}