From 2efa86cbdf3e60602fe5524aeaa0befdf14eafcf Mon Sep 17 00:00:00 2001 From: Ana Gelez Date: Mon, 25 Mar 2024 14:32:02 +0100 Subject: [PATCH] Fix smart quotes in PDF outline (#3790) --- crates/typst/src/text/smartquote.rs | 16 ++++++++++++++-- tests/ref/bugs/3662-pdf-smartquotes.png | Bin 0 -> 11950 bytes tests/typ/bugs/3662-pdf-smartquotes.typ | 12 ++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 tests/ref/bugs/3662-pdf-smartquotes.png create mode 100644 tests/typ/bugs/3662-pdf-smartquotes.typ diff --git a/crates/typst/src/text/smartquote.rs b/crates/typst/src/text/smartquote.rs index dbcca6dc3..4bb5ca019 100644 --- a/crates/typst/src/text/smartquote.rs +++ b/crates/typst/src/text/smartquote.rs @@ -2,7 +2,9 @@ use ecow::EcoString; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, StrResult}; -use crate::foundations::{array, cast, dict, elem, Array, Dict, FromValue, Smart, Str}; +use crate::foundations::{ + array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str, +}; use crate::layout::Dir; use crate::syntax::is_newline; use crate::text::{Lang, Region}; @@ -26,7 +28,7 @@ use crate::text::{Lang, Region}; /// # Syntax /// This function also has dedicated syntax: The normal quote characters /// (`'` and `"`). Typst automatically makes your quotes smart. -#[elem(name = "smartquote")] +#[elem(name = "smartquote", PlainText)] pub struct SmartQuoteElem { /// Whether this should be a double quote. #[default(true)] @@ -85,6 +87,16 @@ pub struct SmartQuoteElem { pub quotes: Smart, } +impl PlainText for Packed { + fn plain_text(&self, text: &mut EcoString) { + if self.double.unwrap_or(true) { + text.push_str("\""); + } else { + text.push_str("'"); + } + } +} + /// State machine for smart quote substitution. #[derive(Debug, Clone)] pub struct SmartQuoter { diff --git a/tests/ref/bugs/3662-pdf-smartquotes.png b/tests/ref/bugs/3662-pdf-smartquotes.png new file mode 100644 index 0000000000000000000000000000000000000000..c272a8ffd58124166f9b0637a89d3f4dddb3746c GIT binary patch literal 11950 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 Q|22pXDrqQ|DOd&nKga`d=>Px# literal 0 HcmV?d00001 diff --git a/tests/typ/bugs/3662-pdf-smartquotes.typ b/tests/typ/bugs/3662-pdf-smartquotes.typ new file mode 100644 index 000000000..36dc8a15d --- /dev/null +++ b/tests/typ/bugs/3662-pdf-smartquotes.typ @@ -0,0 +1,12 @@ +// Smart quotes were not appearing in the PDF outline, because they didn't +// implement `PlainText` +// https://github.com/typst/typst/issues/3662 + +--- += It's "Unnormal Heading" += It’s “Normal Heading” + +#set smartquote(enabled: false) += It's "Unnormal Heading" += It's 'single quotes' += It’s “Normal Heading” \ No newline at end of file