From 17ee3df1ba99183fc074e91dfba3e9189dae1c0c Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Sun, 14 Jul 2024 09:48:40 -0400 Subject: [PATCH] Wrap outline entry body in LRE/RLE + make smart quotes ignore directional control characters (#4491) Co-authored-by: Laurenz --- crates/typst/src/layout/inline/collect.rs | 4 ++-- crates/typst/src/layout/inline/linebreak.rs | 5 +++++ crates/typst/src/layout/inline/mod.rs | 2 +- crates/typst/src/model/outline.rs | 8 ++++++-- crates/typst/src/text/mod.rs | 10 ++++++++++ crates/typst/src/text/smartquote.rs | 6 +++--- .../issue-4476-rtl-title-ending-in-ltr-text.png | Bin 0 -> 6312 bytes tests/ref/smartquote-with-embedding-chars.png | Bin 0 -> 571 bytes tests/suite/model/outline.typ | 7 +++++++ tests/suite/text/smartquote.typ | 5 +++++ 10 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png create mode 100644 tests/ref/smartquote-with-embedding-chars.png diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst/src/layout/inline/collect.rs index f1607460b..b6a847f57 100644 --- a/crates/typst/src/layout/inline/collect.rs +++ b/crates/typst/src/layout/inline/collect.rs @@ -201,7 +201,7 @@ pub fn collect<'a>( ); let peeked = iter.peek().and_then(|(child, _)| { if let Some(elem) = child.to_packed::() { - elem.text().chars().next() + elem.text().chars().find(|c| !is_default_ignorable(*c)) } else if child.is::() { Some('"') } else if child.is::() @@ -302,7 +302,7 @@ impl<'a> Collector<'a> { } fn push_segment(&mut self, segment: Segment<'a>, is_quote: bool) { - if let Some(last) = self.full.chars().last() { + if let Some(last) = self.full.chars().rev().find(|c| !is_default_ignorable(*c)) { self.quoter.last(last, is_quote); } diff --git a/crates/typst/src/layout/inline/linebreak.rs b/crates/typst/src/layout/inline/linebreak.rs index 9deaa92a8..075d24b33 100644 --- a/crates/typst/src/layout/inline/linebreak.rs +++ b/crates/typst/src/layout/inline/linebreak.rs @@ -953,3 +953,8 @@ where } } } + +/// Whether a codepoint is Unicode `Default_Ignorable`. +pub fn is_default_ignorable(c: char) -> bool { + DEFAULT_IGNORABLE_DATA.as_borrowed().contains(c) +} diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index f89de1690..821b4f57e 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -10,7 +10,7 @@ use comemo::{Track, Tracked, TrackedMut}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::finalize::finalize; use self::line::{commit, line, Line}; -use self::linebreak::{linebreak, Breakpoint}; +use self::linebreak::{is_default_ignorable, linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText, diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs index 090472850..ec1e5f1b8 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -483,7 +483,7 @@ impl OutlineEntry { impl Show for Packed { #[typst_macros::time(name = "outline.entry", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let mut seq = vec![]; let elem = self.element(); @@ -500,7 +500,11 @@ impl Show for Packed { }; // The body text remains overridable. - seq.push(self.body().clone().linked(Destination::Location(location))); + crate::text::isolate( + self.body().clone().linked(Destination::Location(location)), + styles, + &mut seq, + ); // Add filler symbols between the section name and page number. if let Some(filler) = self.fill() { diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index 7648f08fa..d42e4df8b 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -1299,3 +1299,13 @@ cast! { ret }, } + +/// Pushes `text` wrapped in LRE/RLE + PDF to `out`. +pub(crate) fn isolate(text: Content, styles: StyleChain, out: &mut Vec) { + out.push(TextElem::packed(match TextElem::dir_in(styles) { + Dir::RTL => "\u{202B}", + _ => "\u{202A}", + })); + out.push(text); + out.push(TextElem::packed("\u{202C}")); +} diff --git a/crates/typst/src/text/smartquote.rs b/crates/typst/src/text/smartquote.rs index 236d06363..797f0804b 100644 --- a/crates/typst/src/text/smartquote.rs +++ b/crates/typst/src/text/smartquote.rs @@ -123,7 +123,7 @@ impl SmartQuoter { /// Process the last seen character. pub fn last(&mut self, c: char, is_quote: bool) { - self.expect_opening = is_ignorable(c) || is_opening_bracket(c); + self.expect_opening = is_exterior_to_quote(c) || is_opening_bracket(c); self.last_num = c.is_numeric(); if !is_quote { self.prev_quote_type = None; @@ -150,7 +150,7 @@ impl SmartQuoter { self.prev_quote_type = Some(double); quotes.open(double) } else if self.quote_depth > 0 - && (peeked.is_ascii_punctuation() || is_ignorable(peeked)) + && (peeked.is_ascii_punctuation() || is_exterior_to_quote(peeked)) { self.quote_depth -= 1; quotes.close(double) @@ -168,7 +168,7 @@ impl Default for SmartQuoter { } } -fn is_ignorable(c: char) -> bool { +fn is_exterior_to_quote(c: char) -> bool { c.is_whitespace() || is_newline(c) } diff --git a/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png b/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png new file mode 100644 index 0000000000000000000000000000000000000000..09506966ee8e2ae3b038e9fedb6b89c46b844e0b GIT binary patch literal 6312 zcmV;Z7+2?sP)LP!X&z?qWk$(A9Mn z6?+#fiyg(@d+)tuf8@YLG#eMQKPv;NWoO%9YozU(e6auWU6Y zwzajPtW3JPx~8Y6*C5cKeCW_2EiJ9Fu`yxk@9*Eedv`-a!wOa#7#KKw`0&S%AJJDJ zc6N5_*ROAFZC!&vzkByiMMWhlDr%uII5@bfs!ICV)6=tb^X={JQo@mpMMXud#`(Z) z(wRVSY-~i4WN2t;baYfWb!22jItovoJYhNqVsdg)80l?pZe9yO@7S?JRaKP@f3df$ ztnA#mbNc%F&z?Qg)YME*PsbP#5TK!bF!Bt_wQJXAW@hlm)2B~C>eQ)I_^-6I z6znW4EHFNQ{`}anV>qv`ukXf<8|&7s)6>&4H8tI`Ws91cT3lQl%X)izA2@J;>2Kb= zA%kIYb#-NZBO{|TXU?o{pgTG`fQi%k#f&^VJNx5j5&DrMM|SPnMXKTF?c2AR7#<#u zPC^Hr;^JcT!oosyLqkJ!#LLTzVT5FTeLaew_VjRzH-MaPV%a`Z^_pghpLWyPHzJ0_LD0BRm_EuI_R##UO$mql%;1c**Sy}wt zwrv}GyL|aF`iT=KXgfPQX<3SvE?OigCC-4;E*Bu} zqUpH*!Gi~pk&(FM{rmTd{nD&kQoVQ z6ke{eH#IeF-@cu6j7}zzaN&$gEuNz#5z`_J1~tGUeY3uwpPw{Kn#%w@Dmfkz(=#$M ztgWqiJo3ii;2^1KnVo!6ddbVnd;It@_ZzvAi-@#XUS2L;h8HhhB!{KQ)%Vl1qN3k?hm z&^K+`8PXZEak#Mk+&sl)a6Qk1y>4GU;Uw z;S@8BH!xae37vBwVZq=<(?t$J2xLYsnoEmOdJ`AVAT2dDH6?Q$atSU2EsG4a474mV z(5nXM{(%wcZ)9X#*`7W)yN6!KgC~5ePbu}LTG3x zOx1VlYTxhIP$-k+{!#ks)vMIhRCo`bFfcIi8yC#Z&W7dKym>R_DDTvie{dk8Xrj`g zc!5piRl268=F+80yhKwLQI0Z^qDJ$F{VOlQp;4g3s1y`RDSP+sWuH`QXV0FktE&@1 zBNgRBAf5ytf-_*9C^w)(DMLgp4xfibB2O$WEkPC#Vg4B77a!>HP+)bsQ6aT{@+kdOeQ0)v>6l7gLr9)=G( zJ1s3uU0wa&y?cB~z@D3%3k|Rw(5qZ1lxu5iVJe6<_-nqEoIij5!-o%mOWmB4lXLaz zRk{FzoB*c*Ku3=rMFCdAnJ+1@{)_mz4I4I`Jb6;Y8MFxpB{nvepMpfCWf7t9-!T}s z{WCPFIFiaK>=RyzFM%SuVIBm{Mi}A)cp`xpf{_6Wcf>~%Vmu-uLh&cN_O_B$5XAxf zrW@ZtA3;Tgd<(%VZ$$4@&?oR#FD$TIMbrqqNg^aNvhzv%Ecz17V;&PazDB=VwAiR+ zpmx(YY&NrJX3wm(X6@PY|IhlBIC5m1W<>^bq8}fPcc-VPtrb~rQL=~!$!D=Tt)V%E zRnG^KB|NCFuTQ46wUxw!gM+>xU(@jLFelA26Y!`heaQHw}Lf3T5} z5f0_3R8>z;kG0P!6wN7MO;qSd=k7?<=sw=j(NS9;R9Q;m?8*rq3F8s3##@ajSxGG4 z$T_bwrDeLlzMfrXW@b=ye(wY>m&;ZL?wK80B%N#_3uTHvF8R~OiC30W)^x@p0KHC2 zlUmD2>4c(qV$XOYSky%Gz!=yZ>-* zb$w4};q~(3(q@+Hd}r_Td}(oa|BJhPRy}w$UmW-Dy=RAHE7sn+{Xh>gkDipWFs~=i zUb&N#R9X1u?b?g^a>%^#{y^poB9)x#!?;sZQ$l+h8yoBD>YAFGfCxc4z*RPbTz~@n z&cwuoZ7<{^#H3UzH8(eB8}Q-c&jb_O5f2Ouh^7z;H#Rn=)*Ydv9RDEB2%0bi?gj!D zh-7=au&_W9qu_Ti68aY*2jg^gbpaqyQVs*-Qg)!|oj*1h@M8`?W#MB=D8Yfvu8^ur|@R5Sow)AuOg4Ts94i2yro7 zTU(nKnGYX?tT46U32cd@5S8I|B6W0hFe2*GKwd1$47-ZZU`;*q3FtXu2Zn}*ARy$C zNDl@g09cD%8Xq4QYzH^`(`bghOPcl9VhHOfZBxFEcQJ<*Q)zwBn8&avNs*1T~ zfKGyn9gzMJA9DKsdWE%hybt|Zj+oCA|amHm1rsS|1P%k)4Go&|8 z6!TPDTg&6vnpVo`sR3N)QCIZj;4GU1aWr%i6b@2D94VWCIWlmOsMWtmGzhwx0|C>~ zjD$t?eORi@gmMWJt;RpgOORv4P#s$(RFHkm3a$rRBxa5RMMG1bN5>=uZGz840Qu7KHSO)~ zXbK7!dCPbVTFz3KQH@GCm*^~u565%}@T2rQJNL8FRuIK;9DFvxH*oD6lrCHp!ME@Q z1h=l#R=N@?A|Vv~6Dy)g8rl$>KMf&-bkz?WI)t_m=q)b7Ib4Rxow?)O-^`tR=R0To zb^pd_>lKl0QEQKjGF_#d_$jrN&vRF0*4Nhq9xXL@7mvHJDpX?g`H$IbUXJJIzTr;f z&*Sm2+3ycU+U?H8#pPfy`Yi)j+lVxqZJ`X#Twd00ZW?~p>mQwo65^OnXNnRT4!@7b z6B#<4FOl^0)Dm7@-MHK#wOl3c%xE+g@AbYd7T9Wio=m1vqi&Mij!3uL6Gl<-7t2v< z^6>B|;@%@exyqjq=FBjs;3;%k3Y|iyRS|mewd>nswY7+ktA9Q!2_4Yn)$2DRU%P|V z7W(ef6I4{NQLTPk!oX+eAL<1~6>1hoRj6b_(P%UP2l16R^YV?24cKIBYsPd-xb)n6N#UA`WV}zSlx{E%CBN+&!wWG9MS58tVxM zB@ZU29B5d_;k9IC09V@C*^y+RbkDkv&c%NUIf8ZIY)OVoLO@itS`C%o-`@v}c6WDU zMS?W2GLFnq@!;UV5NvFXg$VGk5HV1R*+Q#Uw)68Eg|Vz)aBPf$G|(eHtZ;F4kROuP zFqNQ^ua?u(_eV!3XJ_Z-^G+>scM-J_0>Nk*N~r-<+RcHL=dI>`v<8fC+`v}<;*aJPD`Ov z=(H3%g-)TnWbCJbiowe5G5=bj-B!s~N4oI8;2@8b94X`-kdi$iW4yCf2 zLNglFO;3b@vWu*)u3wpvRq=mBeOI|a8>6hP9O&mOlKzvif<@G*jXq*H>oOA^AkwBR zW$Y+QEM%Uvm#+kLL=mZ?E!RjJ^q+lDsZ`%qCXGcUdWfqM;iCi=aN`@RgP(-zrsaUL zk5Yz^#p$d{wcQzw$J5i(t{3XAN*5VlisPn@5l27MvY?sqtyS@viiN1f6*3P{v1)5* z8-Nli+5S( zr4a3|higDq;8=?N^Lx^eG}ytxL9$vFZ2KeH(7^NK%O!Zp7YgT1A|&(23QB(4FNApZagTe-WaU6BoG&XJ8c!tQ_EiE zLD4i+AV;E#>#@L>kDakjj94zTyV~1otC3DN= zmue>!?h8o5h^$P*w5uLJK}Z$w@fQ{bbxtgW*x0hTOTwkPKwQM#-JLwnQHVrjLe7y( zLRG>!l>34s#fT1-HB9Tn9gQ6r7e|64SJxuxCm9?&EeUhB^|ve~{zz(I9U%PWG?FJu z_muudT#aIVYFuY8A||8E3@|e|-&zTnExZTlVMK!C4#YI7F||h!5dCxY2CqpbbN*#nwp!E$>v~K#gt* z<$2AD+ZyNwx~+k3pd0A62D*W6YoHtG2Dm^uf0i}_78WA$xuzDkKxjHu>8??D@-vu~qm+Mf}PEk$@EOcf-jXn{M` zfGR4)TFiFLzY@|B9O2ICkIv7}y-FPrDIZR=09+j~=#*ymq8S6FK|0baNLv-EU`^3L zg7eSHfJ+927xuNTt}AUv`W|WpME?E&jkhj2mSQLXBKI0QsOzPkw+6L_ldqX>&|$B& z6^zlI7;8wW_f;RuClD)58wrcaA2gy#b3G!0HSjf-?JLz(qCQAfb`~vURhq7iwxn3R zcLJ>U%&W#Ckts7ds*q6`FVCe!F+*9IhO5if)XNX^J)BZ1g6~i9DkXz>fGXgO0^w?SMlEunuK?V2$n3t`^I>f!MuC8Ro zI$=Ru8LXnoOo0w@VNna|^Y|erGwzjB)y;Ns9DPJbMe&6z1wZAuL0snh7`(r~zX#vn zD--h+U&g%X24z%>h?2ZSJrRH|yeOc|deIUW09eeigzNlThmdUR8(gO_2_i>uWN0ww zB68wb8W5L}=nxvXE<IknFy+m1oHC+-R4Vmx>N(7_f+_Y*gkS`tR+M z_ep4o^h?!O;2;T&f_9LtOn+OZ9UaI8u8WNfOgVdp*2qP2G58ie#SvX;nWL%c;RKz? zmVuTDT)B@7`BcOblK|{w-rLvU3Ug}83uY$2)tD79n|gw^s@mwB6xSk5fnr>Xw^jOW z#pKGoi-Ds{8%LE9id1}fc*sV1qJZYU%n1|J@)NF$r*=gxNLvPa3L!dMK;!W?{&&id z<-xNSIX#Kj<^d{?KS^beGL#)xn^QIAgBPd7&y1yRpp!O->v~LwGg0hVgF|Hq4Q|Sn zo`B|{MQCxmUP0@izF7^4ox8o`(Ey$GdcoXy7{cQn!twEOM(b3fCNsA%vZisJG#eBL zKIWCR2mx+4uhLd32HnTWP86*diTILo`XJT;rsdg0x&fU98Ow!O;ja<%9Ed~g9MJBl z0G&+k1yeQ#Ygb<0n&u?!i0r5+ewmfioj}_1^i6HI2iOxC+xCiC?tFy4FR{dt66EzD zW%M2ew0FK}UtC<+v3#__5ui~D-v&!jm7N3H+{bl(*v+^rGR4Gn3q?d>p_`mW&2;m+ zD#W0)~sTBl8iWbXx=6KsV5B4Rizj#f%)iXq2+52S;L>fS)JHFeww`6R9d%pn?9tk~ZNj zu{7#p(4N%AJO!);CgLNOz;Z+g+=mC5Qr4iNH^uM28>DedX$sVDvh zdV`4y2}z603Bpm_VoyZ61qG2Qm=^&okyy!I5{+lUQ7aTvE88{QXEzXSr-UgotYtzMrfdC zzTCwBmkkC-Hy+~_i(AP7E8Wv5ru)&jy}flySDlr&?%TV5GHjsR8t4YPt$}W!8|bzM ex`A%{!2So6cNeeLwXXaC0000u4^P)xbKrt&1Jh&0;Z$YGsrMr_W&h0Aa`P6vp>`{=fr1hu5=(CS8jHE3g9ZML4z6B-m!6 z?%dBC1WNnTGrR+&(}Jy5wR;7^hKKFxa;Jn}94;p>BM7AnfipmMDSB zq?>exJtF4dxBcZ=9mA2-b)tF>vYU z3KWiUi1bzZ<^XU!=vbBwW_Hr`7LbfwTJ`nfjDI=Ii%zW3^hsV?Xsw1$RlDN=A1(8K z@whoMK58?*KkfkQ0t2Y}<9x zaWNKfp_}}=Z!=+FD6*mOnb&Xgc#a9!n_J=^wl{QZ_&z=^Jo(QpL>@a$On@r54