From 50dcacea9a3d9284ef1eeb9c20682d9568c91e70 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:23:14 -0500 Subject: [PATCH 01/31] Convert unopened square-brackets into a hard error (#5414) --- crates/typst-syntax/src/parser.rs | 45 +++++++++++++++-------------- tests/ref/single-right-bracket.png | Bin 118 -> 0 bytes tests/suite/scripting/blocks.typ | 37 ++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 21 deletions(-) delete mode 100644 tests/ref/single-right-bracket.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index e087f9dd3..cb5e2dd85 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -47,14 +47,9 @@ fn markup_exprs(p: &mut Parser, mut at_start: bool, stop_set: SyntaxSet) { debug_assert!(stop_set.contains(SyntaxKind::End)); at_start |= p.had_newline(); let mut nesting: usize = 0; - loop { - match p.current() { - SyntaxKind::LeftBracket => nesting += 1, - SyntaxKind::RightBracket if nesting > 0 => nesting -= 1, - _ if p.at_set(stop_set) => break, - _ => {} - } - markup_expr(p, at_start); + // Keep going if we're at a nested right-bracket regardless of the stop set. + while !p.at_set(stop_set) || (nesting > 0 && p.at(SyntaxKind::RightBracket)) { + markup_expr(p, at_start, &mut nesting); at_start = p.had_newline(); } } @@ -69,15 +64,12 @@ pub(super) fn reparse_markup( ) -> Option> { let mut p = Parser::new(text, range.start, LexMode::Markup); *at_start |= p.had_newline(); - while p.current_start() < range.end { - match p.current() { - SyntaxKind::LeftBracket => *nesting += 1, - SyntaxKind::RightBracket if *nesting > 0 => *nesting -= 1, - SyntaxKind::RightBracket if !top_level => break, - SyntaxKind::End => break, - _ => {} + while !p.end() && p.current_start() < range.end { + // If not top-level and at a new RightBracket, stop the reparse. + if !top_level && *nesting == 0 && p.at(SyntaxKind::RightBracket) { + break; } - markup_expr(&mut p, *at_start); + markup_expr(&mut p, *at_start, nesting); *at_start = p.had_newline(); } (p.balanced && p.current_start() == range.end).then(|| p.finish()) @@ -86,8 +78,21 @@ pub(super) fn reparse_markup( /// Parses a single markup expression. This includes markup elements like text, /// headings, strong/emph, lists/enums, etc. This is also the entry point for /// parsing math equations and embedded code expressions. -fn markup_expr(p: &mut Parser, at_start: bool) { +fn markup_expr(p: &mut Parser, at_start: bool, nesting: &mut usize) { match p.current() { + SyntaxKind::LeftBracket => { + *nesting += 1; + p.convert_and_eat(SyntaxKind::Text); + } + SyntaxKind::RightBracket if *nesting > 0 => { + *nesting -= 1; + p.convert_and_eat(SyntaxKind::Text); + } + SyntaxKind::RightBracket => { + p.unexpected(); + p.hint("try using a backslash escape: \\]"); + } + SyntaxKind::Text | SyntaxKind::Linebreak | SyntaxKind::Escape @@ -108,9 +113,7 @@ fn markup_expr(p: &mut Parser, at_start: bool) { SyntaxKind::RefMarker => reference(p), SyntaxKind::Dollar => equation(p), - SyntaxKind::LeftBracket - | SyntaxKind::RightBracket - | SyntaxKind::HeadingMarker + SyntaxKind::HeadingMarker | SyntaxKind::ListMarker | SyntaxKind::EnumMarker | SyntaxKind::TermMarker @@ -201,7 +204,7 @@ fn equation(p: &mut Parser) { let m = p.marker(); p.enter_modes(LexMode::Math, AtNewline::Continue, |p| { p.assert(SyntaxKind::Dollar); - math(p, syntax_set!(Dollar, RightBracket, End)); + math(p, syntax_set!(Dollar, End)); p.expect_closing_delimiter(m, SyntaxKind::Dollar); }); p.wrap(m, SyntaxKind::Equation); diff --git a/tests/ref/single-right-bracket.png b/tests/ref/single-right-bracket.png deleted file mode 100644 index 9867424ddfa324301c82cc4dde8072d9dfaa899f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS_0VEhE<%|3RQnsEhjv*Ddl7HAcG$dYm6xi*q zE9WORe@;PCL(QJexeYq|46;71IC}Wpqgv*ak0;k1UYz=M#nHuL{ZT$l3=9kQOnGR} Rb7?8aKu=dcmvv4FO#p!jD Date: Sun, 8 Dec 2024 13:25:47 -0300 Subject: [PATCH 02/31] Ensure par and align interrupt cite groups and lists (#5526) --- crates/typst-realize/src/lib.rs | 6 +++-- ...03-cite-group-interrupted-by-par-align.png | Bin 0 -> 1487 bytes tests/ref/issue-5503-cite-in-align.png | Bin 0 -> 393 bytes ...sue-5503-enum-interrupted-by-par-align.png | Bin 0 -> 1004 bytes ...sue-5503-list-interrupted-by-par-align.png | Bin 0 -> 415 bytes ...ue-5503-terms-interrupted-by-par-align.png | Bin 0 -> 569 bytes tests/suite/model/cite.typ | 22 ++++++++++++++++++ tests/suite/model/enum.typ | 12 ++++++++++ tests/suite/model/list.typ | 15 ++++++++++++ tests/suite/model/terms.typ | 15 ++++++++++++ 10 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-5503-cite-group-interrupted-by-par-align.png create mode 100644 tests/ref/issue-5503-cite-in-align.png create mode 100644 tests/ref/issue-5503-enum-interrupted-by-par-align.png create mode 100644 tests/ref/issue-5503-list-interrupted-by-par-align.png create mode 100644 tests/ref/issue-5503-terms-interrupted-by-par-align.png diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index c46a15351..fd43e8304 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -836,7 +836,9 @@ static CITES: GroupingRule = GroupingRule { tags: false, trigger: |content, _| content.elem() == CiteElem::elem(), inner: |content| content.elem() == SpaceElem::elem(), - interrupt: |elem| elem == CiteGroup::elem(), + interrupt: |elem| { + elem == CiteGroup::elem() || elem == ParElem::elem() || elem == AlignElem::elem() + }, finish: finish_cites, }; @@ -859,7 +861,7 @@ const fn list_like_grouping() -> GroupingRule { let elem = content.elem(); elem == SpaceElem::elem() || elem == ParbreakElem::elem() }, - interrupt: |elem| elem == T::elem(), + interrupt: |elem| elem == T::elem() || elem == AlignElem::elem(), finish: finish_list_like::, } } diff --git a/tests/ref/issue-5503-cite-group-interrupted-by-par-align.png b/tests/ref/issue-5503-cite-group-interrupted-by-par-align.png new file mode 100644 index 0000000000000000000000000000000000000000..166331587009d036a445d95c3478dac114015d58 GIT binary patch literal 1487 zcmV;=1u*)FP)!yuQY8a(c$b#{K>MR8&^^`TG0&{PObh?CtTCmY(OVnAlq|_4Tf+9>Wv%9pkw9L%RFfux7Y;?D` zw}*#^j*gCujEwN`@P>wlv*4n|t%V=tHy1T!ttEanCa>3l$4fQ zT3oNOxp8xQz`(-u^YcJJK%k(Ym6esSu&_KmL0Vj7t*x(QW^Ui#;o#unsHm!vl$>d6 zbDo}_U0-LZsjFOGX1Tk=yS>G|zQkW)X}`hA!^O?T$Ir>j)9UK%>+9{$&(YD-+t%3N z-rwcY)Z9^1Tkh`ei;Ih&pPyb}XsW8Ld3%F}hmW$fyn1|uy}!rS*4W_S;Q9Ia)79Ob zpQoRpsfvu0wz$AfP*_b*S4&M*R8&-TcYjPyR?g7a^YivnQ&-p7;nmmQmzkk|fr+lL zxPE|$@$vEQ@AFSlS>4~~v+#k$0008gNklq~ zTwUuyK+6(9UoH?cvAP5B*3%wHv!}9#O+Z7Bp8!m@ z@Gs1ds5cjD`2oKw$1z`zgfbsGb0A&K(unG8}wY-nnhf&4DjKT$J$w|z^ zR7L6M#KcdOs+Smjc zBM4jFy=$alH!OxPQ;y^633~n7Rce;oxswwyb57rXi%PoAn(0VngmM5yg#dK6o5Vh# zu#E7tvb8ka z{2CLB;gQw{2R~Q7UOA3aM)(}HH1h582vaVh^X3b}H(}S#-HgAxO9B1KWB{dL8#V!6 zCrkiL?8Er@--Q!&%7lXl4eZA}On&@%VC&!kJ89G$KEjtN$8mM7neCswloy04cmVpM z;sSitvA;<)_43SC8D@9)SZ0m pbaN+c-A%PF5dXG(nQ~lx@*8WC00B1gL@odT002ovPDHLkV1mnpGGG7z literal 0 HcmV?d00001 diff --git a/tests/ref/issue-5503-cite-in-align.png b/tests/ref/issue-5503-cite-in-align.png new file mode 100644 index 0000000000000000000000000000000000000000..aeb72aa0d169fee63a9e8db9bef41fbd4427cc4a GIT binary patch literal 393 zcmV;40e1e0P)gwwD_V(D=*xK6KU0q!@H8n;?MvIG!m6er0K}p)&_3000000NkvXXu0mjfe@M+w literal 0 HcmV?d00001 diff --git a/tests/ref/issue-5503-enum-interrupted-by-par-align.png b/tests/ref/issue-5503-enum-interrupted-by-par-align.png new file mode 100644 index 0000000000000000000000000000000000000000..9cc942b4cc09c57c1cce0c1408e01cf2e972699f GIT binary patch literal 1004 zcmVb|=u zb^-ueRHpRzG#~^MOfbPy85Ry-X9)A!D|ykbC}9Yj>+o+d(z=p&>|kJI38L_N!=edh za4>xc;*F77#|%F0>K~tk_jIy?O^dVRLnr$H0F_(s9Rw@bWwZCf!2hi$Z61QSd! z!BY`-H3wyZ>$Q)!$pX81ZGN;>4Jv&Iru;X;xrVe)62o}7q#~N?^??Hhb8AluiZ}E@ zY8d>VT%_~`xW6l?tBQsC5~PO5G5!w*g)p3pa&=+t*aDQ5NDjB1F0vrwWf+bxJrin6 zH6)q$?sl_$kM#xe3<#gRd_)!)9?t}AHNgZE{G;&eR1Z72t7@_l_mLUQZBUAh;PN46 zaFJ1j2To{yWd^sjH;C}=E+jC6Zz+@Gdm^1+0mDZP?J-;$000y#+W%HFgZ(#aKRp@? zchjRML$q836HG9{QxA4J?DD|Nj+Dv*4{9d6_IZ^RXs_@JHP1Ah#Bg7xLLF`8syAzz zp4M6p;#87}mly{4-=l`U0QGzE@M;{?7XgW30m5GhMc{l?=T&O_3vszja@evZrw+;X zFubUAkN-|e)g1G>)zjkR3y!A*WP$m%HXm&@!2}chgK)pj$`I~u6?3Q8zz(jyAf_`J zU0r1L#mpGo z)Q1?c1P-=ryM`F_Ct`U;&Z~jk^c)NgE8Xe;0veVfsgZ!`MiN_ zYwM04kC*rQ*bYcAc63-dA;)rcYnyo5YJv%#Hn82s6n-k!nM{VTko%k14^A+>@w`bm zO|`=<$B)ksR*dEuWr4F$S;h{Y1=s=r08={33z@;Xjdzgq766cah&!GUq~#)*V1fz$ a_Iv?W=#otA7_LeH0000%wxAq))we5$W( zqlC2pa+cDYcP+VTJR*i+Dn*v6&+-Cj;SS8@k7eBpDJ=RStliO^Bmz=m_MHw6*?dl? zVh={Q{h~d8!Ue)G!wfV0U*Pjo89<9qJ9}~jR0>Gm{IU~Z>0x|qB*o?b8 z7ryBfF${f69!Vm)3R*Z0eXE+*#iI^FIMy56H54>4XJRFt8jH}u_sX5}R7q#+S4H5a z!_CDo!wfV0Ggulw1Q2Mep-Tv3008Rhdu=BlUlwa3xRu$UA76~6h&y{rKE43va$|Dl zl@=ZXT*8z;gM06Cju4g%UdNO&;SYJ&n+;2t3xr{Y8D{v~eF5@?n6e%z(Fy=G`P)qI zPjl*XWq^m#t;VPD44m`UXj0{@!Sz(QzjZrWMz=*izgN zJ0*c<@`HhcxiUaCf-I~&!C2;R@!?+#_OEX78&gW)%@EQBcmA$9sSq6w)V^DQwYoFe z%y=TImia3CU&jAIH$w$NFu?>9d;#G@qh)BYhm#(jRr@o#2kNAU_fsff5l^lF!MI=W z30`442t?Tf@qc^_M=GZu>}FRnvzObepZ&?@)+&2jC4rmYb}19;9^e+ZVtnZh-L?vn zz?z=`)HWaso??9cA6_faiViPd|CVsy>^H{pYE!G1mJN5KN|n~!W)U3@xLF=y7r)1` z$q*OvaZ#3wxqQ@I1QSd!!Doca8!L+HU&sLK(4m4(w((`rU_z&{&hg&a@NcZfi%EUE zB(RXTUeZ66v3U6UlW@iBs(>P8kpy-sY}U}}@ue%%CO+IzgY9%4dyW!-vEl}n9>wgN z*`mXNa^(Vkl(p?NJ@!P3n&xvRw1vKxx;`opf(a&=;FBBycoS&Zfmn~t00000NkvXX Hu0mjfmu3tE literal 0 HcmV?d00001 diff --git a/tests/suite/model/cite.typ b/tests/suite/model/cite.typ index 902800818..b328dda49 100644 --- a/tests/suite/model/cite.typ +++ b/tests/suite/model/cite.typ @@ -114,6 +114,28 @@ B #cite() #cite(). #show bibliography: none #bibliography("/assets/bib/works.bib", style: "chicago-author-date") +--- issue-5503-cite-in-align --- +// The two aligned elements should be displayed in separate lines. +#align(right)[@netwok] +#align(right)[b] + +#show bibliography: none +#bibliography("/assets/bib/works.bib") + +--- issue-5503-cite-group-interrupted-by-par-align --- +// `par` and `align` are block-level and should interrupt a cite group +@netwok +@arrgh +#par(leading: 5em)[@netwok] +#par[@arrgh] +@netwok +@arrgh +#align(right)[@netwok] +@arrgh + +#show bibliography: none +#bibliography("/assets/bib/works.bib") + --- cite-type-error-hint --- // Test hint for cast error from str to label // Error: 7-15 expected label, found string diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index ed33157e8..c5e562159 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -163,3 +163,15 @@ a + 0. // Enum item (pre-emptive) #enum.item(none)[Hello] #enum.item(17)[Hello] + +--- issue-5503-enum-interrupted-by-par-align --- +// `align` is block-level and should interrupt an enum +// but not a `par` ++ a ++ b +#par(leading: 5em)[+ par] ++ d +#par[+ par] ++ f +#align(right)[+ align] ++ h diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index aa117672c..138abf70e 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -218,3 +218,18 @@ World part($ x $ + parbreak() + list[A]) part($ x $ + parbreak() + parbreak() + list[A]) } + +--- issue-5503-list-interrupted-by-par-align --- +// `align` is block-level and should interrupt a list +// but not a `par` +#show list: [List] +- a +- b +#par(leading: 5em)[- c] +- d +- e +#par[- f] +- g +- h +#align(right)[- i] +- j diff --git a/tests/suite/model/terms.typ b/tests/suite/model/terms.typ index 07aa827dc..61fe20b0d 100644 --- a/tests/suite/model/terms.typ +++ b/tests/suite/model/terms.typ @@ -75,3 +75,18 @@ Not in list --- issue-2530-term-item-panic --- // Term item (pre-emptive) #terms.item[Hello][World!] + +--- issue-5503-terms-interrupted-by-par-align --- +// `align` is block-level and should interrupt a `terms` +// but not a `par` +#show terms: [Terms] +/ a: a +/ b: b +#par(leading: 5em)[/ c: c] +/ d: d +/ e: e +#par[/ f: f] +/ g: g +/ h: h +#align(right)[/ i: i] +/ j: j From fbcd624eebf9763d5cedbbc433e8b5ffee8402d8 Mon Sep 17 00:00:00 2001 From: Borna Punda Date: Sun, 8 Dec 2024 17:29:24 +0100 Subject: [PATCH 03/31] Add support for Croatian quotes (#5539) --- crates/typst-library/src/text/smartquote.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 925e86d65..2f89fe298 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -211,7 +211,7 @@ impl<'s> SmartQuotes<'s> { /// Swiss / Liechtensteinian German, Estonian, Icelandic, Italian, Latin, /// Lithuanian, Latvian, Slovak, Slovenian, Spanish, Bosnian, Finnish, /// Swedish, French, Swiss French, Hungarian, Polish, Romanian, Japanese, - /// Traditional Chinese, Russian, Norwegian, and Hebrew. + /// Traditional Chinese, Russian, Norwegian, Hebrew and Croatian. /// /// For unknown languages, the English quotes are used as fallback. pub fn get( @@ -250,6 +250,7 @@ impl<'s> SmartQuotes<'s> { "ru" | "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"), "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), + "hr" => ("‘", "’", "„", "”"), _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), _ => default, }; From e6de044b1da1e1c801a69149d757bada7154a83b Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Sun, 8 Dec 2024 11:29:49 -0500 Subject: [PATCH 04/31] Add test for issue #4573 (#5542) --- tests/suite/scripting/destructuring.typ | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/suite/scripting/destructuring.typ b/tests/suite/scripting/destructuring.typ index 3c0c754c8..87bdefa73 100644 --- a/tests/suite/scripting/destructuring.typ +++ b/tests/suite/scripting/destructuring.typ @@ -373,3 +373,23 @@ // Error: 6-12 too many elements to destructure // Hint: 6-12 the provided array has a length of 3, but the pattern expects 2 elements #for (x, y) in ((1,2,3), (4,5,6)) {} + +--- issue-4573-destructuring-unclosed-delimiter --- +// Tests a case where parsing within an incorrectly predicted paren expression +// (the "outer" assignment) would put the parser in an invalid state when +// reloading a stored prediction (the "inner" assignment) and cause a panic when +// generating the unclosed delimiter error. See the comment in the issue for +// more details. +#{ + ( + // Error: 5-7 expected pattern, found keyword `if` + // Hint: 5-7 keyword `if` is not allowed as an identifier; try `if_` instead + // Error: 9 expected comma + // Error: 13-17 unexpected keyword `else` + // Error: 20 expected comma + if x {} else {} + // Error: 5-6 unclosed delimiter + { () = "inner" + ) = "outer" +} + From 62567fc91e2d58d5d12457bbeddc5d7950d7c570 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 8 Dec 2024 13:35:54 -0300 Subject: [PATCH 05/31] Fix multiple footnotes in footnote entry (#5545) --- crates/typst-layout/src/flow/compose.rs | 15 ++++++++++++++- ...issue-5256-multiple-footnotes-in-footnote.png | Bin 0 -> 796 bytes tests/suite/layout/flow/footnote.typ | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 tests/ref/issue-5256-multiple-footnotes-in-footnote.png diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 343b47833..9650a7bc1 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -470,7 +470,20 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { // Lay out nested footnotes. for (_, note) in nested { - self.footnote(note, regions, flow_need, migratable)?; + match self.footnote(note, regions, flow_need, migratable) { + // This footnote was already processed or queued. + Ok(_) => {} + // Footnotes always request a relayout when processed for the + // first time, so we ignore a relayout request since we're + // about to do so afterwards. Without this check, the first + // inner footnote interrupts processing of the following ones. + Err(Stop::Relayout(_)) => {} + // Either of + // - A `Stop::Finish` indicating that the frame's origin element + // should migrate to uphold the footnote invariant. + // - A fatal error. + err => return err, + } } // Since we laid out a footnote, we need a relayout. diff --git a/tests/ref/issue-5256-multiple-footnotes-in-footnote.png b/tests/ref/issue-5256-multiple-footnotes-in-footnote.png new file mode 100644 index 0000000000000000000000000000000000000000..f9c1733515950a9a022e784c477d151273be14c6 GIT binary patch literal 796 zcmV+%1LOROP) z_4V%W^WER)_xJhV;OF=E`2GF;>+JB;)!ot4+qAa6@$vNV@bK#E@9pmL>gwu;hlktS z+vn))=;-LAq@-eEV&vrHSXfxKwY8d?pWfcytgNi9ueW-8ft#G7k&~Nxe1xZ|v9q(a z)Yjf_ae2?t*+@!KaB_Oh&(~yTZq3cnl9Zgs$k5*2;*^z{%F4|4_V)Ah^M!|x$;;EE zrKzi|u!e|`%+1l8oS>+8I{ys4?F%F4>h%+x|eOw-fTu&}VGsCb) zO-)HjNpW#;*x1;XmX?o?kMHmAudlB?Jw5I1?SO!QQBhI!^z=?nPF-DH{{H@`sHouJ z;COg=-QC^k>FN9X``_Q+=I8D6^Y;4s`uqF*ii(Qj$FKwd00BrzL_t(|+U?gxR|7E= zhT)H72HN6Mytuo&ySuwvTWFC&U8X;r*|O=XB!_aJWdAO5?mZ_UgphdfeL;%Tvnxo! z8zhD6>MtP$=bXY~Fve)55JLW#XiWG$NiosGBobwCKSI4YQtpb$oJe zc1dh_bc8jvw2n>8&n$`!n~bc{sMe@c(+gt5oasFKs3fVZa3DQFEYY+emL$SSU404e)>>USU>|#;!LaV z==2055H8cw-Sy;twnMZGQ!T7EpBJLBR1>WB#|J4Z(`tkBRYo}fSnl2q-p-Mg4Z+y%cHkXAM8X2`yP%U0#_}4FE+f3MYA!9IIU|IS aAHD(Hcs_t>m%3&E0000, C @fn, D @fn, E @fn. // Test whether an empty footnote would cause infinite loop #show footnote.entry: it => {} #lorem(3) #footnote[A footnote] + +--- issue-5256-multiple-footnotes-in-footnote --- +// Test whether all footnotes inside another footnote are listed. +#footnote[#footnote[A]#footnote[B]#footnote[C]] From d04cc61eee2b9519e82b7759a929314299cb4a34 Mon Sep 17 00:00:00 2001 From: Tetragramm <9815373+Tetragramm@users.noreply.github.com> Date: Sun, 8 Dec 2024 10:43:25 -0600 Subject: [PATCH 06/31] Add missing functions to the gradient object. (#5528) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: Laurenz --- .../typst-library/src/visualize/gradient.rs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 2be7e3701..e16e5d88a 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -697,6 +697,8 @@ impl Gradient { } /// Returns the angle of this gradient. + /// + /// Returns `{none}` if the gradient is neither linear nor conic. #[func] pub fn angle(&self) -> Option { match self { @@ -706,6 +708,54 @@ impl Gradient { } } + /// Returns the center of this gradient. + /// + /// Returns `{none}` if the gradient is neither radial nor conic. + #[func] + pub fn center(&self) -> Option> { + match self { + Self::Linear(_) => None, + Self::Radial(radial) => Some(radial.center), + Self::Conic(conic) => Some(conic.center), + } + } + + /// Returns the radius of this gradient. + /// + /// Returns `{none}` if the gradient is not radial. + #[func] + pub fn radius(&self) -> Option { + match self { + Self::Linear(_) => None, + Self::Radial(radial) => Some(radial.radius), + Self::Conic(_) => None, + } + } + + /// Returns the focal-center of this gradient. + /// + /// Returns `{none}` if the gradient is not radial. + #[func] + pub fn focal_center(&self) -> Option> { + match self { + Self::Linear(_) => None, + Self::Radial(radial) => Some(radial.focal_center), + Self::Conic(_) => None, + } + } + + /// Returns the focal-radius of this gradient. + /// + /// Returns `{none}` if the gradient is not radial. + #[func] + pub fn focal_radius(&self) -> Option { + match self { + Self::Linear(_) => None, + Self::Radial(radial) => Some(radial.focal_radius), + Self::Conic(_) => None, + } + } + /// Sample the gradient at a given position. /// /// The position is either a position along the gradient (a [ratio] between From 57f7c167d867094660077d3da75a0207497aa36e Mon Sep 17 00:00:00 2001 From: Jakob Peters Date: Sun, 8 Dec 2024 08:52:57 -0800 Subject: [PATCH 07/31] Document integer literal parsing (#5462) Co-authored-by: Laurenz --- crates/typst-library/src/foundations/int.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index e936353cc..bddffada3 100644 --- a/crates/typst-library/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -11,7 +11,12 @@ use crate::foundations::{ /// /// The number can be negative, zero, or positive. As Typst uses 64 bits to /// store integers, integers cannot be smaller than `{-9223372036854775808}` or -/// larger than `{9223372036854775807}`. +/// larger than `{9223372036854775807}`. Integer literals are always positive, +/// so a negative integer such as `{-1}` is semantically the negation `-` of the +/// positive literal `1`. A positive integer greater than the maximum value and +/// a negative integer less than or equal to the minimum value cannot be +/// represented as an integer literal, and are instead parsed as a `{float}`. +/// The minimum integer value can still be obtained through integer arithmetic. /// /// The number can also be specified as hexadecimal, octal, or binary by /// starting it with a zero followed by either `x`, `o`, or `b`. From 468a60103dca9c6788be2207c9785d5ba771c800 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 8 Dec 2024 16:55:34 +0000 Subject: [PATCH 08/31] Fix multiline annotations in over- elems in math changing the baseline (#5459) --- crates/typst-layout/src/math/mat.rs | 4 +++- crates/typst-layout/src/math/shared.rs | 1 - crates/typst-layout/src/math/underover.rs | 4 ++-- tests/ref/math-cases-linebreaks.png | Bin 0 -> 570 bytes tests/ref/math-mat-linebreaks.png | Bin 0 -> 651 bytes .../ref/math-underover-multiline-annotation.png | Bin 0 -> 1672 bytes tests/ref/math-vec-linebreaks.png | Bin 0 -> 856 bytes tests/suite/math/cases.typ | 5 +++++ tests/suite/math/mat.typ | 5 +++++ tests/suite/math/underover.typ | 7 +++++++ tests/suite/math/vec.typ | 5 +++++ 11 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 tests/ref/math-cases-linebreaks.png create mode 100644 tests/ref/math-mat-linebreaks.png create mode 100644 tests/ref/math-underover-multiline-annotation.png create mode 100644 tests/ref/math-vec-linebreaks.png diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index 6c8b04553..24104f4ee 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -127,7 +127,9 @@ fn layout_vec_body( let denom_style = style_for_denominator(styles); let mut flat = vec![]; for child in column { - flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?); + // We allow linebreaks in cases and vectors, which are functionally + // identical to commas. + flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows()); } // We pad ascent and descent with the ascent and descent of the paren // to ensure that normal vectors are aligned with others unless they are diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 13477c10b..74e62e8f0 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -121,7 +121,6 @@ pub fn stack( alternator: LeftRightAlternator, minimum_ascent_descent: Option<(Abs, Abs)>, ) -> Frame { - let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect(); let AlignmentResult { points, width } = alignments(&rows); let rows: Vec<_> = rows .into_iter() diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index b1d4825b6..1a2c8db66 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -297,7 +297,7 @@ fn layout_underoverspreader( if let Some(annotation) = annotation { let under_style = style_for_subscript(styles); let annotation_styles = styles.chain(&under_style); - rows.push(ctx.layout_into_run(annotation, annotation_styles)?); + rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows()); } 0 } @@ -305,7 +305,7 @@ fn layout_underoverspreader( if let Some(annotation) = annotation { let over_style = style_for_superscript(styles); let annotation_styles = styles.chain(&over_style); - rows.push(ctx.layout_into_run(annotation, annotation_styles)?); + rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows()); } rows.push(stretched.into()); rows.push(MathRun::new(vec![body])); diff --git a/tests/ref/math-cases-linebreaks.png b/tests/ref/math-cases-linebreaks.png new file mode 100644 index 0000000000000000000000000000000000000000..543d5384c11a270a8a56f95e91e4f5ec7ac64d3f GIT binary patch literal 570 zcmV-A0>%A_P)lQAABqA!TMHEz9cRe8qCP#{hjKH8aMcJl}i-<6+$P9wogh3C2 z9$-Qlq}1S~m52`p2z#sqtf$p4{Ie*W;1j*P)1M%KUG3EJzHddF`rhwFq zuOWb2FNlT)?nE^_rf7H3syuj!rM6+>S%~W1hNYHWQX0%!Bp-Yg8enf7IwDfGunL3Y zMi=#7I5NY+by_FYE@NEjPc3Le#1;UFH5nkO(Sdec{YUT# zJ7W;TJ1$!tQGFT&m~jJSNvgAwue8zdeq&V|NR+h#*S(L+V=G2w<)zTV7>r zI0P`vTFV>jEY2Hi_@%07*qo IM6N<$g7^yiXaE2J literal 0 HcmV?d00001 diff --git a/tests/ref/math-mat-linebreaks.png b/tests/ref/math-mat-linebreaks.png new file mode 100644 index 0000000000000000000000000000000000000000..52ff0a8bbc61bba3604655887b7aeb893fc9115d GIT binary patch literal 651 zcmV;60(AX}P)Fp_9-lGDtAGC{c8XR6@&wB3QQ48A}g}S`?Zq2AQdpA-F(8@h}ob zL~bT61Y6V&wl$2HZaQ@>Hn6R^Z-?Juy|=>c@!;=!d7j&c_c^=;Y*9N3wkRFeVIAIP zu>Za`I~Qf)NK1pZG~WFw3ODud(w3dg?FE~Te{P~9ySGiV$^HXAl>k`z4zN%z2Rmy4 z#vY9t04nChV51#~7xUTzU~3kG_a^|ik!JS_5Ga;{1BC$b8{VU~D8SwyLhyJQ05N|& z2tcV*2!41GrL#66*xZVesY3{^d5qF+lMr0pkJ6p%0EAHs=NvAo2OxNQ4FIak0N(lm zV5SN!XCPlMKKA#EQ$1+K&VZIRPw-OB$4nNszX9gwGrY8X2IhQVSjm2QA)7pnmFYhv*5Er3HQ zegV`v#Nc$v24I<;90F1&5QDMW0m4auz7at*X7QFz>CPW%z<5%=m~ltqx>i lmX#p_tLd-~>#z<3e*pLZl?g+Rl0X0e002ovPDHLkV1mb*BP##^ literal 0 HcmV?d00001 diff --git a/tests/ref/math-underover-multiline-annotation.png b/tests/ref/math-underover-multiline-annotation.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8f0c80250910c87ad9ae023ff3cb1c76cd2205 GIT binary patch literal 1672 zcmV;326y?1P)9k`JtF@vwIs|DIqvB(cfUQIcLWN}s zZxGle5!r!^DyX+-}OO8*UtBUMO(l%CZZg?z>R@H9J{7F8V*wpZ>yq!O{ z%80Sb9Ge>Eo&`O6q_)}b!Kyas@X2ZZ7Qzb`thM+s5#g?QA>qy@dvRSQhOS9Krfq5~ zt+bjuhIuBLu4_QkVCR9+ukdhz^HS-}Wc`7zOMuK4XFS!ZJlTBKOPF)cpmBLh@BjP? zYn028Wi4r=5tsnkV%($n-uzRN$y->)>6!Rb-F<$Asje}pDl?l08VxX%piRA(Qky?h zHO#Z%SP&r>qB76;ec zVedP6OAf8s?eD_B$V<8+YUi>tVzUReN9OYVd# z3Gr@*qGY42v^Bjru@Q}mxu`4PY!=~-Y#=rB060?dSWdx<@q-A7d}CUzOV{fum0#90&)(f$$3qPiL0(v5K+ebI^5zP$B^ge%#y# z9ZmJDZLSUmYwegD8l)1R;o)%0gEv{JU&#qb!NZ&f-1+bFCm}HO!(rBz49vcM9%pzP z19iT_VI7dnN@=+EQPK9)Df72BoF7ANFl!sh1`MiKo#X-MeTCzE z;1JCJW|==^LPHKX6>seUb_@aF9uph@BmW{u`YdcJJXMqfG7tFgFP{0K#zv z8wqi8Th7!^ZIE8apX}5}!<}GO*{hy^qe6=9hrwzn5;IOtc@JmG6EE3W!IGYT)c}QB z+@%#QWh#(4-Kp^j*I_6&c8ybJR*9k&Z6R|pAA##0z?qF~AfD6pvK$YG0xxuj5t8_Z zp=2ZBArQ1$5DJ&@qPr16{~35lxq-bJ-{FbH=YAH#7;BqaT4&fD6J1=d?fKgM1qA0% zk4I}+Qp7km z%7L1t7QKJr+jRy-QHHCvs~eDYsu~iC3?t3W-u^9Ss}GH}$GR`u{Ry{!%IUHy#n`^C zG|$hFi#-LQ>I9xeuNYa}e=V5^;->uxk1OWlN}CmyzR2_YEORbypCte9izy3 zlm0t*@h8kctNU)#Ew#193`50zW!;~)SGik^fiDU07)x?@yp%99W?oWw^h{4s;qA7{ zoXrj+BVs20SchtJ!(SXQgc$yzK7O~Nb#ue^8S7Wq!VDI7;I$YC2f~5yi}fWyLVz@{ S>l_mR00006nP)L;@+%=DDnTU^m zbxqq|-Z|}IoD1vXScnIY;p(4-l$SfPq8O6z=AmK-7>JYJLZ0YIt`y1;>cqiQLQEv~ z?9vL|ltN!@+r-23T|)6Aa7J{CxPLcfS~c2(&y75Faoa)2ldULh6vmZ&5f|Qo+&Qn0 z!#GMsU3@tkQc?RNrVK_yw1}gsAu~03X)j@1%b_kl@DP`Ja)^W0trzizFStHidU03A zP!|W+5vSG53dMUF3EZ}Wx_E6LdHeo=)T|)s&>Gren}?AEo7*AV14wH0uu&FUmrar& zNCl}1BI)EZD`m08Q;+-ADF^UXKq`F2>-t@>^_~_+Pu!K^_KEL5C!Uyr_*S{vojCT> zC}VNV>#_zn;%z;dV{F7`*$D1%RB;s#?-IxoZ+r}D2f9Yi+RdowDKo;`>7%O*OBzwDnwhnrIR@4Jm#Qgm)i;4vX#0xqJ}sJ{KQ`U zo4}>}sEg0s5nJ2LK`BGTGxVd_eQmgMQ>lxq;&3T$H_D+)vP7I!fGfKuzZp8Iin@3@ z0MZnMxM>)N-if&7Fr;cfGVw41XQ+!&lv;?+c!bPC^MSlbt=la^-YXJuZv>(*Zagc5 z{q*4DE}`G#I&ofw5Snatkyhv&B?B=EH$!Uhj+}B*V>F~i2RRjqScu=}xn`|=0T(v7 zW=;zeF*-ls7}_KD1!_;2aje#Jh>e&}AI3eOK>TVLQs;4hPq8u*BilGG&O~Aphilv> i{m&(dC9x!4lGh&#Ht1u*xA0;B0000 Date: Sun, 8 Dec 2024 18:06:25 +0100 Subject: [PATCH 09/31] Add support for converting text in SVGs to paths (#5390) --- crates/typst-layout/src/image.rs | 1 + .../typst-library/src/visualize/image/mod.rs | 9 +++++++- .../typst-library/src/visualize/image/svg.rs | 23 +++++++++++++++++-- crates/typst-pdf/src/image.rs | 6 ++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 84a602823..628fe10d6 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -55,6 +55,7 @@ pub fn layout_image( elem.alt(styles), engine.world, &families(styles).collect::>(), + elem.flatten_text(styles), ) .at(span)?; diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 868a3c5b4..fddb4acb9 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -94,6 +94,12 @@ pub struct ImageElem { /// ``` #[default(ImageFit::Cover)] pub fit: ImageFit, + + /// Whether text in SVG images should be converted into paths before + /// embedding. This will result in the text becoming unselectable in + /// the output. + #[default(false)] + pub flatten_text: bool, } #[scope] @@ -246,13 +252,14 @@ impl Image { alt: Option, world: Tracked, families: &[&str], + flatten_text: bool, ) -> StrResult { let kind = match format { ImageFormat::Raster(format) => { ImageKind::Raster(RasterImage::new(data, format)?) } ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::with_fonts(data, world, families)?) + ImageKind::Svg(SvgImage::with_fonts(data, world, flatten_text, families)?) } }; diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index f7a498a83..6b6a1b6b2 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -22,6 +22,7 @@ pub struct SvgImage(Arc); struct Repr { data: Bytes, size: Axes, + flatten_text: bool, font_hash: u128, tree: usvg::Tree, } @@ -32,7 +33,13 @@ impl SvgImage { pub fn new(data: Bytes) -> StrResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; - Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) + Ok(Self(Arc::new(Repr { + data, + size: tree_size(&tree), + font_hash: 0, + flatten_text: false, + tree, + }))) } /// Decode an SVG image with access to fonts. @@ -40,6 +47,7 @@ impl SvgImage { pub fn with_fonts( data: Bytes, world: Tracked, + flatten_text: bool, families: &[&str], ) -> StrResult { let book = world.book(); @@ -60,7 +68,13 @@ impl SvgImage { ) .map_err(format_usvg_error)?; let font_hash = resolver.into_inner().unwrap().finish(); - Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree }))) + Ok(Self(Arc::new(Repr { + data, + size: tree_size(&tree), + font_hash, + flatten_text, + tree, + }))) } /// The raw image data. @@ -73,6 +87,11 @@ impl SvgImage { self.0.size.x } + /// Whether the SVG's text should be flattened. + pub fn flatten_text(&self) -> bool { + self.0.flatten_text + } + /// The SVG's height in pixels. pub fn height(&self) -> f64 { self.0.size.y diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 9651d31ba..bff7bfefa 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -208,7 +208,11 @@ fn encode_svg( ) -> Result<(Chunk, Ref), svg2pdf::ConversionError> { svg2pdf::to_chunk( svg.tree(), - svg2pdf::ConversionOptions { pdfa, ..Default::default() }, + svg2pdf::ConversionOptions { + pdfa, + embed_text: !svg.flatten_text(), + ..Default::default() + }, ) } From 4729d3d3bdf52268d143b9fe0ba6b097eae32bf8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 8 Dec 2024 19:36:04 +0100 Subject: [PATCH 10/31] Fix language-dependant figure caption separator in outline (#5550) --- crates/typst-library/src/model/figure.rs | 1 + ...sue-5370-figure-caption-separator-outline.png | Bin 0 -> 2078 bytes tests/suite/model/figure.typ | 6 ++++++ 3 files changed, 7 insertions(+) create mode 100644 tests/ref/issue-5370-figure-caption-separator-outline.png diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index e871fbeb8..fd843ee53 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -308,6 +308,7 @@ impl Synthesize for Packed { // Fill the figure's caption. let mut caption = elem.caption(styles); if let Some(caption) = &mut caption { + caption.synthesize(engine, styles)?; caption.push_kind(kind.clone()); caption.push_supplement(supplement.clone()); caption.push_numbering(numbering.clone()); diff --git a/tests/ref/issue-5370-figure-caption-separator-outline.png b/tests/ref/issue-5370-figure-caption-separator-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..a9b0d06e15ce2ff1785bcf91ef6fd038be1f4a25 GIT binary patch literal 2078 zcmV+(2;ujMP)W@V*iY0>zS1`+Kd+KVy`nnZ(&_AV;gL_~=dlu0F7YGu)$RJ0@7J2l$V z{kivE@At#QckxqsKi*J#mFG{$Ip^Mc?m7Saoafvd`qK=eN|7cOh|yxSNQ@Sv#b}Wj zEk=vP=zo=-o__P@&4h#mgL%kGGb&Fnp92RD%$zwhFE6ig+x_fwT+_({Su-MY17$Btvij%8$I==ZvQ{rZg?H?CZ{ zqLsJ$_3Jli&>$vQ4o2tX7+V88$tHSSE9 zFrj<*?znGiR@t;^Q{B3C`}FBU)!p5_Y15`e$LZ6j@7=pcZ{x?0C#AG*-J1Jy3p+bI z9OvP^efyTQ!wPveN!f zwQAKGF=9mVDltw5B<)fr!!ifj`}gm!nWzpOI*c1Pj+L=!(ISi^ke!{Kv9(^kdi(b6 z>(QeJ=crrjvbMIqeEBjN49jq{Ns}fF!`9Z8U{@LG*|TR65fP(DkJc}F_39OAnr;ac zwG&jlOpj=$#2nZ(^%j;S;WlK*5DM7crcIj{FJ6#RVq;^e1_cF`NFW@dokCQRif>kk z=pzP?A3u&qxkaLdOLzuEYhhs_d4rP~0Egn^<5@jy8$>I2p@r3^)?9XWHieEIJF@Yv zU%wth^>%gwM#v+vva;eTeRuuv;e*P1D(bl@506ErHwFd}4j(>DSTJn|2Z!0SXEQ=N zpEYY1u^bZ<)4O+XH&+)M8*2uD%jBQw)2H)P$mt9~h0e#1A16(kL_+iM@bK~R898z! z?*IJxlc&3N>(*`Cw(Z})pK|l&&FLgPJu5IM`26|vM9IR13!NN|E*C>}QD|uBnl)=w z8AnJ&^1)Ie%FoXyLU|!MdGh4br%&a}P(Hy$Vzd}75~IavFi_s!8HJbk@a4-Wg zjB0MQrfG3zAcj%>jV`mj+7H806IF`DXfaxh7Kzbfv=}WGh|yxSNQ@Sv#b}WjEk=vP zXfaxh7Kzbfv`CB=qs3^E7%fJN#Aq>Ej22ab43#Tz01jp#hEdIb$x;1v^&c0*kY7ZX zUqly)(PFenjQ+1cq5z_(8b&{U{CM!-!O*P$P=RwWm@cp;AZJRq)90{Z!@!pSZi8uJ zyc4)JuxAXW2+Run6rf90CZaJqEG&!yL?rMqCS40ihS%E@Cr*?;1Z)RL6$KzwFlmhU z10M~JlEFkluK{mUW&VbxX3d(g$rNo3j1hPorVVCi*REYa(|CJjc@j7P-AV(yK=DkS zI+e@I>{bHE``pL7}VdcNyu0SE?F3n&?fU=OZaw@yD1xaxFjg0z4OUy!!17gN$VEMRQISy+RvKy&G(b?cG(XK|w zV2E)dW6YQ_>L{XHat;_Y5)@aNY3ksu1AG)Yk(uak3$RJCkGF-@t5;Ka`SRtwdGp{0 zGArIp;I-+O^GG-u(ieWQsLA8{A>e3$+Z2+D7=(YxW57h{Z}>tZTPW9(TPefTRB6Gx zNZf^I!?mOr4ndA%CxFGq3Zn;JHrPejFgS->Gjbq&v=HEoP~M} Date: Mon, 9 Dec 2024 04:33:30 -0500 Subject: [PATCH 11/31] Fix sizing of quadratic shapes (square/circle) (#5451) Co-authored-by: Laurenz Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> --- crates/typst-layout/src/shapes.rs | 67 ++++++++++++++---- .../circle-beyond-page-width-overflows.png | Bin 0 -> 620 bytes tests/ref/circle-size-beyond-default.png | Bin 0 -> 1158 bytes tests/ref/rect-size-beyond-default.png | Bin 0 -> 185 bytes ...re-overflow.png => square-no-overflow.png} | Bin tests/ref/square-overflow-forced-height.png | Bin 0 -> 468 bytes tests/ref/square-overflow-forced-width.png | Bin 0 -> 457 bytes tests/ref/square-size-beyond-default.png | Bin 0 -> 198 bytes tests/suite/visualize/circle.typ | 12 ++++ tests/suite/visualize/rect.typ | 6 ++ tests/suite/visualize/square.typ | 27 ++++++- 11 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 tests/ref/circle-beyond-page-width-overflows.png create mode 100644 tests/ref/circle-size-beyond-default.png create mode 100644 tests/ref/rect-size-beyond-default.png rename tests/ref/{square-overflow.png => square-no-overflow.png} (100%) create mode 100644 tests/ref/square-overflow-forced-height.png create mode 100644 tests/ref/square-overflow-forced-width.png create mode 100644 tests/ref/square-size-beyond-default.png diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index a35021721..db9acece3 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -348,29 +348,48 @@ fn layout_shape( pod.size = crate::pad::shrink(region.size, &inset); } - // Layout the child. - frame = crate::layout_frame(engine, child, locator.relayout(), styles, pod)?; - - // If the child is a square or circle, relayout with full expansion into - // square region to make sure the result is really quadratic. + // If the shape is quadratic, we first measure it to determine its size + // and then layout with full expansion to force the aspect ratio and + // make sure it's really quadratic. if kind.is_quadratic() { - let length = frame.size().max_by_side().min(pod.size.min_by_side()); - let quad_pod = Region::new(Size::splat(length), Axes::splat(true)); - frame = crate::layout_frame(engine, child, locator, styles, quad_pod)?; + let length = match quadratic_size(pod) { + Some(length) => length, + None => { + // Take as much as the child wants, but without overflowing. + crate::layout_frame(engine, child, locator.relayout(), styles, pod)? + .size() + .max_by_side() + .min(pod.size.min_by_side()) + } + }; + + pod = Region::new(Size::splat(length), Axes::splat(true)); } + // Layout the child. + frame = crate::layout_frame(engine, child, locator, styles, pod)?; + // Apply the inset. if has_inset { crate::pad::grow(&mut frame, &inset); } } else { - // The default size that a shape takes on if it has no child and - // enough space. - let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)); - let mut size = region.expand.select(region.size, default.min(region.size)); - if kind.is_quadratic() { - size = Size::splat(size.min_by_side()); - } + // The default size that a shape takes on if it has no child and no + // forced sizes. + let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(region.size); + + let size = if kind.is_quadratic() { + Size::splat(match quadratic_size(region) { + Some(length) => length, + None => default.min_by_side(), + }) + } else { + // For each dimension, pick the region size if forced, otherwise + // use the default size (or the region size if the default + // is too large for the region). + region.expand.select(region.size, default) + }; + frame = Frame::soft(size); } @@ -411,6 +430,24 @@ fn layout_shape( Ok(frame) } +/// Determines the forced size of a quadratic shape based on the region, if any. +/// +/// The size is forced if at least one axis is expanded because `expand` is +/// `true` for axes whose size was manually specified by the user. +fn quadratic_size(region: Region) -> Option { + if region.expand.x && region.expand.y { + // If both `width` and `height` are specified, we choose the + // smaller one. + Some(region.size.x.min(region.size.y)) + } else if region.expand.x { + Some(region.size.x) + } else if region.expand.y { + Some(region.size.y) + } else { + None + } +} + /// Creates a new rectangle as a path. pub fn clip_rect( size: Size, diff --git a/tests/ref/circle-beyond-page-width-overflows.png b/tests/ref/circle-beyond-page-width-overflows.png new file mode 100644 index 0000000000000000000000000000000000000000..941cb0092b24a4ea7548e69361bb6f4b0f9a4fc6 GIT binary patch literal 620 zcmV-y0+aoTP)%GI2?lHn00kCJ4m@}U3OsfM3Ov0J3aoB`0w0cp0^fFk0|!nx@NyCq z_;?f?xX0qai)*03=L6ut{sITq6mVd{=D>*^P+-dg4!pF=fo%aCcqKuCJ8wAfdJ-JC z_lg6b4ub>FZIj@xDhEE80tXIfIq>xeIIu30;Q1{Q+;_==E8XD0dov_h(Ma%6g#()+ zIItm+;N4jg97~blP>uw9OC;C@4qTof!9^blHY5_950YSACc&CQg40nFtj3)Nr=w20 zT)OPE2gHz^HYCcEBpB`*3GTn3EqhK|Z!-}RT=2sQpSC*-d>X*|w+_Vs0000~)y>-`na;?nq zk32d$ZxUP6S=}5R9sfIQeK}?JrW&t{qMULrN2E;p)0lZn-Sr}pzcDn)^1PH6n3J15 zxrZ-l1u7TT$A=Dg4j* z;ZBz=`|W4_$ugALINdTvR#xx#w=PERhacSPh4y#<_Er)**vHg=$ z?DxyuZp?GJcWlnD2Rn@MpRQ+ja0w8Y$kdUl>qY}a}IeU>Y|#Mj)n zWb48B?S~g!dVeb+f2VaSJ&NZb$;}pSyuC<_}JPn-~ZXqka(ztb9T#ecQ)tR+Jy%vN^(E^ zA?p5d-m)_P^ySOSe3;~9h=EHBkzyQlZPv74Pt-xFiF@7&d?EQ;F4gUe<)<;;Q;3PJB-1(f7fuh zJ!0|s63(?M!R2*8fob_dKDTY_629lJXc4rHd{DfPd#i!aHk~lt&8DnZ4bu2qH+n5R zJb$4aTh({F2FJ%w86CKL#M5PyPh5VnvDo+mD>G;M=dF=zUN~>q`D@>^=IfJBZ+m?A z@#$k>pI@$|lP#*Frv{Vh7MYA3ptK{NeXE`m#@03aEhbboFyt I=akR{03I?NnE(I) literal 0 HcmV?d00001 diff --git a/tests/ref/rect-size-beyond-default.png b/tests/ref/rect-size-beyond-default.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4d80fe19b377ab56b2f06cce9e8e543971238e GIT binary patch literal 185 zcmeAS@N?(olHy`uVBq!ia0vp^6+pa%i5W;Leq#~^Qak}ZA+G=b|8Hn$sIRYgaByHu zoVFh*p5f`@7*cWT?KMTd1_K_qhqDwd4xX_PZi+g0uy8^O)7g&&zFy90&%W7RxB0)k zN#66U+Qp!=x`ATG6Hh+*w{yi!)mf`|&M*#}=%Y6IWXiv32USY1eb{c%U-7ee(#a?L kcCILH2`%wcTl`%+7@(Ksy;cUHx3vIVCg!0L4s85&!@I literal 0 HcmV?d00001 diff --git a/tests/ref/square-overflow.png b/tests/ref/square-no-overflow.png similarity index 100% rename from tests/ref/square-overflow.png rename to tests/ref/square-no-overflow.png diff --git a/tests/ref/square-overflow-forced-height.png b/tests/ref/square-overflow-forced-height.png new file mode 100644 index 0000000000000000000000000000000000000000..f7cb0ee351bc41d9b56d229cc9c7d2affb412cb4 GIT binary patch literal 468 zcmeAS@N?(olHy`uVBq!ia0vp^6+m3c0VEi%2k%` z*L7{h(rHo6(^?iNDrqcQxWcrf?U3As!>f61rzr$`p6&2%oaC6qH7P+sG*MM4aEnO? z3+L2IW9{|(542=_ZIsymzHHg~vU3Hi0+y}f5LWv$KcSRSlxel7(>=`=rv%2z zJ+|=5Q~@{LhItAM2NoZCAX{{{NE3$3Zcd7k9P_;RXZPMIR+JVYEDv8@8 zFC|Meb58dS582@+a`f4TE3aM`r9C_>nlYX6Or>JqiOcE_?cy)*P5knAGEMLu7GH36#({L(U?B^_e<{;G`4A>&QdV$XBB qiN)76IajIQSe|ny21{`K|Hf literal 0 HcmV?d00001 diff --git a/tests/ref/square-overflow-forced-width.png b/tests/ref/square-overflow-forced-width.png new file mode 100644 index 0000000000000000000000000000000000000000..4667181687ecf6ee0f9081d52fc7e6404e28acd4 GIT binary patch literal 457 zcmeAS@N?(olHy`uVBq!ia0vp^Hb8um14uALo#10(U|^i!>EaktaqI0(TW_aCiDQNK zuGv?Dyw^Hx^z`!laH#YG9RlKTc@T+lVeEB)DD-6mlT9l zT%MiOv59NsY~xdEa*6%_va;=dQNHvc&$@MwSU>CVe)&m0#_`k^FHzW>R-my>TRRctTg|ECtp#US=P>1_FlZ<5Vk z;Vg^U9Lg0we_3Hz(4S*0*U^@=!*9}(bpm^?nf{c1eKtzcqh!T8*WL^B8}6og_U3&# z9%~lMxK7M>7ifJ@#9FMsXL|Hr%^V7_ahY z;z90c3+XdSwqmlsetKBsZaQEn!ukAhh-kyN&)0WF7*!cG&U+BppJT!-Fem?5=!)EM zFUwgm(Z`dWolYIUHnH}{@vxwEALo9#^yTRe&UaVenlwCk(d#yOM(m+yJ;6d(&TsI) nwS!Mc7zREoIQW1-OywWer4uzJAMd+j2#O<5S3j3^P6P_V&($L*m)mX)KgwXoW8Ee|)xc2*ro1Y(HSYbJ$>a>)k_njX Date: Mon, 9 Dec 2024 06:55:58 -0300 Subject: [PATCH 12/31] Forbid footnote migration in pending floats (#5497) Co-authored-by: Laurenz --- crates/typst-layout/src/flow/compose.rs | 32 +++++++++++++++--- crates/typst-layout/src/flow/distribute.rs | 16 ++++++--- crates/typst-layout/src/pages/collect.rs | 2 +- crates/typst-library/src/model/footnote.rs | 2 +- ...ssue-5435-footnote-migration-in-floats.png | Bin 0 -> 448 bytes tests/suite/layout/flow/footnote.typ | 20 +++++++++++ 6 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 tests/ref/issue-5435-footnote-migration-in-floats.png diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 9650a7bc1..326456752 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -214,6 +214,13 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { } /// Lay out the inner contents of a column. + /// + /// Pending floats and footnotes are also laid out at this step. For those, + /// however, we forbid footnote migration (moving the frame containing the + /// footnote reference if the corresponding entry doesn't fit), allowing + /// the footnote invariant to be broken, as it would require handling a + /// [`Stop::Finish`] at this point, but that is exclusively handled by the + /// distributor. fn column_contents(&mut self, regions: Regions) -> FlowResult { // Process pending footnotes. for note in std::mem::take(&mut self.work.footnotes) { @@ -222,7 +229,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { // Process pending floats. for placed in std::mem::take(&mut self.work.floats) { - self.float(placed, ®ions, false)?; + self.float(placed, ®ions, false, false)?; } distribute(self, regions) @@ -236,13 +243,21 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { /// (depending on `placed.scope`). /// /// When the float does not fit, it is queued into `work.floats`. The - /// value of `clearance` that between the float and flow content is needed - /// --- it is set if there are already distributed items. + /// value of `clearance` indicates that between the float and flow content + /// is needed --- it is set if there are already distributed items. + /// + /// The value of `migratable` determines whether footnotes within the float + /// should be allowed to prompt its migration if they don't fit in order to + /// respect the footnote invariant (entries in the same page as the + /// references), triggering [`Stop::Finish`]. This is usually `true` within + /// the distributor, as it can handle that particular flow event, and + /// `false` elsewhere. pub fn float( &mut self, placed: &'b PlacedChild<'a>, regions: &Regions, clearance: bool, + migratable: bool, ) -> FlowResult<()> { // If the float is already processed, skip it. let loc = placed.location(); @@ -291,7 +306,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { } // Handle footnotes in the float. - self.footnotes(regions, &frame, need, false)?; + self.footnotes(regions, &frame, need, false, migratable)?; // Determine the float's vertical alignment. We can unwrap the inner // `Option` because `Custom(None)` is checked for during collection. @@ -326,12 +341,19 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { /// Lays out footnotes in the `frame` if this is the root flow and there are /// any. The value of `breakable` indicates whether the element that /// produced the frame is breakable. If not, the frame is treated as atomic. + /// + /// The value of `migratable` indicates whether footnote migration should be + /// possible (at least for the first footnote found in the frame, as it is + /// forbidden for the second footnote onwards). It is usually `true` within + /// the distributor and `false` elsewhere, as the distributor can handle + /// [`Stop::Finish`] which is returned when migration is requested. pub fn footnotes( &mut self, regions: &Regions, frame: &Frame, flow_need: Abs, breakable: bool, + migratable: bool, ) -> FlowResult<()> { // Footnotes are only supported at the root level. if !self.config.root { @@ -352,7 +374,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { let mut relayout = false; let mut regions = *regions; - let mut migratable = !breakable && regions.may_progress(); + let mut migratable = migratable && !breakable && regions.may_progress(); for (y, elem) in notes { // The amount of space used by the in-flow content that contains the diff --git a/crates/typst-layout/src/flow/distribute.rs b/crates/typst-layout/src/flow/distribute.rs index 5b293d352..7a1cf4264 100644 --- a/crates/typst-layout/src/flow/distribute.rs +++ b/crates/typst-layout/src/flow/distribute.rs @@ -240,7 +240,8 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { // Handle fractionally sized blocks. if let Some(fr) = single.fr { - self.composer.footnotes(&self.regions, &frame, Abs::zero(), false)?; + self.composer + .footnotes(&self.regions, &frame, Abs::zero(), false, true)?; self.flush_tags(); self.items.push(Item::Fr(fr, Some(single))); return Ok(()); @@ -323,8 +324,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { } // Handle footnotes. - self.composer - .footnotes(&self.regions, &frame, frame.height(), breakable)?; + self.composer.footnotes( + &self.regions, + &frame, + frame.height(), + breakable, + true, + )?; // Push an item for the frame. self.regions.size.y -= frame.height(); @@ -347,11 +353,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { placed, &self.regions, self.items.iter().any(|item| matches!(item, Item::Frame(..))), + true, )?; self.regions.size.y -= weak_spacing; } else { let frame = placed.layout(self.composer.engine, self.regions.base())?; - self.composer.footnotes(&self.regions, &frame, Abs::zero(), true)?; + self.composer + .footnotes(&self.regions, &frame, Abs::zero(), true, true)?; self.flush_tags(); self.items.push(Item::Placed(frame, placed)); } diff --git a/crates/typst-layout/src/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs index 1903d6ac5..0bbae9f4c 100644 --- a/crates/typst-layout/src/pages/collect.rs +++ b/crates/typst-layout/src/pages/collect.rs @@ -53,7 +53,7 @@ pub fn collect<'a>( // The initial styles for the next page are ours unless this is a // "boundary" pagebreak. Such a pagebreak is generated at the end of - // the scope of a page set rule to ensure a page boundary. It's + // the scope of a page set rule to ensure a page boundary. Its // styles correspond to the styles _before_ the page set rule, so we // don't want to apply it to a potential empty page. if !pagebreak.boundary(styles) { diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index d9971dd11..ffc78ea05 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -194,7 +194,7 @@ cast! { /// before any page content, typically at the very start of the document. #[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)] pub struct FootnoteEntry { - /// The footnote for this entry. It's location can be used to determine + /// The footnote for this entry. Its location can be used to determine /// the footnote counter state. /// /// ```example diff --git a/tests/ref/issue-5435-footnote-migration-in-floats.png b/tests/ref/issue-5435-footnote-migration-in-floats.png new file mode 100644 index 0000000000000000000000000000000000000000..672a5af8354377b57d01f583670e3fe56ccfc6e9 GIT binary patch literal 448 zcmV;x0YCnUP)0{{R382;U20002GP)t-s|Ns90 z007kCU#jxWdE7zro3yoS>YZrkI(XaB_Nje1yWp%!-VZa&&z4_4T~I#>dCU zXlQ8G*49^7S3EpCsi~>7w6xRH(|~}0PEJnZ;^Iq7OH@=;Nl8hptgMldk%fhYm6esg zzP`c1!NkPG>FMe8^z`-h_xt<%i;Ii%^Ye!-V1EDr0KrK_K~#9!?b%hX0x%Rs(eqlK zyW{Te&itpv4EO>LBzsL@CvDn~dj$Xh000000010rOgMP}$M9Jx$>O72ltn1yW$}>B z$s&Y6_&j2a{3!r%;dTe|NW)mXzZELKWehD qgr78>Tf)yTuL(IMZh74RF4i6h93@VhU)O{H0000, C @fn, D @fn, E @fn. --- issue-5256-multiple-footnotes-in-footnote --- // Test whether all footnotes inside another footnote are listed. #footnote[#footnote[A]#footnote[B]#footnote[C]] + +--- issue-5435-footnote-migration-in-floats --- +// Test that a footnote should not prompt migration when in a float that was +// queued to the next page (due to the float being too large), even if the +// footnote does not fit, breaking the footnote invariant. +#set page(height: 50pt) + +#place( + top, + float: true, + { + v(100pt) + footnote[a] + } +) +#place( + top, + float: true, + footnote[b] +) From f960fe60121ff7d03dfc5db429b756d7efd0cc1f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 9 Dec 2024 11:43:48 +0100 Subject: [PATCH 13/31] Bump hashbrown (#5552) --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d9d80b57..94ce026e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -890,9 +890,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hayagriva" @@ -1169,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.15.0", + "hashbrown 0.15.2", "rayon", "serde", ] From bb0c8140950b3eec020a4f0147bbc4ea65f3952a Mon Sep 17 00:00:00 2001 From: wznmickey Date: Mon, 9 Dec 2024 05:56:42 -0500 Subject: [PATCH 14/31] Forbid base prefix for numbers with a unit (#5548) Co-authored-by: Laurenz --- crates/typst-syntax/src/lexer.rs | 6 ++++++ tests/suite/layout/length.typ | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index 1314016fa..358c25b20 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -766,6 +766,12 @@ impl Lexer<'_> { return self.error(eco_format!("invalid number suffix: {}", suffix)); } + if base != 10 { + let kind = self.error(eco_format!("invalid base-{base} prefix")); + self.hint("numbers with a unit cannot have a base prefix"); + return kind; + } + SyntaxKind::Numeric } diff --git a/tests/suite/layout/length.typ b/tests/suite/layout/length.typ index 5ba70072d..71d79da9a 100644 --- a/tests/suite/layout/length.typ +++ b/tests/suite/layout/length.typ @@ -74,3 +74,8 @@ // Hint: 2-24 use `length.to-absolute()` to resolve its em component (requires context) // Hint: 2-24 or use `length.abs.inches()` instead to ignore its em component #(4.5em + 6in).inches() + +--- issue-5519-length-base --- +// Error: 2-9 invalid base-2 prefix +// Hint: 2-9 numbers with a unit cannot have a base prefix +#0b100pt From 17f20c6944d569d5f0bb57caee37d9f208d87d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Tue, 10 Dec 2024 10:57:22 +0100 Subject: [PATCH 15/31] Basic HTML pretty-printing (#5533) Co-authored-by: Laurenz --- crates/typst-html/src/encode.rs | 48 ++++++++++++++++++++++-- crates/typst-library/src/html/dom.rs | 55 +++++++++++++++++++++++++++- crates/typst-realize/src/lib.rs | 2 +- 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index d4ff83d67..b87b0e1d6 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -8,14 +8,30 @@ use typst_syntax::Span; /// Encodes an HTML document into a string. pub fn html(document: &HtmlDocument) -> SourceResult { - let mut w = Writer { buf: String::new() }; + let mut w = Writer { pretty: true, ..Writer::default() }; w.buf.push_str(""); + write_indent(&mut w); write_element(&mut w, &document.root)?; Ok(w.buf) } +#[derive(Default)] struct Writer { buf: String, + /// current indentation level + level: usize, + /// pretty printing enabled? + pretty: bool, +} + +/// Write a newline and indent, if pretty printing is enabled. +fn write_indent(w: &mut Writer) { + if w.pretty { + w.buf.push('\n'); + for _ in 0..w.level { + w.buf.push_str(" "); + } + } } /// Encode an HTML node into the writer. @@ -67,9 +83,30 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { return Ok(()); } - for node in &element.children { - write_node(w, node)?; + let pretty = w.pretty; + if !element.children.is_empty() { + w.pretty &= is_pretty(element); + let mut indent = w.pretty; + + w.level += 1; + for c in &element.children { + let pretty_child = match c { + HtmlNode::Tag(_) => continue, + HtmlNode::Element(element) => is_pretty(element), + HtmlNode::Text(..) | HtmlNode::Frame(_) => false, + }; + + if core::mem::take(&mut indent) || pretty_child { + write_indent(w); + } + write_node(w, c)?; + indent = pretty_child; + } + w.level -= 1; + + write_indent(w) } + w.pretty = pretty; w.buf.push_str(" SourceResult<()> { Ok(()) } +/// Whether the element should be pretty-printed. +fn is_pretty(element: &HtmlElement) -> bool { + tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta) +} + /// Escape a character. fn write_escape(w: &mut Writer, c: char) -> StrResult<()> { // See diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index ee94279f2..3d558fb0f 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -470,6 +470,59 @@ pub mod tag { wbr } + /// Whether nodes with the tag have the CSS property `display: block` by + /// default. + /// + /// If this is true, then pretty-printing can insert spaces around such + /// nodes and around the contents of such nodes. + /// + /// However, when users change the properties of such tags via CSS, the + /// insertion of whitespace may actually impact the visual output; for + /// example, shows how + /// adding CSS rules to `

` can make it sensitive to whitespace. In such + /// cases, users should disable pretty-printing. + pub fn is_block_by_default(tag: HtmlTag) -> bool { + matches!( + tag, + self::html + | self::head + | self::body + | self::article + | self::aside + | self::h1 + | self::h2 + | self::h3 + | self::h4 + | self::h5 + | self::h6 + | self::hgroup + | self::nav + | self::section + | self::dd + | self::dl + | self::dt + | self::menu + | self::ol + | self::ul + | self::address + | self::blockquote + | self::dialog + | self::div + | self::fieldset + | self::figure + | self::figcaption + | self::footer + | self::form + | self::header + | self::hr + | self::legend + | self::main + | self::p + | self::pre + | self::search + ) + } + /// Whether the element is inline-level as opposed to being block-level. /// /// Not sure whether this distinction really makes sense. But we somehow @@ -480,7 +533,7 @@ pub mod tag { /// /// /// - pub fn is_inline(tag: HtmlTag) -> bool { + pub fn is_inline_by_default(tag: HtmlTag) -> bool { matches!( tag, self::abbr diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index fd43e8304..6ab6d81c5 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -823,7 +823,7 @@ static PAR: GroupingRule = GroupingRule { RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment ) && content .to_packed::() - .is_some_and(|elem| tag::is_inline(elem.tag))) + .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) }, inner: |content| content.elem() == SpaceElem::elem(), interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(), From a5ade167dd24fe3efdb3391dcb72a09b4dd1a268 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 11 Dec 2024 06:28:24 -0500 Subject: [PATCH 16/31] More `CapturesVisitor` tests (#5506) --- crates/typst-eval/src/call.rs | 113 ++++++++++++++++++++++---------- crates/typst-ide/src/tooltip.rs | 15 +++++ 2 files changed, 94 insertions(+), 34 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 513d1dd2c..fc934cef5 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -593,14 +593,8 @@ mod tests { use super::*; #[track_caller] - fn test(text: &str, result: &[&str]) { - let mut scopes = Scopes::new(None); - scopes.top.define("f", 0); - scopes.top.define("x", 0); - scopes.top.define("y", 0); - scopes.top.define("z", 0); - - let mut visitor = CapturesVisitor::new(Some(&scopes), Capturer::Function); + fn test(scopes: &Scopes, text: &str, result: &[&str]) { + let mut visitor = CapturesVisitor::new(Some(scopes), Capturer::Function); let root = parse(text); visitor.visit(&root); @@ -613,44 +607,95 @@ mod tests { #[test] fn test_captures() { + let mut scopes = Scopes::new(None); + scopes.top.define("f", 0); + scopes.top.define("x", 0); + scopes.top.define("y", 0); + scopes.top.define("z", 0); + let s = &scopes; + // Let binding and function definition. - test("#let x = x", &["x"]); - test("#let x; #(x + y)", &["y"]); - test("#let f(x, y) = x + y", &[]); - test("#let f(x, y) = f", &[]); - test("#let f = (x, y) => f", &["f"]); + test(s, "#let x = x", &["x"]); + test(s, "#let x; #(x + y)", &["y"]); + test(s, "#let f(x, y) = x + y", &[]); + test(s, "#let f(x, y) = f", &[]); + test(s, "#let f = (x, y) => f", &["f"]); // Closure with different kinds of params. - test("#((x, y) => x + z)", &["z"]); - test("#((x: y, z) => x + z)", &["y"]); - test("#((..x) => x + y)", &["y"]); - test("#((x, y: x + z) => x + y)", &["x", "z"]); - test("#{x => x; x}", &["x"]); + test(s, "#((x, y) => x + z)", &["z"]); + test(s, "#((x: y, z) => x + z)", &["y"]); + test(s, "#((..x) => x + y)", &["y"]); + test(s, "#((x, y: x + z) => x + y)", &["x", "z"]); + test(s, "#{x => x; x}", &["x"]); // Show rule. - test("#show y: x => x", &["y"]); - test("#show y: x => x + z", &["y", "z"]); - test("#show x: x => x", &["x"]); + test(s, "#show y: x => x", &["y"]); + test(s, "#show y: x => x + z", &["y", "z"]); + test(s, "#show x: x => x", &["x"]); // For loop. - test("#for x in y { x + z }", &["y", "z"]); - test("#for (x, y) in y { x + y }", &["y"]); - test("#for x in y {} #x", &["x", "y"]); + test(s, "#for x in y { x + z }", &["y", "z"]); + test(s, "#for (x, y) in y { x + y }", &["y"]); + test(s, "#for x in y {} #x", &["x", "y"]); // Import. - test("#import z: x, y", &["z"]); - test("#import x + y: x, y, z", &["x", "y"]); + test(s, "#import z: x, y", &["z"]); + test(s, "#import x + y: x, y, z", &["x", "y"]); // Blocks. - test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); - test("#[#let x = 1]#x", &["x"]); + test(s, "#{ let x = 1; { let y = 2; y }; x + y }", &["y"]); + test(s, "#[#let x = 1]#x", &["x"]); // Field access. - test("#foo(body: 1)", &[]); - test("#(body: 1)", &[]); - test("#(body = 1)", &[]); - test("#(body += y)", &["y"]); - test("#{ (body, a) = (y, 1) }", &["y"]); - test("#(x.at(y) = 5)", &["x", "y"]) + test(s, "#x.y.f(z)", &["x", "z"]); + + // Parenthesized expressions. + test(s, "#f(x: 1)", &["f"]); + test(s, "#(x: 1)", &[]); + test(s, "#(x = 1)", &["x"]); + test(s, "#(x += y)", &["x", "y"]); + test(s, "#{ (x, z) = (y, 1) }", &["x", "y", "z"]); + test(s, "#(x.at(y) = 5)", &["x", "y"]); + } + + #[test] + fn test_captures_in_math() { + let mut scopes = Scopes::new(None); + scopes.top.define("f", 0); + scopes.top.define("x", 0); + scopes.top.define("y", 0); + scopes.top.define("z", 0); + // Multi-letter variables are required for math. + scopes.top.define("foo", 0); + scopes.top.define("bar", 0); + scopes.top.define("x-bar", 0); + scopes.top.define("x_bar", 0); + let s = &scopes; + + // Basic math identifier differences. + test(s, "$ x f(z) $", &[]); // single letters not captured. + test(s, "$ #x #f(z) $", &["f", "x", "z"]); + test(s, "$ foo f(bar) $", &["bar", "foo"]); + test(s, "$ #foo[#$bar$] $", &["bar", "foo"]); + test(s, "$ #let foo = x; foo $", &["x"]); + + // Math idents don't have dashes/underscores + test(s, "$ x-y x_y foo-x x_bar $", &["bar", "foo"]); + test(s, "$ #x-bar #x_bar $", &["x-bar", "x_bar"]); + + // Named-params. + test(s, "$ foo(bar: y) $", &["foo"]); + // This should be updated when we improve named-param parsing: + test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]); + + // Field access in math. + test(s, "$ foo.bar $", &["foo"]); + test(s, "$ foo.x $", &["foo"]); + test(s, "$ x.foo $", &["foo"]); + test(s, "$ foo . bar $", &["bar", "foo"]); + test(s, "$ foo.x.y.bar(z) $", &["foo"]); + test(s, "$ foo.x-bar $", &["bar", "foo"]); + test(s, "$ foo.x_bar $", &["bar", "foo"]); + test(s, "$ #x_bar.x-bar $", &["x_bar"]); } } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index adfbeda50..4eaaeda1f 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -337,6 +337,21 @@ mod tests { fn test_tooltip_closure() { test("#let f(x) = x + y", 11, Side::Before) .must_be_text("This closure captures `y`"); + // Same tooltip if `y` is defined first. + test("#let y = 10; #let f(x) = x + y", 24, Side::Before) + .must_be_text("This closure captures `y`"); + // Names are sorted. + test("#let f(x) = x + y + z + a", 11, Side::Before) + .must_be_text("This closure captures `a`, `y`, and `z`"); + // Names are de-duplicated. + test("#let f(x) = x + y + z + y", 11, Side::Before) + .must_be_text("This closure captures `y` and `z`"); + // With arrow syntax. + test("#let f = (x) => x + y", 15, Side::Before) + .must_be_text("This closure captures `y`"); + // No recursion with arrow syntax. + test("#let f = (x) => x + y + f", 13, Side::After) + .must_be_text("This closure captures `f` and `y`"); } #[test] From ef4fc040b279104f6c95a5ea2f9a9d10fb0e9019 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 11 Dec 2024 06:31:04 -0500 Subject: [PATCH 17/31] Improve raw trimming (#5541) --- crates/typst-syntax/src/lexer.rs | 109 +++++++++++++++++++++++-------- tests/ref/raw-empty-lines.png | Bin 0 -> 92 bytes tests/suite/text/raw.typ | 55 +++++++++++++++- 3 files changed, 137 insertions(+), 27 deletions(-) create mode 100644 tests/ref/raw-empty-lines.png diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index 358c25b20..b0cb5c464 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -251,8 +251,9 @@ impl Lexer<'_> { } } - /// Lex an entire raw segment at once. This is a convenience to avoid going - /// to and from the parser for each raw section. + /// We parse entire raw segments in the lexer as a convenience to avoid + /// going to and from the parser for each raw section. See comments in + /// [`Self::blocky_raw`] and [`Self::inline_raw`] for specific details. fn raw(&mut self) -> (SyntaxKind, SyntaxNode) { let start = self.s.cursor() - 1; @@ -313,6 +314,35 @@ impl Lexer<'_> { (SyntaxKind::Raw, SyntaxNode::inner(SyntaxKind::Raw, nodes)) } + /// Raw blocks parse a language tag, have smart behavior for trimming + /// whitespace in the start/end lines, and trim common leading whitespace + /// from all other lines as the "dedent". The exact behavior is described + /// below. + /// + /// ### The initial line: + /// - A valid Typst identifier immediately following the opening delimiter + /// is parsed as the language tag. + /// - We check the rest of the line and if all characters are whitespace, + /// trim it. Otherwise we trim a single leading space if present. + /// - If more trimmed characters follow on future lines, they will be + /// merged into the same trimmed element. + /// - If we didn't trim the entire line, the rest is kept as text. + /// + /// ### Inner lines: + /// - We determine the "dedent" by iterating over the lines. The dedent is + /// the minimum number of leading whitespace characters (not bytes) before + /// each line that has any non-whitespace characters. + /// - The opening delimiter's line does not contribute to the dedent, but + /// the closing delimiter's line does (even if that line is entirely + /// whitespace up to the delimiter). + /// - We then trim the newline and dedent characters of each line, and add a + /// (potentially empty) text element of all remaining characters. + /// + /// ### The final line: + /// - If the last line is entirely whitespace, it is trimmed. + /// - Otherwise its text is kept like an inner line. However, if the last + /// non-whitespace character of the final line is a backtick, then one + /// ascii space (if present) is trimmed from the end. fn blocky_raw(&mut self, inner_end: usize, mut push_raw: F) where F: FnMut(SyntaxKind, &Scanner), @@ -323,12 +353,10 @@ impl Lexer<'_> { push_raw(SyntaxKind::RawLang, &self.s); } - // Determine inner content between backticks. - self.s.eat_if(' '); - let inner = self.s.to(inner_end); + // The rest of the function operates on the lines between the backticks. + let mut lines = split_newlines(self.s.to(inner_end)); // Determine dedent level. - let mut lines = split_newlines(inner); let dedent = lines .iter() .skip(1) @@ -339,35 +367,61 @@ impl Lexer<'_> { .min() .unwrap_or(0); - // Trim single space in last line if text ends with a backtick. The last - // line is the one directly before the closing backticks and if it is - // just whitespace, it will be completely trimmed below. - if inner.trim_end().ends_with('`') { - if let Some(last) = lines.last_mut() { + // Trim whitespace from the last line. Will be added as a `RawTrimmed` + // kind by the check for `self.s.cursor() != inner_end` below. + if lines.last().is_some_and(|last| last.chars().all(char::is_whitespace)) { + lines.pop(); + } else if let Some(last) = lines.last_mut() { + // If last line ends in a backtick, try to trim a single space. This + // check must happen before we add the first line since the last and + // first lines might be the same. + if last.trim_end().ends_with('`') { *last = last.strip_suffix(' ').unwrap_or(last); } } - let is_whitespace = |line: &&str| line.chars().all(char::is_whitespace); - let starts_whitespace = lines.first().is_some_and(is_whitespace); - let ends_whitespace = lines.last().is_some_and(is_whitespace); - let mut lines = lines.into_iter(); - let mut skipped = false; - // Trim whitespace + newline at start. - if starts_whitespace { - self.s.advance(lines.next().unwrap().len()); - skipped = true; - } - // Trim whitespace + newline at end. - if ends_whitespace { - lines.next_back(); + // Handle the first line: trim if all whitespace, or trim a single space + // at the start. Note that the first line does not affect the dedent + // value. + if let Some(first_line) = lines.next() { + if first_line.chars().all(char::is_whitespace) { + self.s.advance(first_line.len()); + // This is the only spot we advance the scanner, but don't + // immediately call `push_raw`. But the rest of the function + // ensures we will always add this text to a `RawTrimmed` later. + debug_assert!(self.s.cursor() != inner_end); + // A proof by cases follows: + // # First case: The loop runs + // If the loop runs, there must be a newline following, so + // `cursor != inner_end`. And if the loop runs, the first thing + // it does is add a trimmed element. + // # Second case: The final if-statement runs. + // To _not_ reach the loop from here, we must have only one or + // two lines: + // 1. If one line, we cannot be here, because the first and last + // lines are the same, so this line will have been removed by + // the check for the last line being all whitespace. + // 2. If two lines, the loop will run unless the last is fully + // whitespace, but if it is, it will have been popped, then + // the final if-statement will run because the text removed + // by the last line must include at least a newline, so + // `cursor != inner_end` here. + } else { + let line_end = self.s.cursor() + first_line.len(); + if self.s.eat_if(' ') { + // Trim a single space after the lang tag on the first line. + push_raw(SyntaxKind::RawTrimmed, &self.s); + } + // We know here that the rest of the line is non-empty. + self.s.jump(line_end); + push_raw(SyntaxKind::Text, &self.s); + } } // Add lines. - for (i, line) in lines.enumerate() { - let dedent = if i == 0 && !skipped { 0 } else { dedent }; + for line in lines { let offset: usize = line.chars().take(dedent).map(char::len_utf8).sum(); self.s.eat_newline(); self.s.advance(offset); @@ -383,6 +437,9 @@ impl Lexer<'_> { } } + /// Inline raw text is split on lines with non-newlines as `Text` kinds and + /// newlines as `RawTrimmed`. Inline raw text does not dedent the text, all + /// non-newline whitespace is kept. fn inline_raw(&mut self, inner_end: usize, mut push_raw: F) where F: FnMut(SyntaxKind, &Scanner), diff --git a/tests/ref/raw-empty-lines.png b/tests/ref/raw-empty-lines.png new file mode 100644 index 0000000000000000000000000000000000000000..dcf0d926142a1089d82c8dedb3803e8686c522e8 GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^6+mpr1SA;hUTARxDP>O=$B>F!$v^tV4SKvDPx!Z$ oi#@_#%N7WIqhDT})M=B+V1NDiu_LRjOF-H@UHx3vIVCg!02g{2TmS$7 literal 0 HcmV?d00001 diff --git a/tests/suite/text/raw.typ b/tests/suite/text/raw.typ index fa9e630fa..1ba216302 100644 --- a/tests/suite/text/raw.typ +++ b/tests/suite/text/raw.typ @@ -282,10 +282,40 @@ int main() { --- raw-blocky --- // Test various raw parsing edge cases. + #let empty = ( name: "empty", input: ``, text: "", + block: false, +) + +#let empty-spaces = ( + name: "empty-spaces", + input: ``` ```, + text: "", + block: false, +) + +#let empty-newlines = ( + name: "empty-newlines", + input: ``` + + +```, + text: "\n", + block: true, +) + +#let newlines-backtick = ( + name: "newlines-backtick", + input: ``` + +` + +```, + text: "\n`\n", + block: true, ) #let backtick = ( @@ -423,8 +453,18 @@ test block: true, ) +#let extra-first-line-ws = ( + name: "extra-first-line-ws", + input: eval("``` \n```"), + text: "", + block: true, +) + #let cases = ( empty, + empty-spaces, + empty-newlines, + newlines-backtick, backtick, lang-backtick, lang-space, @@ -438,10 +478,11 @@ test blocky-dedent-lastline2, blocky-tab, blocky-tab-dedent, + extra-first-line-ws, ) #for c in cases { - let block = c.at("block", default: false) + let block = c.block assert.eq(c.text, c.input.text, message: "in point " + c.name + ", expect " + repr(c.text) + ", got " + repr(c.input.text) + "") assert.eq(block, c.input.block, message: "in point " + c.name + ", expect " + repr(block) + ", got " + repr(c.input.block) + "") } @@ -556,6 +597,18 @@ print(y) --- issue-3601-empty-raw --- // Test that empty raw block with `typ` language doesn't cause a crash. ```typ +``` + +--- raw-empty-lines --- +// Test raw with multiple empty lines. + +#show raw: block.with(width: 100%, fill: gray) + +``` + + + + ``` --- issue-3841-tabs-in-raw-type-code --- From 5e0e58d26ef656836aae8d16477bb059e97883a3 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 11 Dec 2024 06:33:59 -0500 Subject: [PATCH 18/31] Add number-syntax edge case tests (#5560) --- tests/ref/double-percent.png | Bin 0 -> 496 bytes tests/suite/foundations/float.typ | 20 ++++++++++++++++++++ tests/suite/layout/length.typ | 27 +++++++++++++++++++++++++++ tests/suite/layout/relative.typ | 8 ++++++++ 4 files changed, 55 insertions(+) create mode 100644 tests/ref/double-percent.png diff --git a/tests/ref/double-percent.png b/tests/ref/double-percent.png new file mode 100644 index 0000000000000000000000000000000000000000..61a0d6143cd1615b0fa0051d0442b32be6fd2491 GIT binary patch literal 496 zcmV+=2f`SbJhrlzKag@yFj3(}_~!2H?CkmK@#wC} z)^MQTk+#!kn$&BZ+=Z^zaiQj?#p$ujwY9a5j*hIXtmfwC)^eiSeW~fQ&FJXp>gwu| zk&(y8$MMbHoSdBU^75mjqwT@g;g-6}%F6cO>gu-7`||eg?(V$2yr7_<{{H@khK6)> zbh5It#KgqGDRSzy&iUx@&|Q?$VwdpA+xFh+_xJby`~2XNx8aw%>a@-K@b&-x{>PGM zX#fBKrb$FWRCwC$(?t%$KoCUHa+sN!nVFdxY~TMVk>bQRm`IWOt-g9ws|F#2{4FP1O>0000 Date: Wed, 11 Dec 2024 16:46:10 +0100 Subject: [PATCH 19/31] Fix crash due to consecutive weak spacing (#5562) --- crates/typst-layout/src/inline/collect.rs | 37 +++++++++--------- crates/typst-layout/src/inline/linebreak.rs | 2 + ...ue-5244-consecutive-weak-space-heading.png | Bin 0 -> 346 bytes .../ref/issue-5244-consecutive-weak-space.png | Bin 0 -> 194 bytes ...issue-5253-consecutive-weak-space-math.png | Bin 0 -> 138 bytes tests/suite/layout/spacing.typ | 18 +++++++++ 6 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 tests/ref/issue-5244-consecutive-weak-space-heading.png create mode 100644 tests/ref/issue-5244-consecutive-weak-space.png create mode 100644 tests/ref/issue-5253-consecutive-weak-space-math.png diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index fbcddee5c..23e82c417 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -256,8 +256,7 @@ impl<'a> Collector<'a> { } fn push_text(&mut self, text: &str, styles: StyleChain<'a>) { - self.full.push_str(text); - self.push_segment(Segment::Text(text.len(), styles)); + self.build_text(styles, |full| full.push_str(text)); } fn build_text(&mut self, styles: StyleChain<'a>, f: F) @@ -266,33 +265,33 @@ impl<'a> Collector<'a> { { let prev = self.full.len(); f(&mut self.full); - let len = self.full.len() - prev; - self.push_segment(Segment::Text(len, styles)); + let segment_len = self.full.len() - prev; + + // Merge adjacent text segments with the same styles. + if let Some(Segment::Text(last_len, last_styles)) = self.segments.last_mut() { + if *last_styles == styles { + *last_len += segment_len; + return; + } + } + + self.segments.push(Segment::Text(segment_len, styles)); } fn push_item(&mut self, item: Item<'a>) { - self.full.push_str(item.textual()); - self.push_segment(Segment::Item(item)); - } - - fn push_segment(&mut self, segment: Segment<'a>) { - match (self.segments.last_mut(), &segment) { - // Merge adjacent text segments with the same styles. - (Some(Segment::Text(last_len, last_styles)), Segment::Text(len, styles)) - if *last_styles == *styles => - { - *last_len += *len; - } - + match (self.segments.last_mut(), &item) { // Merge adjacent weak spacing by taking the maximum. ( Some(Segment::Item(Item::Absolute(prev_amount, true))), - Segment::Item(Item::Absolute(amount, true)), + Item::Absolute(amount, true), ) => { *prev_amount = (*prev_amount).max(*amount); } - _ => self.segments.push(segment), + _ => { + self.full.push_str(item.textual()); + self.segments.push(Segment::Item(item)); + } } } } diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 236d68921..7b66fcdb4 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -971,11 +971,13 @@ where } /// Estimates the metrics for the line spanned by the range. + #[track_caller] fn estimate(&self, range: Range) -> T { self.get(range.end) - self.get(range.start) } /// Get the metric at the given byte position. + #[track_caller] fn get(&self, index: usize) -> T { match index.checked_sub(1) { None => T::default(), diff --git a/tests/ref/issue-5244-consecutive-weak-space-heading.png b/tests/ref/issue-5244-consecutive-weak-space-heading.png new file mode 100644 index 0000000000000000000000000000000000000000..c1ef792494783488b66a9fbf529e0fe999faf73f GIT binary patch literal 346 zcmV-g0j2(lP)oHuy~s?qsV*&_bGyBj{1L_kE8wX|LctLbg`Jx z7Mx;$Hb^|?thC$huoh3|IL|gp3%hOpI?7``~LqUEX%wAF+M*5 sF+be<11>;DD~VByM=c(;cu-gj0Q#vf!c~}!1^@s607*qoM6N<$g0-fy;s5{u literal 0 HcmV?d00001 diff --git a/tests/ref/issue-5244-consecutive-weak-space.png b/tests/ref/issue-5244-consecutive-weak-space.png new file mode 100644 index 0000000000000000000000000000000000000000..7a102ddfb66053fd1a3e0f94a88e52bcea4856b1 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS_0VEhE<%|3RQk|YIjv*Ddl7HAcG$dYm6xi*q zE4Ouqm00y2reNc(Vq4xWUw(ACv3m2%`d9zuqc2{5bg#cmpzYP?liV*dKECd0PTPJ! zXQ%Ae!2K`N3*T>CefiY^o!jv*Ddl7HAcG$dYm6xi*q zE4Q`cN)nguzuncJ)OSVJ?3U))*L1_$?|q(>fc*bBThlCunmaq**Bxp2QQ`X1X?ykJ fo0%|;608gidztzjdZ$;N0a@bd>gTe~DWM4fGX65$ literal 0 HcmV?d00001 diff --git a/tests/suite/layout/spacing.typ b/tests/suite/layout/spacing.typ index f59389956..d5cd122c4 100644 --- a/tests/suite/layout/spacing.typ +++ b/tests/suite/layout/spacing.typ @@ -58,3 +58,21 @@ This is the first line \ #h(2cm, weak: true) A new line // Non-weak-spacing, on the other hand, is not removed. This is the first line \ #h(2cm, weak: false) A new line + +--- issue-5244-consecutive-weak-space --- +#set par(linebreaks: "optimized") +#{ + [A] + h(0.3em, weak: true) + h(0.3em, weak: true) + [B] +} + +--- issue-5244-consecutive-weak-space-heading --- +#set par(justify: true) +#set heading(numbering: "I.") + += #h(0.3em, weak: true) test + +--- issue-5253-consecutive-weak-space-math --- +$= thin thin$ a From a3ad0a0bba3ce3abb1d8ed84656f54cc2d74be25 Mon Sep 17 00:00:00 2001 From: Joshua Gawley <16921823+joshuagawley@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:26:52 +0000 Subject: [PATCH 20/31] Document new counting symbols (#5568) --- crates/typst-library/src/model/numbering.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index b7f27bb9b..4e2fe4579 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -60,8 +60,9 @@ pub fn numbering( /// Defines how the numbering works. /// /// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `α`, `Α`, `一`, `壹`, - /// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `①`, and `⓵`. They are - /// replaced by the number in the sequence, preserving the original case. + /// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `١`, `۱`, `१`, `১`, `ক`, + /// `①`, and `⓵`. They are replaced by the number in the sequence, + /// preserving the original case. /// /// The `*` character means that symbols should be used to count, in the /// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six From 7f139517b9ba8cf4f51083d5387971571d1aa950 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:18:34 +0300 Subject: [PATCH 21/31] Derivation comment for calculation in `repeat` (#5575) --- crates/typst-layout/src/repeat.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/typst-layout/src/repeat.rs b/crates/typst-layout/src/repeat.rs index b761438c8..bfc7b32ce 100644 --- a/crates/typst-layout/src/repeat.rs +++ b/crates/typst-layout/src/repeat.rs @@ -33,8 +33,17 @@ pub fn layout_repeat( let fill = region.size.x; let width = piece.width(); - // count * width + (count - 1) * gap = fill, but count is an integer so - // we need to round down and get the remainder. + // We need to fit the body N times, but the number of gaps is (N - 1): + // N * w + (N - 1) * g ≤ F + // where N - body count (count) + // w - body width (width) + // g - gap width (gap) + // F - available space to fill (fill) + // + // N * w + N * g - g ≤ F + // N * (w + g) ≤ F + g + // N ≤ (F + g) / (w + g) + // N = ⌊(F + g) / (w + g)⌋ let count = ((fill + gap) / (width + gap)).floor(); let remaining = (fill + gap) % (width + gap); @@ -52,7 +61,7 @@ pub fn layout_repeat( if width > Abs::zero() { for _ in 0..(count as usize).min(1000) { frame.push_frame(Point::with_x(offset), piece.clone()); - offset += piece.width() + gap; + offset += width + gap; } } From d3620df4c63b5856832b23b26eba71aecd9f543e Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 16 Dec 2024 08:45:57 -0500 Subject: [PATCH 22/31] Add reversed numbering (#5563) --- crates/typst-layout/src/lists.rs | 8 ++++-- crates/typst-library/src/model/enum.rs | 23 +++++++++++++++--- .../ref/enum-numbering-reversed-overriden.png | Bin 0 -> 666 bytes tests/ref/enum-numbering-reversed.png | Bin 0 -> 620 bytes tests/suite/model/enum.typ | 17 +++++++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 tests/ref/enum-numbering-reversed-overriden.png create mode 100644 tests/ref/enum-numbering-reversed.png diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 08c2a2f45..0d51a1e4e 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -74,6 +74,7 @@ pub fn layout_enum( regions: Regions, ) -> SourceResult { let numbering = elem.numbering(styles); + let reversed = elem.reversed(styles); let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { @@ -86,7 +87,9 @@ pub fn layout_enum( let mut cells = vec![]; let mut locator = locator.split(); - let mut number = elem.start(styles); + let mut number = + elem.start(styles) + .unwrap_or_else(|| if reversed { elem.children.len() } else { 1 }); let mut parents = EnumElem::parents_in(styles); let full = elem.full(styles); @@ -127,7 +130,8 @@ pub fn layout_enum( item.body.clone().styled(EnumElem::set_parents(smallvec![number])), locator.next(&item.body.span()), )); - number = number.saturating_add(1); + number = + if reversed { number.saturating_sub(1) } else { number.saturating_add(1) }; } let grid = CellGrid::new( diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index e0121ba24..eb3c2ea45 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -9,7 +9,7 @@ use crate::foundations::{ cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, Styles, TargetElem, }; -use crate::html::{attr, tag, HtmlElem}; +use crate::html::{attr, tag, HtmlAttr, HtmlElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; @@ -127,8 +127,7 @@ pub struct EnumElem { /// [Ahead], /// ) /// ``` - #[default(1)] - pub start: usize, + pub start: Smart, /// Whether to display the full numbering, including the numbers of /// all parent enumerations. @@ -144,6 +143,17 @@ pub struct EnumElem { #[default(false)] pub full: bool, + /// Whether to reverse the numbering for this enumeration. + /// + /// ```example + /// #set enum(reversed: true) + /// + Coffee + /// + Tea + /// + Milk + /// ``` + #[default(false)] + pub reversed: bool, + /// The indentation of each item. #[resolve] pub indent: Length, @@ -217,7 +227,12 @@ impl EnumElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::ol) + let mut elem = HtmlElem::new(tag::ol); + if self.reversed(styles) { + elem = + elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed"); + } + return Ok(elem .with_body(Some(Content::sequence(self.children.iter().map(|item| { let mut li = HtmlElem::new(tag::li); if let Some(nr) = item.number(styles) { diff --git a/tests/ref/enum-numbering-reversed-overriden.png b/tests/ref/enum-numbering-reversed-overriden.png new file mode 100644 index 0000000000000000000000000000000000000000..827f38d1d57648b5a673159ab19f849df0198375 GIT binary patch literal 666 zcmV;L0%iS)P)<7Vw)<{`yZN`diYAYt0Tl?Ym3C?!T&h?0=E}rkz`|ib;=kR<_4gS%oVVGft8U8Eq zH&3N=iUJw@T$7a=F14Eq{;tfUa?A_uRPe0}rXv#V)xO@AB7*f#a4=AAXg_?sbe{0ZV&{VA!30+>8Q1PJb8_`|Kp1AYN_f)wK+NmS4r7=K-t3r~6FasW zZx2zyUrnE-PBdn%lT`5P#9*fc@3^PZ5Pn=+^U{rh=6NDGkxbViloR?(Jj?Vn;WTQB%QgqfyL@F90No;N<8t3EuA~ zoUzV$sx?&ZGZTa`^3#J@EePqU+W=;%;7o7l`sHw`-}T=f|H%u4VTP-PbDj+`Z>a31 z_f&AaE7&e}M1fS#FcCat$FXJEe>EC@(7+Gda%!I(KJB7`F*{zHmBWYqL@=x_q)HWU z1Xh$Ff=Apy$0s56iZRE)8ZQur8Lk#CMx`9;8P>g`f>Y;To)bINfV7PYo^M-Mi@nl> zFsNYk)`lc_`{)5Oc;RZWE>pR2gbcRKL7ga;I@Sa{N)y4$n#Xr>GAMNa&Jl}}7YM@) zR}25n=6~hOeaB!kc&T8=z7xBWF zl<_?P9o**wR6!v%Tiy`DYXAzoxfo`c;r|}~1Cu#{AAX{UQUCw|07*qoM6N<$f?anv Ak^lez literal 0 HcmV?d00001 diff --git a/tests/ref/enum-numbering-reversed.png b/tests/ref/enum-numbering-reversed.png new file mode 100644 index 0000000000000000000000000000000000000000..12d77df45ae3408bddf5f972aa4510d1f077ac8d GIT binary patch literal 620 zcmV-y0+aoTP)0(K(MrbiwL%b*utJx2WZEZHh`Rhp0upkz! zCa$O?(B?GfX^z^~4rn_z=RrC<=M$X$Fz>v1Cg}5Ay}vili-!lkC0L_VLSYJ1ctgO6 zAEKKj34iTL6#C6=%Xw=yKis{eia80w{hqKU-vJ#UH)Ok>gBlPo;*p|-4)ehx% z>wqm;xYHPqN~Pi0Zvt>^fiJ@sL1}hZ7{V)lXOh*RJSlC*x4k02sAUi(08g16aXil5 z9JksS2@|ch`B0z>BB&AO3ZEe1Fh^<{{GXunQ~TUj5kUQow$}MHr4Z zqc=VMH#Zrbm{iM4i)!5OBn*2swOX`9lt%EfA&Q5FKKYT=ZW8DMq44^Gg$SZ4!f>Qe zfirUebUr*0?KR1YA)>Hk-n2YawgIDCfg} Date: Mon, 16 Dec 2024 22:09:38 +0800 Subject: [PATCH 23/31] Consider parameters when iterating items in scope (#5517) --- crates/typst-ide/src/matchers.rs | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index 4aeba29be..18262f701 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -123,6 +123,36 @@ pub fn named_items( } } + if let Some(v) = parent.cast::().filter(|v| { + // Check if the node is in the body of the closure. + let body = parent.find(v.body().span()); + body.is_some_and(|n| n.find(node.span()).is_some()) + }) { + for param in v.params().children() { + match param { + ast::Param::Pos(pattern) => { + for ident in pattern.bindings() { + if let Some(t) = recv(NamedItem::Var(ident)) { + return Some(t); + } + } + } + ast::Param::Named(n) => { + if let Some(t) = recv(NamedItem::Var(n.name())) { + return Some(t); + } + } + ast::Param::Spread(s) => { + if let Some(sink_ident) = s.sink_ident() { + if let Some(t) = recv(NamedItem::Var(sink_ident)) { + return Some(t); + } + } + } + } + } + } + ancestor = Some(parent.clone()); continue; } @@ -269,6 +299,17 @@ mod tests { assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b")); } + #[test] + fn test_param_named_items() { + // Has named items + assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a")); + assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a")); + + // Doesn't have named items + assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a")); + assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b")); + } + #[test] fn test_import_named_items() { // Cannot test much. From 8b1e0d3a233950bd8fd553e118ec6342efb42855 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:10:42 +0100 Subject: [PATCH 24/31] Improve `symbol` `repr` (#5505) --- .../typst-library/src/foundations/symbol.rs | 42 ++++++++++++- tests/suite/symbols/symbol.typ | 62 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index fcb3a3ce5..880305284 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -246,10 +246,50 @@ impl Debug for List { impl crate::foundations::Repr for Symbol { fn repr(&self) -> EcoString { - eco_format!("\"{}\"", self.get()) + match &self.0 { + Repr::Single(c) => eco_format!("symbol(\"{}\")", *c), + Repr::Complex(variants) => { + eco_format!("symbol{}", repr_variants(variants.iter().copied(), "")) + } + Repr::Modified(arc) => { + let (list, modifiers) = arc.as_ref(); + if modifiers.is_empty() { + eco_format!("symbol{}", repr_variants(list.variants(), "")) + } else { + eco_format!("symbol{}", repr_variants(list.variants(), modifiers)) + } + } + } } } +fn repr_variants<'a>( + variants: impl Iterator, + applied_modifiers: &str, +) -> String { + crate::foundations::repr::pretty_array_like( + &variants + .filter(|(variant, _)| { + // Only keep variants that can still be accessed, i.e., variants + // that contain all applied modifiers. + parts(applied_modifiers).all(|am| variant.split('.').any(|m| m == am)) + }) + .map(|(variant, c)| { + let trimmed_variant = variant + .split('.') + .filter(|&m| parts(applied_modifiers).all(|am| m != am)); + if trimmed_variant.clone().all(|m| m.is_empty()) { + eco_format!("\"{c}\"") + } else { + let trimmed_modifiers = trimmed_variant.collect::>().join("."); + eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c) + } + }) + .collect::>(), + false, + ) +} + impl Serialize for Symbol { fn serialize(&self, serializer: S) -> Result where diff --git a/tests/suite/symbols/symbol.typ b/tests/suite/symbols/symbol.typ index 30d87e44f..f2f6abf85 100644 --- a/tests/suite/symbols/symbol.typ +++ b/tests/suite/symbols/symbol.typ @@ -49,3 +49,65 @@ --- symbol-unknown-modifier --- // Error: 13-20 unknown symbol modifier #emoji.face.garbage + +--- symbol-repr --- +#test( + repr(sym.amp), + `symbol("&", ("inv", "⅋"))`.text, +) +#test( + repr(sym.amp.inv), + `symbol("⅋")`.text, +) +#test( + repr(sym.arrow.double.r), + ``` + symbol( + "⇒", + ("bar", "⤇"), + ("long", "⟹"), + ("long.bar", "⟾"), + ("not", "⇏"), + ("l", "⇔"), + ("l.long", "⟺"), + ("l.not", "⇎"), + ) + ```.text, +) +#test(repr(sym.smash), "symbol(\"⨳\")") + +#let envelope = symbol( + "🖂", + ("stamped", "🖃"), + ("stamped.pen", "🖆"), + ("lightning", "🖄"), + ("fly", "🖅"), +) +#test( + repr(envelope), + ``` + symbol( + "🖂", + ("stamped", "🖃"), + ("stamped.pen", "🖆"), + ("lightning", "🖄"), + ("fly", "🖅"), + ) + ```.text, +) +#test( + repr(envelope.stamped), + `symbol("🖃", ("pen", "🖆"))`.text, +) +#test( + repr(envelope.stamped.pen), + `symbol("🖆")`.text, +) +#test( + repr(envelope.lightning), + `symbol("🖄")`.text, +) +#test( + repr(envelope.fly), + `symbol("🖅")`.text, +) From 75273937f762ebeb05f71e91434b298b52b44670 Mon Sep 17 00:00:00 2001 From: Johann Birnick <6528009+jbirnick@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:22:00 -0800 Subject: [PATCH 25/31] Transform high level headings to HTML (#5525) --- crates/typst-library/src/html/dom.rs | 14 ++++++++---- crates/typst-library/src/model/heading.rs | 28 +++++++++++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 3d558fb0f..5b6eab4d6 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -122,8 +122,8 @@ impl HtmlTag { let bytes = string.as_bytes(); let mut i = 0; while i < bytes.len() { - if !bytes[i].is_ascii_alphanumeric() { - panic!("constant tag name must be ASCII alphanumeric"); + if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) { + panic!("not all characters are valid in a tag name"); } i += 1; } @@ -220,8 +220,10 @@ impl HtmlAttr { let bytes = string.as_bytes(); let mut i = 0; while i < bytes.len() { - if !bytes[i].is_ascii_alphanumeric() { - panic!("constant attribute name must be ASCII alphanumeric"); + if !bytes[i].is_ascii() + || !charsets::is_valid_in_attribute_name(bytes[i] as char) + { + panic!("not all characters are valid in an attribute name"); } i += 1; } @@ -621,5 +623,9 @@ pub mod attr { href name value + role } + + #[allow(non_upper_case_globals)] + pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index fc0e4ad25..ec9cf4e99 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,14 +1,15 @@ use std::num::NonZeroUsize; +use ecow::eco_format; use typst_utils::NonZeroExt; -use crate::diag::SourceResult; +use crate::diag::{warning, SourceResult}; use crate::engine::Engine; use crate::foundations::{ elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, }; -use crate::html::{tag, HtmlElem}; +use crate::html::{attr, tag, HtmlElem}; use crate::introspection::{ Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, }; @@ -272,9 +273,26 @@ impl Show for Packed { // Meanwhile, a level 1 Typst heading is a section heading. For this // reason, levels are offset by one: A Typst level 1 heading becomes // a `

`. - let level = self.resolve_level(styles); - let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level.get().min(5) - 1]; - HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) + let level = self.resolve_level(styles).get(); + if level >= 6 { + engine.sink.warn(warning!(span, + "heading of level {} was transformed to \ +
, which is not \ + supported by all assistive technology", + level, level + 1; + hint: "HTML only supports

to

, not ", level + 1; + hint: "you may want to restructure your document so that \ + it doesn't contain deep headings")); + HtmlElem::new(tag::div) + .with_body(Some(realized)) + .with_attr(attr::role, "heading") + .with_attr(attr::aria_level, eco_format!("{}", level + 1)) + .pack() + .spanned(span) + } else { + let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1]; + HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) + } } else { let realized = BlockBody::Content(realized); BlockElem::new().with_body(Some(realized)).pack().spanned(span) From 51020fcf3cd6fbe62d148d2188b9aaac4445bf63 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Mon, 16 Dec 2024 21:23:13 +0100 Subject: [PATCH 26/31] Get numbering of page counter from style chain (#5589) --- .../src/introspection/counter.rs | 30 ++++++++++-------- tests/ref/counter-page-display.png | Bin 0 -> 150 bytes tests/suite/introspection/counter.typ | 8 +++++ 3 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 tests/ref/counter-page-display.png diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index b884844c6..f67c6daab 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -366,20 +366,22 @@ impl Counter { .custom() .or_else(|| { let styles = styles?; - let CounterKey::Selector(Selector::Elem(func, _)) = self.0 else { - return None; - }; - - if func == HeadingElem::elem() { - HeadingElem::numbering_in(styles).clone() - } else if func == FigureElem::elem() { - FigureElem::numbering_in(styles).clone() - } else if func == EquationElem::elem() { - EquationElem::numbering_in(styles).clone() - } else if func == FootnoteElem::elem() { - Some(FootnoteElem::numbering_in(styles).clone()) - } else { - None + match self.0 { + CounterKey::Page => PageElem::numbering_in(styles).clone(), + CounterKey::Selector(Selector::Elem(func, _)) => { + if func == HeadingElem::elem() { + HeadingElem::numbering_in(styles).clone() + } else if func == FigureElem::elem() { + FigureElem::numbering_in(styles).clone() + } else if func == EquationElem::elem() { + EquationElem::numbering_in(styles).clone() + } else if func == FootnoteElem::elem() { + Some(FootnoteElem::numbering_in(styles).clone()) + } else { + None + } + } + _ => None, } }) .unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into()); diff --git a/tests/ref/counter-page-display.png b/tests/ref/counter-page-display.png new file mode 100644 index 0000000000000000000000000000000000000000..040b634d5976906bbce629db4767413e3ea52211 GIT binary patch literal 150 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS<0VEg>Eo$`yQn8*cjv*Ddl7HAcG$dYm6xi*q zE4Q@*!dTXJw4ICn#G{kb_Ol&%J8$v(fBp56 rU(q$IQ{MiMpS$$C8}|gTF$LX>f+v31PV_aI53 Date: Tue, 17 Dec 2024 10:25:15 +0100 Subject: [PATCH 27/31] Remove deprecated things and compatibility behaviours (#5591) --- crates/typst-library/src/foundations/mod.rs | 1 - crates/typst-library/src/foundations/ops.rs | 16 --- .../typst-library/src/foundations/styles.rs | 62 +----------- crates/typst-library/src/foundations/ty.rs | 18 ---- .../src/introspection/counter.rs | 46 +-------- .../typst-library/src/introspection/locate.rs | 92 ++---------------- .../typst-library/src/introspection/query.rs | 20 +--- .../typst-library/src/introspection/state.rs | 80 +-------------- crates/typst-library/src/layout/measure.rs | 22 +---- crates/typst-library/src/model/outline.rs | 11 +-- docs/changelog/0.11.0.md | 8 +- docs/changelog/0.12.0.md | 4 +- docs/changelog/earlier.md | 2 +- tests/ref/context-compatibility-locate.png | Bin 1523 -> 0 bytes tests/ref/context-compatibility-styling.png | Bin 380 -> 0 bytes tests/ref/outline-indent-no-numbering.png | Bin 4654 -> 2924 bytes tests/ref/outline-indent-numbering.png | Bin 10607 -> 7101 bytes tests/suite/foundations/context.typ | 40 -------- tests/suite/foundations/type.typ | 8 -- tests/suite/model/outline.typ | 4 - tests/suite/styling/fold.typ | 12 +-- 21 files changed, 35 insertions(+), 411 deletions(-) delete mode 100644 tests/ref/context-compatibility-locate.png delete mode 100644 tests/ref/context-compatibility-styling.png diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 28f983186..d960a666c 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -119,7 +119,6 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { global.define_func::(); global.define_func::(); global.define_func::(); - global.define_func::