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}