Skip to main content

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 <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///
34/// ```example
35/// #set quote(block: true)
36/// #show quote: set align(center)
37/// #show quote: set pad(x: 5em)
38///
39/// #quote[
40///   You cannot pass... I am a servant of the Secret Fire, wielder of the
41///   flame of Anor. You cannot pass. The dark fire will not avail you,
42///   flame of Udûn. Go back to the Shadow! You cannot pass.
43/// ]
44/// ```
45#[elem(Locatable, Tagged, ShowSet)]
46pub struct QuoteElem {
47    /// Whether this is a block quote.
48    ///
49    /// ```example
50    /// An inline citation would look like
51    /// this: #quote(
52    ///   attribution: [René Descartes]
53    /// )[
54    ///   cogito, ergo sum
55    /// ], and a block equation like this:
56    /// #quote(
57    ///   block: true,
58    ///   attribution: [JFK]
59    /// )[
60    ///   Ich bin ein Berliner.
61    /// ]
62    /// ```
63    pub block: bool,
64
65    /// Whether double quotes should be added around this quote.
66    ///
67    /// The double quotes used are inferred from the `quotes` property on
68    /// @smartquote[smartquote], which is affected by the `lang` property on
69    /// @text[text].
70    ///
71    /// - `{true}`: Wrap this quote in double quotes.
72    /// - `{false}`: Do not wrap this quote in double quotes.
73    /// - `{auto}`: Infer whether to wrap this quote in double quotes based on
74    ///   the `block` property. If `block` is `{false}`, double quotes are
75    ///   automatically added.
76    ///
77    /// ```example
78    /// #set text(lang: "de")
79    ///
80    /// Ein deutsch-sprechender Author
81    /// zitiert unter umständen JFK:
82    /// #quote[Ich bin ein Berliner.]
83    ///
84    /// #set text(lang: "en")
85    ///
86    /// And an english speaking one may
87    /// translate the quote:
88    /// #quote[I am a Berliner.]
89    /// ```
90    pub quotes: Smart<bool>,
91
92    /// The attribution of this quote, usually the author or source. Can be a
93    /// label pointing to a bibliography entry or any content. By default only
94    /// displayed for block quotes, but can be changed using a `{show}` rule.
95    ///
96    /// ```example
97    /// #quote(attribution: [René Descartes])[
98    ///   cogito, ergo sum
99    /// ]
100    ///
101    /// #show quote.where(block: false): it => {
102    ///   ["] + h(0pt, weak: true) + it.body + h(0pt, weak: true) + ["]
103    ///   if it.attribution != none [ (#it.attribution)]
104    /// }
105    ///
106    /// #quote(
107    ///   attribution: link("https://typst.app/home")[typst.app]
108    /// )[
109    ///   Compose papers faster
110    /// ]
111    ///
112    /// #set quote(block: true)
113    ///
114    /// #quote(attribution: <tolkien54>)[
115    ///   You cannot pass... I am a servant
116    ///   of the Secret Fire, wielder of the
117    ///   flame of Anor. You cannot pass. The
118    ///   dark fire will not avail you, flame
119    ///   of Udûn. Go back to the Shadow! You
120    ///   cannot pass.
121    /// ]
122    ///
123    /// #bibliography("works.bib", style: "apa")
124    /// ```
125    pub attribution: Option<Attribution>,
126
127    /// The quote.
128    #[required]
129    pub body: Content,
130
131    /// The nesting depth.
132    #[internal]
133    #[fold]
134    #[ghost]
135    pub depth: Depth,
136}
137
138impl QuoteElem {
139    /// Quotes the body content with the appropriate quotes based on the current
140    /// styles and surroundings.
141    pub fn quoted(body: Content, styles: StyleChain<'_>) -> Content {
142        let quotes = SmartQuotes::get_in(styles);
143
144        // Alternate between single and double quotes.
145        let Depth(depth) = styles.get(QuoteElem::depth);
146        let double = depth % 2 == 0;
147
148        Content::sequence([
149            TextElem::packed(quotes.open(double)),
150            body,
151            TextElem::packed(quotes.close(double)),
152        ])
153        .set(QuoteElem::depth, Depth(1))
154    }
155}
156
157/// Attribution for a [quote](QuoteElem).
158#[derive(Debug, Clone, PartialEq, Hash)]
159pub enum Attribution {
160    Content(Content),
161    Label(Label),
162}
163
164impl Attribution {
165    /// Realize as an em dash followed by text or a citation.
166    pub fn realize(&self, span: Span) -> Content {
167        Content::sequence([
168            TextElem::packed('—'),
169            SpaceElem::shared().clone(),
170            match self {
171                Attribution::Content(content) => content.clone(),
172                Attribution::Label(label) => CiteElem::new(*label)
173                    .with_form(Some(CitationForm::Prose))
174                    .pack()
175                    .spanned(span),
176            },
177        ])
178    }
179}
180
181cast! {
182    Attribution,
183    self => match self {
184        Self::Content(content) => content.into_value(),
185        Self::Label(label) => label.into_value(),
186    },
187    content: Content => Self::Content(content),
188    label: Label => Self::Label(label),
189}
190
191impl ShowSet for Packed<QuoteElem> {
192    fn show_set(&self, styles: StyleChain) -> Styles {
193        let mut out = Styles::new();
194        if self.block.get(styles) {
195            out.set(PadElem::left, Em::new(1.0).into());
196            out.set(PadElem::right, Em::new(1.0).into());
197            out.set(BlockElem::above, Smart::Custom(Em::new(2.4).into()));
198            out.set(BlockElem::below, Smart::Custom(Em::new(1.8).into()));
199        }
200        out
201    }
202}