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}