Support quotes in HTML output (#5673)

This commit is contained in:
Michael Färber 2025-01-23 13:21:34 +01:00 committed by GitHub
parent e61cd6fb9e
commit b3fb6c2326
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 120 additions and 32 deletions

View File

@ -2,13 +2,14 @@ use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{
cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart,
StyleChain, Styles,
StyleChain, Styles, TargetElem,
};
use crate::html::{tag, HtmlAttr, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{
Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem,
};
use crate::model::{CitationForm, CiteElem};
use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget};
use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem};
/// Displays a quote alongside an optional attribution.
@ -158,6 +159,7 @@ impl Show for Packed<QuoteElem> {
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
let mut realized = self.body.clone();
let block = self.block(styles);
let html = TargetElem::target_in(styles).is_html();
if self.quotes(styles) == Smart::Custom(true) || !block {
let quotes = SmartQuotes::get(
@ -171,50 +173,65 @@ impl Show for Packed<QuoteElem> {
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();
if !html {
// Add zero-width weak spacing to make the quotes "sticky".
let hole = HElem::hole().pack();
realized = Content::sequence([hole.clone(), realized, hole]);
}
realized = Content::sequence([
TextElem::packed(quotes.open(double)),
hole.clone(),
realized,
hole,
TextElem::packed(quotes.close(double)),
])
.styled(QuoteElem::set_depth(Depth(1)));
}
let attribution = self.attribution(styles);
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()),
);
realized = if html {
let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized));
if let Some(Attribution::Content(attribution)) = attribution {
if let Some(link) = attribution.to_packed::<LinkElem>() {
if let LinkTarget::Dest(Destination::Url(url)) = &link.dest {
elem = elem.with_attr(
HtmlAttr::constant("cite"),
url.clone().into_inner(),
);
}
}
}
elem.pack()
} else {
BlockElem::new().with_body(Some(BlockBody::Content(realized))).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);
if let Some(attribution) = attribution.as_ref() {
let attribution = match attribution {
Attribution::Content(content) => content.clone(),
Attribution::Label(label) => CiteElem::new(*label)
.with_form(Some(CitationForm::Prose))
.pack()
.spanned(self.span()),
};
let attribution =
[TextElem::packed('—'), SpaceElem::shared().clone(), attribution];
if !html {
// Use v(0.9em, weak: true) to 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;
}
realized += Content::sequence(attribution).aligned(Alignment::END);
}
realized = PadElem::new(realized).pack();
} else if let Some(Attribution::Label(label)) = self.attribution(styles) {
if !html {
realized = PadElem::new(realized).pack();
}
} else if let Some(Attribution::Label(label)) = attribution {
realized += SpaceElem::shared().clone()
+ CiteElem::new(*label).pack().spanned(self.span());
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<blockquote cite="https://typst.app/home">
Compose papers faster
</blockquote>
<p>
<a href="https://typst.app/home">typst.com</a>
</p>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>
When you said that “he surely meant that she intended to say “I'm sorry””, I was quite confused.
</p>
</body>
</html>

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<blockquote>
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
</blockquote>
<p>
— Plato
</p>
<blockquote>
… 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.
</blockquote>
<p>
— from the Henry Cary literal translation of 1897
</p>
</body>
</html>

View File

@ -84,3 +84,26 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum].
// With custom quotes.
#set smartquote(quotes: (single: ("<", ">"), double: ("(", ")")))
#quote[A #quote[nested] quote]
--- quote-plato html ---
#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.
]
--- quote-nesting-html html ---
When you said that #quote[he surely meant that #quote[she intended to say #quote[I'm sorry]]], I was quite confused.
--- quote-attribution-link html ---
#quote(
block: true,
attribution: link("https://typst.app/home")[typst.com]
)[
Compose papers faster
]