typst/crates/typst-library/src/text/quote.rs

187 lines
6.6 KiB
Rust

use super::{SmartquoteElem, SpaceElem, TextElem};
use crate::layout::{BlockElem, HElem, PadElem, Spacing, VElem};
use crate::meta::{CitationForm, CiteElem};
use crate::prelude::*;
/// Displays a quote alongside it's author.
///
/// # Example
/// ```example
/// Plato is often misquoted as the author of #quote[I know that I know
/// nothing], however, this is a derivation form his original quote:
/// #set quote(block: true)
/// #quote(attribution: [Plato])[
/// ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι
/// ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
/// ]
/// #quote(attribution: [from the Henry Cary literal translation of 1897])[
/// ... I seem, then, in just this little thing to be wiser than this man at
/// any rate, that what I do not know I do not think I know either.
/// ]
/// ```
///
/// By default block quotes are padded left and right by `{1em}`, alignment and
/// padding can be controlled with show rules:
/// ```example
/// #set quote(block: true)
/// #show quote: set align(center)
/// #show quote: set pad(x: 5em)
///
/// #quote[
/// You cannot pass... I am a servant of the Secret Fire, wielder of the
/// flame of Anor. You cannot pass. The dark fire will not avail you,
/// flame of Udûn. Go back to the Shadow! You cannot pass.
/// ]
/// ```
#[elem(Finalize, Show)]
pub struct QuoteElem {
/// Whether this is a block quote.
///
/// ```example
/// #quote(attribution: [René Descartes])[cogito, ergo sum]
///
/// #set quote(block: true)
/// #quote(attribution: [JFK])[Ich bin ein Berliner.]
/// ```
block: bool,
/// Whether double quotes should be added around this quote.
///
/// The double quotes used are inferred from the `quotes` property on
/// [smartquote]($smartquote), which is affected by the `lang` property on
/// [text]($text).
///
/// - `{true}`: Wrap this quote in double quotes.
/// - `{false}`: Do not wrap this quote in double quotes.
/// - `{auto}`: Infer whether to wrap this quote in double quotes based on
/// the `block` property. If `block` is `{false}`, double quotes are
/// automatically added.
///
/// ```example
/// #set text(lang: "de")
/// #quote[Ich bin ein Berliner.]
///
/// #set text(lang: "en")
/// #set quote(quotes: true)
/// #quote(block: true)[I am a Berliner.]
/// ```
quotes: Smart<bool>,
/// The attribution of this quote, usually the author or source. Can be a
/// label pointing to a bibliography entry or any content. By default only
/// displayed for block quotes, but can be changed using a `{show}` rule.
///
/// ```example
/// #quote(attribution: [René Descartes])[cogito, ergo sum] \
///
/// #show quote.where(block: false): it => [
/// "#it.body"
/// #if it.attribution != none [(#it.attribution)]
/// ]
/// #quote(attribution: link("https://typst.app/home")[typst.com])[
/// Compose papers faster
/// ]
///
/// #set quote(block: true)
/// #quote(attribution: <tolkien54>)[
/// You cannot pass... I am a servant of the Secret Fire, wielder of the
/// flame of Anor. You cannot pass. The dark fire will not avail you,
/// flame of Udûn. Go back to the Shadow! You cannot pass.
/// ]
/// #bibliography("works.bib", style: "apa")
/// ```
///
/// Note that bilbiography styles which do not include the author in the
/// citation (label, numeric and notes) currently produce attributions such
/// as `[---#super[1]]` or `[--- [1]]`, this will be fixed soon with CSL
/// support. In the mean time you can simply cite yourself:
/// ```example
/// #set quote(block: true)
/// #quote(attribution: [J. R. R. Tolkien, @tolkien54])[In a hole there lived a hobbit.]
///
/// #bibliography("works.bib")
/// ```
#[borrowed]
attribution: Option<Attribution>,
/// The quote.
#[required]
body: Content,
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum Attribution {
Content(Content),
Label(Label),
}
cast! {
Attribution,
self => match self {
Self::Content(content) => content.into_value(),
Self::Label(label) => label.into_value(),
},
content: Content => Self::Content(content),
label: Label => Self::Label(label),
}
impl Show for QuoteElem {
fn show(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body().clone();
let block = self.block(styles);
if self.quotes(styles) == Smart::Custom(true) || !block {
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
let quote = SmartquoteElem::new().with_double(true).pack();
realized =
Content::sequence([quote.clone(), hole.clone(), realized, hole, quote]);
}
if block {
realized = BlockElem::new().with_body(Some(realized)).pack();
if let Some(attribution) = self.attribution(styles).as_ref() {
let mut seq = vec![TextElem::packed('—'), SpaceElem::new().pack()];
match attribution {
Attribution::Content(content) => {
seq.push(content.clone());
}
Attribution::Label(label) => {
seq.push(
CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack(),
);
}
}
// Use v(0.9em, weak: true) bring the attribution closer to the
// quote.
let weak_v = VElem::weak(Spacing::Rel(Em::new(0.9).into())).pack();
realized += weak_v + Content::sequence(seq).aligned(Align::END);
}
realized = PadElem::new(realized).pack();
} else if let Some(Attribution::Label(label)) = self.attribution(styles) {
realized += SpaceElem::new().pack() + CiteElem::new(*label).pack();
}
Ok(realized)
}
}
impl Finalize for QuoteElem {
fn finalize(&self, realized: Content, _: StyleChain) -> Content {
let x = Em::new(1.0).into();
let above = Em::new(2.4).into();
let below = Em::new(1.8).into();
realized
.styled(PadElem::set_left(x))
.styled(PadElem::set_right(x))
.styled(BlockElem::set_above(VElem::block_around(above)))
.styled(BlockElem::set_below(VElem::block_around(below)))
}
}