diff options
Diffstat (limited to 'crates/typst-library/src/model/quote.rs')
| -rw-r--r-- | crates/typst-library/src/model/quote.rs | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs new file mode 100644 index 00000000..110825f1 --- /dev/null +++ b/crates/typst-library/src/model/quote.rs @@ -0,0 +1,237 @@ +use crate::diag::SourceResult; +use crate::engine::Engine; +use crate::foundations::{ + cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, + StyleChain, Styles, +}; +use crate::introspection::Locatable; +use crate::layout::{ + Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, +}; +use crate::model::{CitationForm, CiteElem}; +use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; + +/// Displays a quote alongside an optional attribution. +/// +/// # 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(Locatable, ShowSet, Show)] +pub struct QuoteElem { + /// Whether this is a block quote. + /// + /// ```example + /// An inline citation would look like + /// this: #quote( + /// attribution: [René Descartes] + /// )[ + /// cogito, ergo sum + /// ], and a block equation like this: + /// #quote( + /// block: true, + /// 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], which is affected by the `lang` property on [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") + /// + /// Ein deutsch-sprechender Author + /// zitiert unter umständen JFK: + /// #quote[Ich bin ein Berliner.] + /// + /// #set text(lang: "en") + /// + /// And an english speaking one may + /// translate the quote: + /// #quote[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 => { + /// ["] + h(0pt, weak: true) + it.body + h(0pt, weak: true) + ["] + /// 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") + /// ``` + #[borrowed] + attribution: Option<Attribution>, + + /// The quote. + #[required] + body: Content, + + /// The nesting depth. + #[internal] + #[fold] + #[ghost] + depth: Depth, +} + +/// Attribution for a [quote](QuoteElem). +#[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 Packed<QuoteElem> { + #[typst_macros::time(name = "quote", span = self.span())] + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { + let mut realized = self.body().clone(); + let block = self.block(styles); + + if self.quotes(styles) == Smart::Custom(true) || !block { + let quotes = SmartQuotes::get( + SmartQuoteElem::quotes_in(styles), + TextElem::lang_in(styles), + TextElem::region_in(styles), + SmartQuoteElem::alternative_in(styles), + ); + + // Alternate between single and double quotes. + let Depth(depth) = QuoteElem::depth_in(styles); + let double = depth % 2 == 0; + + // Add zero-width weak spacing to make the quotes "sticky". + let hole = HElem::hole().pack(); + realized = Content::sequence([ + TextElem::packed(quotes.open(double)), + hole.clone(), + realized, + hole, + TextElem::packed(quotes.close(double)), + ]) + .styled(QuoteElem::set_depth(Depth(1))); + } + + if block { + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()); + + if let Some(attribution) = self.attribution(styles).as_ref() { + let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()]; + + match attribution { + Attribution::Content(content) => { + seq.push(content.clone()); + } + Attribution::Label(label) => { + seq.push( + CiteElem::new(*label) + .with_form(Some(CitationForm::Prose)) + .pack() + .spanned(self.span()), + ); + } + } + + // Use v(0.9em, weak: true) bring the attribution closer to the + // quote. + let gap = Spacing::Rel(Em::new(0.9).into()); + let v = VElem::new(gap).with_weak(true).pack(); + realized += v + Content::sequence(seq).aligned(Alignment::END); + } + + realized = PadElem::new(realized).pack(); + } else if let Some(Attribution::Label(label)) = self.attribution(styles) { + realized += SpaceElem::shared().clone() + + CiteElem::new(*label).pack().spanned(self.span()); + } + + Ok(realized) + } +} + +impl ShowSet for Packed<QuoteElem> { + fn show_set(&self, styles: StyleChain) -> Styles { + let mut out = Styles::new(); + if self.block(styles) { + out.set(PadElem::set_left(Em::new(1.0).into())); + out.set(PadElem::set_right(Em::new(1.0).into())); + out.set(BlockElem::set_above(Smart::Custom(Em::new(2.4).into()))); + out.set(BlockElem::set_below(Smart::Custom(Em::new(1.8).into()))); + } + out + } +} |
