typst_library/model/quote.rs
1use crate::diag::SourceResult;
2use crate::engine::Engine;
3use crate::foundations::{
4 cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
5 StyleChain, Styles, TargetElem,
6};
7use crate::html::{attr, tag, HtmlElem};
8use crate::introspection::Locatable;
9use crate::layout::{
10 Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
11};
12use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget};
13use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
14
15/// Displays a quote alongside an optional attribution.
16///
17/// # Example
18/// ```example
19/// Plato is often misquoted as the author of #quote[I know that I know
20/// nothing], however, this is a derivation form his original quote:
21///
22/// #set quote(block: true)
23///
24/// #quote(attribution: [Plato])[
25/// ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι
26/// ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
27/// ]
28/// #quote(attribution: [from the Henry Cary literal translation of 1897])[
29/// ... I seem, then, in just this little thing to be wiser than this man at
30/// any rate, that what I do not know I do not think I know either.
31/// ]
32/// ```
33///
34/// By default block quotes are padded left and right by `{1em}`, alignment and
35/// padding can be controlled with show rules:
36/// ```example
37/// #set quote(block: true)
38/// #show quote: set align(center)
39/// #show quote: set pad(x: 5em)
40///
41/// #quote[
42/// You cannot pass... I am a servant of the Secret Fire, wielder of the
43/// flame of Anor. You cannot pass. The dark fire will not avail you,
44/// flame of Udûn. Go back to the Shadow! You cannot pass.
45/// ]
46/// ```
47#[elem(Locatable, ShowSet, Show)]
48pub struct QuoteElem {
49 /// Whether this is a block quote.
50 ///
51 /// ```example
52 /// An inline citation would look like
53 /// this: #quote(
54 /// attribution: [René Descartes]
55 /// )[
56 /// cogito, ergo sum
57 /// ], and a block equation like this:
58 /// #quote(
59 /// block: true,
60 /// attribution: [JFK]
61 /// )[
62 /// Ich bin ein Berliner.
63 /// ]
64 /// ```
65 block: bool,
66
67 /// Whether double quotes should be added around this quote.
68 ///
69 /// The double quotes used are inferred from the `quotes` property on
70 /// [smartquote], which is affected by the `lang` property on [text].
71 ///
72 /// - `{true}`: Wrap this quote in double quotes.
73 /// - `{false}`: Do not wrap this quote in double quotes.
74 /// - `{auto}`: Infer whether to wrap this quote in double quotes based on
75 /// the `block` property. If `block` is `{false}`, double quotes are
76 /// automatically added.
77 ///
78 /// ```example
79 /// #set text(lang: "de")
80 ///
81 /// Ein deutsch-sprechender Author
82 /// zitiert unter umständen JFK:
83 /// #quote[Ich bin ein Berliner.]
84 ///
85 /// #set text(lang: "en")
86 ///
87 /// And an english speaking one may
88 /// translate the quote:
89 /// #quote[I am a Berliner.]
90 /// ```
91 quotes: Smart<bool>,
92
93 /// The attribution of this quote, usually the author or source. Can be a
94 /// label pointing to a bibliography entry or any content. By default only
95 /// displayed for block quotes, but can be changed using a `{show}` rule.
96 ///
97 /// ```example
98 /// #quote(attribution: [René Descartes])[
99 /// cogito, ergo sum
100 /// ]
101 ///
102 /// #show quote.where(block: false): it => {
103 /// ["] + h(0pt, weak: true) + it.body + h(0pt, weak: true) + ["]
104 /// if it.attribution != none [ (#it.attribution)]
105 /// }
106 ///
107 /// #quote(
108 /// attribution: link("https://typst.app/home")[typst.com]
109 /// )[
110 /// Compose papers faster
111 /// ]
112 ///
113 /// #set quote(block: true)
114 ///
115 /// #quote(attribution: <tolkien54>)[
116 /// You cannot pass... I am a servant
117 /// of the Secret Fire, wielder of the
118 /// flame of Anor. You cannot pass. The
119 /// dark fire will not avail you, flame
120 /// of Udûn. Go back to the Shadow! You
121 /// cannot pass.
122 /// ]
123 ///
124 /// #bibliography("works.bib", style: "apa")
125 /// ```
126 #[borrowed]
127 attribution: Option<Attribution>,
128
129 /// The quote.
130 #[required]
131 body: Content,
132
133 /// The nesting depth.
134 #[internal]
135 #[fold]
136 #[ghost]
137 depth: Depth,
138}
139
140/// Attribution for a [quote](QuoteElem).
141#[derive(Debug, Clone, PartialEq, Hash)]
142pub enum Attribution {
143 Content(Content),
144 Label(Label),
145}
146
147cast! {
148 Attribution,
149 self => match self {
150 Self::Content(content) => content.into_value(),
151 Self::Label(label) => label.into_value(),
152 },
153 content: Content => Self::Content(content),
154 label: Label => Self::Label(label),
155}
156
157impl Show for Packed<QuoteElem> {
158 #[typst_macros::time(name = "quote", span = self.span())]
159 fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
160 let mut realized = self.body.clone();
161 let block = self.block(styles);
162 let html = TargetElem::target_in(styles).is_html();
163
164 if self.quotes(styles) == Smart::Custom(true) || !block {
165 let quotes = SmartQuotes::get(
166 SmartQuoteElem::quotes_in(styles),
167 TextElem::lang_in(styles),
168 TextElem::region_in(styles),
169 SmartQuoteElem::alternative_in(styles),
170 );
171
172 // Alternate between single and double quotes.
173 let Depth(depth) = QuoteElem::depth_in(styles);
174 let double = depth % 2 == 0;
175
176 if !html {
177 // Add zero-width weak spacing to make the quotes "sticky".
178 let hole = HElem::hole().pack();
179 realized = Content::sequence([hole.clone(), realized, hole]);
180 }
181 realized = Content::sequence([
182 TextElem::packed(quotes.open(double)),
183 realized,
184 TextElem::packed(quotes.close(double)),
185 ])
186 .styled(QuoteElem::set_depth(Depth(1)));
187 }
188
189 let attribution = self.attribution(styles);
190
191 if block {
192 realized = if html {
193 let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized));
194 if let Some(Attribution::Content(attribution)) = attribution {
195 if let Some(link) = attribution.to_packed::<LinkElem>() {
196 if let LinkTarget::Dest(Destination::Url(url)) = &link.dest {
197 elem = elem.with_attr(attr::cite, url.clone().into_inner());
198 }
199 }
200 }
201 elem.pack()
202 } else {
203 BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack()
204 }
205 .spanned(self.span());
206
207 if let Some(attribution) = attribution.as_ref() {
208 let attribution = match attribution {
209 Attribution::Content(content) => content.clone(),
210 Attribution::Label(label) => CiteElem::new(*label)
211 .with_form(Some(CitationForm::Prose))
212 .pack()
213 .spanned(self.span()),
214 };
215 let attribution = Content::sequence([
216 TextElem::packed('—'),
217 SpaceElem::shared().clone(),
218 attribution,
219 ]);
220
221 if html {
222 realized += attribution;
223 } else {
224 // Bring the attribution a bit closer to the quote.
225 let gap = Spacing::Rel(Em::new(0.9).into());
226 let v = VElem::new(gap).with_weak(true).pack();
227 realized += v;
228 realized += BlockElem::new()
229 .with_body(Some(BlockBody::Content(attribution)))
230 .pack()
231 .aligned(Alignment::END);
232 }
233 }
234
235 if !html {
236 realized = PadElem::new(realized).pack();
237 }
238 } else if let Some(Attribution::Label(label)) = attribution {
239 realized += SpaceElem::shared().clone()
240 + CiteElem::new(*label).pack().spanned(self.span());
241 }
242
243 Ok(realized)
244 }
245}
246
247impl ShowSet for Packed<QuoteElem> {
248 fn show_set(&self, styles: StyleChain) -> Styles {
249 let mut out = Styles::new();
250 if self.block(styles) {
251 out.set(PadElem::set_left(Em::new(1.0).into()));
252 out.set(PadElem::set_right(Em::new(1.0).into()));
253 out.set(BlockElem::set_above(Smart::Custom(Em::new(2.4).into())));
254 out.set(BlockElem::set_below(Smart::Custom(Em::new(1.8).into())));
255 }
256 out
257 }
258}