From 20ce48dc36978e0995a03d7f97975deff99834b3 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Mon, 7 Oct 2024 10:13:09 +0200 Subject: [PATCH 001/280] Fix panic for empty breakable block equations (#5116) --- crates/typst/src/math/equation.rs | 6 ++++++ tests/suite/math/multiline.typ | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs index 83d3fc7483..963a35c5e1 100644 --- a/crates/typst/src/math/equation.rs +++ b/crates/typst/src/math/equation.rs @@ -379,6 +379,12 @@ fn layout_equation_block( equation_builder.size.y = height; } + // Ensure that there is at least one frame, even for empty equations. + if equation_builders.is_empty() { + equation_builders + .push(MathRunFrameBuilder { frames: vec![], size: Size::zero() }); + } + equation_builders } else { vec![full_equation_builder] diff --git a/tests/suite/math/multiline.typ b/tests/suite/math/multiline.typ index 3c0ed2f49e..34e66b99c3 100644 --- a/tests/suite/math/multiline.typ +++ b/tests/suite/math/multiline.typ @@ -147,11 +147,15 @@ $ a + b $ Shouldn't overflow: $ a + b $ +--- issue-5113-pagebreaking-empty --- +// Test empty breakable equations. +#show math.equation: set block(breakable: true) +#math.equation(block: true, []) + --- issue-1948-math-text-break --- // Test text with linebreaks in math. $ x := "a\nb\nc\nd\ne" $ - --- issue-4829-math-pagebreaking-wrong-number --- // Test numbering of empty regions of broken equations. #set page(height: 5em) From deb0308980b5928f9eee5dccbf3cd1e552eea90d Mon Sep 17 00:00:00 2001 From: Armin Brauns Date: Mon, 7 Oct 2024 08:22:59 +0000 Subject: [PATCH 002/280] Fix "thumbnail" key in manifest files not being optional (#5122) --- crates/typst-syntax/src/package.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs index c28a1f21c7..387057f375 100644 --- a/crates/typst-syntax/src/package.rs +++ b/crates/typst-syntax/src/package.rs @@ -86,7 +86,8 @@ pub struct TemplateInfo { pub entrypoint: EcoString, /// A path relative to the package's root that points to a PNG or lossless /// WebP thumbnail for the template. - pub thumbnail: EcoString, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub thumbnail: Option, /// All parsed but unknown fields, this can be used for validation. #[serde(flatten, skip_serializing)] pub unknown_fields: UnknownFields, From 82a2c3619ad6884fc3a50baa787a6fdc288f13e7 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Mon, 7 Oct 2024 10:23:32 +0200 Subject: [PATCH 003/280] Add exception for NewCM Math Bold (#5126) --- crates/typst/src/text/font/exceptions.rs | 3 +++ tests/ref/issue-3650-italic-equation.png | Bin 1497 -> 1546 bytes 2 files changed, 3 insertions(+) diff --git a/crates/typst/src/text/font/exceptions.rs b/crates/typst/src/text/font/exceptions.rs index 9234d76c98..40998bb993 100644 --- a/crates/typst/src/text/font/exceptions.rs +++ b/crates/typst/src/text/font/exceptions.rs @@ -181,6 +181,9 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { .family("New Computer Modern"), "NewCM10-Regular" => Exception::new() .family("New Computer Modern"), + "NewCMMath-Bold" => Exception::new() + .family("New Computer Modern Math") + .weight(700), "NewCMMath-Book" => Exception::new() .family("New Computer Modern Math") .weight(450), diff --git a/tests/ref/issue-3650-italic-equation.png b/tests/ref/issue-3650-italic-equation.png index 296d9d36157f17e6892f14636ef265d0e50438cb..97036c7830aadd82baf6a41fec4ae0365a4c6d47 100644 GIT binary patch delta 1528 zcmVISPGHWh4p;z??ul!5zpIvc zMZZ>2IU-m;J9<%)ap~bkG{5ZVfJoW7LWSr`;>_Z;%YW|{q;YA}u5c+B*8|jVP7#aQ zEscnv**D|j6I>u+2%W7S-OW`}3IKh{j4n(k0CYJn3G5GRgkyBz)>?$p?g2n`5yKtY z%_Ci!WiME*5{3pjLJ=mcojA;+O}7VJ-H2WVH>uM=pcZ;m17(nX8UR#%u%fUH2Cf?b zGzIrT)PH`-1oaBz2;d`y+cb$D+Vi`7iWl~?0JQPZs{&NC`2kpbZg{2h>E-R}+BM~# zz`gm+Kf~9#O><4je^##^0laR(IhwwqamP8nGvNg-mauf|wICC-u=(yBb|imMPa|MF zb9}Onso<0Gfo8DIcDP-g>rB45nVJ0>w-y!y%zx@`0^KTr6@^S|y%fT?>WN?hN@f~n zR1d+wXML!{U^|?7sy>|k@%d&EiY>`_o7j-+TqlL%7=V=1Cw`J@x2El7O-oKHApb-4 zg_t?n1@p~v|GJ~L!_=z4SP2@a805kgdQ}Fc1)}c)P_Mmhq+1PC=mF@e8t6pZuCzc) zt$!UCNVIV3lSX-S^$Jf>;`owyy5PSebv)X&dmwb?f-opj8Ls-^O*_EHomzRcH})XZ z{u>Cnm19jeImHgJ(y5K%EI`*FLY^fcRN=;OE&^<^5!SC>vh)GG;q3#|crH2yB+2&$ z*GxR1`Ep^R_hAg<`XM>>JMoNG7;ged@PFFrZp3A7{4a8@e;?-DYZF|VP&%gl!4pTm zYIcsPIskkUaK07yD1s&-pG?C9=(@~{%I$sdt{Ncky87iJTw&=vp6lZ}MhNi$_nDPBw_+i^u_?t10>jliOEAPCLiOWVL)9`WY%PX}BZW1eZ zB&n14C794`3%&?16(0T=;A)&CHV!2D06jaSA_wuC zb!1Z+91YrL05~tZ45HiIGzE68%D!O6(DQoI1PRAwT efF1Dv8vYw)2hMsin>CgI00007&Pzlc@l;2| zZ91J~4m=_-6(TZmB5D;C#EAl)fC5@+ErJ|X1XKj&AfPbIxfYS5P*?)$e2w(XGo@5R{=+pyY zNW@;<2R7%yEdnKUSvd7^2sP$n&(le8O^TG%xI?w*WCftRkaM|gJ1NpL$wuTQ&?>7{ z+=5jA6v->Wv-or!mIQf|V{m4YdM$o2?Q<>5EYWbZ+AL=_gYOw6Qa0bis?dI+EPxdN zT5m)~JUdu$XMc0%sGno-43g9Fpgg*GJ3#NESvT?Ifft$a1ew5ADhBWeK$iS%AD#vx zB%cm}LvY)&zKwSQNc*;^6K9Q8+F18ThtbDQyil?9*c zF^nJ1&r`*5Fs0NYqI|o805h9P+}mYPodl3^_VkZ3-HxpNtnVq#1hRiuP7|}ZsLaQb zy}A9w_+eU=U}^*n+_G}1iU&$VoedI)08r`fm>5#wsxPpcCbNTP6# zrhiZH=6WDJMXl>i;^l%bCg^!88wj1#0zjq-d(uj0!Kd7Md8)lh!q9R_7#>Qnm%qhX zaGhHp&+iGb!vsP#&VWaQSFL^oZ~E{sEq=>S0;ft=T71(xW*w4?!3Ty?wo3f0q+JBK zd_26Bnz^}qnmMpHO6vgJm|Fj8|Dz{Pcz?&@p44<0@XEVEMjD|CJkW~(8=1m{f}ed7oK4cn#}AHy{Q4AZeel>|T$n(jwUD5m%F zf&mBMgEPxt&8l_l2nk^77v<-{7))ZXT*CTF+`$K8RC8B;Hc>!bPS=se6Y_` z!g#j2_niRW%0}q?O%RH*VlV99Fn=P)U}H)~Bw-Q62392{gP#qqOQgvXkd^{y_Z&*F z5;p(%)&@8cxzhk}L0$=>lGC-A56YXy2k(p57m(}D?MeC8N&T#VvdcZMHGB#6@c?;| zOgTZAH=S?yZu`L;S_TX+VYV4j7xOze=8X7{4{n|O=2{Be>2NdgRz3az*?(UF+}Q)5 z4?i$Dny>(PH*;zv03el2)gUqwp8AOv#s?=q)Xg;f1KS?MK9HZbW*w Date: Mon, 7 Oct 2024 05:23:59 -0300 Subject: [PATCH 004/280] Fix excluded PDF pages being written (#5133) --- crates/typst-pdf/src/page.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index f112c81cc8..631eec1106 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -106,10 +106,12 @@ pub fn write_page_tree(ctx: &WithRefs) -> SourceResult<(PdfChunk, Ref)> { ); } + let page_kids = ctx.globals.pages.iter().filter_map(Option::as_ref).copied(); + chunk .pages(page_tree_ref) - .count(ctx.pages.len() as i32) - .kids(ctx.globals.pages.iter().filter_map(Option::as_ref).copied()); + .count(page_kids.clone().count() as i32) + .kids(page_kids); Ok((chunk, page_tree_ref)) } From bb39d8f10a3820a1fbda6c50d8df5745ccc11de2 Mon Sep 17 00:00:00 2001 From: Orange <89233794+Ang149@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:59:49 +0800 Subject: [PATCH 005/280] Improve hint when provided array for destructuring has fewer elements than expected (#5139) Co-authored-by: Laurenz --- crates/typst/src/eval/binding.rs | 64 +++++++++++++++---------- tests/suite/scripting/destructuring.typ | 16 +++---- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/crates/typst/src/eval/binding.rs b/crates/typst/src/eval/binding.rs index 4201faed4a..89e536c09b 100644 --- a/crates/typst/src/eval/binding.rs +++ b/crates/typst/src/eval/binding.rs @@ -1,6 +1,8 @@ use std::collections::HashSet; -use crate::diag::{bail, At, SourceResult}; +use ecow::eco_format; + +use crate::diag::{bail, error, At, SourceDiagnostic, SourceResult}; use crate::eval::{Access, Eval, Vm}; use crate::foundations::{Array, Dict, Value}; use crate::syntax::ast::{self, AstNode}; @@ -96,10 +98,7 @@ where match p { ast::DestructuringItem::Pattern(pattern) => { let Ok(v) = value.at(i as i64, None) else { - bail!( - pattern.span(), "not enough elements to destructure"; - hint: "the provided array has a length of {len}", - ); + bail!(wrong_number_of_elements(destruct, len)); }; destructure_impl(vm, pattern, v, f)?; i += 1; @@ -108,10 +107,7 @@ where let sink_size = (1 + len).checked_sub(destruct.items().count()); let sink = sink_size.and_then(|s| value.as_slice().get(i..i + s)); let (Some(sink_size), Some(sink)) = (sink_size, sink) else { - bail!( - spread.span(), "not enough elements to destructure"; - hint: "the provided array has a length of {len}", - ); + bail!(wrong_number_of_elements(destruct, len)); }; if let Some(expr) = spread.sink_expr() { f(vm, expr, Value::Array(sink.into()))?; @@ -125,22 +121,7 @@ where } if i < len { - if i == 0 { - bail!( - destruct.span(), "too many elements to destructure"; - hint: "the provided array has a length of {len}, but the pattern expects an empty array", - ) - } else if i == 1 { - bail!( - destruct.span(), "too many elements to destructure"; - hint: "the provided array has a length of {len}, but the pattern expects a single element", - ); - } else { - bail!( - destruct.span(), "too many elements to destructure"; - hint: "the provided array has a length of {len}, but the pattern expects {i} elements", - ); - } + bail!(wrong_number_of_elements(destruct, len)); } Ok(()) @@ -193,3 +174,36 @@ where Ok(()) } + +/// The error message when the number of elements of the destructuring and the +/// array is mismatched. +#[cold] +fn wrong_number_of_elements( + destruct: ast::Destructuring, + len: usize, +) -> SourceDiagnostic { + let mut count = 0; + let mut spread = false; + + for p in destruct.items() { + match p { + ast::DestructuringItem::Pattern(_) => count += 1, + ast::DestructuringItem::Spread(_) => spread = true, + ast::DestructuringItem::Named(_) => {} + } + } + + let quantifier = if len > count { "too many" } else { "not enough" }; + let expected = match (spread, count) { + (true, c) => eco_format!("at least {c} elements"), + (false, 0) => "an empty array".into(), + (false, 1) => "a single element".into(), + (false, c) => eco_format!("{c} elements",), + }; + + error!( + destruct.span(), "{quantifier} elements to destructure"; + hint: "the provided array has a length of {len}, \ + but the pattern expects {expected}", + ) +} diff --git a/tests/suite/scripting/destructuring.typ b/tests/suite/scripting/destructuring.typ index 9d28c195fe..e70f676f5a 100644 --- a/tests/suite/scripting/destructuring.typ +++ b/tests/suite/scripting/destructuring.typ @@ -128,13 +128,13 @@ #let () = (1, 2) --- destructuring-let-array-too-few-elements --- -// Error: 13-14 not enough elements to destructure -// Hint: 13-14 the provided array has a length of 2 +// Error: 6-15 not enough elements to destructure +// Hint: 6-15 the provided array has a length of 2, but the pattern expects 3 elements #let (a, b, c) = (1, 2) --- destructuring-let-array-too-few-elements-with-sink --- -// Error: 7-10 not enough elements to destructure -// Hint: 7-10 the provided array has a length of 2 +// Error: 6-20 not enough elements to destructure +// Hint: 6-20 the provided array has a length of 2, but the pattern expects at least 3 elements #let (..a, b, c, d) = (1, 2) --- destructuring-let-array-bool-invalid --- @@ -192,8 +192,8 @@ --- destructuring-let-array-trailing-placeholders --- // Trailing placeholders. -// Error: 10-11 not enough elements to destructure -// Hint: 10-11 the provided array has a length of 1 +// Error: 6-21 not enough elements to destructure +// Hint: 6-21 the provided array has a length of 1, but the pattern expects 5 elements #let (a, _, _, _, _) = (1,) #test(a, 1) @@ -360,8 +360,8 @@ #for (x, y) in (1, 2) {} --- issue-3275-destructuring-loop-over-2d-array-1 --- -// Error: 10-11 not enough elements to destructure -// Hint: 10-11 the provided array has a length of 1 +// Error: 6-12 not enough elements to destructure +// Hint: 6-12 the provided array has a length of 1, but the pattern expects 2 elements #for (x, y) in ((1,), (2,)) {} --- issue-3275-destructuring-loop-over-2d-array-2 --- From 142c9dff49adf03c30268184621d361839991d36 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:57:20 +0200 Subject: [PATCH 006/280] Refactor sub- and superscript substitution (#5120) --- crates/typst/src/text/shift.rs | 139 ++++++++++++++------------------- 1 file changed, 60 insertions(+), 79 deletions(-) diff --git a/crates/typst/src/text/shift.rs b/crates/typst/src/text/shift.rs index 0029a3035e..003ecf47c8 100644 --- a/crates/typst/src/text/shift.rs +++ b/crates/typst/src/text/shift.rs @@ -51,19 +51,18 @@ impl Show for Packed { #[typst_macros::time(name = "sub", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let body = self.body().clone(); - let mut transformed = None; + if self.typographic(styles) { - if let Some(text) = search_text(&body, true) { + if let Some(text) = convert_script(&body, true) { if is_shapable(engine, &text, styles) { - transformed = Some(TextElem::packed(text)); + return Ok(TextElem::packed(text)); } } }; - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) + Ok(body + .styled(TextElem::set_baseline(self.baseline(styles))) + .styled(TextElem::set_size(self.size(styles)))) } } @@ -111,38 +110,38 @@ impl Show for Packed { #[typst_macros::time(name = "super", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let body = self.body().clone(); - let mut transformed = None; + if self.typographic(styles) { - if let Some(text) = search_text(&body, false) { + if let Some(text) = convert_script(&body, false) { if is_shapable(engine, &text, styles) { - transformed = Some(TextElem::packed(text)); + return Ok(TextElem::packed(text)); } } }; - Ok(transformed.unwrap_or_else(|| { - body.styled(TextElem::set_baseline(self.baseline(styles))) - .styled(TextElem::set_size(self.size(styles))) - })) + Ok(body + .styled(TextElem::set_baseline(self.baseline(styles))) + .styled(TextElem::set_size(self.size(styles)))) } } /// Find and transform the text contained in `content` to the given script kind /// if and only if it only consists of `Text`, `Space`, and `Empty` leaves. -fn search_text(content: &Content, sub: bool) -> Option { +fn convert_script(content: &Content, sub: bool) -> Option { if content.is::() { Some(' '.into()) } else if let Some(elem) = content.to_packed::() { - convert_script(elem.text(), sub) - } else if let Some(sequence) = content.to_packed::() { - let mut full = EcoString::new(); - for item in &sequence.children { - match search_text(item, sub) { - Some(text) => full.push_str(&text), - None => return None, - } + if sub { + elem.text().chars().map(to_subscript_codepoint).collect() + } else { + elem.text().chars().map(to_superscript_codepoint).collect() } - Some(full) + } else if let Some(sequence) = content.to_packed::() { + sequence + .children + .iter() + .map(|item| convert_script(item, sub)) + .collect() } else { None } @@ -165,65 +164,47 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool { false } -/// Convert a string to sub- or superscript codepoints if all characters -/// can be mapped to such a codepoint. -fn convert_script(text: &str, sub: bool) -> Option { - let mut result = EcoString::with_capacity(text.len()); - let converter = if sub { to_subscript_codepoint } else { to_superscript_codepoint }; - - for c in text.chars() { - match converter(c) { - Some(c) => result.push(c), - None => return None, - } - } - - Some(result) -} - /// Convert a character to its corresponding Unicode superscript. fn to_superscript_codepoint(c: char) -> Option { - char::from_u32(match c { - '0' => 0x2070, - '1' => 0x00B9, - '2' => 0x00B2, - '3' => 0x00B3, - '4'..='9' => 0x2070 + (c as u32 + 4 - '4' as u32), - '+' => 0x207A, - '-' => 0x207B, - '=' => 0x207C, - '(' => 0x207D, - ')' => 0x207E, - 'n' => 0x207F, - 'i' => 0x2071, - ' ' => 0x0020, - _ => return None, - }) + match c { + '1' => Some('¹'), + '2' => Some('²'), + '3' => Some('³'), + '0' | '4'..='9' => char::from_u32(c as u32 - '0' as u32 + '⁰' as u32), + '+' => Some('⁺'), + '−' => Some('⁻'), + '=' => Some('⁼'), + '(' => Some('⁽'), + ')' => Some('⁾'), + 'n' => Some('ⁿ'), + 'i' => Some('ⁱ'), + ' ' => Some(' '), + _ => None, + } } /// Convert a character to its corresponding Unicode subscript. fn to_subscript_codepoint(c: char) -> Option { - char::from_u32(match c { - '0' => 0x2080, - '1'..='9' => 0x2080 + (c as u32 - '0' as u32), - '+' => 0x208A, - '-' => 0x208B, - '=' => 0x208C, - '(' => 0x208D, - ')' => 0x208E, - 'a' => 0x2090, - 'e' => 0x2091, - 'o' => 0x2092, - 'x' => 0x2093, - 'h' => 0x2095, - 'k' => 0x2096, - 'l' => 0x2097, - 'm' => 0x2098, - 'n' => 0x2099, - 'p' => 0x209A, - 's' => 0x209B, - 't' => 0x209C, - ' ' => 0x0020, - _ => return None, - }) + match c { + '0'..='9' => char::from_u32(c as u32 - '0' as u32 + '₀' as u32), + '+' => Some('₊'), + '−' => Some('₋'), + '=' => Some('₌'), + '(' => Some('₍'), + ')' => Some('₎'), + 'a' => Some('ₐ'), + 'e' => Some('ₑ'), + 'o' => Some('ₒ'), + 'x' => Some('ₓ'), + 'h' => Some('ₕ'), + 'k' => Some('ₖ'), + 'l' => Some('ₗ'), + 'm' => Some('ₘ'), + 'n' => Some('ₙ'), + 'p' => Some('ₚ'), + 's' => Some('ₛ'), + 't' => Some('ₜ'), + ' ' => Some(' '), + _ => None, + } } From db9debca6d7468692bea3943a4f78a5f35c1b23d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 7 Oct 2024 15:56:45 +0200 Subject: [PATCH 007/280] Fix warning in proc macro (#5150) --- crates/typst-macros/src/scope.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs index d86c6aea9a..8a2f1ce61b 100644 --- a/crates/typst-macros/src/scope.rs +++ b/crates/typst-macros/src/scope.rs @@ -163,6 +163,7 @@ fn rewrite_primitive_base(item: &syn::ItemImpl, ident_ext: &syn::Ident) -> Token let self_ty = &item.self_ty; quote! { + #[allow(non_camel_case_types)] trait #ident_ext { #(#sigs)* } From 1d2a222818991bcc3ade54fdd30480ce1fb8826f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 7 Oct 2024 16:13:24 +0200 Subject: [PATCH 008/280] Error for parent-scoped figures without placement (#5151) --- crates/typst/src/model/figure.rs | 6 ++++++ tests/suite/model/figure.typ | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/crates/typst/src/model/figure.rs b/crates/typst/src/model/figure.rs index 00a9aafd1f..1d3c9c9576 100644 --- a/crates/typst/src/model/figure.rs +++ b/crates/typst/src/model/figure.rs @@ -351,6 +351,12 @@ impl Show for Packed { .with_float(true) .pack() .spanned(self.span()); + } else if self.scope(styles) == PlacementScope::Parent { + bail!( + self.span(), + "parent-scoped placement is only available for floating figures"; + hint: "you can enable floating placement with `figure(placement: auto, ..)`" + ); } Ok(realized) diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index feaf8d3bd4..fbd0ab295a 100644 --- a/tests/suite/model/figure.typ +++ b/tests/suite/model/figure.typ @@ -77,6 +77,11 @@ We can clearly see that @fig-cylinder and #lines(15) +--- figure-scope-without-placement --- +// Error: 2-27 parent-scoped placement is only available for floating figures +// Hint: 2-27 you can enable floating placement with `figure(placement: auto, ..)` +#figure(scope: "parent")[] + --- figure-theorem --- // Testing show rules with figures with a simple theorem display #show figure.where(kind: "theorem"): it => { From 037c0c82160088f5f8026e2b6808db20c874e8ad Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 7 Oct 2024 15:28:30 -0400 Subject: [PATCH 009/280] Fix hint for destructuring to an array with at least 1 element (#5154) --- crates/typst/src/eval/binding.rs | 1 + tests/suite/scripting/destructuring.typ | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/crates/typst/src/eval/binding.rs b/crates/typst/src/eval/binding.rs index 89e536c09b..18468b9ee6 100644 --- a/crates/typst/src/eval/binding.rs +++ b/crates/typst/src/eval/binding.rs @@ -195,6 +195,7 @@ fn wrong_number_of_elements( let quantifier = if len > count { "too many" } else { "not enough" }; let expected = match (spread, count) { + (true, 1) => "at least 1 element".into(), (true, c) => eco_format!("at least {c} elements"), (false, 0) => "an empty array".into(), (false, 1) => "a single element".into(), diff --git a/tests/suite/scripting/destructuring.typ b/tests/suite/scripting/destructuring.typ index e70f676f5a..3c0c754c8e 100644 --- a/tests/suite/scripting/destructuring.typ +++ b/tests/suite/scripting/destructuring.typ @@ -137,6 +137,11 @@ // Hint: 6-20 the provided array has a length of 2, but the pattern expects at least 3 elements #let (..a, b, c, d) = (1, 2) +--- destructuring-let-array-too-few-elements-with-sink-1-element --- +// Error: 6-14 not enough elements to destructure +// Hint: 6-14 the provided array has a length of 0, but the pattern expects at least 1 element +#let (..a, b) = () + --- destructuring-let-array-bool-invalid --- // Error: 6-12 cannot destructure boolean #let (a, b) = true From 7a96c86487fe2eb35d88bd8314b26feec6118898 Mon Sep 17 00:00:00 2001 From: Yip Coekjan <69834864+Coekjan@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:37:11 +0800 Subject: [PATCH 010/280] Fix smartquotes after inline equations (#5149) --- crates/typst/src/text/smartquote.rs | 9 ++++++--- .../issue-5146-smartquotes-after-equations.png | Bin 0 -> 240 bytes tests/suite/text/smartquote.typ | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 tests/ref/issue-5146-smartquotes-after-equations.png diff --git a/crates/typst/src/text/smartquote.rs b/crates/typst/src/text/smartquote.rs index 9b48c03ac2..467165b6a3 100644 --- a/crates/typst/src/text/smartquote.rs +++ b/crates/typst/src/text/smartquote.rs @@ -131,9 +131,12 @@ impl SmartQuoter { } // If we have a single smart quote, didn't recently open a single - // quotation, and are after an alphabetic char, interpret this as an - // apostrophe. - if !double && opened != Some(false) && before.is_alphabetic() { + // quotation, and are after an alphabetic char or an object (e.g. a + // math equation), interpret this as an apostrophe. + if !double + && opened != Some(false) + && (before.is_alphabetic() || before == '\u{FFFC}') + { return "’"; } diff --git a/tests/ref/issue-5146-smartquotes-after-equations.png b/tests/ref/issue-5146-smartquotes-after-equations.png new file mode 100644 index 0000000000000000000000000000000000000000..96be7204a0b86a56c67338bca6e5688d578a5f75 GIT binary patch literal 240 zcmV1cY(Unlq-E+^;+TxYVmIBGUxo@@s{l3i$BKRcj!zP*sb?{qILcfaKYq^Xh?A&7sp^!3*D(mebneH-&qEp-f1#wW1g qf$IKCqb1j<#iJIFT0Cko7ytmdpoEJ~;ed?*0000 Date: Tue, 8 Oct 2024 15:13:14 +0200 Subject: [PATCH 011/280] Allow sticky blocks to be breakable (#5161) --- crates/typst/src/layout/container.rs | 2 -- crates/typst/src/layout/flow/collect.rs | 4 +++- crates/typst/src/layout/flow/distribute.rs | 8 ++++---- tests/ref/block-sticky-breakable.png | Bin 0 -> 405 bytes tests/suite/layout/container.typ | 6 ++++++ 5 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 tests/ref/block-sticky-breakable.png diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs index 996ef6c992..9fdef0be40 100644 --- a/crates/typst/src/layout/container.rs +++ b/crates/typst/src/layout/container.rs @@ -422,8 +422,6 @@ pub struct BlockElem { /// This is, by default, set on heading blocks to prevent orphaned headings /// at the bottom of the page. /// - /// Marking a block as sticky makes it unbreakable. - /// /// ```example /// >>> #set page(height: 140pt) /// // Disable stickiness of headings. diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst/src/layout/flow/collect.rs index 9fc64f0672..a477c3347e 100644 --- a/crates/typst/src/layout/flow/collect.rs +++ b/crates/typst/src/layout/flow/collect.rs @@ -189,7 +189,7 @@ impl<'a> Collector<'a, '_, '_> { self.output.push(spacing(elem.above(styles))); - if !breakable || sticky || fr.is_some() { + if !breakable || fr.is_some() { self.output.push(Child::Single(self.boxed(SingleChild { align, sticky, @@ -203,6 +203,7 @@ impl<'a> Collector<'a, '_, '_> { let alone = self.children.len() == 1; self.output.push(Child::Multi(self.boxed(MultiChild { align, + sticky, alone, elem, styles, @@ -375,6 +376,7 @@ fn layout_single_impl( #[derive(Debug)] pub struct MultiChild<'a> { pub align: Axes, + pub sticky: bool, alone: bool, elem: &'a Packed, styles: StyleChain<'a>, diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst/src/layout/flow/distribute.rs index 65516ccd85..a30b71bacb 100644 --- a/crates/typst/src/layout/flow/distribute.rs +++ b/crates/typst/src/layout/flow/distribute.rs @@ -247,7 +247,7 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { // Lay out the block. let (frame, spill) = multi.layout(self.composer.engine, self.regions)?; - self.frame(frame, multi.align, false, true)?; + self.frame(frame, multi.align, multi.sticky, true)?; // If the block didn't fully fit into the current region, save it into // the `spill` and finish the region. @@ -292,9 +292,9 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { breakable: bool, ) -> FlowResult<()> { if sticky { - // If the frame is sticky and we haven't remember a preceding sticky - // element, make a checkpoint which we can restore should we end on - // this sticky element. + // If the frame is sticky and we haven't remembered a preceding + // sticky element, make a checkpoint which we can restore should we + // end on this sticky element. if self.stickable && self.sticky.is_none() { self.sticky = Some(self.snapshot()); } diff --git a/tests/ref/block-sticky-breakable.png b/tests/ref/block-sticky-breakable.png new file mode 100644 index 0000000000000000000000000000000000000000..91287a7e67a25882fb9b7957b026a60404d5aa09 GIT binary patch literal 405 zcmeAS@N?(olHy`uVBq!ia0vp^6+m3c0VEi%2k%EaktaqI1scx~rGnd2XA z87dj{1jNgZ%I+?Dd+z9|Q+u7Pe+KJKxiaarC*SNT^{bOz&rapm^-AxxTDomg(POX6 ze5tNH&ueTrUe|2ih45-wSw%3+Y5`cyR21awy*$pP4cik0W-6o z*!!T792=?Uow@cJla^e^i+4_ z4^GB!)(nq(7}ArY-KTSPBphsFUpn@ZRp^hD=AqL+4<4?#ZEXA=q@TB8?ZUTHznC++=}+kYC-lIe;U%}wo{0(v zi+?^y+|OMhrJ6IRcd1dDFQXvHA8Bo;`ySgEJqU_^STm1NnP Date: Tue, 8 Oct 2024 17:55:44 +0200 Subject: [PATCH 012/280] Allow unbreakable multi-layouters to expand (#5162) --- crates/typst/src/layout/container.rs | 6 ++++-- crates/typst/src/layout/flow/collect.rs | 16 ++++++++++------ crates/typst/src/layout/flow/distribute.rs | 9 ++++++--- tests/ref/issue-5160-unbreakable-pad.png | Bin 0 -> 145 bytes tests/suite/layout/pad.typ | 4 ++++ 5 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 tests/ref/issue-5160-unbreakable-pad.png diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs index 9fdef0be40..cc1559c666 100644 --- a/crates/typst/src/layout/container.rs +++ b/crates/typst/src/layout/container.rs @@ -487,7 +487,7 @@ impl Packed { engine: &mut Engine, locator: Locator, styles: StyleChain, - base: Size, + region: Region, ) -> SourceResult { // Fetch sizing properties. let width = self.width(styles); @@ -495,7 +495,7 @@ impl Packed { let inset = self.inset(styles).unwrap_or_default(); // Build the pod regions. - let pod = unbreakable_pod(&width.into(), &height, &inset, styles, base); + let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size); // Layout the body. let body = self.body(styles); @@ -518,6 +518,8 @@ impl Packed { // If we have a child that wants to layout with full region access, // we layout it. Some(BlockBody::MultiLayouter(callback)) => { + let expand = (pod.expand | region.expand) & pod.size.map(Abs::is_finite); + let pod = Region { expand, ..pod }; callback.call(engine, locator, styles, pod.into())?.into_frame() } }; diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst/src/layout/flow/collect.rs index a477c3347e..efb16427f4 100644 --- a/crates/typst/src/layout/flow/collect.rs +++ b/crates/typst/src/layout/flow/collect.rs @@ -173,6 +173,7 @@ impl<'a> Collector<'a, '_, '_> { fn block(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { let locator = self.locator.next(&elem.span()); let align = AlignElem::alignment_in(styles).resolve(styles); + let alone = self.children.len() == 1; let sticky = elem.sticky(styles); let breakable = elem.breakable(styles); let fr = match elem.height(styles) { @@ -193,6 +194,7 @@ impl<'a> Collector<'a, '_, '_> { self.output.push(Child::Single(self.boxed(SingleChild { align, sticky, + alone, fr, elem, styles, @@ -200,7 +202,6 @@ impl<'a> Collector<'a, '_, '_> { cell: CachedCell::new(), }))); } else { - let alone = self.children.len() == 1; self.output.push(Child::Multi(self.boxed(MultiChild { align, sticky, @@ -318,6 +319,7 @@ pub struct LineChild { pub struct SingleChild<'a> { pub align: Axes, pub sticky: bool, + pub alone: bool, pub fr: Option, elem: &'a Packed, styles: StyleChain<'a>, @@ -327,8 +329,10 @@ pub struct SingleChild<'a> { impl SingleChild<'_> { /// Build the child's frame given the region's base size. - pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult { - self.cell.get_or_init(base, |base| { + pub fn layout(&self, engine: &mut Engine, region: Region) -> SourceResult { + self.cell.get_or_init(region, |mut region| { + // Vertical expansion is only kept if this block is the only child. + region.expand.y &= self.alone; layout_single_impl( engine.world, engine.introspector, @@ -338,7 +342,7 @@ impl SingleChild<'_> { self.elem, self.locator.track(), self.styles, - base, + region, ) }) } @@ -356,7 +360,7 @@ fn layout_single_impl( elem: &Packed, locator: Tracked, styles: StyleChain, - base: Size, + region: Region, ) -> SourceResult { let link = LocatorLink::new(locator); let locator = Locator::link(&link); @@ -368,7 +372,7 @@ fn layout_single_impl( route: Route::extend(route), }; - elem.layout_single(&mut engine, locator, styles, base) + elem.layout_single(&mut engine, locator, styles, region) .map(|frame| frame.post_processed(styles)) } diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst/src/layout/flow/distribute.rs index a30b71bacb..a738b3e67d 100644 --- a/crates/typst/src/layout/flow/distribute.rs +++ b/crates/typst/src/layout/flow/distribute.rs @@ -226,7 +226,10 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { } // Lay out the block. - let frame = single.layout(self.composer.engine, self.regions.base())?; + let frame = single.layout( + self.composer.engine, + Region::new(self.regions.base(), self.regions.expand), + )?; // If the block doesn't fit and a followup region may improve things, // finish the region. @@ -422,8 +425,8 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> { for item in &self.items { let Item::Fr(v, Some(single)) = item else { continue }; let length = v.share(frs, fr_space); - let base = Size::new(region.size.x, length); - let frame = single.layout(self.composer.engine, base)?; + let pod = Region::new(Size::new(region.size.x, length), region.expand); + let frame = single.layout(self.composer.engine, pod)?; used.x.set_max(frame.width()); fr_frames.push(frame); } diff --git a/tests/ref/issue-5160-unbreakable-pad.png b/tests/ref/issue-5160-unbreakable-pad.png new file mode 100644 index 0000000000000000000000000000000000000000..f370870a5acee458c845c70846e5219ff408ea05 GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS_0VEhE<%|3RQW2gmjv*Ddl7HAcG$dYm6xi*q zE4Q^`LZr->f&`s?^RAj1h<&f!EFsWszf@+6y|sXt|LywH#ET3Oe~h31|8H+)|04h6 rn^Wok&p$gaqwC-X(tPF7!!$#Nm01&~OyJt80 Date: Tue, 8 Oct 2024 17:58:05 +0200 Subject: [PATCH 013/280] Disable line numbers for block equations (#5163) --- crates/typst/src/math/equation.rs | 3 ++- tests/ref/line-numbers-equation-number.png | Bin 0 -> 428 bytes tests/suite/layout/line-numbers.typ | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 tests/ref/line-numbers-equation-number.png diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs index 963a35c5e1..babcbb8b0d 100644 --- a/crates/typst/src/math/equation.rs +++ b/crates/typst/src/math/equation.rs @@ -17,7 +17,7 @@ use crate::layout::{ use crate::math::{ scaled_font_size, MathContext, MathRunFrameBuilder, MathSize, MathVariant, }; -use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; +use crate::model::{Numbering, Outlinable, ParElem, ParLine, Refable, Supplement}; use crate::syntax::Span; use crate::text::{ families, variant, Font, FontFamily, FontList, FontWeight, LocalName, TextElem, @@ -180,6 +180,7 @@ impl ShowSet for Packed { if self.block(styles) { out.set(AlignElem::set_alignment(Alignment::CENTER)); out.set(BlockElem::set_breakable(false)); + out.set(ParLine::set_numbering(None)); out.set(EquationElem::set_size(MathSize::Display)); } else { out.set(EquationElem::set_size(MathSize::Text)); diff --git a/tests/ref/line-numbers-equation-number.png b/tests/ref/line-numbers-equation-number.png new file mode 100644 index 0000000000000000000000000000000000000000..9b4959eb084571ff46ccdff582eeb413ffed5e9d GIT binary patch literal 428 zcmV;d0aN~oP)LAOV>}Vz}rBHtZ50Jkr9O2>|U9piG(y--Ns?eK1IWu>zyVR5*?nISGTf zp~7xcVaOdQz#uLSc9{t)GrQjO5e(vy0i`(!pw~ Date: Thu, 10 Oct 2024 13:59:00 +0200 Subject: [PATCH 014/280] More robust glyph drawing (#5159) --- crates/typst-pdf/src/color_font.rs | 44 ++++++-- crates/typst-pdf/src/content.rs | 85 +++++++------- crates/typst-render/src/text.rs | 15 ++- crates/typst-syntax/src/span.rs | 7 ++ crates/typst/src/realize.rs | 6 +- crates/typst/src/text/font/color.rs | 169 ++++++++++++++++++---------- 6 files changed, 203 insertions(+), 123 deletions(-) diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs index 5182a05940..f6fea3962b 100644 --- a/crates/typst-pdf/src/color_font.rs +++ b/crates/typst-pdf/src/color_font.rs @@ -12,10 +12,11 @@ use indexmap::IndexMap; use pdf_writer::types::UnicodeCmap; use pdf_writer::writers::WMode; use pdf_writer::{Filter, Finish, Name, Rect, Ref}; -use typst::diag::SourceResult; +use typst::diag::{bail, error, SourceDiagnostic, SourceResult}; +use typst::foundations::Repr; use typst::layout::Em; -use typst::text::color::frame_for_glyph; -use typst::text::Font; +use typst::text::color::glyph_frame; +use typst::text::{Font, Glyph, TextItemView}; use crate::content; use crate::font::{base_font_name, write_font_descriptor, CMAP_NAME, SYSTEM_INFO}; @@ -211,9 +212,10 @@ impl ColorFontMap<()> { pub fn get( &mut self, options: &PdfOptions, - font: &Font, - gid: u16, + text: &TextItemView, + glyph: &Glyph, ) -> SourceResult<(usize, u8)> { + let font = &text.item.font; let color_font = self.map.entry(font.clone()).or_insert_with(|| { let global_bbox = font.ttf().global_bounding_box(); let bbox = Rect::new( @@ -230,7 +232,7 @@ impl ColorFontMap<()> { } }); - Ok(if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) { + Ok(if let Some(index_of_glyph) = color_font.glyph_indices.get(&glyph.id) { // If we already know this glyph, return it. (color_font.slice_ids[index_of_glyph / 256], *index_of_glyph as u8) } else { @@ -242,9 +244,13 @@ impl ColorFontMap<()> { self.total_slice_count += 1; } - let frame = frame_for_glyph(font, gid); - let width = - font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em(); + let (frame, tofu) = glyph_frame(font, glyph.id); + if options.standards.pdfa && tofu { + bail!(failed_to_convert(text, glyph)); + } + + let width = font.advance(glyph.id).unwrap_or(Em::new(0.0)).get() + * font.units_per_em(); let instructions = content::build( options, &mut self.resources, @@ -252,8 +258,8 @@ impl ColorFontMap<()> { None, Some(width as f32), )?; - color_font.glyphs.push(ColorGlyph { gid, instructions }); - color_font.glyph_indices.insert(gid, index); + color_font.glyphs.push(ColorGlyph { gid: glyph.id, instructions }); + color_font.glyph_indices.insert(glyph.id, index); (color_font.slice_ids[index / 256], index as u8) }) @@ -321,3 +327,19 @@ pub struct ColorFontSlice { /// represent the subset of the TTF font we are interested in. pub subfont: usize, } + +/// The error when the glyph could not be converted. +#[cold] +fn failed_to_convert(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic { + let mut diag = error!( + glyph.span.0, + "the glyph for {} could not be exported", + text.glyph_text(glyph).repr() + ); + + if text.item.font.ttf().tables().cff2.is_some() { + diag.hint("CFF2 fonts are not currently supported"); + } + + diag +} diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index 79323b6a02..babb3e57a7 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -10,15 +10,15 @@ use pdf_writer::types::{ }; use pdf_writer::writers::PositionedItems; use pdf_writer::{Content, Finish, Name, Rect, Str}; -use typst::diag::{bail, SourceResult}; +use typst::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst::foundations::Repr; use typst::layout::{ Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform, }; use typst::model::Destination; use typst::syntax::Span; -use typst::text::color::is_color_glyph; -use typst::text::{Font, TextItem, TextItemView}; +use typst::text::color::should_outline; +use typst::text::{Font, Glyph, TextItem, TextItemView}; use typst::utils::{Deferred, Numeric, SliceExt}; use typst::visualize::{ FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, @@ -418,46 +418,27 @@ fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult /// Encode a text run into the content stream. fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> { - if ctx.options.standards.pdfa { - let last_resort = text.font.info().is_last_resort(); - for g in &text.glyphs { - if last_resort || g.id == 0 { - bail!( - g.span.0, - "the text {} could not be displayed with any font", - TextItemView::full(text).glyph_text(g).repr(), - ); - } - } - } - - let ttf = text.font.ttf(); - let tables = ttf.tables(); - - // If the text run contains either only color glyphs (used for emojis for - // example) or normal text we can render it directly - let has_color_glyphs = tables.sbix.is_some() - || tables.cbdt.is_some() - || tables.svg.is_some() - || tables.colr.is_some(); - if !has_color_glyphs { - write_normal_text(ctx, pos, TextItemView::full(text))?; - return Ok(()); + if ctx.options.standards.pdfa && text.font.info().is_last_resort() { + bail!( + Span::find(text.glyphs.iter().map(|g| g.span.0)), + "the text {} could not be displayed with any font", + &text.text, + ); } - let color_glyph_count = - text.glyphs.iter().filter(|g| is_color_glyph(&text.font, g)).count(); + let outline_glyphs = + text.glyphs.iter().filter(|g| should_outline(&text.font, g)).count(); - if color_glyph_count == text.glyphs.len() { - write_color_glyphs(ctx, pos, TextItemView::full(text))?; - } else if color_glyph_count == 0 { + if outline_glyphs == text.glyphs.len() { write_normal_text(ctx, pos, TextItemView::full(text))?; + } else if outline_glyphs == 0 { + write_complex_glyphs(ctx, pos, TextItemView::full(text))?; } else { - // Otherwise we need to split it in smaller text runs + // Otherwise we need to split it into smaller text runs. let mut offset = 0; let mut position_in_run = Abs::zero(); - for (color, sub_run) in - text.glyphs.group_by_key(|g| is_color_glyph(&text.font, g)) + for (should_outline, sub_run) in + text.glyphs.group_by_key(|g| should_outline(&text.font, g)) { let end = offset + sub_run.len(); @@ -468,11 +449,12 @@ fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<() let pos = pos + Point::new(position_in_run, Abs::zero()); position_in_run += text_item_view.width(); offset = end; - // Actually write the sub text-run - if color { - write_color_glyphs(ctx, pos, text_item_view)?; - } else { + + // Actually write the sub text-run. + if should_outline { write_normal_text(ctx, pos, text_item_view)?; + } else { + write_complex_glyphs(ctx, pos, text_item_view)?; } } } @@ -534,6 +516,10 @@ fn write_normal_text( // Write the glyphs with kerning adjustments. for glyph in text.glyphs() { + if ctx.options.standards.pdfa && glyph.id == 0 { + bail!(tofu(&text, glyph)); + } + adjustment += glyph.x_offset; if !adjustment.is_zero() { @@ -596,7 +582,7 @@ fn show_text(items: &mut PositionedItems, encoded: &[u8]) { } /// Encodes a text run made only of color glyphs into the content stream -fn write_color_glyphs( +fn write_complex_glyphs( ctx: &mut Builder, pos: Point, text: TextItemView, @@ -621,12 +607,17 @@ fn write_color_glyphs( .or_default(); for glyph in text.glyphs() { + if ctx.options.standards.pdfa && glyph.id == 0 { + bail!(tofu(&text, glyph)); + } + // Retrieve the Type3 font reference and the glyph index in the font. let color_fonts = ctx .resources .color_fonts .get_or_insert_with(|| Box::new(ColorFontMap::new())); - let (font, index) = color_fonts.get(ctx.options, &text.item.font, glyph.id)?; + + let (font, index) = color_fonts.get(ctx.options, &text, glyph)?; if last_font != Some(font) { ctx.content.set_font( @@ -824,3 +815,13 @@ fn to_pdf_line_join(join: LineJoin) -> LineJoinStyle { LineJoin::Bevel => LineJoinStyle::BevelJoin, } } + +/// The error when there is a tofu glyph. +#[cold] +fn tofu(text: &TextItemView, glyph: &Glyph) -> SourceDiagnostic { + error!( + glyph.span.0, + "the text {} could not be displayed with any font", + text.glyph_text(glyph).repr(), + ) +} diff --git a/crates/typst-render/src/text.rs b/crates/typst-render/src/text.rs index c4e8334067..70d5164269 100644 --- a/crates/typst-render/src/text.rs +++ b/crates/typst-render/src/text.rs @@ -4,7 +4,7 @@ use pixglyph::Bitmap; use tiny_skia as sk; use ttf_parser::{GlyphId, OutlineBuilder}; use typst::layout::{Abs, Axes, Point, Size}; -use typst::text::color::{frame_for_glyph, is_color_glyph}; +use typst::text::color::{glyph_frame, should_outline}; use typst::text::{Font, TextItem}; use typst::visualize::{FixedStroke, Paint}; @@ -18,20 +18,19 @@ pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) { let id = GlyphId(glyph.id); let offset = x + glyph.x_offset.at(text.size).to_f32(); - if is_color_glyph(&text.font, glyph) { + if should_outline(&text.font, glyph) { + let state = + state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0))); + render_outline_glyph(canvas, state, text, id); + } else { let upem = text.font.units_per_em(); let text_scale = Abs::raw(text.size.to_raw() / upem); let state = state .pre_translate(Point::new(Abs::raw(offset as _), -text.size)) .pre_scale(Axes::new(text_scale, text_scale)); - let glyph_frame = frame_for_glyph(&text.font, glyph.id); - + let (glyph_frame, _) = glyph_frame(&text.font, glyph.id); crate::render_frame(canvas, state, &glyph_frame); - } else { - let state = - state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0))); - render_outline_glyph(canvas, state, text, id); } x += glyph.x_advance.at(text.size).to_f32(); diff --git a/crates/typst-syntax/src/span.rs b/crates/typst-syntax/src/span.rs index 4d5dd9c37b..85745f60e1 100644 --- a/crates/typst-syntax/src/span.rs +++ b/crates/typst-syntax/src/span.rs @@ -92,6 +92,13 @@ impl Span { } } + /// Find the first non-detached span in the iterator. + pub fn find(iter: impl IntoIterator) -> Self { + iter.into_iter() + .find(|span| !span.is_detached()) + .unwrap_or(Span::detached()) + } + /// Resolve a file location relative to this span's source. pub fn resolve_path(self, path: &str) -> Result { let Some(file) = self.id() else { diff --git a/crates/typst/src/realize.rs b/crates/typst/src/realize.rs index 7c199ff167..896c2a6a8d 100644 --- a/crates/typst/src/realize.rs +++ b/crates/typst/src/realize.rs @@ -1241,9 +1241,5 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) { /// Finds the first non-detached span in the list. fn select_span(children: &[Pair]) -> Span { - children - .iter() - .map(|(c, _)| c.span()) - .find(|span| !span.is_detached()) - .unwrap_or(Span::detached()) + Span::find(children.iter().map(|(c, _)| c.span())) } diff --git a/crates/typst/src/text/font/color.rs b/crates/typst/src/text/font/color.rs index 6b6ae9b0de..0e6b0c1fa6 100644 --- a/crates/typst/src/text/font/color.rs +++ b/crates/typst/src/text/font/color.rs @@ -6,51 +6,138 @@ use ttf_parser::{GlyphId, RgbaColor}; use usvg::tiny_skia_path; use xmlwriter::XmlWriter; -use crate::layout::{Abs, Axes, Frame, FrameItem, Point, Size}; +use crate::layout::{Abs, Frame, FrameItem, Point, Size}; use crate::syntax::Span; use crate::text::{Font, Glyph}; -use crate::visualize::Image; +use crate::visualize::{FixedStroke, Geometry, Image}; -/// Tells if a glyph is a color glyph or not in a given font. -pub fn is_color_glyph(font: &Font, g: &Glyph) -> bool { +/// Whether this glyph should be rendered via simple outlining instead of via +/// `glyph_frame`. +pub fn should_outline(font: &Font, glyph: &Glyph) -> bool { let ttf = font.ttf(); - let glyph_id = GlyphId(g.id); - ttf.glyph_raster_image(glyph_id, 160).is_some() - || ttf.glyph_svg_image(glyph_id).is_some() - || ttf.is_color_glyph(glyph_id) + let glyph_id = GlyphId(glyph.id); + (ttf.tables().glyf.is_some() || ttf.tables().cff.is_some()) + && !ttf + .glyph_raster_image(glyph_id, u16::MAX) + .is_some_and(|img| img.format == ttf_parser::RasterImageFormat::PNG) + && !ttf.is_color_glyph(glyph_id) + && ttf.glyph_svg_image(glyph_id).is_none() } -/// Returns a frame with the glyph drawn inside. +/// Returns a frame representing a glyph and whether it is a fallback tofu +/// frame. +/// +/// Should only be called on glyphs for which [`should_outline`] returns false. /// /// The glyphs are sized in font units, [`text.item.size`] is not taken into /// account. #[comemo::memoize] -pub fn frame_for_glyph(font: &Font, glyph_id: u16) -> Frame { - let ttf = font.ttf(); - let upem = Abs::pt(ttf.units_per_em() as f64); +pub fn glyph_frame(font: &Font, glyph_id: u16) -> (Frame, bool) { + let upem = Abs::pt(font.units_per_em()); let glyph_id = GlyphId(glyph_id); let mut frame = Frame::soft(Size::splat(upem)); + let mut tofu = false; + + if draw_glyph(&mut frame, font, upem, glyph_id).is_none() + && font.ttf().glyph_index(' ') != Some(glyph_id) + { + // Generate a fallback tofu if the glyph couldn't be drawn, unless it is + // the space glyph. Then, an empty frame does the job. (This happens for + // some rare CBDT fonts, which don't define a bitmap for the space, but + // also don't have a glyf or CFF table.) + draw_fallback_tofu(&mut frame, font, upem, glyph_id); + tofu = true; + } + + (frame, tofu) +} - if let Some(raster_image) = ttf.glyph_raster_image(glyph_id, u16::MAX) { - draw_raster_glyph(&mut frame, font, upem, raster_image); +/// Tries to draw a glyph. +fn draw_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + glyph_id: GlyphId, +) -> Option<()> { + let ttf = font.ttf(); + if let Some(raster_image) = ttf + .glyph_raster_image(glyph_id, u16::MAX) + .filter(|img| img.format == ttf_parser::RasterImageFormat::PNG) + { + draw_raster_glyph(frame, font, upem, raster_image) } else if ttf.is_color_glyph(glyph_id) { - draw_colr_glyph(&mut frame, upem, ttf, glyph_id); + draw_colr_glyph(frame, font, upem, glyph_id) } else if ttf.glyph_svg_image(glyph_id).is_some() { - draw_svg_glyph(&mut frame, upem, font, glyph_id); + draw_svg_glyph(frame, font, upem, glyph_id) + } else { + None } +} + +/// Draws a fallback tofu box with the advance width of the glyph. +fn draw_fallback_tofu(frame: &mut Frame, font: &Font, upem: Abs, glyph_id: GlyphId) { + let advance = font + .ttf() + .glyph_hor_advance(glyph_id) + .map(|advance| Abs::pt(advance as f64)) + .unwrap_or(upem / 3.0); + let inset = 0.15 * advance; + let height = 0.7 * upem; + let pos = Point::new(inset, upem - height); + let size = Size::new(advance - inset * 2.0, height); + let thickness = upem / 20.0; + let stroke = FixedStroke { thickness, ..Default::default() }; + let shape = Geometry::Rect(size).stroked(stroke); + frame.push(pos, FrameItem::Shape(shape, Span::detached())); +} + +/// Draws a raster glyph in a frame. +/// +/// Supports only PNG images. +fn draw_raster_glyph( + frame: &mut Frame, + font: &Font, + upem: Abs, + raster_image: ttf_parser::RasterGlyphImage, +) -> Option<()> { + let image = Image::new( + raster_image.data.into(), + typst::visualize::ImageFormat::Raster(typst::visualize::RasterFormat::Png), + None, + ) + .ok()?; + + // Apple Color emoji doesn't provide offset information (or at least + // not in a way ttf-parser understands), so we artificially shift their + // baseline to make it look good. + let y_offset = if font.info().family.to_lowercase() == "apple color emoji" { + 20.0 + } else { + -(raster_image.y as f64) + }; + + let position = Point::new( + upem * raster_image.x as f64 / raster_image.pixels_per_em as f64, + upem * y_offset / raster_image.pixels_per_em as f64, + ); + let aspect_ratio = image.width() / image.height(); + let size = Size::new(upem, upem * aspect_ratio); + frame.push(position, FrameItem::Image(image, size, Span::detached())); - frame + Some(()) } +/// Draws a glyph from the COLR table into the frame. fn draw_colr_glyph( frame: &mut Frame, + font: &Font, upem: Abs, - ttf: &ttf_parser::Face, glyph_id: GlyphId, ) -> Option<()> { let mut svg = XmlWriter::new(xmlwriter::Options::default()); + let ttf = font.ttf(); let width = ttf.global_bounding_box().width() as f64; let height = ttf.global_bounding_box().height() as f64; let x_min = ttf.global_bounding_box().x_min as f64; @@ -87,8 +174,7 @@ fn draw_colr_glyph( transforms_stack: vec![ttf_parser::Transform::default()], }; - ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter) - .unwrap(); + ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?; svg.end_element(); let data = svg.end_document().into_bytes(); @@ -98,54 +184,22 @@ fn draw_colr_glyph( typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg), None, ) - .unwrap(); + .ok()?; let y_shift = Abs::raw(upem.to_raw() - y_max); let position = Point::new(Abs::raw(x_min), y_shift); - let size = Axes::new(Abs::pt(width), Abs::pt(height)); + let size = Size::new(Abs::pt(width), Abs::pt(height)); frame.push(position, FrameItem::Image(image, size, Span::detached())); Some(()) } -/// Draws a raster glyph in a frame. -fn draw_raster_glyph( - frame: &mut Frame, - font: &Font, - upem: Abs, - raster_image: ttf_parser::RasterGlyphImage, -) { - let image = Image::new( - raster_image.data.into(), - typst::visualize::ImageFormat::Raster(typst::visualize::RasterFormat::Png), - None, - ) - .unwrap(); - - // Apple Color emoji doesn't provide offset information (or at least - // not in a way ttf-parser understands), so we artificially shift their - // baseline to make it look good. - let y_offset = if font.info().family.to_lowercase() == "apple color emoji" { - 20.0 - } else { - -(raster_image.y as f64) - }; - - let position = Point::new( - upem * raster_image.x as f64 / raster_image.pixels_per_em as f64, - upem * y_offset / raster_image.pixels_per_em as f64, - ); - let aspect_ratio = image.width() / image.height(); - let size = Axes::new(upem, upem * aspect_ratio); - frame.push(position, FrameItem::Image(image, size, Span::detached())); -} - /// Draws an SVG glyph in a frame. fn draw_svg_glyph( frame: &mut Frame, - upem: Abs, font: &Font, + upem: Abs, glyph_id: GlyphId, ) -> Option<()> { // TODO: Our current conversion of the SVG table works for Twitter Color Emoji, @@ -211,9 +265,10 @@ fn draw_svg_glyph( typst::visualize::ImageFormat::Vector(typst::visualize::VectorFormat::Svg), None, ) - .unwrap(); + .ok()?; + let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); - let size = Axes::new(Abs::pt(width), Abs::pt(height)); + let size = Size::new(Abs::pt(width), Abs::pt(height)); frame.push(position, FrameItem::Image(image, size, Span::detached())); Some(()) From f1f2de889a2d7a7533453956595acb82ab1da125 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Thu, 10 Oct 2024 14:03:11 +0200 Subject: [PATCH 015/280] Resolve lengths in math with scaled font size (#5168) --- crates/typst/src/layout/rel.rs | 6 ++++++ crates/typst/src/math/accent.rs | 8 ++++---- crates/typst/src/math/cancel.rs | 8 +++----- crates/typst/src/math/ctx.rs | 4 ++-- crates/typst/src/math/matrix.rs | 13 ++++++------- crates/typst/src/math/stretch.rs | 7 ++++--- tests/ref/math-accent-sized-script.png | Bin 0 -> 331 bytes tests/ref/math-spacing-script.png | Bin 0 -> 346 bytes tests/suite/math/accent.typ | 4 ++++ tests/suite/math/spacing.typ | 4 ++++ 10 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 tests/ref/math-accent-sized-script.png create mode 100644 tests/ref/math-spacing-script.png diff --git a/crates/typst/src/layout/rel.rs b/crates/typst/src/layout/rel.rs index 2af63ba3f4..cce0ac861c 100644 --- a/crates/typst/src/layout/rel.rs +++ b/crates/typst/src/layout/rel.rs @@ -86,6 +86,12 @@ impl Rel { None } } + + /// Convert to a relative length with the absolute part resolved at the + /// given font size. + pub fn at(self, font_size: Abs) -> Rel { + self.map(|abs| abs.at(font_size)) + } } impl Debug for Rel { diff --git a/crates/typst/src/math/accent.rs b/crates/typst/src/math/accent.rs index dbc0fad671..08ed723802 100644 --- a/crates/typst/src/math/accent.rs +++ b/crates/typst/src/math/accent.rs @@ -1,11 +1,11 @@ use crate::diag::{bail, SourceResult}; use crate::foundations::{ - cast, elem, func, Content, NativeElement, Packed, Resolve, Smart, StyleChain, Value, + cast, elem, func, Content, NativeElement, Packed, Smart, StyleChain, Value, }; use crate::layout::{Em, Frame, Length, Point, Rel, Size}; use crate::math::{ - style_cramped, FrameFragment, GlyphFragment, LayoutMath, MathContext, MathFragment, - Scaled, + scaled_font_size, style_cramped, FrameFragment, GlyphFragment, LayoutMath, + MathContext, MathFragment, Scaled, }; use crate::text::TextElem; @@ -123,7 +123,7 @@ impl LayoutMath for Packed { let width = self .size(styles) .unwrap_or(Rel::one()) - .resolve(styles) + .at(scaled_font_size(ctx, styles)) .relative_to(base.width()); // Forcing the accent to be at least as large as the base makes it too diff --git a/crates/typst/src/math/cancel.rs b/crates/typst/src/math/cancel.rs index ef07a1c8ee..0bc28f0788 100644 --- a/crates/typst/src/math/cancel.rs +++ b/crates/typst/src/math/cancel.rs @@ -1,13 +1,11 @@ use comemo::Track; use crate::diag::{At, SourceResult}; -use crate::foundations::{ - cast, elem, Content, Context, Func, Packed, Resolve, Smart, StyleChain, -}; +use crate::foundations::{cast, elem, Content, Context, Func, Packed, Smart, StyleChain}; use crate::layout::{ Abs, Angle, Frame, FrameItem, Length, Point, Ratio, Rel, Size, Transform, }; -use crate::math::{FrameFragment, LayoutMath, MathContext}; +use crate::math::{scaled_font_size, FrameFragment, LayoutMath, MathContext}; use crate::syntax::Span; use crate::text::TextElem; use crate::visualize::{FixedStroke, Geometry, Stroke}; @@ -120,7 +118,7 @@ impl LayoutMath for Packed { let mut body = body.into_frame(); let body_size = body.size(); let span = self.span(); - let length = self.length(styles).resolve(styles); + let length = self.length(styles).at(scaled_font_size(ctx, styles)); let stroke = self.stroke(styles).unwrap_or(FixedStroke { paint: TextElem::fill_in(styles).as_decoration(), diff --git a/crates/typst/src/math/ctx.rs b/crates/typst/src/math/ctx.rs index 6da16406ed..35eb665c6b 100644 --- a/crates/typst/src/math/ctx.rs +++ b/crates/typst/src/math/ctx.rs @@ -11,7 +11,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{Content, Packed, Resolve, StyleChain, StyleVec}; +use crate::foundations::{Content, Packed, StyleChain, StyleVec}; use crate::introspection::{SplitLocator, TagElem}; use crate::layout::{ layout_frame, Abs, Axes, BoxElem, Em, Frame, HElem, PlaceElem, Region, Size, Spacing, @@ -220,7 +220,7 @@ impl MathContext<'_, '_, '_> { if let Spacing::Rel(rel) = elem.amount() { if rel.rel.is_zero() { self.push(MathFragment::Spacing( - rel.abs.resolve(styles), + rel.abs.at(scaled_font_size(self, styles)), elem.weak(styles), )); } diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs index 6a3012ba09..15ada1f360 100644 --- a/crates/typst/src/math/matrix.rs +++ b/crates/typst/src/math/matrix.rs @@ -63,7 +63,6 @@ pub struct VecElem { /// #set math.vec(gap: 1em) /// $ vec(1, 2) $ /// ``` - #[resolve] #[default(DEFAULT_ROW_GAP.into())] pub gap: Rel, @@ -81,7 +80,7 @@ impl LayoutMath for Packed { styles, self.children(), self.align(styles), - self.gap(styles), + self.gap(styles).at(scaled_font_size(ctx, styles)), LeftRightAlternator::Right, )?; @@ -182,7 +181,6 @@ pub struct MatElem { /// #set math.mat(row-gap: 1em) /// $ mat(1, 2; 3, 4) $ /// ``` - #[resolve] #[parse( let gap = args.named("gap")?; args.named("row-gap")?.or(gap) @@ -196,7 +194,6 @@ pub struct MatElem { /// #set math.mat(column-gap: 1em) /// $ mat(1, 2; 3, 4) $ /// ``` - #[resolve] #[parse(args.named("column-gap")?.or(gap))] #[default(DEFAULT_COL_GAP.into())] pub column_gap: Rel, @@ -268,6 +265,9 @@ impl LayoutMath for Packed { } } + let font_size = scaled_font_size(ctx, styles); + let column_gap = self.column_gap(styles).at(font_size); + let row_gap = self.row_gap(styles).at(font_size); let delim = self.delim(styles); let frame = layout_mat_body( ctx, @@ -275,7 +275,7 @@ impl LayoutMath for Packed { rows, self.align(styles), augment, - Axes::new(self.column_gap(styles), self.row_gap(styles)), + Axes::new(column_gap, row_gap), self.span(), )?; @@ -322,7 +322,6 @@ pub struct CasesElem { /// #set math.cases(gap: 1em) /// $ x = cases(1, 2) $ /// ``` - #[resolve] #[default(DEFAULT_ROW_GAP.into())] pub gap: Rel, @@ -340,7 +339,7 @@ impl LayoutMath for Packed { styles, self.children(), FixedAlignment::Start, - self.gap(styles), + self.gap(styles).at(scaled_font_size(ctx, styles)), LeftRightAlternator::None, )?; diff --git a/crates/typst/src/math/stretch.rs b/crates/typst/src/math/stretch.rs index e411da9306..18a42b18c2 100644 --- a/crates/typst/src/math/stretch.rs +++ b/crates/typst/src/math/stretch.rs @@ -2,10 +2,11 @@ use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::LazyArray16; use crate::diag::SourceResult; -use crate::foundations::{elem, Content, Packed, Resolve, Smart, StyleChain}; +use crate::foundations::{elem, Content, Packed, Smart, StyleChain}; use crate::layout::{Abs, Axis, Frame, Length, Point, Rel, Size, VAlignment}; use crate::math::{ - GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, VariantFragment, + scaled_font_size, GlyphFragment, LayoutMath, MathContext, MathFragment, Scaled, + VariantFragment, }; use crate::utils::Get; @@ -91,7 +92,7 @@ pub(super) fn stretch_fragment( glyph, stretch .unwrap_or(Rel::one()) - .resolve(styles) + .at(scaled_font_size(ctx, styles)) .relative_to(relative_to_size), short_fall, axis, diff --git a/tests/ref/math-accent-sized-script.png b/tests/ref/math-accent-sized-script.png new file mode 100644 index 0000000000000000000000000000000000000000..cf468dd15020993777fd6a668c73f8c5436b3e35 GIT binary patch literal 331 zcmV-R0kr;!P);GjN z1|q2a|Nh_nm*y6K&#^f2f1TYagvig}|NpnpA#pWK`~Uy`e&i_b`u}e`g8PqF7CT(~ z|9{yJ2>Z_RGpjDVs(-iQ4^;Bl#!U}tWbqGqATbxpS^iDu#)khdPryTc!joGoXk_t` zkpKUG&4C5$@hVh>h5s+Up^?QWvi|>{{sd^}tN;H$J!skT=qXg<^Vk3XA5Q+imsW}E zU+J`&pd@|Q>;L~V)qVYoVG^rq|Ns9z?Z8bMS&T@wXV4X$p-%zw2i=}W&qu4pQHw_{ d9<>+?007V|ciXq)rIP>v002ovPDHLkV1kugtPlVI literal 0 HcmV?d00001 diff --git a/tests/ref/math-spacing-script.png b/tests/ref/math-spacing-script.png new file mode 100644 index 0000000000000000000000000000000000000000..3ded962292a0956eac26996796f8b206e6801f12 GIT binary patch literal 346 zcmV-g0j2(lP)aTK4am&zyhEc}n(;;l!UUqcw{ s-cX_B+DC+v(Mn>};!%r7Ev6R$02i=qVo*RE5dZ)H07*qoM6N<$f|?|zs{jB1 literal 0 HcmV?d00001 diff --git a/tests/suite/math/accent.typ b/tests/suite/math/accent.typ index 9f57d69b07..87ed815868 100644 --- a/tests/suite/math/accent.typ +++ b/tests/suite/math/accent.typ @@ -31,3 +31,7 @@ $ tilde(integral), tilde(integral)_a^b, tilde(integral_a^b) $ --- math-accent-sized --- // Test accent size. $tilde(sum), tilde(sum, size: #50%), accent(H, hat, size: #200%)$ + +--- math-accent-sized-script --- +// Test accent size in script size. +$tilde(U, size: #1.1em), x^tilde(U, size: #1.1em), sscript(tilde(U, size: #1.1em))$ diff --git a/tests/suite/math/spacing.typ b/tests/suite/math/spacing.typ index 707c09bb81..db8b905c63 100644 --- a/tests/suite/math/spacing.typ +++ b/tests/suite/math/spacing.typ @@ -49,6 +49,10 @@ $integral f(x) thin dif x$, // Both are weak, collide $integral f(x) #h(0.166em, weak: true)dif x$ +--- math-spacing-script --- +// Test spacing in script size +$x^(a #h(1em) b) + x^x^(a #h(1em) b) + sscript(a #h(1em) b)$ + --- math-spacing-ignorant --- // Test spacing with ignorant elements $#metadata(none) "text"$ \ From 92aacdb480286a8a1962a49843e0c271f6ef93a8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 10 Oct 2024 17:51:48 +0200 Subject: [PATCH 016/280] Fix scales for unit conversion (#5169) --- crates/typst-ide/src/jump.rs | 2 +- crates/typst-render/src/text.rs | 13 +++++----- crates/typst-svg/src/text.rs | 20 ++++++++------- crates/typst/src/layout/abs.rs | 25 ++++++++++-------- crates/typst/src/text/font/color.rs | 5 ++-- tests/ref/gradient-presets.png | Bin 17311 -> 17315 bytes tests/suite/layout/length.typ | 38 +++++++++++++++------------- 7 files changed, 54 insertions(+), 49 deletions(-) diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index b798defabd..e48db98653 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -205,7 +205,7 @@ mod tests { macro_rules! assert_approx_eq { ($l:expr, $r:expr) => { - assert!(($l.to_raw() - $r.to_raw()).abs() < 0.1, "{:?} ≉ {:?}", $l, $r); + assert!(($l - $r).abs() < Abs::pt(0.1), "{:?} ≉ {:?}", $l, $r); }; } diff --git a/crates/typst-render/src/text.rs b/crates/typst-render/src/text.rs index 70d5164269..1685e67dc3 100644 --- a/crates/typst-render/src/text.rs +++ b/crates/typst-render/src/text.rs @@ -13,27 +13,26 @@ use crate::{shape, AbsExt, State}; /// Render a text run into the canvas. pub fn render_text(canvas: &mut sk::Pixmap, state: State, text: &TextItem) { - let mut x = 0.0; + let mut x = Abs::zero(); for glyph in &text.glyphs { let id = GlyphId(glyph.id); - let offset = x + glyph.x_offset.at(text.size).to_f32(); + let offset = x + glyph.x_offset.at(text.size); if should_outline(&text.font, glyph) { - let state = - state.pre_translate(Point::new(Abs::raw(offset as _), Abs::raw(0.0))); + let state = state.pre_translate(Point::with_x(offset)); render_outline_glyph(canvas, state, text, id); } else { let upem = text.font.units_per_em(); - let text_scale = Abs::raw(text.size.to_raw() / upem); + let text_scale = text.size / upem; let state = state - .pre_translate(Point::new(Abs::raw(offset as _), -text.size)) + .pre_translate(Point::new(offset, -text.size)) .pre_scale(Axes::new(text_scale, text_scale)); let (glyph_frame, _) = glyph_frame(&text.font, glyph.id); crate::render_frame(canvas, state, &glyph_frame); } - x += glyph.x_advance.at(text.size).to_f32(); + x += glyph.x_advance.at(text.size); } } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 6af933988a..4a99b4b8e0 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -55,15 +55,15 @@ impl SVGRenderer { scale: f64, ) -> Option<()> { let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?; - let upem = Abs::raw(text.font.units_per_em()); - let origin_ascender = text.font.metrics().ascender.at(upem).to_pt(); + let upem = text.font.units_per_em(); + let origin_ascender = text.font.metrics().ascender.at(Abs::pt(upem)); let glyph_hash = hash128(&(&text.font, id)); let id = self.glyphs.insert_with(glyph_hash, || RenderedGlyph::Image { url: data_url, - width: upem.to_pt(), - height: upem.to_pt(), - ts: Transform::translate(Abs::zero(), Abs::pt(-origin_ascender)) + width: upem, + height: upem, + ts: Transform::translate(Abs::zero(), -origin_ascender) .post_concat(Transform::scale(Ratio::new(scale), Ratio::new(-scale))), }); @@ -260,9 +260,10 @@ fn convert_svg_glyph_to_base64_url(font: &Font, id: GlyphId) -> Option Option bool { - self.0 + EPS >= other.0 + self.0 + AbsUnit::EPS >= other.0 } /// Compares two absolute lengths for whether they are approximately equal. pub fn approx_eq(self, other: Self) -> bool { - self == other || (self - other).to_raw().abs() < EPS + self == other || (self - other).to_raw().abs() < AbsUnit::EPS } /// Whether the size is close to zero or negative. pub fn approx_empty(self) -> bool { - self.to_raw() <= EPS + self.to_raw() <= AbsUnit::EPS } /// Returns a number that represent the sign of this length @@ -254,13 +251,19 @@ pub enum AbsUnit { } impl AbsUnit { + /// The epsilon for approximate length comparisons. + const EPS: f64 = 1e-4; + /// How many raw units correspond to a value of `1.0` in this unit. - fn raw_scale(self) -> f64 { + const fn raw_scale(self) -> f64 { + // We choose a raw scale which has an integer conversion value to all + // four units of interest, so that whole numbers in all units can be + // represented accurately. match self { - AbsUnit::Pt => 1.0, - AbsUnit::Mm => 2.83465, - AbsUnit::Cm => 28.3465, - AbsUnit::In => 72.0, + AbsUnit::Pt => 127.0, + AbsUnit::Mm => 360.0, + AbsUnit::Cm => 3600.0, + AbsUnit::In => 9144.0, } } } diff --git a/crates/typst/src/text/font/color.rs b/crates/typst/src/text/font/color.rs index 0e6b0c1fa6..e26d323cfd 100644 --- a/crates/typst/src/text/font/color.rs +++ b/crates/typst/src/text/font/color.rs @@ -186,9 +186,8 @@ fn draw_colr_glyph( ) .ok()?; - let y_shift = Abs::raw(upem.to_raw() - y_max); - - let position = Point::new(Abs::raw(x_min), y_shift); + let y_shift = Abs::pt(upem.to_pt() - y_max); + let position = Point::new(Abs::pt(x_min), y_shift); let size = Size::new(Abs::pt(width), Abs::pt(height)); frame.push(position, FrameItem::Image(image, size, Span::detached())); diff --git a/tests/ref/gradient-presets.png b/tests/ref/gradient-presets.png index de4968944997368e7b55afe17c5b9537d4363ec5..7fdb235c2f95b5ef12e5e9941654ae99ef08aec1 100644 GIT binary patch literal 17315 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 literal 17311 zcmZ^pQ*b6sxUIiv^2Nr)wr$&!Ol;eBGO=yjwrzW2+jes1-*u|?)xPS!>bGxt)w5Qw zFa(B3x;B>4bSUM2|9zh z#V3(AXfHSn;V!pl7Hf(uPny>iN}?zkNQ1lzI+i~{gc^ZI^-GB+Pm$I_LX_00JeF#B z^|#>i%Pgm|+`PN`{vWOcr$8wf^BWm+(@ykRWl7aP?Kq1@N~Dr0B(#{YWhCc<1Js0u zrIly#yR5QvxmXY<0MSHzNtQ}8wYl1_>!2hW2T^#->Y;eT(6Y|dX-!9WarwT4fi_^Q zprK+Zbx^BP7$g8fR?G{uGfV=JkZb3z|5z{|!(7KG+>QuV97iRYtR6K^8lx3fh=v0a zwc2a&5#sp8IFOZa>()x{&Zy?tMt5yKvs<17J+kTj%JI2%IeW%G8qI;vPV2V)a=e=c z8-T!;`Q#J6#U2;w+WQk>+sI?V`r)HV6L?rCVT>Dw8(jvaC{q*;?JSx>p?nC7mkN;1hxCx_e@UlNnT`O6U$0<5^uD z@VIyT+*$?zL|10a8mf7=TH(k99diSKMx)3isT9S(2*Gl50UsW;GAz76Fl}*6QUy>H zx_h}U3#3AxgTESwI7?YNAR{4x6A*%eI!J7Hs${5_ z`KnH8$2jIukf)#Xqh5TbvC3PnBa|h#dJrJn62g{0eKGadD?;}gSdfrfD1%6c6U(I@ z#7$Cl{m|zC0x;bGMQ8nPYg3|3dO~3kv%p=w{#I~-1!o99hd1xN4q&uaX5Nk}M<*g0 zw-1QEbCa6M%RE5qTJG95OceaS@EYEdbtXbpdm^emLYpfsI=#XdCV|EYSB@T>Ayy7$ zmtsL>#fzFh;cwE`PO`Ncd*V(-VR-% zZX_Xnt_^?kW|%K4jt?l6(Q8`xzPxgZedfmS8lpeAbMW^)&5?xBp5kw|cq+$bLJ?M~ z?%Aya0(Iw6;7zZ=X^iCPe3ONoE${r(UDy!@xH5+FSxUDJ?C%g0+*yC03t1d5uw)(z zaARHKT%jf3pZct27LK!-`E#VuVN4?VLfpkGnah)u>7h^PER?aL6d)An6u`U{OUS98 z*h@8R7Ue%gNyEuXskooRqzTPuA5R+e9|XCuR9Gr0{O}INQQ)YtmgT_&k4W`+fMACL z6s7tU1&PQ)!og0cjHqJvgb39G8O56k%sQsUC9qmAC6W1so3ZdSLcxgrYVUMHlU;ef zKZT@(M-Y)!prwSPE|6mmli|M`eP7hhDGfEy**sP zVz*h}<3g^y-p1bFo~e&Gl2cN+4TrDsyzIn0#JFT4&8mBDho$K|CSNw>R3~2zhqvvI zBPB6pAKeT@Y7P(?P;ZSN= z0StH~g{M1)3>-Z>(}YJh&Yw(N+iMLrr+fGu&M#(CrgZD|7n;5Cr_;XICgph<=>QWd zHh;Svb&ZXuj&-L~NZdEyrgVLIa}+)HUZ7HOIK5iCtFJEEw>iAuej5*+7D=OnsZNMj%R)tr`f1 z3z9T3IhrdF2lEHW7~M;V#vzF@YWAOCuh3eUn_nz^^Q4xBZ{(3hJ(esqytQ_n0;2J> z)HR!W7J`deRyWTFnNqd-|63 z9qmhX<8Z^cYHxdZqtkR zv`Wi6?H{={m!};@Oknx05h78zwbhKp<&BL=pm;*?#SIM?rn5SN;qbYvUTRq@RQG*D zSI)Ve^I?yP`chzW?-pIpQ+h`24aBZOIz#1<~_6&NN1dZ95Ds#tFY)@!Ns-2HF$9p+4Wd?$KxX)DZxIjt?h{ z1u8k->ZGJ4kr@n7y|cRo%=HccSS%(gV&;*QL@tAe!EP|IXfEHoS)>H?sqfmRA|wG1 z6G6e}KENAX`i0X_pV3a}xfkVThYP-Zn%&)K@N{c?gUSAttmboZkz1F|@!sJ%SRX(f z=vTG=Y_(k%B7osWHo-*7F~H5dZEax@YE@QKk@ftDG0Qq!S}p;Qdb)hG*_IRQ`zecB zj|2Rnxo(ODTV5eYqs%e7DE|-Esr!AAm5bO&zN11+tD*OL6 z=|oX2@jsaG!5S4tNl|km`=j>o^ar9{bVV3iHqgYM3Rx6bMuQnF1sdm0_KV6ttBimH zsuP^f%I)VL92JN^Y?ek z^hpZX(N!WUnejjdGw|rMjcZaK8}R`A6}S!A#Bgzf!jy9J9D#u!i~6?(4ukB)3(qwp zNdb&?2wy(jboc?+eYdTTP?^50M3NVTk?wPh&TWpP5uF{}0QsY-womk<$J7aXn^dFL z;wU+dmGvO39eaV|(>cdFW(;?|_`~(m(z`@u^Lpi6^@mO((nd+8Hj(c`5>JB=Kc8sp4>Bl{UDrM9 ziG!vb$P}7nHr|B~NgT(|E#71RXMyO_B*s zsU_?QzjqFr0zsLX_(HA)P%5;r7SmBzY&K@EE>u8uO`x>;jd**J)BZuSq=19-Nh-C} zLtqXJSa2eUizV~np_!xtk|6mpDAiRi!V8W9_m;Wryy_*yFHPnr+}9r9|JFx`FZ0C4 z3Wm8G;Xii6a^7Fh_IZ511Eq%&hZl0|UFX1kmSGp1?X6Ic5+12dt-u39@WRJMWM-k} zSiTpv*m|C6;NN!rZd8wVe6OlCzO6C|mrdORmVlG&D8d^IjHX;-S`?atOdy3Daj0T6 z&!}Ch}_*O;RgK`;cy!1V#BnAv&3b3Ir|ZL%8+#85!Q6 zk|}e|WPsA8?r^iJ#ji11n$e$7w(zDQkU*jT)Ms8D_EfX*o9r?1it*5h3N_= zW<+5f`Nadp|EGRI9;|TI6G6O^0~;PT-3p$OyeYn`&`jZPzJ17xV zg)uFstK;jC%59&^?IJF0=&Z2yYn_Q~iIs4n?M7l^hGTl~ryF!YGJq|~;5R+l4V^=~LD^_tD9rpBrJhuqb!}LVN~}>br-4O6_WsXe-1c zsWiI34#@Gg$Zq(4M|zU#106tOM9(^gsE{J+l^%9%*7OXAEV~&#>vtPxY`?6t>X@RL zYE*K0WU-@$$q3qj``1L%vmUrJ=>A2jzAD)!%X5o>^dEYf*3!xXI$B#F_q0Lg&enI1 z6&zGb<-NJhP_8|GbWjS)mD{0F6}ioOu#oKgH&}L9g&x54`eW}RqhuOLe{RrxYcgjZL|z|lbN>f{>5BgkCy9YiWC#TJ{y=fXG{ z^4m(-t8%60vvMFXbmwy+L^A+2m%-g)H0EcKc z4}VFnT%)O4d=OQz31{~At&Ey@1}U!A!z#lY0q#GBRdJ%Ek#ce-$6B5NNs|=2XCqRh zphp(W?GymdzUj`zv@zra z5(_LWbrnf8W7*i>Ix3u!hVkP@OA1t?Jr1Z{*kS|W!H>TDG(x5HQDnvb`*YD|0cIh3 zrC^aGKPe!$;K^K2Sr@~v1>Nj)6d-$N{c#ZiTUNWU!g@s{KL`NCh*02CG&prA3Yjos zsWa(*K#I^P6q-o(#o=3iCAGzj9iJP{ylV#izasAmIrEuB02<#|y8)Q~%j*Ut_BkKb z+B$@|$UX);lDK^Au~1WqmJEq;ig+JF%2t$R)W!h=i3^C@M?gf78gRs@mZYJJUqM2W zO-$+6$Btsu2Xp?pF$iXIAsEk-;PI+$)V@n;ZmfrIgN6YcF~3 zwgmXrpVLMxxGT@zKmqW0*(9^OpMs)&t-h71F>Lz00@E*eC%`yoC%|TsgS5rbPh`q>kW@rhZ@i16V3BfkS_jogQViJVFT5bMrkaqBD zPh3tM{4709ET`x?3zF1N;t!k|XElBr%o4&D7+4v4=Cd!ai@8$t@AnmUN%45AE)bEx zq@pv6Vrph`vIaJVoI@G=8jl*xPMlk4n-bKTmgU^#aSTdS%1eTj7+W0O3u-ZAS zZMXlSi8I#LdolFZzm5_nESaKvuE+~pXwhZEY4vIs6Oam5haD(n7KM~YqKGW6cWeeP zpx+<5(1!ui9x$^XS|wX%mN?Fw38pQSKa;G4xIWXz{c~4u56RF-`3g5@g}u8X35qxY zrYduGB7=*E^L>T)P3|(?x3qh=M0?ZxWu3T_-d|cqG=iNKJ00;E`TkyT|Kz%tH1 zm&&N`5#6ddkqLm_kyv1YNvX7O5U5z!=~7%ujb>(_-C-Y5fn?QI%sX4FJ38yR)}b+v zHTG!CbMMO|_7Mfa^kMnG(EHU0YZns}Vnk*D$jlB`6qy)~J}B?L=2Dj78)6F&7YWJ2x^;-=k|qfrQh>qOA5qowRdPB@XZLO{y6jk`J@ zIz>=MqU`^(gj;GO_&)4cT`RU9OAr2_Gk{P~*Uv@48eWIjULEzs`Q0H-d|sES4g{UK z9s)RlaQ35aX>o7+?X<{$Vpm?Hz8D7xq1E1;>FR8Q21mJ8qrN*UUI& zP|JZG_t&^su0F@m6(4Sw!ZF@kM=5fuY;Rype#y-{TVN$RUcQH>5w7LsPt%>UkTI&Z zbd=w2uX2Enm`oqfTN8jS{W_EDe)c=R&R{SdMHV#VM-tPABmZvI+8=uqiPdIWwq~y=PBrPdgea-3D z$=Mpd;(bxis9Q;9l4pgk11RL@Emr(YhkpzJ7?EeUQ+K>C*Q2*X(KM6Uv((p9Xc@-J zLos4TG^OQ%h?x;-kVky=`|AGC2(G>~f-+}?!Xa(Cw$vx-EH*~^qRxhz0>k_zpN`dA zt_RomE7kmV7t^=GxBP_L*gZvifiBh3Ev8l&0CA1zPxotGivZSyc>2pied|^GhnKN- zPa9lr*DsgMs8L6Be_>3R8S{1J^8UCJV0!e#VjLh9Yx z)fBELCtqP?$k@Y09TYmf%=mgVQtUK0Mb`V&6Ke*%$`Ttf?8u4rLa>W$1inJ5{nX!X zfX8w129rL4pXM-psIywiC2!;H>@9h5G;VCxkt1hzyBiMa!F;|A3d|iqYrg6xzF#P2 z=@4DR04di=+n%JyV7^-UY+Fe zF^5CS@9FbS->cuM6(obAeWb?jlBD)V1HSjmVqTC zMw|(~Reet+M0&zg6T%;B<3rHT*|!R1O56#TnUo4KZD97l`M?a2nwX!GcP1tIc!ENv zIFVzcx$)1z0bN*UP)OL4@uTx2=>f^hT|5-w^k8Z$on)PQ7+%B1u#j4c5ukGuIU!?j zQpUmP1?Q*aQzVJF3l$m!z(Da2EAdo(w#CF$-O&L<3XFa#ce4D{Q9QFCg-qyzw09c| zVF;O%AW5@ZJX^dQw7^LZZKaOzy^hB1$O=0j%fp_*Ftcjres~C-m&s;8OQ#d7Os0J_ zV-NOwNYS$^_I859Vn!0fX<3Dh&RxltYRo96ikj2Z$r%3D1h`ye9fl6G&0dQ9WB2!3 zpdjx&8T|QsKiJk6F9?SN(>;DqkJ)m?7US_M40L_A8`q=1c%MC>^`-DH_O-Ov8Om`Z zw_ZPwWb+sThtD&_#7|I^Jt?GOpFz0R2%wyX9UQ+kwPeo2HPm@Ui;9g-eT~=mDMHSh z3OrQ_yt4Ph-<9;@=d4A`gC=3XZ`|syvs-%klx&Tsej#O*7#p6}(@SFjTTh+Qu_v_( zFPG;EcSM#s6m5w`3(qH#-+lx;?w6bWDYaws#%&<$5@fE+_o2Six2aPf9hQPz=P6I6Ls$LQPlZih z+qb9vzQ*dM>4bC)LK1M`XNB5N(Z`H8eel=1#%Eo@ z1=6SFbSX%!lXDD9D6P)e~|Zh6-IsaVYs#GsK|rGG@+-ZSL(A*AKd|g6#KM-G%bd(9DpcrEg~Ee96qe>+R3ly(etY% zdKiI1vzdI+l=q2oTI?1QZ0XT6$GhgzEvUiLAhjx`W*Ei|hJ+}Ouz30Q{DBqD)>j%Tt5fS@b*1^M{-DU%(1#lx$qt>b z@Q9JMh@5B*B5)2Z$?@-eO)5Bw1rk&dgVX|6GEMYPAG{`RJt6S8Q1sLn)tNXKS~T%* zEd50=>4!Dpk5GO>*f44`d+$YDgiq}L^sjYBuwiHRV2u@CCiU8zwxLs19;aU|`r7m# z-r@q=ABXD$856nZ3}bld{4dF~)xQvAq9)`8F-l za`S%(e4G%5B@BZ~^z5?%mK^ziX{N`U@bs6MXm~!kYP&UA?{KW&j4_zMo)A_YVNN4j zL3`Oj(a`yfo)b$GLT#CDx2PPehj zKJC=W{!(;U5lQ`>ocVGQ-BMb8%ae4Tfi$eU-g=LpbLnFbFFaB?Fh-wZCL0C7a!tAD zc-NQutwR7{>G__eUHRL<)YzV`r|JE;eQhdJ^c(6~+*)=BEwO^{5i;Yel5QEMJLb4)XNBoHt)u|5yHhVX1jaKM>(2Sh0X&CbX9^`=2+DzQF-E zDOcAwOr*0*OedyMg0scK+Z78}KoSl!pM=gBy~U7sYT&(y%0~jLlMoh?;>@yt`&l%} zU_#8y#(M9F_|L!%%)!qfH3Wgp05Rrc*`x57j!Z^U;}JY{5ZJ~3_t$B(md2&;v`!tD zXnOdp{x2^Z8H0#WEGG%%H~8FnB&9?hi92uvF}UqwQXirqRAeV?Q_PAj6VBdKQpn*L zuENb|rHAfqs^cC1?nMi@VCnV6?0MDYzN~z!t|Kyg*`KYn-AUD?OEyjBtrZi;Y&+KN zRa>HJvKu)@tjcpbDh(n~Ww2&jg6i{>c)eT}O0Zi7iK6qG@z@#n?`7^Tzqd-NI&amo zd3CsMzYSQ+_T?#g5Xn5xX@5rfFEO*LT+|4GYtPK^cbyx>oO&Y(4!7cUeY@=*LGn|& zyU)$7o;PoQPTz7`4!vLk()r!qxzsJ6}Wn&>mOD5f&~$ zkuzq)-f4uEecCKf5_K4E4~Mh()Hc_4T3TsB2|Uf@UXKbo__op=b-I@<#Z&oQY63SEtc(l-I%UY>q!ku%geI3ZMEY0~4b2~%! zI7~-B{25&A1P2Jyoi}|Y;P$DET{RQ^**ooe_Ud;0?)R1wpzAEeBx6*!Pv#IG=z|s^ zU~l{(w-h|!&~nAS(SGjd=)@18S*~JbF`ueh+!X+?lVpnPP90Id@Atc26|g5Vowj99n0UyG@tK4+^!kfG6m1js7t^>fm0 z;$^1$ZZ}pV$Q&DHU8*K)ZO+!ZB^c@<7$D&qhBVS>S3rX9q}GKoe7s_qg&-idsB&w8 zR%D?+ok!}sBWFD-PXScC>%C`5d4pxHo4yvZ0Dou|jnu$ua21?$>(>kMAQC)SR9U#< z4dz3fK@_^)1F>SY({34lrGr80swZ+(rz#Mwu!8M$(X|!Vxo^^G&*2ciGnHsp*e-s8 z00vJ3p=6~%=w~mgE1&h!SCwmc zq{bqM7sqg5dKnDKty;?yHG7`VhCf&13TOdV{Y@mKEht@RWRQsnIXaURNuW%At1-0Y zbQQ(~RgYilJ@1i%svq^}vE3dw*wILBvcX}CWL2LNMEPYr@=n^2wV!FWPu;b>E8GLf z*U-P&ty7;^l#QbEFK?7I=B^C!pfrrCIZUoVt^NqkMptG8v`z5?sBgMQvTD($O;3)FB4{=q$br% z`?MyQmwO{_ZxBHOCvE+BNP0i`fC$tC<39V<`6CeIDo@nChHnRN2_MiD3bi6L~Cm~KIpQ#uvfayrL9(8=P-+a7)=z>M(d#To;MERQT!*2NpbUz6Z0f&<(67$0Kg1)15=&$_Rlo<)YZ}3C% zVwEzv-wC(Gg|(B_hm%-OMjZSrz4wjZnr!yx62*c=a}Io_72<`}3JUelg{7PH)U%Xv z9Zf-&AgDo!%M<=1b#FQ8`VfV#i*Dc2N9ROBHJ`h|1$tywAc2}osm-z`ZiPNK`w!Pj zLLPrAY0nkucNHXUAT~?ahBWmUU{Dgi1m*aWiHnv6nS3xo8V*U^FTnMB zu)+MVxa6y*_JrxF=;B}yOy4H0p`l+)y%mKoKoK@0FMXOr&QbM3nAqBG>LHHK>0xwh zPX`zz3gx5}5{r*b>*rwvyhQ${V@;EmCq{=6i7P5vC^Sk>MJ*2`Ig<`D0})7IHrJq& zl_0I-+mmES94?tp)Y`jo3a{<#Zl%8Jd8(n-ZGXGXaCkqG_dZDvN#@Oo&G^GGloHc< zmk<(JxYuDU^Y!A7t_p@?@PJ&od_0+FfRQ1qO_suw9CeQ?W}JpgGapg@PgNM9$*kkRik*L9}TWf z<~!@M8Y2ivsnsT62*$>MQ#6aH^6qnl@9n+=klfXwH{=Pk{rVX(?e@GwJrg21BORRS zx{ap${dAahO}qIzy+>cne|nz7ezB?>vI6Ic{`uOT<=XAzFnu;&!7C{sP{j zxZw8Dx8memd%C{#g-g1F3FP#?#EqiwE>=;oXlk1B{l@;StkJ{8aJ|qwV%yAPPj2no zr?9fFU->OZANt%3`Xcm%D zj-*4`e|->numH?uj)aihs%nyLWxO-j+;{D_6WediMa@tETO&JZyM1{CFW54puAVKMmcC-P zf5+hDny>am)`CY!P#lWl^{&Mvw|m!Zjywg81*gGuXme2HG2rUA>+&DH%;P3yJb_WX zzNg#9iytEFn+6=InfJP)@+_F1C}&QBbp`ReRmoh|p>E{@ayusLNz@ z!Ea;BqgeN`?Av>A134kB`%=Gh--6oXt-2Pqf1lkG<+z#{j7~>Kb~&aQrtBn(V?#?f ziSyMmq%@Xn)!ZkpU%${zSVa zbI$KUQ_=io2#d?-ae*6moMDL#;>EGQ)c65gMGH)bvYp<3I{H)C$s)Kj-o4g~8WqD~ zKc>sadpF*hx?z+(H;$03Ah|~o$=7Xh)VbEuqX%nd4Z5@4dU-&)!dQGJ`oV8|yz~`>)Ov;PgM0=%QMR|G4jy1(AEi zF|iEboR>|dH?!!;XLbp<{S;${2{gUa3r0dq(^wihmo+7^sEUQVxQligUb*qKFtD zDt|Wt7NXYfwEJG{ zAPPIF$L!DtVC_SBH{Sm7VFM|@9CMw*+imM&EUf8JX=vsHO;l=T(e)hB1S1vA=XqO8 zQi4mCA+ORdLO69Q0M5zCIHHlRto?l$l$4dErnCvJH&%b$iRbyfb!t3f0rC>R<~*SyrcesfVD_UxTUVT-Rvy>nvYflXm^&S84bJMBDJ^s zrD30aeY!q#&p|6$2cKFv#7U}ro^sxC|EC+LKY?Q)!_=6Gxu?3@2->aZz{0($?L&xA>G!GcUOOtS$s*Woy;FSoHYc+JWNCdE*QakMBZ({QrS@RT&EZKcKg6S0rt3`xOy;&;$z-|YA z3OL{NhRoHZ)=&Cv4o>&y`Jvat!{;Wd2RP`@zUS415nIIK>=D4uZb8d=6B)tcU&m`+ zwM5{BA9v&ozjp|QgZGc{=8-FU8haATA;s~es^Kit2!Y(Ge5fUp_vL%lRHBJ{5WNE9 zZ7nx-yy?m4bo}#2j0);p7u;N15>1vdblE#`E+}d!>Nd3G*ubQ~CH&d4^%^)TXIM&5 z4Xq`?x^i&yH~~~@T<9WYQLHHXu|&!CbfUwZzjG91YD;E(VX}t_YO~R+KMi0xe1;XF z5rrJ+I1(bm{GjlZ$9^KP!Voohj(%dnY*`K`g|L26khUU!tblLLW!_FRAfI9XM3|~2 zgYmkvO#L_e-*jP9!V*lvP{Ld0N)D_LXM7IuN^?6PNz#09y@))VkQ!x{PHLG}m)Ft9 z?;#2E+*FFRg%a}N#S9nX0pY`Vb($llpZhx;^XA?MFWdAAMq(olO3Y&2u}o7R8*SrF z=YF$u@Dul|^C8MBhSBHTHh}O;kA*dB>*Y#?Yc`E+42EH=b^ic?RKG-)tXJByx_JGg zs+ye3sOzf7Cj1F$n=U!H{n4CY#qp*o55kls3Xq_*37g9cK@39S9f8_v#~I`7b!_x} z7Hx{ZO;kV-?MnI_5k%O2(4JrGXhO;1BYUK2XcV%Q~V33bE$gy=5dmtJ6bPzuG&W*M#qyb89(wFyi9H$Q4G zdH9g`Sg9!3(aJ*=k)cK!vih2V&}Xa>8opd_&IgA_DPF@3DkdHNh$1b~eU_Ky7MX>C zOec07+9gW^uasSFUChhg>ne7=W~G6l9Hxp;WrbK257|t^K?I2(Pm;j|(PRc(5Pnm~ zmNn-Isx{&bA~^U6)AX~LjqqO#IA7>dvZ^aB&*N#jH7$+UMF3b3UT~5`uBzSKDA>@) zGfRP(Wnr+rqXluGKY}<*_@s;D{qCnQ#ZAcX=klcGWwn)E?AiU9KFz`9R^C2OmSM|F zXK%M(+g0aF<5n%++qKsc&nAy1j}J=vpBHkr%f68A&xhsBW;7NNO>k?uE_sqq&>eD3 zm;L^%gKD+tnWS5>dLXb20kU?6>e#_*vR1NMXdCd_L0=uu9o~ysXJqy|#8}sGKO(k* zcNkg`dBDL1pnLro0PrkCBt#FYz;ROJKMInm8xK{JD2Z~lyZe9(;EsRR8){6@TE9xYe%y4hu;3zngd&vTa_cG%L$A#bVd$j z3vPBS(eTe6Nr$HyEfW~@2?-0tfSpV<(Rhv;Z-9bGt7H!xBP}mIxhpYgX(cx7pZtV8*ALxY zl-R_?0pxPwo4$IcLj59$*JO6ma$DYyXNM3l=pr$ApEab?chjg0X)?ry`-6qp(LOn?o+yvAOemfiqfykw*}P{R40CKmgRG4D zZbvaPC^q~=74?D+l`jwg@jI-uGf#k>EA#6N2I2d2iWW_Eo6qN*uj?VkK}ves&&MYz zrKc|v^d2hOu7tR_26|F&i+&zyX}rHh55rRr55z23*1F@}jg5`0y57zYH%OYEp8A^W z?3T;=6drWk&lbB7i0%b1^F^6nI0Mnw65V$TWeeQTuVNwV?;aqk6J2L_!s-lBDhX-# z9LyySBU|ZNlpZe4pVX#fNgT7#xsqvV47GW64Q*}GxOO!ADMu!8<&j0XfZVbEey{f@ zBMXWpwT9jMSIV)mNBNv`n@ZJD)^K%)w8&m4*r8G^7XN)-BF00ZLuiIV23s?MiB%Zw zB!oBTKdu<5i_77xv^6@9F0Rsx!nU^0LlGz~*J_Xs=+XP>aoIZA`uh4AEjFZlTR~vL z=ugcyAU=!{$BBoT*0vUNPVKVMkJ^0OmP7epeAx453>ibYz7J&Y?d`!E8~qtYErLUz zo3Li4IvD>^#5 zPoL6h0obkW5A^kFGFY>~*}_bY$I+ox7BUk*E&_(xtdae0coV`!Z59s7iDcS*AA1~l z@u~{^&LlHLaQ!+K9o7+(g%jV#Nj(aeiri6tw(ug4Ws-m(0Rf38#()kI2HQwX4ujh(#%;3;Ph9CjuU~`JtBj*#0uphs zS2~AzU;kiE!$R56z;2(Qq@*K=T89no%TFPyy4uE#T0`utzR(ONccTkTJ*a3rR*P{F zfJwCu{6o`I{h7Ig|NZ|a8jeJwt15HH4rYqpi2 zu@qVKqsu6Tp`6a+D)Y0|i&bD)Hoxlf%8KmQ8|rI`@6pQ>P~BZ>5YNu3iVET zMU`gvhcv1((+NwR8nCReqk@{5mG*o@7?>p(`)SyegwRZ*HBLYPp8yfeB)J{k`cn>U zgA_MQByXu!xxN zvHk#f{V416!IUw;gv1K5N8aIp!(_y1)r-t!fgs4W2kIe;=>y4Ge*)tCU;{=0y*-S|3D$1BX`=cEyfiQON*mZ!jgdx*s^hDS*ZL; zlYt%p-plKF3$WkVdDYMdfh?E{FvD>o!ufPCMnY+w@q;huaV1pC8qKv8AKiaj``)BJ zHft9%qf8|nabJ9I*@*z{(qz8}FJA~_E(ShG9re*cyU5v?4ZNNzMP#BD(E}}aI(8dc zD?AGv*gPl0e%A9bayd4&wV)irsbTTIzg4TJw1iZq<21AB50|kbog+7f(Ns1Fe+V-X zrbv%Vao0zODIql3td1Yt`%8OW1RgxUhbJ#D9|QzyfppO_B;*}iggiZ|5B;c!3ZLHU zz@GQ>vA^_DmeLtIGW5=u`z}JEuzkJSSPs{?{g*`BI#yeMz8*Ye#j^1YM=O+G8JEl@ z((5>7^UJT=mnB*#dFZ;$4Frjj_D2|hw8px2Gp~^g7db%hL!&O3At^{11wuRG()hg7 z>AsJG%Jk8&Wm)#)V$7feZ?b!?su-c}&S@$<)Yba;(o!MZd$yN_|FvcS{-**V;v+R? z?GKZ%o5SGX-gwujXQbz63*)PZ_I50=F|*jH0*^m@v|9VV=7&{+L10LieZu{JF+7@g0vYbyRs^qVz}gwp`WjXxw*a)75HL6JO3(# z61?PytjdJNccEs$|LkpMT8O-GQ~2I_|2PXc&fE(=Rkvf+h7OZa+x+c(r!keL848lo zC)OBAlR6eSwk7bJLPV4Z4ze8@JzMQm01af-vHDeg{mv2dYJzI6vI@N%5qvoz&GYZilrSn>PkpMJUN?qGg(76nLNJIfALlR6OlNEy$3vFgr`>RBM@I94afFc)9aXSR-?0?^Fvc?I+ z)Y7CwZB>Gwsd0G;0u3vj^}e>Tk&Vs7jAv#h$n?t0#KP8MPrxSHgp4OD zn%=VdS7}E=O;(b>g*wIJ=G?@xVlTUEGXqFQM^jr4G5^K_RB%&NCBw15CGp68R2wXp zb!5o%uK7Kz8Vx*3*wYOci}iAFY8@!d1c&@}<||AvC#PT`I47rx7u$XB17%;e91@-l z_DHl2j|0b{6y{!od%qcPAXIPEV}yT@T!7NC><5k0teNdf=l=`s0uucdGtaG>HO5yk zBgFUMq6u7mjyBb^eE1G?hJ>v+lPT)$#R;gi{wXkWIr>enIu4dzi}vum@$|`=s!D=@ z1Qr&b1_qWstp@>F<+lNGeG2*U@;hL!!zJZwV@FYM30oQ&vNHQzX(*8FEp_8$5~{Ib2TS8d6>UD_^rJ(Odp z0>dcGpwomsJ?bzyic)~hVEEcuZ#jR7dWhl^p?Gn45RZIG(VVnXKzB>!YqN`~U=+tu z45#pxoL?J7o!YY^4bz-VES|KtX&O8FBvPe9Jv8QUTzR}nzJ8X5MkgvNG$ZXzO&5#^ z+CC}H)7naH+O7KiNoqX#I{Ub4EditJL*3$+L zxmo|LP9)NFwRU#>eEV);)kC^Q?yqeVkKdQ^!>?tF!-po%82ite4P~fOBwq+-Gi}@60n*VtCs1imy&x813ce zYtR4ZoI2z^xaGmK4t4fWTZTKj8mZd4SPTY2u997;eJoQd<9&S8VQeNt-A)Jc+R}&+ zj38{dM*ofsri@k1FKJ|irf?7@2VrO{S=XM`WLK6Y1%|JV7#Hv9zjQ=ea`Seu%*pPaN(Ir70OqA(x`!M ztHy*ydAe`8x2w92*S7+WCOQysGlEHFvBGOlUCuT=jh_ z?@HwIoO`!V{9X9cw^0|B_G`r+BiY81z*azatnBPxYAb##VoKVZ!@ zHq+PpuK2enZSBCTmNR!Ap020{yMP=A?0=u=N;J9>jqXBPN0yGPf2jNqHGk*(d`zV% P00000NkvXXu0mjf1kOpc diff --git a/tests/suite/layout/length.typ b/tests/suite/layout/length.typ index c40752f78c..5ba70072d4 100644 --- a/tests/suite/layout/length.typ +++ b/tests/suite/layout/length.typ @@ -9,24 +9,26 @@ --- length-to-unit --- // Test length unit conversions. -#test((500.934pt).pt(), 500.934) -#test((3.3453cm).cm(), 3.3453) -#test((4.3452mm).mm(), 4.3452) -#test((5.345in).inches(), 5.345) -#test((500.333666999pt).pt(), 500.333666999) -#test((3.5234354cm).cm(), 3.5234354) -#test((4.12345678mm).mm(), 4.12345678) -#test((5.333666999in).inches(), 5.333666999) -#test((4.123456789123456mm).mm(), 4.123456789123456) -#test((254cm).mm(), 2540.0) -#test(calc.round((254cm).inches(), digits: 2), 100.0) -#test((2540mm).cm(), 254.0) -#test(calc.round((2540mm).inches(), digits: 2), 100.0) -#test((100in).pt(), 7200.0) -#test(calc.round((100in).cm(), digits: 2), 254.0) -#test(calc.round((100in).mm(), digits: 2), 2540.0) -#test(5em.abs.cm(), 0.0) -#test((5em + 6in).abs.inches(), 6.0) +#let t(a, b) = assert(calc.abs(a - b) < 1e-6) + +#t((500.934pt).pt(), 500.934) +#t((3.3453cm).cm(), 3.3453) +#t((4.3452mm).mm(), 4.3452) +#t((5.345in).inches(), 5.345) +#t((500.333666999pt).pt(), 500.333666999) +#t((3.523435cm).cm(), 3.523435) +#t((4.12345678mm).mm(), 4.12345678) +#t((5.333666999in).inches(), 5.333666999) +#t((4.123456789123456mm).mm(), 4.123456789123456) +#t((254cm).mm(), 2540.0) +#t((254cm).inches(), 100.0) +#t((2540mm).cm(), 254.0) +#t((2540mm).inches(), 100.0) +#t((100in).pt(), 7200.0) +#t((100in).cm(), 254.0) +#t((100in).mm(), 2540.0) +#t(5em.abs.cm(), 0.0) +#t((5em + 6in).abs.inches(), 6.0) --- length-to-absolute --- // Test length `to-absolute` method. From b5b92e21e9ae345cd900602ddbc6e3a980f538f3 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 10 Oct 2024 15:52:55 +0000 Subject: [PATCH 017/280] Fix bug in `math.frac` layout code (#5177) --- crates/typst/src/math/frac.rs | 5 +++-- tests/ref/gradient-math-dir.png | Bin 2582 -> 2615 bytes tests/ref/gradient-math-frac.png | Bin 787 -> 733 bytes tests/ref/gradient-math-root.png | Bin 1569 -> 1550 bytes tests/ref/issue-3658-math-size.png | Bin 320 -> 322 bytes tests/ref/issue-3696-equation-rtl.png | Bin 660 -> 661 bytes tests/ref/issue-math-realize-show.png | Bin 1802 -> 1782 bytes tests/ref/math-accent-bounds.png | Bin 333 -> 327 bytes tests/ref/math-attach-high.png | Bin 1297 -> 1273 bytes tests/ref/math-attach-horizontal-align.png | Bin 1894 -> 1898 bytes tests/ref/math-attach-nested.png | Bin 957 -> 979 bytes tests/ref/math-attach-subscript-multiline.png | Bin 772 -> 764 bytes tests/ref/math-attach-to-group.png | Bin 649 -> 597 bytes tests/ref/math-binom-multiple.png | Bin 526 -> 538 bytes tests/ref/math-binom.png | Bin 323 -> 339 bytes tests/ref/math-cancel-angle-func.png | Bin 938 -> 915 bytes tests/ref/math-cancel-display.png | Bin 1315 -> 1291 bytes tests/ref/math-cancel-inline.png | Bin 621 -> 624 bytes tests/ref/math-cases.png | Bin 1238 -> 1281 bytes tests/ref/math-class-chars.png | Bin 1346 -> 1344 bytes tests/ref/math-dif.png | Bin 1049 -> 1039 bytes tests/ref/math-equation-numbering.png | Bin 4646 -> 4684 bytes tests/ref/math-frac-associativity.png | Bin 484 -> 474 bytes tests/ref/math-frac-baseline.png | Bin 594 -> 610 bytes tests/ref/math-frac-gap.png | Bin 0 -> 266 bytes tests/ref/math-frac-large.png | Bin 563 -> 573 bytes tests/ref/math-frac-paren-removal.png | Bin 506 -> 521 bytes tests/ref/math-frac-precedence.png | Bin 3877 -> 5504 bytes tests/ref/math-lr-call.png | Bin 847 -> 954 bytes tests/ref/math-lr-color.png | Bin 670 -> 672 bytes tests/ref/math-lr-fences.png | Bin 601 -> 602 bytes tests/ref/math-lr-half.png | Bin 396 -> 398 bytes tests/ref/math-lr-matching.png | Bin 1262 -> 1149 bytes tests/ref/math-lr-mid.png | Bin 1401 -> 1399 bytes tests/ref/math-lr-shorthands.png | Bin 452 -> 469 bytes tests/ref/math-lr-symbol-unmatched.png | Bin 349 -> 346 bytes tests/ref/math-lr-unbalanced.png | Bin 938 -> 950 bytes tests/ref/math-lr-unmatched.png | Bin 542 -> 540 bytes tests/ref/math-mat-baseline.png | Bin 802 -> 818 bytes tests/ref/math-mat-semicolon.png | Bin 1108 -> 1111 bytes tests/ref/math-nested-normal-layout.png | Bin 1313 -> 1312 bytes tests/ref/math-op-scripts-vs-limits.png | Bin 863 -> 894 bytes .../math-optical-size-frac-script-script.png | Bin 552 -> 532 bytes ...math-optical-size-prime-large-operator.png | Bin 685 -> 689 bytes tests/ref/math-primes-complex.png | Bin 1671 -> 1672 bytes tests/ref/math-primes-spaces.png | Bin 343 -> 343 bytes tests/ref/math-primes-with-superscript.png | Bin 956 -> 957 bytes tests/ref/math-root-large-body.png | Bin 1831 -> 1819 bytes tests/ref/math-root-large-index.png | Bin 638 -> 648 bytes tests/ref/math-size.png | Bin 729 -> 715 bytes tests/ref/math-table.png | Bin 784 -> 765 bytes tests/ref/math-underover-brackets.png | Bin 1029 -> 1027 bytes tests/suite/math/frac.typ | 4 ++++ tests/suite/visualize/gradient.typ | 4 ++-- 54 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 tests/ref/math-frac-gap.png diff --git a/crates/typst/src/math/frac.rs b/crates/typst/src/math/frac.rs index 3dd6960c8b..744a15c5e8 100644 --- a/crates/typst/src/math/frac.rs +++ b/crates/typst/src/math/frac.rs @@ -130,8 +130,9 @@ fn layout( )?; let around = FRAC_AROUND.at(font_size); - let num_gap = (shift_up - axis - num.descent()).max(num_min + thickness / 2.0); - let denom_gap = (shift_down + axis - denom.ascent()).max(denom_min + thickness / 2.0); + let num_gap = (shift_up - (axis + thickness / 2.0) - num.descent()).max(num_min); + let denom_gap = + (shift_down + (axis - thickness / 2.0) - denom.ascent()).max(denom_min); let line_width = num.width().max(denom.width()); let width = line_width + 2.0 * around; diff --git a/tests/ref/gradient-math-dir.png b/tests/ref/gradient-math-dir.png index 8a9954adb0abde98a6a2ca8df0fb34c0581ade05..8d33f51f52ebcc72ecacde55c082ae801b846962 100644 GIT binary patch delta 2606 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 jhvuO<9-4>dD*X>lw4@HpPQ}9j0000tXpm>3j7 z6N5%$e7Z3^wQyjoz|X*+Z}?|bLYJxX{)LTu8cg9$yq+{I0o|D0yd zOc$Ul(S->xG=@eP8bf1fgrPAs!q6D{zgreba*KqQ3=Hj~)PEtJPdfn6GgM;?L;L1Q zNVN!e89WNeIMIIbINblJaDZ^kipH$#A6l3wg!?kl-ppzslMSScfn=q3l_tvYZ~Q>8 z5`Ai>|LEYoR0`%R{R&Azi^M8@6V;bRvav@y;$gTEmA^br>XL0rwk=siveeN$R~=|d z3*Bzy22Sp>_J3pF3PX#=)5h99?WC=P!-t14wP2qArM0OYXwE39V>g_t;1by|(}dMj zDng6mhYn>Ow%+9;m8G7|le@ZD@>Bv%d0QcM{ANRZwU@X{$RS*mWJ1K9S#am(Xk?ZM zGj@F4DdHY|I@I4D#Py;H9#^I=MG1Ze+lIfe=u>_E9)AGT3WES(IC%~&4ClM;a_^e_ zLQtMBsL#G@fyu_pPxaR$N^rPjTkM)|9lMK_c+X}vBD2fmeAIx8=-3@QHE^DNr#9Hq zL{pCwc6uC_lRt+ZA4+)#talt1x?=|u1!&IJAv;Uo-m=p}PE}CLli7T?Vy@sTcnj)T z&DAJ0$A2hP^IXY!sz^1_F>$A>m3kM7ywj7=Ga_KnN=*SuEJS+3$%H4j7k zg>eX`>cO~Xovj(B#W}~&?Y1K8#N%)#E|TmvnGV${eHq>iE6tYR@qV<-3iQ568zlV;*q14H|0WIje`43>XtWr<*DAJug)opUl-8y|-F r!h$d~hDI0~Lt|)!p)oYV&{yp@?BgUqNS%Ex00000NkvXXu0mjftGiYP delta 763 zcmV?3*hQq!RA|57>day`e-1O>IVS)e ziW8UsLt|)!p)oXuMi?4HBMgn9|GT_K*Xs0LF2v9iwC=DEReu5iHy?I|z|a$Oecp?y z;sUPR?%G~gnu^l?gJ}&1N$^n4ObfM7)>3F$fp+z#AEQA|7r`yqk%VbDsE+zObD@J z*4QRR*r#QOFmcSryV8tZdZPbYhpRni1~8Vga^B06q4mPmuEwzje`_h*khLvYuS?^* zuxs3Lg@<$WE>2|nLXr9$0J^g#?2-KhjKK&|yc4`V!+#^IrCkG1T>1(m{I;zeTWy)j zFr=-E=9Xx6L;xeo{V?x_i*}sTlU$ADH_4YuQY^>I%R|o85uZ9LDSPv=?yU`oIfOmR zn;<2O;IrrB(k%cGtOo!__YsV!$@Q`*l*H1KC>MF9K)-3M4I|u`8UZytruVqM!;KEN zTHJP6vwuOQIO?H!BD3cTjg$%h6tvmX)69IWL_%Yu=}fi!*DxZ1C;%@2-&cXJhMtNN z#qiVdhn;E1b_C;veOOD&%@qAU9a_@^cY3G$N-2hS9SB2X=tJ<{3R9Kv?#H3i7Z`d{ zs8qQ|WO-cSj-Dtxq**h})?>oqrzlaSYEr4j7}wH=SnRdz2~+Jz0b* tA#v)!0}H~?7#d+{42_`?hQ`q7j~~2=CTxERGQ9u*002ovPDHLkV1h7WdY%9P diff --git a/tests/ref/gradient-math-root.png b/tests/ref/gradient-math-root.png index 4c2e4272e7569799eaec6728c6152f9ae2f3738a..defbe8996d4e22d8337696dfc795d683749e0826 100644 GIT binary patch delta 1546 zcmV+l2KD)&42}$t7k?`V0ssI2JRIBH000HmNkl3U{W)Y%d7n=}B zTyO;v5(vaSH~a#`kKhu-73&QMBq&`}T8c_rXw$t-6VfJW8mC*frjC=evE4Yf<2{)f z&v>@8yx3>zB%)hNt13z7(U(`RqRAQOv2`>an;_u65F!Ehg?~VFAbO1v?4I|BKy)BF z5FHSR4n#ljgvlD^GLMh2agY`v!a%s51YwAJX(X6T(E|PuSUvp-5!NDDh;T)AMKDtD zAx2s_S{Mm&(I{Mm8p%VPH&XDo-hJYYx1S`pL;*n790oCm*u@W8-dp@P$pQ*Nv-sdz z!L=WS$g&8Q9Dj}G-^lJ04l6BTWwl*LQSj93@jNwf zhln+~M+>_=*ZVPUf&jzqFkqadCjn@p`dw`^Brz{-2q)>4TeT?egsWxtN3XdSMSF%e z^cijh6ZTB)_%dG3?IuhBCai7tek|dr5zTr!mJJu!bbrd++PxM0A_e$i`j-7 zk+RL6N~xz*=`PH7=d`X&^>lVo+1B$$U)=fYLhY;ldaB5N*{{XZ9)KU#7Xb`jsP5g7 zTg6*PZTK4i+Ld2ybzoUrhs1!k(fRTM>r=%Xn(T580cf?muh0zc9GeQu6a8^_(+Ptz z{C~7pvt5zOdfRtbCMT`Y5#wyH3P7LtDtUYLiKgWdC)aHiy8$d$wB&&I`w?$9mJsLD zBXpv^N8|sZTH?=hwaq8ZWS*w-^!%88al$^@p=B~IV;DT6#m9}g0tM*-x+mJ zN^eaVdlS~-Dd(ulXZ|ZiR5URk-0KKFoPWI?Qv;Rpo^3s89va3jzNuop&d6LMH zM$kjA4O!5E|CbtBFtb6*A$6sHUIyd+JC$BMwljQ(HC?iBPnj(lIQwXvEK;lD< z--m=upfN=FsSw%yRD_}+q{tG&wG0VDI007_E>F`qhSUi`o~*Hozph^(OFfB9A%7Fi zE)>8|rJnLZ5LpP&NJUNvkOhsSWy&H5@cc`UJoCb%_u!_|%KOjdP)_LtNYI<@mG<#(0_f)^)8o|1#(d-y;+1T{sACk?8`WFE+pJy)&4x4HT9k2SW3By zvMxtMMMY&Et{dF=zbU5X#rlfnlzI77u{32s(iFXjc{=Mx%UBthFemtgj(_UvzUsJM zv^QI`$0}TRu>vqqjj-N=C~RciQS58m40a&U>-bPV|^H*ketsR?*yISdpZ8 z`k;3VKt-s(*7gDDvB!p->3`sz;k`r^^L;b|pdd;SZ@9}H1hBdK&m1>7L?HU!V8hzt zZfNt`j(F`6IuK_w{obWKpZ`|dH{e_q!7KV+QH4InFGbl{l*Lq``}d>MtdwSz5~~JV wNAOLhfIxI0Iv@}ohz>*t1fm1c0oU^{M|9#b%?O(500000NkvXXt^-0~f-tM+ApigX delta 1565 zcmV+&2IBdS4518=7k?}W0ssI2AsOpr000H(NklK~SJv#GL|dp#;dKNt!rrNz#)zBpr_VF-ePM4g22k^+$1Al&?IbS45MbS5>gYb2Q4 zb-@A>0l{=hogzu^q|~MCBy~z>smHw!-E!+4*Rb>W=Znr!*#>{)|M#;l8iQYc1Kon| zpXOiqVBKo?|9|6m|I!z_P2a@#(cD7uB&3bMfbXnP32Ks=e-cvUJ#q0?5ni3zZ&t95b?<T{(IDCP z6#jsG;qy!QIuCIO0B1z`tL6mO@kL?2!fk9A)26q+seks5ZE4Y^c{%Wm%uI?>6z7pE ze1;8khLQ<62agKXLe3OChRxXi=G0L$Sr!I>!;N&An`z6yh6&G@<1uqpbTraBHc_7p zRcuQFs1~hb1L?@2EO+GQ5H01}kT)*Ty|gd@pgs09d`Oy7w4^k$`45FO1Q`SY9|95N z>PSd!kk2p!0b?pxUkTgRc70Em-c^X1-4+B!Tkl-_N-D)_?8@PDdHkmf{j~hM1O(F66t~`Kvq}jhezYuR6>CMegR{)gKMzd;fV$sD@ zSRR$9`v6?J=#Eh9-61}Lb;O;ONptapox`7{P?dWI8hd`M=L$TrV8v!yvFYZ}4~0_J zG7S+OD=fz<3&+b__z}EqF5n``QLc@)I)Cd>he|%hR4X=?cV}iamzXFiO@Gy)(%EeL zt_N<~_2lGZFWgf&-C`TV>Rr@rJHSZSrKU-y5!-Py+i^QvrBUys2FAcN*6n^m>z+fb zrMJdQ?RN>*0sfqO8sQZ`{g?JH;T8C6;4koV^%DT=4m!?B_#F)1Qh42C`_;h<9)EXe z0N8~nt{Cne_`^@^a61V3Q_bsd-TuF8w=oYhZ<#Bt>+Zf@-yUE-XgG}M90^zbm;EQm zRyo+mblI5zxzjDW+kpU)cY;QpVX`xtS9GbnMRyD7ZU#tqM;DB74MNj6V{Q`Dz}(cq zbgu+D1p$TttxJ$wf^7AjpfeeS)PLR1NQdkWBMnkxQUft2ndX0FO(Gzn6X^t9x+&eR zQKx7mna7>?-EiY=S8eD}$!ziDZ1v{@EAGBgFI%OQ;g}-!Q>K_`03L*NU&g69srM6D zhkZEZjGUIEixf&=Y!2ZQSk9|W;}7@fM%fDQTb?~!nD`+T8|#{yA1khj27jb2%}hJ< z<&rXZIqy2o7flZ32QqGA^D!kRNBGDA=SL2{#eNb-62@8EDA{h$PgUGp_~^7WWqB~e zHW^Da--hvE@WCJphWXJg_5&K>$Xcdd&$K}(M5>TzrHppU;58eYj2cyDWemV55C3Ej z12AAt*e4_QQbts_(gRDoWPf4PBOukM z#9nh4z&FN`6<$`NFDJiu)f=4HvSpYBAzFp=e0jWe^9txCu!+Pa4Yv-%h zn^AG1g?7k3e#}XH-8wRDCDk`t-A5~wXeC;qL@Uusv_gqiq7_Q?-xkKNauzROw-oSQ P00000NkvXXu0mjfbU*ub diff --git a/tests/ref/issue-3658-math-size.png b/tests/ref/issue-3658-math-size.png index db8fccf9aa5307311b9bfa2c833aed715fc1a754..bd14f781ed6a14f4410bc7cdf037ed51cd070b83 100644 GIT binary patch delta 295 zcmV+?0oeY)0>T22B!3A>L_t(|+U?e{YQj(y#&I8@L;EN#ol0r@5M2xQAqpLY1RX>{ zK|#^r5~GVEE)FIrh@xO{au(4+)CekO5QG@#GYjV%a{k-p4nOX_a5*6#R7hZf1r8>x z(xP0j`lXOQV_dL8*O8#W2cri%@lY3-2X+&G8wl(;&JF@=ntxWpyzzL7dE@3Pr$gYK zSFK{MdYSPc@FJ{NAn?pI*Sv84`J41oWP<%Vts%pnzyecv{`yXOTQI@u?|}3Z=Ykcw z^B)v=pRIvTJTL_2flr0>76RM0J%PZMWpyF2YnIOtxZ<`_pr0!_!oYpYfxz95umOR+ t%jL=ow;vu!Paqv8*gw#YGVBR_UpEgw2;#hc*=+y-002ovPDHLkV1lMghqC|x delta 293 zcmV+=0owk;0>A>0B!34~?MLkN zY7jw1QPi!-;4B6eVNhJUQe5hYpR-X=uj{)O?pe(JzrYX9>cj|COksr;jwWpJUZr5` z-eldqNx>$ujHtkipCSgPR$5^hIQ;t>L*O6?rV!Y+?IZ-I&3~RCaMz1_5V#Xozd=*K zgb;Z1-gh8y(=d9naN_cTb@eX&OpVCFUIqiN3;78I_I2ISlN& r6Ir-+cFww}3m(C=;4Ss+DSTQ-RvifJq9@<_00000NkvXXu0mjf;9Q7g diff --git a/tests/ref/issue-3696-equation-rtl.png b/tests/ref/issue-3696-equation-rtl.png index 1c21a8d7c528addc0d2b91af30ba28d67d98bb54..1a4bbcb54d07d5192eb6aadff8970ede2cd5e5ed 100644 GIT binary patch delta 636 zcmV-?0)zdO1(gMmB!65*19M8}+@Vn^*&tBsYD*{eQPu*GSWA1M$wVrv9h@ zmgQ}@yA@dreI=Yl zS)rQqA7j&*|12ddJVVslHb%T6m1>{2K)t7AG*6w=U98SM2zau;Y4RTDs#MMWIqK~u z(z9x;%e#pu>wl72pG8rO0NSWLug0S$m2I7c;f2Epbm-6hZVV)VFPf<8TK z@eq}^LE?=kSixn0VHe;7m~#ZcF9Tkd{fOZhE94n2rGIenJ+V$e5r7UTplrma0UU=p zsWsba=+0GJ7f zT%>5W$ZP_DpjOI{0wh{$NMgOiKS_pIW90)+jQ1(XGlB!62;L_t(|+U?d~OH*M0$8q0BFCc=hgbL{*mQXh}!i%IJEuss< z=nulIR${K2qp(Cw*JwDI#XK6bY32wm6(>`dP17tUj*7XZ)lxUzIr}EG4JOlBScshO z)eoMV&&~7jWUwjAkXbTIX34BSV?DVNVXb}|9ucemG|5e#cYpmW)-}>JTt&Pytf=?- z|7Cd%4>lufw!84}S(gRj)kKM@B|NG#nPpsZ9|d4;dCjL{x$jW$b}$b}R1a_gC`Au7iy)m+JPiSk58*n3 z*VD72JEE>4R+L~F^?JPsfOC1hNyP&1e#}PVJq8tk4md{z*n(hn6z!6}bfNbQ^?@!u zYwj@RRwwbs60G0?z+wdO0q`9NNPw4x0RC`{74i%gk$>m>NUY diff --git a/tests/ref/issue-math-realize-show.png b/tests/ref/issue-math-realize-show.png index d6b727c1de8861ca1d2bb320167d7f186f460761..f4bde48639b132499077a47be69f329ab7aeb567 100644 GIT binary patch delta 1766 zcmVDKWy{oIM+1c5EfPi&%b?ofyiHV6#O-+A)fA8<_wY9aev9YD4rRnME!;X%Qh=_>H%*^}y{5m>1)z#Isw6x95&9k$!baZs2q<^H~;NZ^A&Ubl%zrVjs zOiY7=gOHGrnVFelVq#cWSjNW2jg5^S9v-5iqL!AHe0+RvZf={KoAUDV@bK_AH#cx_ za708z*Vosco}R+O!h(W=$H&Li)YOE8gwM~*c^!YHB4VCBDAC=I8BrczEjS>YtyV z?d|RJ^MCd}K0ZG`KP)UPCMG7pz`)(z-I$n|($dnBl9Fw0ZKI>3W@cvM;^O4wgywlUux3{-%Z*RD`xVpN!%F4=- zk&(#A$h*6{sHmve*x29S-~0Rfhlhvu_V(-R>-hNig@uLu{QRb-rrzG({{H@$Gt|fc z00Z_(L_t(|+U?inQ(IXa#_?~`2!YTBXpt6MoNm!lfw~vg-QC^Y-Q9h=m94u$Deks7 z1b>2t!2L6p0<#MlmN(70VTbdXIj^3X`^@~#J!kGkk|b%yuqLFQZeeX=&d;c7L2$+j zGujA4xDV=5Hf+3_Ew_s`9n?#KbOQgLlU-sLGDDAdvnD%71cD*QwsNiwIBW+nR1v6{_2`8SV12T&n?h zP*CJSlO#!+rWOy5YGN`WV`&#IXBiq*$7JS>rAAzCGgDA{5v6;ODd?gQ<>+x}ZY*wN zQp@}iWdK^gJW^KjU#ZAq19^7TF0MNWCk$&?$Yg@!5GCdic+OXNSbr#^ z9E{hiFoDD6lkWVBQGQcC+D}~ZEXJ{?F=n#Vf0@gQdpteod3qWE=mtX8L*JZpe1ZCL zX*>|1b6N3aUS4y(ymkWEe1K3p^v^yg64*A91_2}|0$~d0ugz)Hj)S`=Y~sYUFDb!A zTdcFRG&}9$rw-_4#0uwP>X(52D}P)`$Cx}Fo4yCozn77)z&VW=qIy3nb0otA>|WUSyIa zNz+k?wF0-;y3QAOsQnJNuooVy#VxiM;~ohH++z*yapDMW;WKamw`gd`-D<4CfdA6j zs7p!d=mJXTT#mYo*8xzjpfdBS03h<4N+aqrrUFpj$4n_+Ft-PAoPRup$^vX1K$mS$ zei=!UB>g|!eV5-#+@XcWI}xJjt%9mKP!M&9=&0xzJ&r(c02bE+e4+Qe10j0eE-d<_ zXN!fytvi128k(!z6u;09lv#-#p95g_L5wq>QC|03Gar1&9>}Y{IluLKp!nB9J;3qR zT2aL|%6DzIvF#&v!(Z8k8=)VR5{vh2z&{6XgjkuH68+Q89W9+-A7F33*+NiKYa75K zJ;3Sg?30M0)oO9Td|*#S9bho>FTGyhgJ_NW_REV)lBAjEZvfmvG*(r(F#rGn07*qo IM6N<$g4nUW@Bjb+ delta 1786 zcmV+9<3>geh1>FMd_ z=k4d`=jP_-<>lq$W+}qpR+1lRP+S=LK+1A+L*x1$@($Ue;(9qD&&(F@z z&dtru%*@Qo%gf2i(8|in$;rvc$jHUV&&S8d#>U3O#m&XV#l*zK!^6YE!otD9!N9=4 zy}!r5zrVh|zJI*E!M(k`yu7@-ySuu&y1BWzx468xxVX2sx3;#nwY9ahw6w9ayR);i zva+(Vv9YkQu&uARudlDIt*xxAtf{NBtE;Q3s;a4}si>%^r>Cc;rlzH(rKF^!p`)v# zqobjsrlO*vp`oFmprD_hpPrtcoSmVaot>MTo0^)MnSYs?n3$NCmzR{5o|cxDm6es0 zl$4W`lai8>kdc>>k&%#)kdKd#j*gCvjg5?qjEReri;Ihjii(MeiHL}ZhlhuRhK`1Y zhJ}TNgoK2FgNuWMgMxyBfPjF1e}8>`gnoX0eSLj=e0+O*dwP0$cX@$%d3kntfOvR# zb#--gbboYnb8~WXa&d8SaBy&MZ*OjHZf$LCY;0_3YI167YH4X{XlQ6>XJ=++W@Tk% zWMpJwVq#xmX<=buUSMcoU|?QeURzydTwGjQT3T6ISy)(DRaakES65b6R#Q_`PEc4- zP*6`#PfbltOiWBlN=ivdNk~XYMMqCZM@L3RMt?;`MMOkILqkJ8KuAD9KtDe}K0ZD= zIyyKwI5#&pG&D3aGBPnSF)b}EEG#T4Dk>%>CM6{$BO@arAt4|jARZnb9UUDQ7#PFW zt)c(`1P4h(K~#9!?brKLRAn5;@%PycmR%%U7X-5`HG&`^2tsTP3`ir)G;VQ=7Ba|P zAb%pY0?EJ@kqQV5G0ZN~MeHuo-KeNQt?Z&ek=e!iFG66(ogL&C_c@Eh^O|{n`plfq z%=10ZGv^>lk~Cx32YQcn5#3hvG^$z@_~<1zG=qltPSmCBT6r(aKW9gdC#ov2rnfTx zHSeLca8#8%%R2!QvSQQa`z}e6G+oWUzkjsU0;%Z%sLH>+Gyj#f>`G1g3RQVw53>+3 zNI?ziTF{*F2=NDaGcg2J)ti^@MDu*XA-e&0P*CJSlO#!+rdE}Vi)At?6KNDK*B&~~ zkIC$wNR7DMPNtwVBT7gKQ!wWtO8H{E%Bw8fnbcNaL}`U=?Yk)@=SoEu!%qro7k~S0 z0n6ow$zn2rWr&i$3>r36m(6D~r9qsp_@;WsX>ru7fFrHTcRKwA#x+j)=tgnHEjaMn zfdl8%cdN^aJ$!t2`}i0D2mw-7gLmW^{(z@L8U*B;T~>TsZEb&T?Ir+gCs6MN0mWy; z0PDumQh=&9ph@BUWjJlMcFPH;A%8li-gN^m8nL;(E8A&`|7xN69L0E^23-TLWM;xq zGfVFQU}M9p2Iq7zl;kxC(NRU!T!7o3Vow83_S@KQK#1HPS^!vRp{@fbw%2JvjDVA8 zC;I(DoP-Y2j3B3CDj3%i1

JEeLX`JCrdMF~%~S4IK1G5c^+@i5?mrUVo3Xwh=^0 z`jRPRb|Z>zHB&Gq%6|??lB8)$f7gSii&4!*dcFRd(WSw3#}~nup&C+XP>zlk&M46C6y#Ith)hBg+)c578Tv9$Mo~H4?KrR)(+l@ zfY<{lV7j}o@T0=QTl8G{b${^7PaslSrSET9ArCcgjy5}P^`&oJ|J~Jyq{+_6S?N_C zSC)ZevHs47uT%LrT{XHi@*L<+@iB}32qUdfV(By0|DQPV$`MdBt-!oksDB#@nZn`6jWv#&VL1Beyud3E@LkM zecsHJ;zj))fcD4&RF-Q^0-~%X^2Yn|B$MSZzWzG9DDzsnuFzBX6P+HoO)&pScM2zZ<^!dG= z&5hr39F8Xcotd=+OevPPND}8hKSy}f9CtxW29|!V!Z07*qoM6N<$g3%?sJ^%m! diff --git a/tests/ref/math-accent-bounds.png b/tests/ref/math-accent-bounds.png index d9876182875684a6c2e5a2c061a9b7de0dcc2d4c..6ddc543660a059a258bcd263081f5e7ccc7be52d 100644 GIT binary patch delta 313 zcmV-90mlB#0>=W77k?WF00000l5Z6H0003FNklvIT zqk2p1rpN3=75`XDyHx%C_y7M-dj6k3^S=v7UIB~2{eO4~&3`O@pEm9P|F%#6U;h8^ zb^8B%NK$HtNk#0MlJvaG`cM>(D z?#TmEN&Kw-n=&;Z7QgIUIA=gC{&BDH?toalYyGCtrq8IwL&RbLWI=EP(M1~<00000 LNkvXXu0mjf6WySO delta 319 zcmV-F0l@yp0?h)D7k?ZG00000k7^`B0003LNkl^Z);!|G#4>yytO) zwiY+N`=8s}5IjBYFsk_5O4_FC_l^H=@A^Nv`TyB-VE%HL#DD%(G_&|q2GExN7ys}6 z{~vtl|FfT9!FH(RwZ8W>v-sa5i~rx7!Ax0@ttb|sKKA?fKbi$~%)VKVK=frt2>(=D zn|oVZ575GQoj`DhW)`1!ZvxYk8{tCjaMmAMCN9t0K;qjRo7~G77QdW$j)oRv#^PES zh4>gq(OeIz{(e5sw0*aK{tyL(>i!YR2q%H_=opc)OWJsHIKc|`elKDP)l9}Y0Rs2go!6D4sim=9k(r;+$a z4hP2KX3d9#IYSzKIMAUPBazLLt-jQQvDh~a8b1l7Y)it~62`7x$5>AtvhK!mFdw%4 z?Lfj%>ifAsz;o+v6m=EU* z$SOzC%U#4cJb!W)S#o+t4%auKI}k|-jB{;1&ZGD=Pw&B zmBqS3^WnQL6jNFIP(?-HXc&d$J-c8WF0b1unhrZ>0FQeVx|&gab|b#i%@;2A|s zuJ;28NO0^pTSH9SO@-UGSHgHH41CV6 z>-QKb1LgsEW10g%coTUwy~q}nwfevgWOPI~^nlG&n2L0$@&mT&FY}+s;mBGMV0T&g zD8OOYZh!lj;Ji3BNlr09)=3ugfKKO`ZMCMuf4l<@H1($<&z+{Vedln z2$Kva!z&bapVJ5=@|@G_wKlwXU`P`s(cU4wN7ja0f5+JU@|E4{d;w!+4^}p8gT5ay z9)AQ1P>ttU9(KqL19siOW_9>(4aQHMJ+ZQ3=OgeJJ>V~DeK1-yh+?|vw#8vPJgljy z5sdzl1eXUv02Y?7CM^$(T^Ie5;oy$TCjuZ1*0zh5hQ~{Q_Q+m9FOmnevkOH#8CV!r zPcyO81AyN3YXBA9s9e`93qOnkc7XhEtAAj+dRRpEBnae3fYX^LAzb4ZaHSVm7T($= zhgIXLOB)7|rPU@5=2GTA@d8*C?%7ccbH@R0d4@>4%zMZJnOQ(KK%cjZoKtKT@He+u z6~?n28ae@nsxGl{ZsMbUBmmz=xZ?&0G?b7!5c!~=;hTk)g=g1-a+;Y4I4E@$Z-1}n zwFN+24L?2xn4H?nZ)Z-WxR{*XZCRMtLpT7a^R&VeQMeTac(-32G&a?%gKJH0>%yNl zL=OW@c|4v7GA{s`rAj!wJO~3IEMHAq7A~@NGy*JzI_`@ksNOO53qUN??~4H-hDO?! zEDLvRE_@0=aNk`LjjgW?01(N)f?h%_DZ{ex(o1$^g?X3Ic{vN&iBczu68Ayt!o0l) z*?jf|bUsW)Cj3;4B2sN-`0EQ8%Pqu!b>X&IjP(sCk1)w_GMo(mZ^C~9oj(D%R)U?G P00000NkvXXu0mjf%pgvC delta 1277 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 W30HqKwIeV90000U zMUa#gL6?yPrJzKC8kwaf>p@v+c$xQ!CJir4mzi4Fb()$w-E`BY&NFgz3}i*wcmT@U~_Yj z;!^0xc^qid>o4f_b-pl{79MV2x3y?W){|SCSDOhec7UChk`hq4*(gA_(RjsZYzZk$ z+R9YC^5Y0Bd4FH~v)4?KZ%YBC-%XjW}WOg;nKu1j#VRHh&D`WvYlFvI)Q`0_e<`S1rJV zIN|Yy1tM#t!@6*MqUt#?0Q4n@`hn5Tc?|%l#fhrv@XHt6e<~U7Iz5TcQTNdKbVBuc zbF;f$?>W&e6?UAoat zu4{%5S|H9dc2UN#%gz^Q&Mb5{I57WDPZ&=H$z7^4Im1IPE4cEqevxNE#&AuzE^4&n z<~UGLks@Q52rfJ5%6JWxoE78?dty+%1+4j$=~Ct1Tzy)twiE>AM?eWHVI{1DXZZtp WUKma!X;qB?0000bjdd-*C_sZ5=09 z$3yeE|K8h_yVj)i9d?n9QqvP2NgUIt3P>{M8n-9%e5Xg+FsT!aT=w-6Rqu@RGaF#C1C-69*5>|Eq;QtjQFp8)l(OMzPK%f?Y_sRSeAopQza=w5DO#kQ%q~ZBWX`E zq4*o?S%GQH!p32Gy>B4P+euc9!>SjoGoO75@H!bK6((nIy38`J)fxGVFOVsKRG6Fl zI#^+Ol$aG~0v6gu3&{6FoEUe8qpRLXhb<@0IG_LZQ-25v^}uws=oc_<*iV?}Qm6qgt(2$3nAuBcO)W euo_mw>-+(;dKiu&WOH5s0000@) zeA_`ea1O_{GtR*n=ko@>Jp6t<@O053T~^o%TVX5wKZjH2JAV}BVyD6bW`tKV{6qc4 zXx8AcTV&SRhla_w0FCirFqkVwA`#Khusa7NHN#vkA&ZWtF)}>or+v772HD4)$I$SR z>Kb93S!gLdd~am9bd0sa#pBT8zYt$vIETUqG!2gd{21w(M^m@%%~gH^Ez6-agcexx zPu*TYY2R0Xa(^O`LZK16`yAlVoZj#?WN}9T#OvhG<~$jYOjl^yiOLvAMm%h-0M3xk zaLoZScK|q9$Bz9aNbcon+JTAylJBLO4Z!Eq8BTiC;Ty9gNi3?uwl-nr55xfOT-6zl zjzjpSB-#q!e@07I5cZ!IgsL1)Us?4Ohzr6VL2!d)`+w(+glD|!iHB0iR1=GhRia|z zzDINiHWIEnkWKd-oPqvdNY?qr4}#&{KLNj2XBZbs;vgWK`lk5RpL~v2GzDm?rT+j7 zOy~_4!zes1qh;>`z~O3T8=89G=na!a;T@X1L4ct}w$u#RTsjm^fMlW{EnkkB6{fca zkU!Tz%R;4$Y2oxOU}>1xGNAVR6sCo7<-u;V?Lxy~!Nf2wkv1k@TPKVaw!&6;^M(Hc XZ!w@QEuN;aX)N07<_wKP|mHs>nMP;scS{MA88*Xa*bM*E;eXb)JcZ|Nc?!=N9rm%2 z6;i!^t{NJ4mjR)1B;MUeSniAq+S}R&j0{JQdtu^6DKedXBn`d5$Z*3OfRFMpC8{=m zeX=0>ZcVv2ANWWUJ2b>CEux z9Xc7blOC?Vg*?oBNiV`Cec@OsOj?EJ_3+Pf)~!`3<$p&N#gz=tG{+4Ac11a;D0K+F z!T|ciSTZl3twfMs9A1j^r(Ik##G%K36F!H=-*jLKdU|BxnkwX<`sT>u4mZGNo#BRn zHoXqVXQ)+4^0koAegT9Ldz6HF62@%H0Lz6s!)gm@0#A@}9zkewT8okTa8hTO84s!6 zA{E(%uy$vjEvv}1U)K@VWA|@>C`&t$y4I1SaP>0~&QBq=H=+As*A#vW_mS92yePD0y(r{Wfmnt);zk2L|Cw9$>@QZYc6YUZuo>w*>=ub zIJon>`CXjJ54`Za?!F(s@b+*(j(84;8+OBP_|Sx3&3PP}8-MXQgA@9wWX$rrESX0q zaKcfz8jy^y+k*)o5`mhsur?(b|M*ihAu**X3!g{kG>O2H^j%dr>qp|>6+(~SBH_;} z3J-*ls4i3af=qZoQMhxGhD>%U3j2~YBL*a8SA5wwt^aIOu2?$6Utu#G(Ysg~Nhj>>N9*O|vW8Hb!&Vc2_tt2ddsA9Qx zU58?<1Q2NSDMW;u z)uMJOqAU|4FQwd&8=Vl%Xq2I5%==5o1y(LjvF5@{NoI;SuL4Vej(SEHoclIDcjAF0awSax){G>F<26 zW^FesWU#Qs2qQHQvWd05C6K`t%(MWj{eI>;6?|=FrNI`syAD}Ttk*>F=)`wVRAxFc zN(9?BkkQ$&5y57EX0YE(1eafC249wQ_?NKdEHn7bLIk(^^tPDxd*^wn(|u9U7<{cn z@VKD2BYSH2Qh$=RlMmwv<3#Xnt=v+CFH~nnINZt00000NkvXXu0mjf D{5$&W diff --git a/tests/ref/math-binom.png b/tests/ref/math-binom.png index 85ab08f92a68c495990f13b59442a7ecb82e8fae..361e95f7d44de14bc2761b5ec19e55b90b3e52dc 100644 GIT binary patch delta 312 zcmV-80muHs0@DJJB!3!7L_t(|+U?e{Y63wJ24LS%5=g)&&?b^XlYP8??z3-9cIL3d#P`I+WxH$fMggn!NiWyfKK((gAAxK+|?`Q|vUOI=GX2wbTjOAs7Oy#j%A-T37<2Z7Ur_+^m3Za8xe zXCQFC6Tfuw5ZLkb+UwEPb0BcLDCt{e9?O?rY(wB}QnI;_Tr?$Ofe{UT0MoUGZ;+&iY5h{r!XNkG~ZhT8`!`%0v`ZBxb&!K=%T0q0000< KMNUMnLSTY{KZtt( delta 296 zcmV+@0oVT10>c83B!3D?L_t(|+U?e{Naz|Pz;enC%0&Hs3~X>6cLo3*3Q@d59)hv;I}?0eyNm)J?vo*YvDl*D>Jhd#P>S* zF#t{%usHdI;7JA_0n;(S_!h{k;Oh<$yaRk(0i7EWe6R&(A%DQb4A>rO;N>0w=Wp@LkNb^ug+61djJsa=!6g#k`?1qqy;;55npZ8-N6&Pm{6AE$m%0@qtO zwdxW$tN=u1GqZ98PzfdQIt|dB1I)Vs>9qtVvq{363>tXUOv25P2qvF|k5R$Tjqh;d uMFumC8J5hoIgJlPzP!ctfY&so^zoPVQmb8Ae)nlb-_OBRJ^ zflMR_t9jU}aU&qx4j|KP%dw&!OumCf28ek(#@H_Tfdi|sUOpO$xu4L5n01L!Boy%pq(X`pt~sefc}y+vSTCdpkSSp}f@yzZvZ zFdUl!*eNQ`K(zzdesmO(x(@>wrl=DX)d^r{^*98FpJp+S`qeQ#3dg~vEMXn>wO((& zS7`XOi4lIR^H#mKBEpU#R@kGhcOb|)Fw1ZQLhf|!Mu;6i-UmdJ+|`;pjSn^yIMg%w*aVA2GjzeR5NmKBplehVSjweeU;zPEW+y?oUb7F zz;A~5fiO3i#s~aN+T5h$$pzIN^vE-du%f9qlYb7gbY}kja1)~)F8?q(EC|T}Eq$vK z&3TB*+h4CClJa#uSGV}Y{&o7X&4}7k0F{XF;DUnuXa`_EZa`3_Xkl0S-l2+@%+q(( z4Pu5BA&$g2f1WuS)!77wedkJ4dge9zPCDh_nB1fDvckp$SGvG3wx*c?JPqDqY5@DP z%70tHwJO*x435eGtjjMo3k@%_u2vR2a-&1@5OxpalHX{f0w`Ai5tq(~P@6vcc1br5<@lnfshDo#X1O%ynt zdokeN3mbGnx4PerbH2Ox%l+*9&N&?JDR4qM#ef+w1OD&8JAaN70uS`Z6)fDw1_wgu z^Kcx%eInh5J=|4q!lu+{tFeMXLfrF6tdBW(dPLb@BC6Vr4a1-}_?@=2def zBSbK-^|UcKy?<4EIdM6jt6H;%mb8wYTk0q+(;K>1d3=)LduLL5q6bm$))#~CifjUCw8 z_>c$|3a(IUzb<@C1Xr~S=z_KG1lXDpge66~A+i5?V z5rL`u!heA52JL8d=GjR(bf!COB~n5@vP;@8xY5>E2-B9ZD(D1Vv}XRRGJhAy=?Zyki!iblfAYZx%Q z|MrmBldHEV;29|ZPENSv=htkHAcx-}YKhV5Wb-?BQl`V2k6)rl*It}k+h=kC=Yuql z?|ZR3oeb%~TpVVhcaE+3Oiv+c8`1}Fr=%+15uRBmm6Ai&8=30_u(zs9L3hOLmL}LP z<9`eN2Xqy+lYC0(7G7dA9$JDfS2kWv7ksu2Yro=KTLaxCrI{K^z*{XI(m{8AV3}^I zku#)UQI^}&K@RO)C57$+my8~`wfPz8nHH&XgPa=XkwWvb9w#kueOlQZ`M{zor^p&2 zL$x|7z{h3B5_|G4DK~y;1K!^rNTsCne<{+00000NkvXXu0mjf DIWVx^ diff --git a/tests/ref/math-cancel-display.png b/tests/ref/math-cancel-display.png index 30d30a59689ca972dfe2a21542041d9e6012544e..46b6b203e75c09bfc5d6e5b1e579867ea3de6368 100644 GIT binary patch delta 1271 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 delta 596 zcmV-a0;~P-1nmTnB!4tXL_t(|+U=JAOA}!n$NgUlqW*)RpDcpDK!_j%iwLpk3rIv% zXy2%ApfPRPkX@|iRm)k;t+1}rtZa~?gl%n2h2~-@9mmYJ+coyQtA`2C6QereE`odg z@_{$Uy|@o|?|W+SkEpT5w%8WiV$}?>w0p0N-!r>fM-{Xcihr+w(i2Sb0p;^OCMj(} z6^7>xi9ZD%&w(Ol z?GYSY;`EmtUH$egJiakF@~t zd4CcEK>9YQFn=a)d!^ToagWnTv~~OnS~eoeB|FjO1H+P3AIW5liIZaB;3Mv?FGD9@ zu)*u~S5NkSU=xJzy1;VU(dp`4`R6oTnS=|b`?yQ0(f!Vh0?Fc!@b@L1YfF`8+89+j3kv2^{eJn}< diff --git a/tests/ref/math-cases.png b/tests/ref/math-cases.png index 456b2550a5f8dccf9a410340e2412ccea15de0ea..ed0423deff906bb33262e243fb2c6877ec16a012 100644 GIT binary patch delta 1261 zcmVLUH?LeZ9R~Z4mY9OL!;^DsJnZ?Ldi?rdJ`az=4O-=`y zQKbhuABap9+2E=5ZM82wSO)J!oD4B2PD_jq9G*~NYrHh zOKM$-A6DkB=fJm)9Xkr}%I~9wz=i+vQ+Oofz&&IgSFpz{aUx4Yl#@`YaCo1gf0_f2 zmICs!dU-OOeZ&5Cvr^&27Xe}#d94kPk~+)g-O_ZMt27mI_&i^pRVw_`D@Zy+kQGL{ zk}_;S(|`5&@1xC9STgdiPT5i8_*&;}R~ra#134UX6JKF_Mg?AK@f(SErY- zjS%k2;q!O6>MNXl{5W^Zcv4L*W1U_Bu;VBW*kj#IPLP8=;;p)!m5xNXZuuql|#=p-?Zf0GwW9U|jwjPq;h| z*Z?rkHG*WR?a~q0W(FD?2ak)i0M$!dP;OTrz6`8`J?$SkvQSq6pRu}0I)f|a=tNFL zJ%4WuDHmRE0`?13m(^Zy&~h56E0_z!FjhnofVK2w1G0O+FtTf*6EDt&?H5cd(!GRR zV`2exxgaq$!I|7+B~aSQSXqk!V)HG}BWo?@QdU2s*-KJWO~CNnW)(;Kyo4QjgNSbE zkmvzRULOoz{r3f;HFSp{%dZ*!Ho6UC6(H7*ND7mfsMx4e%W^&u@YplId-dm9VC|`uMOOhHMzjk8K-xS5u78F7 zN`)~uI{^wuO%70a?+XFG&}(veT8UkrY8MHLitW;DL$Gltny1ZVJbkn_56M(9#hon zorI!(YrFZqbLy>r38&8n$jaQf>Sit;a|X~7N-?=VF0ZE7kE5n*?Xp%puItK3f2jbB!8VrL_t(|+U?ohPg8dQhjIS{@0>TAyI3^YjoFPeGjo~Q#h98I z6ECLdT%0jZ6OBrh7dH`k6H$C&LBW>Ufh{dkUMk{iRj^P|z<>s)2C&jnP@zC;fu3ic zv^k-r>qt1IG3n>BH&1f<%lDj9!t(D^Fi^%N#a+0iuAs=QU_f06euPc2oIEc`fY~frgrA7B(ygcy?+vlhhr?Tu|x- z+SSH3yd+Hg3Vyef?7)i*>s}ec*aN~|H_B>6^-XnsK(ktbPfb!CqTL-NHP0mc=b{kd z@!cz@9_qVnW>_^j?BOEZ&j{=yL9wgV`204{^16QJaeNBFF;lBJUUQcK~n z4oN>EL3gkL(fUqOZq7U`w0<)J64MUh#5n=KTk1Y0sZALGZWXiHHS@4lUtS543SCc` z&W2A*WA6s0YN_~#nTNx>eg?n7Y|(%m1CsU;|L2*iKYs#~nK&21>t4PsjgTUIslB74 z<537pA`~8T5yVo%BsxTbQ|@qmQOa(RR3*@ves}^Tg#gyvlj~e^g;k3IHng$mB}m3m zRYPz%qlArmxx&X506cT^sS`IO-;t_Xty^90RzZP#geBK}J_jjuoh^xLh(Ig-r^D z!Wt%A|M?~O87BVDug)0y<^fvcLJugID!vSnFiemJ z1An31FMojZQbUNaO}_`A^$tG5L#VAVAA>d%Qx(Spm`c=}K~lAkCt2wegwU~8{eXS< zLxde^PY~&cK=i@|U1hai+@=3LAjQ>V^O(dNywzwkv$Nj8Gl_NjvF^m*L58%S4JeP`4ZroJs`ZqF}RXA z_Md+F{HLbD9|$Jji}~L3zpZOWGT&_JBo5p8Z|^KCx3os|a1~{MWLpz8e8-N7-m`hd z7$X_(I+^zb$wY~!_hD|$)ys8mDm%1~BN8@#6{P{O&KrwPn1u8wg@uGfPgKIt>5hH= giG^d~SU5`l1;MO{JccLMcmMzZ07*qoM6N<$f(#o!MgRZ+ diff --git a/tests/ref/math-class-chars.png b/tests/ref/math-class-chars.png index 6bcaaf40804a3335275bbc00cf2bd5206a9d0b4e..b9abf4c5862d55d05d8f356b4e1c4f73ebf2f34e 100644 GIT binary patch delta 1325 zcmV+|1=9M$3cw1GB!34i5uqR=6AnPJHyZU==1zrH$&V~@JFLl>-4!Mo^|~~ zuwxnJKTm+xFMl=8cN{`6+b08#od^4R;CyLfDN?Yd$#xT96yWx-I=K}5YQT^6!6FOW zgY+rbE((_v>r-)wD7?)EuH3v+0wdzyov;cg(9<&^*(5pW# z0B_QV#!st{qF?krJiK3@;;LD$&-;hH4##!qlmFb1k3M(Ac^wWwFTxdhVd1j3;npPd zrb^mCFMp}k%kb_o{;{zLFT*3xd_w(_Q1E{Yc24kuO*lOXx0ptFfo`|Jh^fVca6&E5 zY!1EbhVEFXP4FaK2h8e*4q9A*CGN)upydNAUDSCJj%kF_%GK$60d}1Ss4w4`xe0)& zoIuRe*&nA=Mf9_ z1?o#zUQ{!{GFV|_!hhPM@A2)lu%fbYZE*{jj!OBV`38VB^I!*BiS5%tQ0L*z<*Xd9 z>;a&+44`(P;Tphv>;QUA<+C1!Q%|wbm0_OOsZ}1b4meGzPZ$AqtNbUU@_< za3QGjfgt?Q-?8l+d#wFn4dC8;;xJ1A?r)ZZTe5y@2Urh~8*-{s2)?_PJ%8r_ zq5*=hJ=iP+$NY|;4WJIdmy273;HcI~-x`22fIv*mLh!xZ({YypM>Ie~;Td&uDOkat zgsXQ;!IxOnonSA+lhdGO2CZB-WW$M*V_t=m^Wzg1!-EH0=62YenN{OOxa$j&D}Z9{ zzSJB52Hw8`_;7Zg7vc02)(+Vhy?O)!5E0>3B-#q zef>jYXeEJ`9qlpG=u#(_f))JIH5C{H53V-|E=`ewzjAXj=g>7Et_=uFNbi<`JGc}= zP!BL*0X}v##K^#~qi*3(W7`@M(`4Yo9O${~Ukv2k0cW`Fvw0afpQ-ZJtR_C7VH~)c jH>yr91uIy={|oqUdYF@ysnk?F00000NkvXXu0mjfW@Cd0 delta 1327 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 LNkvXXu0mjf^@jEQ diff --git a/tests/ref/math-equation-numbering.png b/tests/ref/math-equation-numbering.png index 18697302980e7edb6e616fdb4ef8c919a097dc47..7e8757bd1a2deb4788bda0fb5d2b30e4f0d36998 100644 GIT binary patch literal 4684 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 n5w`!dVE)^TxksvF8#3k>EZiExx!_5zQO6BnI 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 Z`gnplFB8=SasNMfIvNJ*wNU%;{{Sqg+Qt9? diff --git a/tests/ref/math-frac-associativity.png b/tests/ref/math-frac-associativity.png index a5daca5963b5e8f587a6527369bb1366decaed11..8aa989962171ff96882cc7249cbad6c31d75ad12 100644 GIT binary patch delta 461 zcmV;;0W$vN1KI_x4DIcy+dA#@Jy^G-A z@f=RW`Avt<^7Rbft38;}dU%+Jd6tO6vYhC7CurAe}%*0IM5bGD2MUeX$WE^;;qpR@8FpVaodhhnJ*AiEES%00000NkvXXu0mjf D9fRAu delta 471 zcmV;|0Vw|31LOmc7k@Db00000G*~jK00052Nkl=IE(R1y_xQAx>=k%B=)(u!G-Mo?s^U?EJVhbhjb+2%i=-RggR z#vq>G>CgA_0b#fefK3}xq)gwRxO4xvJgU{tac)O^N3p$jd%`VI zgp+Zfg5tx)S{lV>w(Fn=H%4H77uE5??HJ$%GxT6E3`Yq^@P`5I^y-Wv{2iEU;+k^4 zqNaEjBlKXs4yztiYmgsCu~IA+Thw6Q30qU>R~YJ+2T+Z_RpdBMq6Vw`$CD*gqluXq zpfz54wmiqZ5R8f_ThxMD3~TS@WFW?Hy15#%%{C&KE>RhR+SUk)73!_!2AG-+WitcA7kzYB+kJAWDzhJ*iC*sC9Qf^2A1 zkL2~pP`$$O_H6$IGM#}|ME6Xz)+;>th;u)YHD|teBifhsT&*G80 z5r-;;yuMahcz=Zpu5aptGj9t>TK6~r{OZgDuw*TW0NYMg8u(FaeOcX%mkGlW(!lP- zc%Bc~S>59T_^8fi0oGRw62RuTDglHG>kI142JltgOqL1vdQK>l0HI)BiefQ!7Dbi` zs`&f5{XI}7{K4)pW)QfiN_mFV2?NS6m@VfaF5<|PMhH#%5L8y(fHyHcr3?Mk;&KyUSQ4}Rt zb=W$e^N26pOP6=&iYI$CieNuWceW9X>U6qrwc+JsH_m?+X70!SmZ@|b{+>|;o#~7E zX+*IAR8JdrTPzj@!TJ}!k2RQyB8Xe#^N0c_!`+4)5|Pb~?uLZP-rs6Sc=xttxFO+< kfq_Ns3ZsR!uokYDKZ+(8f;B_Qr2qf`07*qoM6N<$f|_t1h4tpvcIrHU+&aqaN|tVvrE z!0)O`0{JCL8Ca|yPf6P9JgOe+mrT3cI}g)M8ecRRR4x?2{u zd0+HK5kDou>Rb}Pz2o;(32(%s*HsBqe3VipT)5>nsS^Hcwf@lFPFh$CYvE@32O4dU U+|3&x;Q#;t07*qoM6N<$g8iHpVE_OC diff --git a/tests/ref/math-frac-gap.png b/tests/ref/math-frac-gap.png new file mode 100644 index 0000000000000000000000000000000000000000..35563895930eee5376615d1c8b5b2f8c007d7bd8 GIT binary patch literal 266 zcmV+l0rmcgP)~B7mF{1Zv6lM*j6Cx!~bs||Np&+s1S^Q6T5tw%S^EP<_Fv$PXEjyk34W@4X|9|A|Xtg+M@uBc%mA#*XNEm!Q>g?LM6e~>DqE*edx4atF5R)c z=+5?y9fI5tMsA%UoX^#B@xT{;JpABkpiUa}upZXKdbpn9QGffxiXQ!su=#ssv(`rEP~}+T9BJc6;zNb~lu8|~Q5YkCSomwe2m926lTv-V zYZPAE8StUHfl3yDHb@u(e!_zvC=r&LV6w+Kj?(iqx}mV5!h40oKt`53WO)UcFi!AV z1kZI2%{L#vynp5{tn|~w@&To*kCu0_?u~>O?5g?paJ~_63&IgW@B{cvTmsB*l`Wu( z+f|Fu)$48yc6V}Oc4k|Zqc$hJ9VMd%gWm;QNP4bOF1!3=d-~q<%%0&n6WLHT;m@x7 zg|e_?M;@4Z{D5`bY4Ii;9?LLd;7Pg$3&d%)UbR}i0Dt5{n}1i^EVo)?fD|g4)+{`= z(0lu97I-sKBms;DtM{@}7*0zHK+s(@1`v5wz41z6$Mja50CV4perh)Ah$rHj;a{u{ zO7(fwos%c;)+79@k(dFVkua8?b2M00000NkvXXu0mjf<=PWL delta 551 zcmV+?0@(e%1hWK?7k?}W00000ix4C40005{Nkl6lXFdUh-%ajJ11)(Aw|6S`w`35olmHpejneqRas!juD=B zfp~#k@%;zRE`PWSb9bp?W{1|)NzL%vwxz&7+9)W8QQIXBPED>gIBI7iP@gwysE~ZXiC^M*3Y(qzwl3Auqi*T{UdwFM2>i)+ zJzD^Gtj_@BcUoDb=9)mFLu~A)D{?P|@de^>xz4#<0e=9}(UrfeyIVuTqy_NUs94wc z74Tlp)dTSHgYm|T!EkC2fMhI}0ub~Y?_n``FurL%4AASzHDKDPeYaYCLo4GU(ARwU zvlXyfjkMPaICG7cTgm#oFHDAkBNE4=6Z=euABgDn z8yBQJUARUR7h;HvO&;cgG?X0-7oI6e2YI-e3q-bgDBCfc$1$_{sQrqaJ=pJ;-*R)_ zH{Z+G>735@_e;V*m88H5tiTG)85ThDn-E%HicHTO!c(0AUVq5*IRK%N2_6FJV}wjQ z8GZwqPm>5whtr+{kR&qBdk1qp(ESN+qkwUl50d^dIGV>%G|LW;cJ$3{)Nh5im|Fk>jI; zEj_R^!zOETK!1qxjaP?nRfrCs=gD8Svt-J!$PDjIZ%#P1pY~qJrSn`vc32gx`M!k(j}^N#Dv+%U$=3;+$< zXONzZvD;*T9oBfc0Kn2b;6X5-HFgdPn7NyFtt*ko;&oF031Z~dN(k#}VJ^T2p5Aw* zUW2+!s5N|Gy@28wP`85mF+MQhOS=VqhQd!?aLW!@YSVCnXUy0IZO5fpPUi!!rKmFK yVP}=IT+at~+iW&JsC!>OQts6XtiTGKz`g(>eW~@9YXOS@0000WRFrju~$E?{d$RcYGTx@f!Ja1;UebjzRr*p>dmtQHJ z_szG?>DQ~y>3lmm_@i<(Sc5fKgE_+jNJ*3Ia%zRl=%(UCk$(q>Ltf$r5W1P*DUjZ0 z$qbW`oFVgRS#iRM%aJmWBr?3Clf_XOe+Bnbz_>02Y2q<6dKXZ>#tzR7jkTRC;pHZr zY|+7Y_b6~M!@jU-(DRakuSBs%6pu4-+?kIa@jF-&#pYe{Fbx2fRxrar<0+tkanXpv z1SRYnL6{jf`G0d$g33GIo4zfAI_%ZSA9bX1WyZ`5J63H=eiis_v1M2t4vY^7GWft? zFi0?R`kcyQRdIFrD@k^P(*m)XUu6r#+8)H`0#S)_fv{)p*yXx1&#XTc0D$#QJ4i8W z_S{@yKLD6IU3_5nHaaX|h#gKLTb6c7aV^aY4jTJvl4hW8YNyu82kzR2ZXeX;VYr(Q z90;QD2=qlNynJB5Q4NO7b#6{@xG(hp+G2LB74w0g>uq~Gpv_f+c^w~kTd&uLK;4GW j5YqmuHCTf+_#eU_`pO}JREcQ400000NkvXXu0mjf2QcXt diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png index be47707407b7005ab6ceade9b66eed52e6093771..973c433e2c0d267aa57d746e586736c2b77b1a96 100644 GIT binary patch literal 5504 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 literal 3877 zcmV+=58CjFP)_{rvp=`}_O)`uh3#`S|$w_xJbu^!oPp_VxAk^z`)e^YilZ^8WP4@$vES@bK^N z@9ysI_38BO?d|OB?Cb06>gww0>FMa`=;!C>=H}+`-tXn*<^SEteK1m+}zvS+uGXN)!6CT+1c3G*w@$B``uU8 z*4EY4)zs9~)6>(^($dk<(a_M)&(F`!&d$xv&Edb`%*@Qo%gf5j%Ky5N`qCiD$;rsb z$j8UW#>U3Q#l^(L#Q(0D!^6YE!otD9!N9=4zrVk}zP`P^z5k!3yu7@r!pyt7yVj@F zy1Kf#xw*KwxVN{rwzjslwY9Xgw6n9btgzFvva+$Uv9PeP|Cm~@udlAIuC1-DtgNi7 ztE;N2s;Q}|sHmu?r>CZ-rvHUzrKP2$q@<&xqoSgsp`oFmprD_hpPrtcot>SWoSd7R zoBwZjnwpw@qqCWrnV6WEmzS5pdcKyHmj8Anm6es0l$4W`lai8>k&%&*kdTj$kB*Lx zjg5_hiM@=BjEjqlii(PfiHZMRG>C|ZhlhuThK7ZOg@lBJgM)*Df`Wm8fq;O3e}8{| zetvy@eSCa;dwYBTKudahdU<(yQ-zv%czAbrcXoDmtxu_Sb#-)fbaQiaa&mHUadB{P zaBpvKZfOiWBmOG`>hN=ZpcNJvOWM@L3RMny$ML_|bGLqkGBLP0@6KR-V{K0ZA? zJv=--IyyQyI5>y^f;TreHa0djH8nIeG&3_ZF)=YPFfcDKFD@=FEiEl8D=R80Dkvx@ zCMG5$BO@Lj9vvMW8yg!M8X6cF7!?&26B82=5fQK_!Epcp3+72gK~#9!?b>@(RQG|v z@$Wo)b#y0r+VkkJ#*)Q6CCI{IrI7Bdheaz@q754 z-yJUJUR+#UTt2eguo4Pvj6hK7i%Ez+2FZo%T?q8oUt|GF&$nYK_PG{<8xi$kGL=<{ zWba2ZIsK!UDqe?PD7q3m*lT29NCr9ysfd&VW(r&ssFQtM>W{ z8zEQUyKh`~m++deb0XWJu}%$}Nnvv*=a-W8R7A*K!p9nT zLyb;<_wZPZ7>27pAPZkCX5AHGW6V8R3wH_cUY~Hi4&@Y<9_&WWFfLAYf7p*{1!<~z zi~7Cn>Jm10a)nuMM%8-UCG4{$SDyq{;eyeTJY*Dye`*jGS6NN*{Q+i+t9-1AP21`b zHg|FfE!p}s2UqnJcc=6{kL+@iS=i-N{YDCRrP-1o{{oR&*cZssQx=$HAXr;qB^E(} zm1yz5uutJYcpZY&0xNO$!YdaXmJ6%Gj=W@SD~xBsVoId&QH+XL!`dhWWf~xVJ7yF} ziVav$Iv&1>$p!?4qfQ!`PSb4JD4bj$tCSvI0rW6!0~GmqG#m6Ue0uD7~@A z|B$;s4pE%rTj4u7u^yAU0bm-o2E9iOWbZ^K^5lljMC@??azRlujwx#~jTu^uwEZUm!Ws|_v4{pPKbww``$!4^Wf2HA zg|mqKf`SxSP>BDy{=jhgXi4-2Aigz{B~^E0J;2)gqrR8Ok~%Ad4Uceacg(?wnB^5bd0*ms_qYQlZF3l8C~>LVpCxni8cD ztw=|)YBsJn9@RKtO76kUwnmf{i658HD%{)?CfvaX-KO)k2{1-T#006zEtgnsLrG z1BI|d10F0H^|rI_0Ib)sw1TGd0C<)qmDEA^B>?R^EUEk>oZZ8un%r5IR%UxE%P~;<-=$lLs0c7GEWu&;P!X{G6UTRn#Zy7XcdC7R_yr9|B3$VGaXutMiI#m*Xpd# z%Xh3l({^CTr@v_O>7@;S@(U)Pyd5rV8|kUH4y!vJYCYy{BV4h4V5rniSU<9DBGP{N z*RUejLQo{#PD=H{A~_<6z~`)AQ7K4xQ87wvn@ z@m2kA(v|u5vQ>Ro9sLN3Cowsl2As_T!0nJlcy}{`&4&>D>laP_j>%#K{f>m)wFta? zWX}lj_dpP+Mxf3k=F&r;@k8M4jo@FtVDh*4cp=cbV@E`$4M~V@0O^^M5CF=2*zMCp zbf#n#0RQwUlfO>Rl!Rkje7Ox9%%2gImP=Lu(8$_l=vh-P$pqlvzhv?cdF7H-d?h4xyY?iXK4MlR(D{D*X9egYhq)pxcH zB-x-Hnnct!{6ZK2%c6kXZbSwBQ&rOVzmcxYzXE6+nqG|qUI}&ky$2?m2yh!r#wg_} zwx0nY9-ulF(KVmI-vAI0h=qVFY`TI2%Dj}n^T0N*GPa$;hANfsudz+Jg<^-o&^wG> z>er&M>3|-)E~Vj!%wDw;d-fz~u%$%rjy*eaG&mr3Z+x#0c693!c3?yLVEhPn)Ym8U z;ed(R*^Af_`pwzd7Hl{*JNpE-XtoxeJkE&?Y~&rt9N0I^<(JYGTfDri@L#QPGPXK<#SB)yBAa)7k& zBB$zS{SBP!bTwAaRAA+qQk<%)1uL};SZS!msj{|sbz&i>(yIdt`4z%Br%_&?n|lm7 z^_z2Z$C2~6IX8FMb$9II;sDP;>^qvtGBp{;SOlFHv2T>X_hMj_MbNr^-Y_>;8EgTmN zKyVyD&p<9lYT@*1KLEm37{j^EdsNN}dl)b>XfT)eL%_Ecqb?PQMQ4Qv{4w$yz+CF% zz=#H;<=cT{0RY4Zx%Iw4Vb9Fafm(mzRC;TLFT!4F3_!FFr+5}(U*P3rI1iy= zw*@SEdm{Ha6|}WA38!=o`p0vG zJtr$k!zi^BXdCMVG7(kBVU(8*!0LgMQ8-Jx8US@WP*cx0wm>!}ok{>SdIZmt{(YQ9 zKQAIJk!StU18qXkAGS_C*+|VKHp{ z)vaG{#=dK}Ze4ZV5xcn9q#Qw{0u?>J$ZXhv83o7>?}7Ip5MBG=^+AbKDGxT!^^rw220nA06+K~fIZ>WASaL}SGrD!y*@4Bq+SvT+07hJ%O_ zY3k)5as`FX4|CPa_f^QoD7cUH-y$2i@CfIJRd3SRTc_y3$a|QkM=*FHla&P$#ZNG| zX_5u;uP3TEOxcepr*)S*o!-c-UymfH|G#%AkfiRJ*zEc`x_52637L!pfl00000NkvXXu0mjf>Itqr diff --git a/tests/ref/math-lr-call.png b/tests/ref/math-lr-call.png index baf668d4d78ead8ceacc181be97fa06e272e6032..c498061d45bb53add6a3dc78114366883c2d8967 100644 GIT binary patch delta 932 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 ZxB(y9BH6c$iWmR@002ovPDHLkV1m*CjwS#A delta 637 zcmV-@0)qXZ1)c?vEPws~{r>*`{Qv#^{{Q{`{rvs^`~Lg;{r~y?`uP6&`}_O&`~LR* z_xJn$^!@eu`T6wu{`mO#@%-}l_xJMn{q6hj?)Lra`t0fY>+|#T>-7BO`R4EM@8+9>@?fTU9*yrcx()8BX>G|a3$0ipv9#9o&d$!G>8q~O^Tox*qs{S=< zjf;zmh=_DX=i6=W@ct&WMpAsVP0NdR#sM1Q&Uh-P)0^Z zL_|b3Ha0CSEx`Vz8~^|S@kvBMRCwC$)SJ6SMS z78)Miymm7zJ`9A0fA>qVR3OItDe&j=Kd}B)j0dy86=3I~AS18g$9eM`c(*J{X$ef8 zygG*ZMo}_*VZYoBEMyB){T*kInvd|WOPt3|nJl&*92{t diff --git a/tests/ref/math-lr-fences.png b/tests/ref/math-lr-fences.png index 32314cb4b163e65bba12f1381cad10e61c3940b2..c9919d6773544de16cbabf19638b0e50344dd622 100644 GIT binary patch delta 577 zcmV-H0>1s(1lk0UB!3}EL_t(|+U?fwOH)xC25|qZ-Uhirk?)Ma$Ow#j6?+kdP>|WB z63QkN*aSn@NNU$H`-w=+SO|haIIrks{=hl8+ufXVx^cHhc9;h}3}JANXUKWp9C*%) z4}1@tb6OxNEeck!f)(5>aPaETcPX%y8if>iBY+|=IDJCNZhu-`6VDQ)r>`3*3)}(|`JK{CRFx3jA~KPD%<4|0bls zZ|tSGP*|_6<2aV7P)9F>z}vNTu(tYWO(O=@>cg5CxPLU<7wFD2@Yjcxm9Stqy4M^0 z5?9NtG;yCKI9RM(N{hF?a35?`*wA%!pC-oNq*&o&H zbAv~$a|p6${O48Sg6K0I`>gvnS=jOQ+!qZWiAmv`#alLoP0=vTFs6e^0PmaSb$Su- z#AU!EeLA?DmjPc|cHE8(xNCRJ^GODro>&~+;su*%IHvknD_Fq_K8x@Hg#cB^ri+Ga P00000NkvXXu0mjfz*7{q delta 576 zcmV-G0>Ay*1la_TB!3`DL_t(|+U?fcOH@G|2XOzbUV3xQmyCLmV4upehbVdo!619^ zv_&^eC~AY+!jll{k(RAh5C!iS)b;Fo!8AvTPnZ(Qv$~~e?bYnW`d}I<5hos>wlu)ALo?;oJu6y6r;N$ z;IBB51+Mh7nmZyIHuA%U960%-@2G$) zVp$JVz~24fqc#=riZ$|SNe_D(*6>*gqTmFDz#mXRNI%x-kz&K&8=g)nfzdq-C2%;B zT$KthvGr7X-`;z8BoXEZdKQ8dAO*H~nAI#I;pu^m9)Etybo$|Lvnf*!9PRSGuG!&W z|83I#Q8FAy^)vwV8HUF&-ci_f%JF29!acNZ0I=~Umpx%H!Bx!hS3UR~rTjpjCAnmU z!QSSZogF;9QuF&3Bwuwpk#PW;0E0WX>O%|;$btD#_(A(;Fc?LHrvO4%u0I+lO8)BP zomhEP1vy-2qRdqRmpaKC>nh-SyYc=XQLu>u5+$<$Og-LeSi>5=jPNnNh*iRT9)i38 O0000c$GX)BQ2vUJriiY@-=^{0I;kj^E_;)v@&VqNEdi$4@wJY~ZFh&RMq z{L%zOTE713PkB&x?fq_lkXVZ!TYyNn|LfZiQf%?F1#9c}k$-0Kwk^wBZ&D{!lVC9n z(AVMz*0i$t%~cS%1!BHvn>h(Y-vJA~0!vTrrk=$|4gtX%n8mlYgTQVO)AjY86cD{1 zEOZ^joL2e`l$Sv|j?&EH`|kheFG07se8Tb!gM^ zH8>Ji{~l^s{CPH<b{E2*BwuEAfgD?Ga zCEmw^AYyUz|HQ`>TWsz0eL2nzRVm(X|4lytO_3O3q0`+QZZ6 zpzdP*-kghHaz5#k=bS$05HGAl5jsLg=m`BnXmgSYHlO%_o`1AeRQ36<2TqS2a{vp6 zy#Uik?F3iuzaBV!q7Ps*8o5gu|0DgX383%vO(Q`4XM53O4Ndc5qcb{vCxx`>G-n@$ zjgEGkP6+95yLj6dHaecH8$vq4vUu{|^U|JH)9*_Y1 zrPqIbS3d|)lm`np`bei`@zrV>b)VW1{7 zz4-<`c?V6`HgMGLr-cjdjUn#V?dMqV-P8--b$j%@YQfdp?0)BXty{Rwq=7lnHUk{G zH9)=1JU5$tkm=pm!*%&#e7WG^0!!)8n`TfyTyh&#`+B9Uzry4ZKdq`FjUNh8Wi$ZL z>e=6mbbpJ^&xJH@PTfJ={%F=eS`pLqrxG?=y0kJYrf07)8#X%eV(tYYZHv-m)kAm= zNS^xo`eDHV31)YuRtVFv5D**-!+sbBl*4f3>mx!(=m;I5#k4!{s&2#wfT7DpK%=0} z6RnK49)zanywE)l4y^F|Q&dTSp%;L~8`cjAsDFsPN?24z9hOk^_2g?Tw8F$hY@Gx@ zqe?RX*(kaGJohhXb8LIBGRg@>AK#Kl_W;%m7f@=!pLPI1Dj9x)IUl9QuuJLh;icZm)qXKAY z=T;7de)yA^Zm59GMBsW^S=k717N9a|6{WU*fbZ<^?g;)Y8cl4HM&pp_l_PW)0^=IZ z`&pWN30yn3YEVra<3J36`JM-uzkdM0vw|O&0BEcQ$TGq)i79~KUu}-(yv`7? z;F5j@h1Qjb=?~mMEU?rv^C{5XjFPiBdjW7B|H=rzH9}9jwr?+*qV|hdTmf{IGxd%_ zU+opqbH+q~GAl@STO3fhj=!x%0D9j7S0+JCJw?64brNz*tW23 zX9s5uK}j7gz?E&(`Dt~=3ZFc3mLj74nM}CPmFIz0AUG@k3;p=Zs&;u)wymbst!T9q uh;#VZvt>|c)M}SunQdp}PcuTlRP;Z<{pi^ZG3?a<0000=VV|sc{ zr<>Kp^!w(Q^u_t4PoC$=ISY7VElANRIz^}GCqm!Q4#1E;2!GUN&JtI7bUt#r^VMlU zdKrM;mt4rxBQiX4dRaYyD-8g0_B=-VnE`<6_iJkbO4lpVC(E+!NuxjOl9s5_W-S|r zl14A*&(*5*CJT#68lAzGE>(KxaNF9m|CN?bi`hJ)LSH{)LzQ;bbQ%ZZ=^h8Cy5Y2x zkUr@}bQIBqjDH4XwAZ7u!-F$oN_zO=EO<{QqNOYlRdJ-o0q;!#SyBSJ$pCY@A{`9vmeD_dM@(<_$Z2^|dZPr-)g1yuQv$lA8P@gqmI0fA z39B{3Vr>J+`#ubC)pECSfIC~y0bZ-Mu)u2d0PU7&Eq_43YAwsLTHSyK`aWj!Rhml@ z)BYE@v=2d#n$79;<|csLoJcG^x2+18Gn@1C%;vc`+HW?O>@}Nj3))o5!l}fxR@C_2Rbv7*=w&{lAN}iI(N6^i)3?CP>m94r~5arD6EpQ1a-)Znrw^$@ICC zMw^OSa#d+*N28@Q3G~tiAe64r7y$NV5?{MUqkpj{f$ms51JU{yPCd?-;lYdn5Y>I- zPJMl(=oFozp90$MK(X6a6dIoaY?GXs|5v&d(G@q!%@m3`eSVObZdRdCejRY}8iCwI z^sO+IPQ&#+`#9vGoX#WWk;tma2cWlK9N`{Kb%#1#l<^Z229Ub6y?xMMfjozCfPfa! zEPtzf4?wT??GSY26LortU*g~(FuN(>1k#BXIN<~s)whgu{t$>NT0;O(pg`X#L-f^T zwxD?I`06=%2zFcme2Vh!{ z=)#d%1g*KIM)zugwZN>?>2v|veE_b5`+p_?pr#c-I|R~7XFX6To_)QLopVfZV?TJ~|#jv<-kyN8nUCL_4C>y{^+W3OcQu*MLBd?%Q{C zItOs!?!!g}eYio5-q{JH0pJxs+yLNmW^oXJjgtUh^n$^JX9S4**NelKGi|CVedq_$Bt+M;3jBcW#l3F$u@YwT8;T_*x~kf^qdkW9mKX0w(5pw4 z>6<-U08VuR^?Wc1d!OYE{vzqId zt%Nz3h9MQWWLb;GP>TFjnxbTqMuFiF6jBisQ`B+tLj^%Vekdyt#KSn?l!=IU9QRJ2 zd+xd8@woe5#I@$V|N3mtp69ha&z{freT@I8$Gor?_QGEH-+zVcgR8NYuVo-r2iJ?k zHM3e!I{pU(s%2Jk9Zg+Z)hZrD$GHyECad5 z4fz1P-2yIP}xDjd>VCkg8dea1R+) z9w-18``{>6lCLsR!!-f<0x->fdVrlbW|M9YhnAxhb^2Jr zCb$P*w31goPn&7L_z_a2(R+K4dQ%I4-me)En3z=n*;zUJI_%(eS=j3I4@rgG<#6US z)oF#P6@PH)0CKf!&H{0x9?u zx!zEKM1Zqf8lm30K&l60bJZ;|0bpp3pZu){SLVIvpM3g3BZmwbGW5C!VN5+rCOip) z4oeMq7B<%*Rr!`@;k&6-ZRhUSP3R!P+7YPXmVcPb2HpM4{{$YP6@>9ERKtW7X*2HE z#WE9aR_Kc;Ts~W`hD+`sRh{`e?xmkyE(~)eum)?ICI(VX;1%!r*b6^$aC;$xUSa#g zaJGLvgI>LVwi+&*+KmV5o?51Y{gZK49C1Je?zA6U%FW6CDtJ-}N;lJ+QMHbBKp?0~ zp?}}DBUSu$tRCg&l1VDq=ORiWnX#g<@k5s|FX*tr7k&DNTNZ#QoO#eCtn)(<-m^&@ z4l%fdfBQ@n4%WDUJF(WW5|?n;VNrNuhjUmHd;z&%@?FD|^@6Z=Jit|~T~)&+DDe9L zTq!_G*M-!{20OT5Vuw?Bd>L~4-vLO#T7QBX-uYSvmKvi0Dn7=NrVgpzg)Iu)smZ%+ z50{Pa3!6rDBNrC!0?t{6+&8rVS@9~EsB#OeWP`g$nfkyfL0Eh7kPA2}3Av@X4HJ}N z4_D66BiHjpoeOwY1C~m501%gY6qkDsZwC_UhYILd5#S>;{ZMxj3v_tNNvpqGcqhqyV8+i zdZg^&&X@AA!Ni9l7aoq&SAA)IU^`OB(!l+1ydoZZ*f83NrFjRP!zPmvOUt`)A2yCQ z^nn>yfu*OKox@7E!?1MWb2a>~wtru^Vq8C)>xO*cGN4Qj;iO#T&gLVf?L_K$1q$4r z+IFBwWM$^Ohq`{?AT!RA;r$!e_bwEHHzf+f*H`V za}D>rf*`yUA-FM`W9v&@!F6AzX9>dP+XP@GC-8()z?^;rXYk^trcNwnwq3Xd-)?GJ z3_$4w6+Ax;XKiLPkPsP}g>XsO%8ZOm1R!m`3NDyw!ULIR7N}s7m*^PubW4&^!!(>= m&^yuKJ(zo8FYJZ?$MD~3^$@tyv`IDq00008k9qS4{gJz5d z72Y?tc0h3Pk=+e3+lqV-%RM2mcI24YM=f7Cd)TlRAHt*rWMQp6X_YTw7L05{Zl4k7 zZj>W+xdCNHFMr{RNsq(2anpKn*$(7>G1Ip_(>ei|R3Qj&je8uXcxXJXDl0^;Di^ty zwIu+&+W|n_RxwyRSqsqf>)MPJ_w6uAPo2izDGNypYXRc4aOmeP>xwZ%AyvOR?Ex}u zd!Pth5mE)f`QC(67jVGlIb&~O>o*G3>;i~}JGB*`wSROWbd%c=f!VYYpdi0+UzZcSCLc%LAyIpwXc?S2 zOIpC~h|Pks42AFj*?FFo_zqecb<1PpESA&jZlo(W&V zpv77pt9lxuPy z*25ALURD;2D6F2PQ^J+^kgCf&hIi>^sfA&#hSy_D)yhDs55MX^AN%1a4!&K+pjUSL zakwC)i9xR^q(BK*P0{0n^i!%7a7ZTZN+AWP#+&xz$bNHXhytEiiM_va+EKKIH9$Bh z%YUNZw%Sf6NjmH|S58#GftRosm6t3E8$b346QsuhUkdCW?pOe#aNa?W@U^)J!n@at z!%+s0@XpnuaHPru+>NcS6?lZhRif|E`_U01@D*x`== zxLS&owg;(`%?`MG-MDhM@PsPl_P+;^hJURzCA|I3T&%Su0#tvBHB}>0=Jz}7FeZ!H z8Lpbp7dDO3BNvnC0WMsE+_wz?`Kbz+1hoSOoMHVaQy(}h0$Z;f@&G4fAh#H=VIs1e z;hO0>1G4Fm0Bt;E88hG9oy1F3bY|MW+@pg;{3?Apf0*w10D^j9m~gB#hhd2TV5U z9|#Wm=y2~Gy6D>M!6rIRkG2lz+U~+;z%(tmbvnQ#7~SA7s6TlrEKp&7)G%3J_C55& z`Zae<3SZiy_M#X*vtRGkng-0fbK$ zd$8-xU_+Rl?7&9jlEW*&COyJhliDE@=3pg^FhqKAj#&2*rk(VyjtuW}irNUm*;EgP zS3?`&dQ1x6mxcky=QTM9&Fy;NN;3@@-a|)f%3`O>T{Ock2i@dxaU5XkC2kNHbIUO0 l{_c@sIOVsfzzY0VegLD5L!DM|Npk=I002ovPDHLkV1j9N#J>Ol delta 426 zcmV;b0agCh1H=Q6B!7!ZL_t(|+U?a(OTus%2k>52(Wz6ugI+)oojP<#D5D56utQyh zu&fT2X#-0l4;tj6G%Z*fWYJ~WpJ_UoF4Wb0+UA4$+o6L~y6m|;U!KcndwAFe;Gb&1 zgiY9l`vK>fcD^lQSh2b>En9c{1gHG~AY5ft-}8t=oAwwiEq`HFD^B)QfyTjkitidu z(9W11W^-eAUmfr!c2~0nGV3^44Lo-ZmmE*59 z6Yj+=(HJZSo%qJBgOV=6g1LTAUNs~4>Z+kSD1&}&EKL4oA4j?3vG=* UV?u@AXaE2J07*qoM6N<$f;Cad-~a#s diff --git a/tests/ref/math-lr-symbol-unmatched.png b/tests/ref/math-lr-symbol-unmatched.png index 38d0a988a7fc26c36bc8495c12e6f58401568081..bb126d3efe6255e9a6f195abe96627a7418b9a40 100644 GIT binary patch delta 332 zcmV-S0ki(y0@?zQ7k?%Q00000DvJP#0003YNklw4Y znz#5MHgWxoPyhcfS-9i>|9Y2Ww6Hk-#*G+k;<-RvyX(6xkXlX)i=R0BUp@zi#eX#a zAFrX6#kX_*#~i>S>i_?ryRK)>pWj70v3~{i@{Q9czNd-B&tR-; zSS*Hu;cW5Wt6=bzE*4)u1OzuA7Ju6g1}|Y2-v`NE8~}^&f#fa>x1j$2yy_Ls#Py~A z1`QLJ=P%r;I{pcepiu$x)BDf=|2PVe|IUA?Z7~8I^xfBotCDzVdZ2}77B}txZ-L9= zMf3mL{H2w}Aw}Qsc;K?Q=*DM5n)$fmSMeEK4S|V|x;N9TA@FU}Eu3<{cbpq-?2TGH eYVoMWbOHdQ8*%-5D>;S$0000stbabP-I3XPmlhWHffCpM zzd&;0U#yAiCs3L;7J~r}iy>fGTYR}21bTnd#p3=pAh?X($NNEYi|FX%VIIfFf^b{> zCF&IIEPfS*$H&1Rfdp+zuK%lh&-}+#a{YHX`;}(3Q_k$;vv5@s7hT?`(9B}j|HsmB zSv+~;e}liYvPd}Sf5UQI7Uw^H>_Rgi*B{CGhs)v(+wyPF%;Nu#@U*#}|AMr+Mw_>z h7LQsyYVq*37y$WF)3nY!U<3dF002ovPDHLkV1noouCV|B diff --git a/tests/ref/math-lr-unbalanced.png b/tests/ref/math-lr-unbalanced.png index eff579ba4c7514d1653118d77bd93c35eb7979f5..022edc382ae3ef3f68d030d6911ad5797e0aebca 100644 GIT binary patch delta 941 zcmV;e15*5|2et>07k^|300000XdA2U000AhNkl75wAz_?l&GF%YKF^nmC5O1U+y6#twqFyl3)i~vAwTpWu67O;Q?{20L-ErNTW z%e4mPwd@lK=RirlfU<8DbDG05m3hxkH2}f@RAw6Oc7I(ci>)et-)mtVSwlu*kbonXG1ne22A&h`0w>g zV9^+s2l(NQwIb62NK= zHi~g^I)4dRzycQVV+a>wt$~9l#eXLOYXnQ*pUSiZp7r@A#g}VGz$4~Ih+hS`T7HC7 zRe*u1?Y7m20O=$E@E{MswDN!|@Qnn43ulYUc5_iFJu=Wk&HAMi%6&*fb`QA$NZe2d zuA0ezi0!ztcA#XrAFjo?Fv*J%x+FS(mY>?BCw~p*sw(i1i~Q%@dY#e#SS$tYka5hr zqHwG^zm0Op-6#bq{t`@Bz%`@_d~ONgq-*-$gS?&mF$}%(o_k&JUUOjW<#PErFn8Dw zjka`775H=kaN8&ick(LV#G$nGK7gS))Qt&J?I6R`t6+e_X;om;EKIujoikGl2D*c6 zrGKn8X#w784wm)GpVgzV-^UBXNGD*{qzXJbi1hs4XN@>*?_T3>H#5i1e{HG52hHi* zy?1w{QJl{1gIoN46$VFDfn_rQRQlvM%kUjaB5v?bD@$JrD81QNRKgu%`JRpZfKou3#Y~ P00000NkvXXu0mjfFEGXN delta 929 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 WIT7B5e}qN=0000HFcT%t zpv&BhI@XCgrB2X@;&dKB#yForImlVYxgsr-QaYfN^JxXz4GQgddD1@Nc@_H73$twN zCwY_imtXRJ^IW~p4*nrKBH{mS*n%bGsM6Vm6*jli+X0fL4S%yT_O+s6GJ(|7g+{wM ztgul$p~mG}0!B)lFYcD=2pB3E2V*$li3A|YM$qILAbEhGsqetHkANHNJN;Z7A!zh_ z7}!rh>RJ>eOoS1x%m=D-0lyEZ$^&Y$>v%*qfz4sD~tn2 z$Waw~7x_sYFgqb@PE-fNILgY*_JOD+ZJ>uRKVRGtI?-|Kqe+1((0(+IGL z@Fp+2n+C9iaHNS%O&vVv1$a#_JhKO+_y&Ma=e7*r2RP9iD!e>2!nVTm(IA~}MtU*q z1AqAG0Z2z02}M85JAmWSxmqB00Hn8pl}q=E2>8+p7^^^vFGsc1xuUuIc;CE?Y@#@Pb|*gpUm->n-yJOyyZ3B1pSE6u?Cl@f%h z)|7s<`}%lP-t=3+mx|8;nosJ6D}Mp*4SzSovm`dy6QHGMB^sf-Eana@9Am@UE;MWh zLCBsx@F#GgSvTxI0^PB(RdC9M;^-QSmDv^$T*>*6JcuG@i4AjC*$qN>OVlv%p-(qV z#S;+5Q&2sBfR_6_s;(3iPb-E9Fz56`e_0%VM``) z23=<6Kv^g1lrljhiknm9Fvj@|%0bRD4nl{tXv<+jDQAHdY!@iB@8y>#zx4ME=tD2e zvaPS=x%lLpJnzfr*~LF(S0wzu4coAVoGOD`SYc~713e(wx__`LbFdQ~iv^_qUUa&x zVTH}&2{m6fq8YDn{r$Gmgywn06qv&aPbUH?!*b4?15$_Noc$Jt52Lxcu{XfwanMYB z2SZ2Dq_6LR#OW}?wZ%YvArSBbbwxl!zMS<1z)>{bKonG!%UflIdfPYv#kFCC^V@*_ z+d$qAKtU_;bFqoFY2$KR_2{~2j?aJm?g%fr*@W=tuM1KPv>a0hnWfY|6&FC~Z!wTcX z8FH#Z-y$E0Nci6z_Op$^s|}a-v9*`UI}3X(0Anun{2i#=1L!rEvW3+gH;i82N!WLO z4Iy~}9kYIOdjy?Wwp1_zTybI?zUji7eG}G9sP4DH{nH?L%dZze9IB!+SiV~f%m58V z*U=P;@P9(AaoF<(Fq{IIhf#b65Cg)v)%_vlHK91@2Oi%6vcO-)J0SS3mH`~`7>0A| zWq2}RNUbUW@|iOgoy* zVShisPuUmI74wsT&jH%w48yg*02yQL@P8zQpwUF==wFRO=&Opp14}0fTHgoithfk5 zy;p>})NU9K9EZM`m^z4eqd2k7a&5j11b1pNq>iA7ULj~9oA*GdsoMP%_`q)%rsfd{ zQ)#H4Wuxc0h^jXYMPqdb8f&s0>`54hNuL1lUiPC?Y(eF}}G2#C6WK?NH`LXg#Q!azX0W~0S#J@E&Kog002ovPDHLk FV1h=v1Cjs$ diff --git a/tests/ref/math-nested-normal-layout.png b/tests/ref/math-nested-normal-layout.png index 8e7d2108319fe9567023b6db5a4535d9ce2bc53e..20db998ddd007e4904b47f896b016c1bdb10e91d 100644 GIT binary patch delta 915 zcmV;E18n@E3ZM#*BmusWC1ii{>*eP0;PL9^>+9>~@Zaj{>hSE%>gnj|>FMa`=;-L? z?&ja?=Hl(;-{$7#<>lr1+u!8mz}*Vfk7)z#J8(#z7)(!?6xwZq-C&(F`! z&d%h;yT976&CSi;$G6PP%#$AhFay88zmrb^6%Wa^rn$Mf#M+A za%p3elL8?aLqkJBK|wn^J1QzFB_$;!BqSpvBOo9klgR=bf1^`y=Kuf#9Z5t%RCwBB z7zLwX6pVsl4q_C=+30U8S7!R`&R`MxTO4=s(xr>5jOlN2*+merr@zH{AbSs6(cj_- zki~NiaL6($DACO3xrYzW#VN^NTuw8K4XmsTaLIAD&z#lINqsv_<{>~MHU)t_1*I#J z7zEgQeJPp%{JG-4sN;35cZotUzB|I(U}J`W)Ve`uE7OSbxU$<@J zni)_xh^(|X`fXwPm$4;C*eQ@itWtP;pxOp>zT{qnl#m7AS zFu+80dn3;wakfnLfmuvzU~-icxlT}p`m`P{Bry3vG7an%y(R}^R%2JO(~RZ5D%I^} zxoiVte{~CfKYsrOA5WXndumg0uai%YVqPZRnGLYH*E()Wp#D6 zW7r%J2&13HQHvP#^!4?H{U-wLZS|&~#j6{6JwwC8!}V?Rffnc6($8XkF$k^V47Au; pg<$}Ix3}$RcYPF$f>AKA006qJt~Ed|p5Oog002ovPDHLkV1iq6={o=b delta 898 zcmV-|1AY9U3ZV*+BmujTC1fGy@!;$0>*es@>gww7?9J-w=;`U{=;-F|=HKh);^*h* zk+2mE;^N}r;o;lm)!^XZkAuPfPjFbbyI2Gn_MG&x~zs0#AdyiSr-{NqP#q;%W$THbF(9Guf$Bxa%Dal@3 zPBV-3EG+bJ$#Hhfo->h?`gR&GK!7G}3IclzN>?T_2(VMxV#SAl_Z`iEz$>})`cqdd z3N$DAWh|18n$|4CN?n_ipPhs;-F9G+=Z`CCJd>5b`0iCR7~=c1sh++bUq|Ap#XxAm#Sq#u zA!*-%WwpWmoiiGd-Q`=shvk0&Ur>axF3d`a4%|nNr=x00_k= zxwO!mn;EMTBM%P`>U$NMPn@fkESXp3Ouy($*~w5@Q&Zy@Iu8Uw>1T1|Vg?;uU0q@S z$v}JCeCTIE@tP*ykg%{YUF$rc#d+5BvzT8DLMu4~Ep}F77y#hoV?Ekk9|fae6bvi? Y0RITDE);lF{Qv*}07*qoM6N<$f`;zYM*si- diff --git a/tests/ref/math-op-scripts-vs-limits.png b/tests/ref/math-op-scripts-vs-limits.png index 418974169c6ba9075fec67b894a6b1b489a80c99..ac1e019990844487477e61ce73304c8bcfb61002 100644 GIT binary patch delta 884 zcmV-)1B?9M2L1++7k@hl00000kv75c0009;NklP_`vKX||p38~7yGga7Z^M9$iz(SI7P(GL$Tb)8Xxj>_Ce z<3B$j?R~ED_pWB>>mgI}2L`^I&OS|%8%{M4nj>SK21b@uqGL}}Cotye;Kq}6CiCB|G9NYmoi^X^KwVZwY;B10>UyF~!Hy99ldTOQ})g=%lT?MmI!Ml??(`dg;z zaKyG^bxr9o8h9)F-W%XWVlUJGJ?rN`p2`ale;-%vs19C>R6Bnw z6;?K+8@f{Tr5SdG^I<0V1&xLacaonDNXlpb(=QRrTYp7IShx6gTs4dDrf6oj`M$J6 zP+6DuZaZXdw~;(pD%1$)FxjuoD>aUviq1B9`g>1a90Zn2iv1rpkK~y<^DkEHy49pN zT?3#!imUoq%xs-lMfbiJ_;FvvcHS}!71-SH?e*-Ol*Qxr!PBxSRmVFF(eb>*5Wwl$ zbA4)Gvwz}vg&hE6h@R+)1EoF-khC1V`GeZ@b*6&=%z^BXnHvJ2YUWtSVs38uB;h}` z(bQ;-erRbqXS8w4c&=!6`ZW4+p?6$HeOl0w@Uy1wBaH#p6t;)6O!F!;d%1W2l->Pm zAsAIpd{;bvVf6KsM1p) z10c4ia{|axx;Ri_rhktrbUBC4uvG#edLvy3;FrVc27m@V=IZRycb=J@*!=geg$u?Q&1LU6aBXnG0J3kJ?@j+)tFn$5{*E`b!({^1&}(Hfn58vX_9yhpItUHne~0000< KMNUMnLSTaVt-A04 delta 853 zcmV-b1FHP~2Hysd7k@km00000j5F~`0009fNklVhZr9vWlb7flQPlKuc@+grMHoLW?Nh>Ozm}Z*)GJd zSarOjA8hv9ifx*k;)xI`nmSqf0D#sOdwrOPy_TS(!hNu-ziMJGnlIwUBX_DWMHiyC z-YGz}44kyc#_C04(oe(~(p0|uTuraBV5afA5M}_grF%&3X{L%5 z8353ZbAL8`e0!ls2S{z3gWKh%hq>zmU=$*z2%}~IYWvSL1v4|lyXki*OH-j0`je$2 z8Kbp74Q7gFsYjtd7y7?z$WLEWgT#zl_f>nD+0%5>q50g(<=sg;9NXgQrO5?}cPAdB zr9SO+(_+@E8ddt{B4&2mGo2sU-G*^%+G%M4&VM{gs>qH3@WeWu2f$*OZUERMaw%U9 z+O>tUP8Nnz)8)-{mZX{fl^3}<3Mp+T=UM6pA1bEQonG2i*4&)6Y;=Xp zG*0=f@&HgbaebV!+*^;GKp>CP$7QBtR&Z-03S!oH(#`KWv~uL3mS z+a!q&ywJNz7*^g?53n&-Zy^g`Pa3+jWr2sQmMkow?9_`nC-!H(L}4HmPc$Z1jU)4M zi3WUTzu?)DU@(Ij9N2?(gbM63=T%dIfrft%U#N88=eTn^FqgJQ1>Un)bHRmcit&eh zeF|3*O!&%ZFn<``__cxI;o5Y9lS`SI_s^{WJ3Ju92L*oF{Mi-)YjSs`oJ2{-RqFs( zxgHEyD*ahG0PK{BPUDMh)P}EweYBR~P{$>4Z--XW-Q}8`?%#q|#uA_-oRbf&>Ane| zJJF#%-yL_>z`mcc^3o-b-9i5rJe8pdvEaN6S!%YfSRODKy+*~SR wTyTJHnYkO`p|+u0or}%y2Q0~M*si-07*qoM6N<$f-lSJumAu6 delta 527 zcmV+q0`UEm1gHd%B!BNoL_t(|+GF@XK!9P?;!%r7EgrRa04zRMKW%_~yry&1;^A)b z>h^)L_;h{Q-q8YN*joJKDiZifM~gqLM*?34B&h!mR2+MM92kpt=bRl7i(ejGu!7DO zuU>Ru?!W&_HutvB*W%qDYFEtsRk!W`=9sT^4r;&815$Dw|9@)t|G+?4T)(hm>8;U9 zV$|YMi$^WS0N<7`91x4Iwgu)7P*7KoT8sqs6=HlU6p_f4J(p=KudGJO`;^@hwku z;0qFd`|#_>hkw6+U;74D^s4*+RI@L>AHIJ8^51{@LJf<5+(ZZ9_S{90XSd@%CmbyY zD|-9we`+61Yo~{MV64L3n_vAiUH%8gS3gHfi~sos!=KufvCbt62xpd?{&)%xg8Ja*PNyIL|98Ir|37^)-7LN`@qcU3_uRjKOFe)5 ze-;&Tn~wEyz}x|8?0xwsbnE|7`di#txr*=Gfj;_M{Iq|~`L?B_Jqg+X07lB^%IpUN R@c;k-00>D%PDHLkV1gNG85;lq diff --git a/tests/ref/math-optical-size-prime-large-operator.png b/tests/ref/math-optical-size-prime-large-operator.png index b38a934edc49e7f7e9b87911ae7f448cb6483a15..c58f9d393c55c1a758a7b53cbf26c8e08e6a5a58 100644 GIT binary patch delta 665 zcmV;K0%rZK1+fK?B!75GL_t(|+U?iPOVe>2z;Urdhrpod&>=d6o$An`f1n`f5FILl z3OiWj!ADBxNF&qe5y>THgLMR2b*J$?~MIcV(a+* z{Stoff8cfbd_R1?87NeS3ahXRtFWT5`MO;hn4&Rx;pv22uz%0{Cy1Sm$pm{3WL4E{ zb^p%AtV}z7R%C)PWj-R=W^*C^q$;ophRNGIiPS)dRBuXjvx4E!$?Oef-to-79A#jd zGCP!kxw3zWZTW{%O$mmb*=uYoIGO6RSU7QWjx>3`gIKtHm>sn@!l%Kuda-a*BT_3N zk`30us%ySPD1SVnK`N4ily*E8AX4oW3D4b#CbkMWrgcYjNS&KET#l3twhM%#rw{(_ zP->cjN4p3uH8)SneF#`83T-~o3&bgnwBNiK>-7LYrLDKsEE?V{0lu286!*AakZdL% zwM0e3)-J=ittSHdSM7Q^clCo?zV$kz(SpI%T_7i%_kYeb+Y$gen5dHz9*deo(I$h{ z{XD{``h`(p6;|Q@3tm$Q_Mc~G%SFqg;NfGs+FD)Ng~Jvar}(RekD_2MJpsU8P8+hq z>&NW?d;oc2Mo$F!SKn%1hK0dA%q4As_sQl&!M>}c%`^0?&bQ;DV2z*jg*DJWQ;Hz? z@q#p5D}R*$@S}PlQRM>216=ij;J$tUrVDXbK%%vKgi4dG3&2_0`E22~Ab8=FR;w+~ zJ+sHe>!&Sdy$dk(^)vwJui5c@afpFqt5R|9Y8*5ejYAk{s{tqu=Ue62)%gzX#8_{f zz`d})7$ATXIp7EzuxJ{IuR>5g1XOsJ!9M{Kz!F*m8oXN)00000NkvXXu0mjf#!E!h delta 661 zcmV;G0&4xS1+4{;B!6^CL_t(|+U?ifOOsI?z;X4atLP?(ZX+)XyAcZUrk9K8s!|lg zz$+0`6f`J8gIs92EJ|IPE+uuov{1JcQ$%nkGfLN-FPXGq&X?Ro+1zw~L-rVAc6pwM z;Q9UoAI{;N-{C}{P!TGu!YZu7io(W}QDtDVE%L%XrQq{HnSWrHY6B+U{q$S8y~SRuNyYa6%%4%OD0lVA;Ohs!0?3Yrcn5M z(k4>*0m4-n0Dtr*k+Aowmv95uvXDx4zpum6Gb0cVU5t*~DfYzS$jHw~#npML92Zi` z1j0D%e;P+>Dzlh&zSm@57%8XkFN zbvaA>$Ivwy8s!`yj~{-q6%T#1VHj)9l@m^XF);UZ)_=8#fsS%H;n&`Z`6aEPclmx0 zo%)4QVHH+k6$aQ$WPh{Sv!dXEREl_C*_cEMO6#Qg@gA3Wf*=0fy+};Vm67D(x zSg-xGU4Oj*Y$bq=96_+X3;KpU`gGr-S%k2^|@LZeJDg|jizqX~N8-xE%Hb7B#(GNLhb-KNBq;&?UX8I05 vGquxlz?1ZcXld&4wE1zU@39L1XYg+}q*>?AmJTZ{00000NkvXXu0mjf*=#)` diff --git a/tests/ref/math-primes-complex.png b/tests/ref/math-primes-complex.png index 080b105d5603d6806e80e9b332bde8d6cef95354..2f5eafee68f3d54dcafe6f74fa88385fa133f472 100644 GIT binary patch delta 1655 zcmV--28j8G4TuepB!5pyL_t(|+U?d`Ok4LI!0~RAqD|VQO_R3AN$ta=NnNJxVQ-U~ zRn4SnWLmZcrlw1_G>~hT8|0>JB?%=h4G9EF5-v%=K$3FHg+K}kmk_|Y5o|6dfWsw( zfQ`X6#@F+0&#{e<-Ip{cwpG=i2Os+ZLqT||ln_>dP#4X}S*TtRvdh|05_E2EdbOl4;6(Ea3vcTi-tE5tSbc8+VieOd0WdXCgHZ8T+ca4K>V8;^$5<>{0Nn4dFO@|a4Z8fS*jIfY=7tzE=*#G1DXoY{=I^K-mn3d zy~MCS>p!lXF(jWRrlqB& zZd>^e2YW`5DR5;Af3i=i#*w)+p(wns%R?t#L$6H$Ozf;&$U|m!74yWAy1xyXIESL} zpG^d)mI;Vzoo-|b^l+-jLy(s1C(5tXq!6<3Qxzxq#n1 z@GODd4fhGK3&1i;PjnPn_BJXA-(H8Lb1kC7BOvww_E!P9)v1|t1eB)&C6`Ye)IB4; zSwXn!D3Z}@0IDWLk1_ym4)9eyw}0@{6biOh^NWQC#?y63Tmb4-H8nLAIf>uz^^HQw zhh6K>RA08GyjZyBcs??kOJ21+2W@@533XL;PN%Ab)aZl}VvE2xC@erIAtK>z7D5y^ zMnpE^IeagL5W-s#d5;Uq@G8Vdw-GOuGo0Zq zy-4~`f(Fs~Q~0(Z!WsSxJgRAIpj3dwet_LtrpntZXnY#R@SST1K=)VoCPXR^7>jm- zZvDg_GlVhRlVO69bi*DkK7ZCD-9QrWX~PfxjY>^k7MCjI2I01xIbcH zcYp^*<0BQm_1#}4i#ERf`|thS&PSt>3bT1hJ6?~soS~A4h3!jUox?{3kqguJ!-Ob3 z)dk-vicXk>9}Pq&Jonn~oKXo={RdZ6!j_drq7nA2))AHPj+^*AiEwE6L?J%Y+anpS z-G@jpYWMx@S_H!*KmGAfL_b)vWXUf)-=0Ol3HB$E4?9jayg!iN8u@TbIvpG*@?q!y z->i2Uq7ZJxkqnU!8#Y#yjmH-HSU47rg}-ItF90QJUniN?H4gv)002ovPDHLkV1kEE B6@>r* delta 1654 zcmV-+28sEI4TlYoB!5mxL_t(|+U?g{Y*Y6g!11aNd)r>Nz3!o!w5^l+uujv|N}DKE zD^(hVty)9HO4(ouBTX|F3ZWTWS%5NV0z?V90>!xi355hF0m>zYBu-2cAPKo&6moN} zPU5?Le7@B=z8zbY+Hzw1#h(Ws`!ApKefsPFJI@g#&&9*>@PBurbHpgTlSqf_2~z-$ zkJ`E;9X9PJ)WTtO)#q%e2+vgz#v&4~Y853jSM6eMr5-imqmATL)Dd*7ric(F7t8G^ z9jYq@4EG+Z3Lik5i<^MaU4(kH7dJ{>`_yhHo3`J)Xyn1gC%wpA^*yWZWBO+NEVYBy ziBqHP_}R7nW`Aq8zExG&ow)d~N`SdVytYgqEI`5W&dSajmW}ucn-}(!1E>n0JH&fa zjJfc;@(~~OA{j=OpTnP*7eShL5BMTdV#xtM4eVKRfNM-Yz zKwE+4kC#a9Lt|1A#)W|8{kg*c`2cPN*f5qg4dQ747j71k#!mp``EPM=?9Z82eMod( z(AERi6?{fcb>#yl8$M07ml}Z;3qFs|HfvBF7Ao>ZlspBWSWt3S6cEBu_=zJv2*ktB z7IwubJb!X9M&VpDVYWvhZ0_`Q5vJz~KG&|MA(>Da9ycNzy$LWq0mFOry#Nb#yu{n> z?soiANyfSiTh)c#XYoq!Mp7zppa)=*y)$@yI-6u8AhRi@siTV4R(0Xxd1Q_(WHV=Y zsJ%T28P;NaQrJFDfYWK?Y|WLYs0;rU7_7WLKYwOcEWqvh{~O)-+-fQtB1GP7DeA)$ ziM-zqA>os^it)1RfMDkKXVuD14ELW2PcZw zFn`=^@Le^{pUc-a6b2dr78A5C4ba{J;3&JX7GX3_B)Z0bn&hXV}L$=o`__PIu4$gEZ zh{EYf6#A^H!a4e3F5`9Ur4^^sI$8B<6Im#@U-vv@gDW*di$cP@s_@&h1ZAWk5sFq9 zQMh~$8nlEO$VXN9We-8_-_{UjKjTW9N@7I zz=RovWeva*t6<7><9f#Ms_W!EWPeXxfbqj6lWDHI?8QcaA-={4eoIG&?pEJ9_H5x> zS(zx*rF&(Fe6zaD%n@Pw__Kv^qDueARk1l1`*cs@gFrm|jo~~qVeU6YBP@JOxJ-9s z!wWEH77g z!r|H%?{cwapN;^-ug=kS?WiWg;rdHd%%)=kG!}3gcDd0fcOEpGh+z0; z@*phjx2MhkSODYJIv|-HGk-)fEPLF#m_Lbm0D27SW34z1w>fQv- delta 307 zcmV-30nGl_0@nhNEq^~t0$~8habHq%OVQ|{IMjQHc88K6XlZE>8q!HB>_}2_xa;D= z;aW0ls6dDyZ3qsvqzgI!+&A!sM;vZ?hM5Nefl*Z$Ud_tPtp(9Z*rKa5SG5r(ioucI9+*gHp@nju$CP+ z34oQ$oy&tWci!5)m_r{#BOh3$dJz0k`$itz%on!|F||DqRknfC>e22q0O`)+E|6vo zL~jz;XYF{RP{@k!8!0JzHXQb72~t#3lP7 zTNnvAW{4wW5@9+SxL>hMocu8I%dSHNp-kp1ZA?>(g}MQ1$3|I=LRt|>6|mBl>-D`| z8m`>sI`0w_6Vuo8`@HYI-+Uh22A+@&*>EwiCP(thobC6G-wcdRpy zEPlF6VMS-SG9Z~n7O4r!lE@Cv1lNfpYZ(bBB=m)cF8~W?!^lqD1(t@m++!x~b}Nv) zvJ8x$uTLUN90vGhoYnXP?0prWFZ=~SLF*7}VzmGTC817UC#@z9G}NA;c;3P%eq=xY z4fOv|%^}CeM1Ma!V*0|@djK|*)^Z!TlMl@7M>g94@PVs3 z=nEf80>?Ldl_$4S_HKYs$0V|EBEW?l2j(!*raN=2=^F#QIbGOqHG}{B=p(;a_yd?S z1N1f_3EcoJQjHoH$@lX=1Q6}Oy*hxY5{STo-EZ9o@PEixmB=Nf{DVC_DtzW8pjrSp z=mB_GzjN*6*tN(S+l%+WB$ens327r6$H3+W@Rb?aGj|9 z;9De>Uer7Mxt$atr*{aTr^^m$rJc0^e%EIxH65t}m^B>$v51nG&am~lWq^sF zM0r`aB57}AJ^wS)0KPASC_Q|(4~;sgp}UdQ59a#7hk;AHEfm%hc}5{cB~o* zC$^w+V;^80L}e36gC!~ccJpnH8eyVBH9KcARMFmdLZ`SDI3m)v*D*G{4a!No@fI5IF1Oi_NQMCGizel*FN)4eF6S9>u(pH}kk1rv6C5FmV0lO7hjf>G8=j)=` zg?a9J3xB{l^Jz#*6!eb(^jU_z=-)*OspFLN+ll zlvB8&`dieD#ojL{M5Cwr7$$!wduC9Zs@jF+${wMUqV-(D&&E*;ydHy~0ga-tuvP(L zsfgNpH)E7ey$_)ZatL=l?LZ|`_o|DPHaBXQ9=Fl+2V(nt5EaKpGvl`AFyZ5~@f^ao zPk+%kbQx)D`F7Oic7Y$>Dc%x5Z8I#f1SO>}l0*12D;gyMbPTqlZ+<6=T*75OGz!sE zZ%5x?%l``(p%E%Z&-O6->K)7+`L12cEqr^@gxb{$=Kw9wWF@*HSF1#PJEJv`+`|2} z%cvdx7#(BPPIUQ=mqDtea`ZubVMI6M@gMxBEg#N@^WppNAEr8h($ diff --git a/tests/ref/math-root-large-body.png b/tests/ref/math-root-large-body.png index 2036364711b1ff8c9e5ec0b2bee9d1937d1de680..f036757166a2c50dec168fdcb1f2db84b9ab0910 100644 GIT binary patch delta 1817 zcmV+!2j=*v4x0{;7k_&Q00000nwuM-000K!Nkl?_~6=j>T#IVU@N5;){yzgIhZ&dg_?-+$TX?CyDib?Su}9D`$U z3|=7Sg=98&ULU4DF%^Wq{ z=mnCH;G%_%y-5Ms@yCRB6>w+!HvTdnfOqZPvIH#Wm~JmV6cCfJSM}LC*P>l z02{X*^9Etucpx0^9|*yYn#Di4SHR#h%J33?G{F2K7g)KS!js^+g!~Rd@VDQ`9WDUw z2tx4AC4UHJ6qO2RLHfM)pzMJlNOvwlxD)$YjKMKD2FKv^n_o}&OTQb zY*0@)4JrTXkJ>g9%ohPJC|q5*?Lb;u_D8|&5#TQiRu{g}73HXhKGqDo`lLo9!1M38 zLA@jZYz=kOTHy~WrJ5taWH>>+qX6_9ja^#d_kW`q^(klMQM)dmvZEcIIzNSawRZuS zDY@m12>Z?FhBU(S=fQWb6ZNjP0#ILDYl;ZdP(Pv(ZfOHVIKrq+hEgKI^LGv%`g~R! zoV^eQJnY{O>Lmb7R+p7jk7$EafyeRb&WJEIMo{lCVwaCu&4o5_IuKt~*32($} zJb#<8pbhq(g1M}$tShKi*bLB;|Ga0BVC(03o!a0B*8sZbQLo}Q0QQD%hy+_2jkDU| z{Kr7S<6XTdva}4y*Er9+eG!#DmEpnG)*fweD!>p&@1m%C_J|BqpNL9r|A9S-ITO$Z z3jp+;N13Nnkl_XPm{8PJvd=;)VhyRN!+&mkU%%p>f7LXisHW+>3?E?6B#L~dx2A)m z9x?2kRE2Z@%D#u-xhysUfFBsY4A4?gU=LP@q|L3VmnzXRpDo* z&6Jpdr_+Jl;(KR-+FqD14RixwUOa4Dv5#+;ia7w6c*0Y{;qg+jhe{Y@Diwg|RDXJ) zK7iA&1L;0+4F?(rC=(A?B4(1|sbMh>@IX8vW5tT?!(tW{xc+8@qcwP%tV1T zdia&RYQ!oMZ=%-D8$r0!a0Qia>*%9*?$_lQLQpgg10o8P|LHyC&Bhdh3yew%`JXUf`4iL;0bE| zy-9-ohH~F(!n`KIJ2-tGwW{|@@SU= zqAz?^f=BXpP3j9fD-g4sn2bF{jlnTE2FKuag58d=oVvk6R#+u^!G8;<*J;#EZR-T5 zdkAHi)DcbrW62O=Wh45*{`dL+$IhSY4|f`P(L3-qFf0gL4+z3Do!~Pz=Vm@E>(YJ2 zDUeltIAwf2;N?2Zy1}O6wY^un^D6wu0Q_Qv%AZ(UxT#}5?*V<8flhk>TE~DC))pqY zg>qnFObo#D`(i!d*MFRSgTPpMpb8H#mj5o8bJrR+7Jb2rMt==8J!=cA_X97f$T@en z>juBlfk3FOGrGb5%W-88>ZL)qUU0*X&svci{>?T00000%2tXK000K=NkliF9c;59GNWA2~OLpNI@UX|@1Aw-Lg9+C-XbPtc!?ghTA%8By{_6D*wwjOH>d!hGR}oms zo5i}P;fX@-O6o8cUu-La01q$8T3g_s88%toE)!t6@g0UP3C_)at3(A{xolHZgb_vr zNQD2nCc^=1a8X!-R=g8s3n!#b?PI{Y7pxyi{ut5~m=@anw0++vH+SuJ4}LL5k% z!wqZF3>#TCr$DSZ2@GSJVHODb#DYm+ZhhKelEsM``T%^f=1P}pc%WB^O9D5SV1_n= zue-FgG^PgL&a&+8BB7uPm|AeP%mXzri(B*+NPma&qsb@OeatN2;Ga7%L+^pF{nDkj zhBrdhyTp27Xx>dot4kx?oItfYe)a zYN1eA`pew4wY8l3`?zJOel4iR%-_me(!H`DbkSvG0%^AwYT)G{aU0#B>-t81K7?|Q zgYHzO3^#7;o5eNevg#G)I{S@2aV|T4I)4vTf%2vG_*$5-XHA6?95){UgTzL<<#R)0 zMn*O0YTF>fH!6=sKo_xh;AIH?X;*@)o8HHl0NS!h69BN8g*dmPl`jUsk;S(?4!NxW zMq5cE0LHUOor&)fPCVVxF!IW97(mob0)XNDk4bR3g)amk6g78 z0M&+@=6b(Kj_OU*3Zo6|e4Oe9SEW_=#r2|lI zD@eiB(bQpfhd8&!`qD{IbuN_Q=zl;(AE@dA(+jTExl)I(KOt@PAae$Ej}Az4?Ik6( zpnBz?4EG*8c1*?b_TfQRK3Q|1%G@cKGJienyzvb!4heP;vK$g$1GCraGvWj`9b%IReDqze9-^qSwE|l6($bxk4)BqGZ^De{%br%mp4QvHp ztfDjor5aukpD=~Ptxza7FMlG!k;1C+YtKLtMS59)Y3*Gbpjd{O_tT8DL5hjsW4!M@gndNhOAS`xBp1%G=B?$B13IUyI( z5@s;np$-k;JDhVr?6R(tceac}ZJ>-f%dkjfvKTlmu1 zP1pv&Q%553z9w>s=YJN)w~aUgpu{QrpY%U_>Jr4}T81wcYKDpcVY-7RX)rea(<&@cvr;Umh48)?pp~Kf(V3%917T T6Geqk00000NkvXXu0mjf%GQnP diff --git a/tests/ref/math-root-large-index.png b/tests/ref/math-root-large-index.png index 8037222cb285f60c0a2deaedaf276bce72f70342..85689823da386744b879c83bb6d72ccd8706d8db 100644 GIT binary patch delta 636 zcmV-?0)zej1c(KY7k?)R00000;n&kF0006{Nklk=L$s*8srgKoROS-nT&bqkc?le2| zO}#zrj)M+Vhxr_z9}i#n@f@B!{*gR8Y=`Zz9sXa#a#_S`SbuW2attrNTMg^8u$QTHw%5-aCg1>4Ma#No# zVq~JvYM7Na9RqHouSunNh#gHcL&?}0sA&~PxC6K?%jbO3dV`Knu8A?7;Bwo@5Y;qfL} zw(HXV-V9$nWJ|4AsjRplmNE&adnTy-O%8)!LB356ZAe~l=wzFG`vj_FhA;R`!UG*s z^UpwotjR1qS_KU)S9rP$G;~B7u+a!OOv3u`P_!b&%zvw!Eky{_Q6K4Y7;s;0Y7&?9 z;phBkN;6FL+-SKXjAF%Ja3;*dqouIf3^SaQqhhyu1ls2z7%f!e2nK3lsSRd0qec-d zohH$Md3gI|1m_NrE+8pPc0*^;YXQ72WCYTQIQ0q;FC>Ym=u>kH^DteN@dtq8&8>}h zh%Qcgf^{(c`r7*;36aYgc?xP(PsNfSQQ*82Abs7f9U4~i>m|T9?H>ct$4j+9a&WCb z3?e!Ijtq>2#(jXOAObMn*Bd@GOhY+FPEBB>=Dy{xkFtvq2}8Sz{n@j_cGwR8HGcqn WM9-GHf)!){0000hgMkmbo!E{GrWHJW(IX~s~dISy&kVuLGBsYwo*DAe6b@Yd<(zwYycC5 zNjSyY!W-{W3V#BHX-)X+>yQ=b_bhJ96b(-eZ;>~@-?1(X(bnKkc-v8!Bw^`}KA%jJ zZYWF}2tqcH>&m!lpYI8sVN?u<05d8&NDM*yHtMELiExMV?%GX%at z&2Ywwiu`04r3O}OCyR%bt5#3fERw_J8bctpJD^5k@qdwjFd!_smiTNpZ42b=6Jss} zqs!p*0_Ni3+zeoX$I_X=Ed=v%2=>81{!n-jK{^Oh9oXf{aFejCH(!H#N{2^|DmdSP z%n3wEiv?_dK>kn*BKtAu{|vHn5=8QmqsNLAgot^}ggfl+{G z=gh&PVa{Ge!>@yP(NbBB78e4|@bqi6bf(eb^&5{pBW#3?@cs|~1$oy-@#2|JhX4Qo M07*qoM6N<$f><6WkN^Mx diff --git a/tests/ref/math-size.png b/tests/ref/math-size.png index b44e4c745866c112ca41b3596dfa70e896445ef4..ad1c98b0130d5ac3fe8464bc4764dedb7bdf8f8d 100644 GIT binary patch delta 704 zcmV;x0zduP1MP&8^38hd(!%RUN0;1US6k&nh+@-`9KZ{$6P@hufE*}CRGvDO1GsLjVUm5NOQ?D3 zLdj>CX_#7wr2Vaz=k#34gg{6fZ6Io$(wl^1mK6y=UW0GG1TFa)xop` z0naudasniaCxA^qfTss|zXDRov=qgigpg(0msAN~PXLW}0P7SWAKwBfk+RxFfRRyP ztPzl_CAjMZ+M=KmK4v$<>Q{ir4FEoX-9o1av2P4CgMaJ?-n8F?rFEdv=28jAZ*E?N zIRGkrAhz!eC(=mE02eq5%M$Qvq+h&$V4hbAr|YZc5goQRY>U}s<6uhchJF*G6r$)i zRKiFSqOD@;ZDQY92%H4bMve~&Z#AWUvBQSipPHRcZ`)T(^K%2S_a*hqo(}8q-@`+F zD6Mn_wSR@n%1|0h+CucQ+Wp=N%#}S+Ri^H10 ziP}M9vaJK9mWeBbre#gw&Oo0FL9O4G%J%?G;5p&(%RK;#M<8Bjy7f*gm^6C;c*WKX m7VS;xe=Z%?VI9`tKlKZDzKdOSYOvY>0000LR_6C?X2d5K)BCd`SZ%rlzS(DmBs!Gfhm*oZ?b9g>;*n4&SzL(@poP?R?vV z4|HL7YtO*W_v!}+&WCex&UsFep-$=We+%0>rlOgEmu`~1o_`5A*F-j%fG@=zd9%pc zjooR69rJ#r=3IQAOD#K*k$@rajusrPy?^G$?G#}{GAAOBK@>yuUCd7T@X0xbD6+b`>O&Ojs+&v^#^MKpAd@>* zI0I<=0Pbah9zt+>dU}N*uw*uyV=2OAb|7yAWZIc1z%3UrdlkUc0JM|~y$Hzp2JuEm zR7(-Q@!1U_8^GfdfH_3jdjK;4&kb{OdI~>5zzLL&Y=4p>oU7dj6QagA zBxi{=d=@0q>QC=a!g+^>ALV{!sTxrBcN%Trd;r;m17&cy;>loUjy_sKKV|ChNq~p! zAoBIb)qlcm#UQdbb*P1FDuakjZLLb-fYrK`n5vy+ALzJ*5FiegqfxT6I^@-O#ASzwEhDT=^CGb;M;~0SEgN=%v;sB_DO@o!) z0Pc%$P~Tg-rTlf8L8cf^nDtMl4(qTE>+s+D4FMej$_#0nHUIzs07*qoM6N<$f+7A_ ADF6Tf diff --git a/tests/ref/math-table.png b/tests/ref/math-table.png index 75d25f3e4010819d5f52316dda54be39b5f453bc..e8dfa982573e28777cf41f1bbee5f33febced82a 100644 GIT binary patch delta 741 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 qJ5WASEIrW5>2*xQG)%)s55E9?!6W+0000 Date: Fri, 11 Oct 2024 05:19:58 -0300 Subject: [PATCH 018/280] Fix hashing of equal decimals with different scales (#5179) Co-authored-by: Laurenz --- crates/typst/src/foundations/decimal.rs | 43 ++++++++++++++++++++++++- tests/suite/foundations/decimal.typ | 7 ++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/crates/typst/src/foundations/decimal.rs b/crates/typst/src/foundations/decimal.rs index 6329b445bf..f2cef59bf3 100644 --- a/crates/typst/src/foundations/decimal.rs +++ b/crates/typst/src/foundations/decimal.rs @@ -1,4 +1,5 @@ use std::fmt::{self, Display, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::Neg; use std::str::FromStr; @@ -88,7 +89,7 @@ use crate::World; /// to rounding. When those two operations do not surpass the digit limits, they /// are fully precise. #[ty(scope, cast)] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Decimal(rust_decimal::Decimal); impl Decimal { @@ -370,6 +371,22 @@ impl Neg for Decimal { } } +impl Hash for Decimal { + fn hash(&self, state: &mut H) { + // `rust_decimal`'s Hash implementation normalizes decimals before + // hashing them. This means decimals with different scales but + // equivalent value not only compare equal but also hash equally. Here, + // we hash all bytes explicitly to ensure the scale is also considered. + // This means that 123.314 == 123.31400, but 123.314.hash() != + // 123.31400.hash(). + // + // Note that this implies that equal decimals can have different hashes, + // which might generate problems with certain data structures, such as + // HashSet and HashMap. + self.0.serialize().hash(state); + } +} + /// A value that can be cast to a decimal. pub enum ToDecimal { /// A string with the decimal's representation. @@ -386,3 +403,27 @@ cast! { v: f64 => Self::Float(v), v: Str => Self::Str(EcoString::from(v)), } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::Decimal; + use crate::utils::hash128; + + #[test] + fn test_decimals_with_equal_scales_hash_identically() { + let a = Decimal::from_str("3.14").unwrap(); + let b = Decimal::from_str("3.14").unwrap(); + assert_eq!(a, b); + assert_eq!(hash128(&a), hash128(&b)); + } + + #[test] + fn test_decimals_with_different_scales_hash_differently() { + let a = Decimal::from_str("3.140").unwrap(); + let b = Decimal::from_str("3.14000").unwrap(); + assert_eq!(a, b); + assert_ne!(hash128(&a), hash128(&b)); + } +} diff --git a/tests/suite/foundations/decimal.typ b/tests/suite/foundations/decimal.typ index d5fd944493..bae0d2e6ae 100644 --- a/tests/suite/foundations/decimal.typ +++ b/tests/suite/foundations/decimal.typ @@ -31,6 +31,13 @@ // Error: 10-19 float is not a valid decimal: float.nan #decimal(float.nan) +--- decimal-scale-is-observable --- +// Ensure equal decimals with different scales produce different strings. +#let f1(x) = str(x) +#let f2(x) = f1(x) +#test(f2(decimal("3.140")), "3.140") +#test(f2(decimal("3.14000")), "3.14000") + --- decimal-repr --- // Test the `repr` function with decimals. #test(repr(decimal("12.0")), "decimal(\"12.0\")") From bb0e0894748e572b29957acefa661e5ed68d5a15 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 12 Oct 2024 14:01:31 +0200 Subject: [PATCH 019/280] Fix logical ordering of floats and footnotes (#5185) --- crates/typst/src/foundations/selector.rs | 4 +- crates/typst/src/introspection/counter.rs | 6 +- .../typst/src/introspection/introspector.rs | 389 +++++++++++------- crates/typst/src/introspection/location.rs | 6 +- crates/typst/src/introspection/tag.rs | 77 ++-- crates/typst/src/layout/container.rs | 6 +- crates/typst/src/layout/flow/collect.rs | 26 +- crates/typst/src/layout/flow/compose.rs | 67 +-- crates/typst/src/layout/flow/distribute.rs | 2 +- crates/typst/src/layout/flow/mod.rs | 15 +- crates/typst/src/layout/frame.rs | 21 +- crates/typst/src/layout/inline/line.rs | 60 ++- crates/typst/src/layout/pages/collect.rs | 11 +- crates/typst/src/layout/pages/finalize.rs | 2 +- crates/typst/src/layout/pages/mod.rs | 3 +- crates/typst/src/layout/place.rs | 9 +- crates/typst/src/lib.rs | 1 - crates/typst/src/realize.rs | 43 +- tests/ref/footnote-break-across-pages.png | Bin 5467 -> 5473 bytes .../footnote-nested-break-across-pages.png | Bin 0 -> 1324 bytes tests/ref/footnote-nested.png | Bin 2555 -> 2539 bytes tests/ref/issue-4966-figure-float-counter.png | Bin 0 -> 1127 bytes tests/ref/place-float-counter.png | Bin 683 -> 670 bytes tests/suite/layout/flow/footnote.typ | 7 +- tests/suite/layout/flow/place.typ | 1 - tests/suite/model/figure.typ | 22 + 26 files changed, 458 insertions(+), 320 deletions(-) create mode 100644 tests/ref/footnote-nested-break-across-pages.png create mode 100644 tests/ref/issue-4966-figure-float-counter.png diff --git a/crates/typst/src/foundations/selector.rs b/crates/typst/src/foundations/selector.rs index 3a9ab3082e..575baa134d 100644 --- a/crates/typst/src/foundations/selector.rs +++ b/crates/typst/src/foundations/selector.rs @@ -10,7 +10,7 @@ use crate::foundations::{ cast, func, repr, scope, ty, CastInfo, Content, Context, Dict, Element, FromValue, Func, Label, Reflect, Regex, Repr, Str, StyleChain, Type, Value, }; -use crate::introspection::{Introspector, Locatable, Location}; +use crate::introspection::{Introspector, Locatable, Location, Unqueriable}; use crate::symbols::Symbol; /// A helper macro to create a field selector used in [`Selector::Elem`] @@ -339,7 +339,7 @@ impl FromValue for LocatableSelector { fn validate(selector: &Selector) -> StrResult<()> { match selector { Selector::Elem(elem, _) => { - if !elem.can::() { + if !elem.can::() || elem.can::() { Err(eco_format!("{} is not locatable", elem.name()))? } } diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index ba126e180c..38da6363e8 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -12,7 +12,7 @@ use crate::foundations::{ Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Smart, Str, StyleChain, Value, }; -use crate::introspection::{Introspector, Locatable, Location}; +use crate::introspection::{Introspector, Locatable, Location, Tag}; use crate::layout::{Frame, FrameItem, PageElem}; use crate::math::EquationElem; use crate::model::{FigureElem, FootnoteElem, HeadingElem, Numbering, NumberingPattern}; @@ -821,8 +821,8 @@ impl ManualPageCounter { for (_, item) in page.items() { match item { FrameItem::Group(group) => self.visit(engine, &group.frame)?, - FrameItem::Tag(tag) => { - let Some(elem) = tag.elem().to_packed::() else { + FrameItem::Tag(Tag::Start(elem)) => { + let Some(elem) = elem.to_packed::() else { continue; }; if *elem.key() == CounterKey::Page { diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst/src/introspection/introspector.rs index 4530776894..113f9afe07 100644 --- a/crates/typst/src/introspection/introspector.rs +++ b/crates/typst/src/introspection/introspector.rs @@ -1,16 +1,15 @@ -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::num::NonZeroUsize; use std::sync::RwLock; -use ecow::{eco_format, EcoVec}; -use indexmap::IndexMap; +use ecow::EcoVec; use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::introspection::{Location, TagKind}; +use crate::introspection::{Location, Tag}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; use crate::utils::NonZeroExt; @@ -20,16 +19,20 @@ use crate::utils::NonZeroExt; pub struct Introspector { /// The number of pages in the document. pages: usize, - /// All introspectable elements. - elems: IndexMap, - /// Maps labels to their indices in the element list. We use a smallvec such - /// that if the label is unique, we don't need to allocate. - labels: HashMap>, - /// Maps from element keys to the locations of all elements that had this - /// key. Used for introspector-assisted location assignment. - keys: HashMap>, /// The page numberings, indexed by page number minus 1. page_numberings: Vec>, + + /// All introspectable elements. + elems: Vec, + /// Lists all elements with a specific hash key. This is used for + /// introspector-assisted location assignment during measurement. + keys: MultiMap, + + /// Accelerates lookup of elements by location. + locations: HashMap, + /// Accelerates lookup of elements by label. + labels: MultiMap, + /// Caches queries done on the introspector. This is important because /// even if all top-level queries are distinct, they often have shared /// subqueries. Example: Individual counter queries with `before` that @@ -37,81 +40,56 @@ pub struct Introspector { queries: QueryCache, } +/// A pair of content and its position. +type Pair = (Content, Position); + impl Introspector { - /// Applies new frames in-place, reusing the existing allocations. + /// Creates an introspector for a page list. #[typst_macros::time(name = "introspect")] - pub fn rebuild(&mut self, pages: &[Page]) { - self.pages = pages.len(); - self.elems.clear(); - self.labels.clear(); - self.keys.clear(); - self.page_numberings.clear(); - self.queries.clear(); + pub fn new(pages: &[Page]) -> Self { + IntrospectorBuilder::new().build(pages) + } - for (i, page) in pages.iter().enumerate() { - let page_nr = NonZeroUsize::new(1 + i).unwrap(); - self.extract(&page.frame, page_nr, Transform::identity()); - self.page_numberings.push(page.numbering.clone()); - } + /// Iterates over all locatable elements. + pub fn all(&self) -> impl Iterator + '_ { + self.elems.iter().map(|(c, _)| c) } - /// Extract metadata from a frame. - fn extract(&mut self, frame: &Frame, page: NonZeroUsize, ts: Transform) { - for (pos, item) in frame.items() { - match item { - FrameItem::Group(group) => { - let ts = ts - .pre_concat(Transform::translate(pos.x, pos.y)) - .pre_concat(group.transform); - self.extract(&group.frame, page, ts); - } - FrameItem::Tag(tag) - if tag.kind() == TagKind::Start - && !self.elems.contains_key(&tag.location()) => - { - let pos = pos.transform(ts); - let loc = tag.location(); - let ret = self - .elems - .insert(loc, (tag.elem().clone(), Position { page, point: pos })); - assert!(ret.is_none(), "duplicate locations"); - - // Build the key map. - self.keys.entry(tag.key()).or_default().push(loc); - - // Build the label cache. - if let Some(label) = tag.elem().label() { - self.labels.entry(label).or_default().push(self.elems.len() - 1); - } - } - _ => {} - } - } + /// Retrieves the element with the given index. + #[track_caller] + fn get_by_idx(&self, idx: usize) -> &Content { + &self.elems[idx].0 } - /// Iterate over all locatable elements. - pub fn all(&self) -> impl Iterator + '_ { - self.elems.values().map(|(c, _)| c) + /// Retrieves the position of the element with the given index. + #[track_caller] + fn get_pos_by_idx(&self, idx: usize) -> Position { + self.elems[idx].1 } - /// Perform a binary search for `elem` among the `list`. - fn binary_search(&self, list: &[Content], elem: &Content) -> Result { - list.binary_search_by_key(&self.elem_index(elem), |elem| self.elem_index(elem)) + /// Retrieves an element by its location. + fn get_by_loc(&self, location: &Location) -> Option<&Content> { + self.locations.get(location).map(|&idx| self.get_by_idx(idx)) } - /// Get an element by its location. - fn get(&self, location: &Location) -> Option<&Content> { - self.elems.get(location).map(|(elem, _)| elem) + /// Retrieves the position of the element with the given index. + fn get_pos_by_loc(&self, location: &Location) -> Option { + self.locations.get(location).map(|&idx| self.get_pos_by_idx(idx)) } - /// Get the index of this element among all. + /// Performs a binary search for `elem` among the `list`. + fn binary_search(&self, list: &[Content], elem: &Content) -> Result { + list.binary_search_by_key(&self.elem_index(elem), |elem| self.elem_index(elem)) + } + + /// Gets the index of this element. fn elem_index(&self, elem: &Content) -> usize { self.loc_index(&elem.location().unwrap()) } - /// Get the index of the element with this location among all. + /// Gets the index of the element with this location among all. fn loc_index(&self, location: &Location) -> usize { - self.elems.get_index_of(location).unwrap_or(usize::MAX) + self.locations.get(location).copied().unwrap_or(usize::MAX) } } @@ -125,20 +103,50 @@ impl Introspector { } let output = match selector { - Selector::Label(label) => self - .labels - .get(label) - .map(|indices| { - indices.iter().map(|&index| self.elems[index].0.clone()).collect() - }) - .unwrap_or_default(), - Selector::Elem(..) | Selector::Can(_) => self + Selector::Elem(..) => self .all() .filter(|elem| selector.matches(elem, None)) .cloned() .collect(), Selector::Location(location) => { - self.get(location).cloned().into_iter().collect() + self.get_by_loc(location).cloned().into_iter().collect() + } + Selector::Label(label) => self + .labels + .get(label) + .iter() + .map(|&idx| self.get_by_idx(idx).clone()) + .collect(), + Selector::Or(selectors) => selectors + .iter() + .flat_map(|sel| self.query(sel)) + .map(|elem| self.elem_index(&elem)) + .collect::>() + .into_iter() + .map(|idx| self.get_by_idx(idx).clone()) + .collect(), + Selector::And(selectors) => { + let mut results: Vec<_> = + selectors.iter().map(|sel| self.query(sel)).collect(); + + // Extract the smallest result list and then keep only those + // elements in the smallest list that are also in all other + // lists. + results + .iter() + .enumerate() + .min_by_key(|(_, vec)| vec.len()) + .map(|(i, _)| i) + .map(|i| results.swap_remove(i)) + .iter() + .flatten() + .filter(|candidate| { + results + .iter() + .all(|other| self.binary_search(other, candidate).is_ok()) + }) + .cloned() + .collect() } Selector::Before { selector, end, inclusive } => { let mut list = self.query(selector); @@ -168,39 +176,8 @@ impl Introspector { } list } - Selector::And(selectors) => { - let mut results: Vec<_> = - selectors.iter().map(|sel| self.query(sel)).collect(); - - // Extract the smallest result list and then keep only those - // elements in the smallest list that are also in all other - // lists. - results - .iter() - .enumerate() - .min_by_key(|(_, vec)| vec.len()) - .map(|(i, _)| i) - .map(|i| results.swap_remove(i)) - .iter() - .flatten() - .filter(|candidate| { - results - .iter() - .all(|other| self.binary_search(other, candidate).is_ok()) - }) - .cloned() - .collect() - } - Selector::Or(selectors) => selectors - .iter() - .flat_map(|sel| self.query(sel)) - .map(|elem| self.elem_index(&elem)) - .collect::>() - .into_iter() - .map(|index| self.elems[index].0.clone()) - .collect(), // Not supported here. - Selector::Regex(_) => EcoVec::new(), + Selector::Can(_) | Selector::Regex(_) => EcoVec::new(), }; self.queries.insert(hash, output.clone()); @@ -210,12 +187,12 @@ impl Introspector { /// Query for the first element that matches the selector. pub fn query_first(&self, selector: &Selector) -> Option { match selector { - Selector::Location(location) => self.get(location).cloned(), + Selector::Location(location) => self.get_by_loc(location).cloned(), Selector::Label(label) => self .labels .get(label) - .and_then(|indices| indices.first()) - .map(|&index| self.elems[index].0.clone()), + .first() + .map(|&idx| self.get_by_idx(idx).clone()), _ => self.query(selector).first().cloned(), } } @@ -224,7 +201,7 @@ impl Introspector { pub fn query_unique(&self, selector: &Selector) -> StrResult { match selector { Selector::Location(location) => self - .get(location) + .get_by_loc(location) .cloned() .ok_or_else(|| "element does not exist in the document".into()), Selector::Label(label) => self.query_label(*label).cloned(), @@ -243,15 +220,11 @@ impl Introspector { /// Query for a unique element with the label. pub fn query_label(&self, label: Label) -> StrResult<&Content> { - let indices = self.labels.get(&label).ok_or_else(|| { - eco_format!("label `{}` does not exist in the document", label.repr()) - })?; - - if indices.len() > 1 { - bail!("label `{}` occurs multiple times in the document", label.repr()); + match *self.labels.get(&label) { + [idx] => Ok(self.get_by_idx(idx)), + [] => bail!("label `{}` does not exist in the document", label.repr()), + _ => bail!("label `{}` occurs multiple times in the document", label.repr()), } - - Ok(&self.elems[indices[0]].0) } /// This is an optimized version of @@ -259,7 +232,7 @@ impl Introspector { pub fn query_count_before(&self, selector: &Selector, end: Location) -> usize { // See `query()` for details. let list = self.query(selector); - if let Some(end) = self.get(&end) { + if let Some(end) = self.get_by_loc(&end) { match self.binary_search(&list, end) { Ok(i) => i + 1, Err(i) => i, @@ -274,14 +247,6 @@ impl Introspector { NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE) } - /// Gets the page numbering for the given location, if any. - pub fn page_numbering(&self, location: Location) -> Option<&Numbering> { - let page = self.page(location); - self.page_numberings - .get(page.get() - 1) - .and_then(|slot| slot.as_ref()) - } - /// Find the page number for the given location. pub fn page(&self, location: Location) -> NonZeroUsize { self.position(location).page @@ -289,12 +254,18 @@ impl Introspector { /// Find the position for the given location. pub fn position(&self, location: Location) -> Position { - self.elems - .get(&location) - .map(|&(_, pos)| pos) + self.get_pos_by_loc(&location) .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() }) } + /// Gets the page numbering for the given location, if any. + pub fn page_numbering(&self, location: Location) -> Option<&Numbering> { + let page = self.page(location); + self.page_numberings + .get(page.get() - 1) + .and_then(|slot| slot.as_ref()) + } + /// Try to find a location for an element with the given `key` hash /// that is closest after the `anchor`. /// @@ -304,7 +275,7 @@ impl Introspector { pub fn locator(&self, key: u128, anchor: Location) -> Option { let anchor = self.loc_index(&anchor); self.keys - .get(&key)? + .get(&key) .iter() .copied() .min_by_key(|loc| self.loc_index(loc).wrapping_sub(anchor)) @@ -317,6 +288,33 @@ impl Debug for Introspector { } } +/// A map from one keys to multiple elements. +#[derive(Clone)] +struct MultiMap(HashMap>); + +impl MultiMap +where + K: Hash + Eq, +{ + fn get(&self, key: &K) -> &[V] { + self.0.get(key).map_or(&[], |vec| vec.as_slice()) + } + + fn insert(&mut self, key: K, value: V) { + self.0.entry(key).or_default().push(value); + } + + fn take(&mut self, key: &K) -> Option> { + self.0.remove(key).map(|vec| vec.into_iter()) + } +} + +impl Default for MultiMap { + fn default() -> Self { + Self(HashMap::new()) + } +} + /// Caches queries. #[derive(Default)] struct QueryCache(RwLock>>); @@ -329,10 +327,6 @@ impl QueryCache { fn insert(&self, hash: u128, output: EcoVec) { self.0.write().unwrap().insert(hash, output); } - - fn clear(&mut self) { - self.0.get_mut().unwrap().clear(); - } } impl Clone for QueryCache { @@ -340,3 +334,120 @@ impl Clone for QueryCache { Self(RwLock::new(self.0.read().unwrap().clone())) } } + +/// Builds the introspector. +#[derive(Default)] +struct IntrospectorBuilder { + page_numberings: Vec>, + seen: HashSet, + insertions: MultiMap>, + keys: MultiMap, + locations: HashMap, + labels: MultiMap, +} + +impl IntrospectorBuilder { + /// Create an empty builder. + fn new() -> Self { + Self::default() + } + + /// Build the introspector. + fn build(mut self, pages: &[Page]) -> Introspector { + self.page_numberings.reserve(pages.len()); + + // Discover all elements. + let mut root = Vec::new(); + for (i, page) in pages.iter().enumerate() { + self.page_numberings.push(page.numbering.clone()); + self.discover( + &mut root, + &page.frame, + NonZeroUsize::new(1 + i).unwrap(), + Transform::identity(), + ); + } + + self.locations.reserve(self.seen.len()); + + // Save all pairs and their descendants in the correct order. + let mut elems = Vec::with_capacity(self.seen.len()); + for pair in root { + self.visit(&mut elems, pair); + } + + Introspector { + pages: pages.len(), + page_numberings: self.page_numberings, + elems, + keys: self.keys, + locations: self.locations, + labels: self.labels, + queries: QueryCache::default(), + } + } + + /// Processes the tags in the frame. + fn discover( + &mut self, + sink: &mut Vec, + frame: &Frame, + page: NonZeroUsize, + ts: Transform, + ) { + for (pos, item) in frame.items() { + match item { + FrameItem::Group(group) => { + let ts = ts + .pre_concat(Transform::translate(pos.x, pos.y)) + .pre_concat(group.transform); + + if let Some(parent) = group.parent { + let mut nested = vec![]; + self.discover(&mut nested, &group.frame, page, ts); + self.insertions.insert(parent, nested); + } else { + self.discover(sink, &group.frame, page, ts); + } + } + FrameItem::Tag(Tag::Start(elem)) => { + let loc = elem.location().unwrap(); + if self.seen.insert(loc) { + let point = pos.transform(ts); + sink.push((elem.clone(), Position { page, point })); + } + } + FrameItem::Tag(Tag::End(loc, key)) => { + self.keys.insert(*key, *loc); + } + _ => {} + } + } + } + + /// Saves a pair and all its descendants into `elems` and populates the + /// acceleration structures. + fn visit(&mut self, elems: &mut Vec, pair: Pair) { + let elem = &pair.0; + let loc = elem.location().unwrap(); + let idx = elems.len(); + + // Populate the location acceleration map. + self.locations.insert(loc, idx); + + // Populate the label acceleration map. + if let Some(label) = elem.label() { + self.labels.insert(label, idx); + } + + // Save the element. + elems.push(pair); + + // Process potential descendants. + if let Some(insertions) = self.insertions.take(&loc) { + for pair in insertions.flatten() { + self.visit(elems, pair); + } + } + } +} diff --git a/crates/typst/src/introspection/location.rs b/crates/typst/src/introspection/location.rs index 70076bcaaa..8a7063fc1b 100644 --- a/crates/typst/src/introspection/location.rs +++ b/crates/typst/src/introspection/location.rs @@ -105,5 +105,9 @@ impl Repr for Location { } } -/// Makes this element locatable through `engine.locate`. +/// Makes this element as locatable through the introspector. pub trait Locatable {} + +/// Marks this element as not being queryable even though it is locatable for +/// internal reasons. +pub trait Unqueriable {} diff --git a/crates/typst/src/introspection/tag.rs b/crates/typst/src/introspection/tag.rs index 7cdea40325..b2bae28e40 100644 --- a/crates/typst/src/introspection/tag.rs +++ b/crates/typst/src/introspection/tag.rs @@ -7,79 +7,48 @@ use crate::foundations::{ }; use crate::introspection::Location; -/// Holds a locatable element that was realized. +/// Marks the start or end of a locatable element. #[derive(Clone, PartialEq, Hash)] -pub struct Tag { - /// Whether this is a start or end tag. - kind: TagKind, - /// The introspectible element. - elem: Content, - /// The element's key hash. - key: u128, +pub enum Tag { + /// The stored element starts here. + /// + /// Content placed in a tag **must** have a [`Location`] or there will be + /// panics. + Start(Content), + /// The element with the given location and key hash ends here. + /// + /// Note: The key hash is stored here instead of in `Start` simply to make + /// the two enum variants more balanced in size, keeping a `Tag`'s memory + /// size down. There are no semantic reasons for this. + End(Location, u128), } impl Tag { - /// Create a start tag from an element and its key hash. - /// - /// Panics if the element does not have a [`Location`]. - #[track_caller] - pub fn new(elem: Content, key: u128) -> Self { - assert!(elem.location().is_some()); - Self { elem, key, kind: TagKind::Start } - } - - /// Returns the same tag with the given kind. - pub fn with_kind(self, kind: TagKind) -> Self { - Self { kind, ..self } - } - - /// Whether this is a start or end tag. - pub fn kind(&self) -> TagKind { - self.kind - } - - /// The locatable element that the tag holds. - pub fn elem(&self) -> &Content { - &self.elem - } - - /// Access the location of the element. + /// Access the location of the tag. pub fn location(&self) -> Location { - self.elem.location().unwrap() - } - - /// The element's key hash, which forms the base of its location (but is - /// locally disambiguated and combined with outer hashes). - /// - /// We need to retain this for introspector-assisted location assignment - /// during measurement. - pub fn key(&self) -> u128 { - self.key + match self { + Tag::Start(elem) => elem.location().unwrap(), + Tag::End(loc, _) => *loc, + } } } impl Debug for Tag { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Tag({:?}, {:?})", self.kind, self.elem.elem().name()) + match self { + Tag::Start(elem) => write!(f, "Start({:?})", elem.elem().name()), + Tag::End(..) => f.pad("End"), + } } } -/// Determines whether a tag marks the start or end of an element. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum TagKind { - /// The tag indicates that the element starts here. - Start, - /// The tag indicates that the element end here. - End, -} - /// Holds a tag for a locatable element that was realized. /// /// The `TagElem` is handled by all layouters. The held element becomes /// available for introspection in the next compiler iteration. #[elem(Construct, Unlabellable)] pub struct TagElem { - /// The introspectible element. + /// The introspectable element. #[required] #[internal] pub tag: Tag, diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs index cc1559c666..d97edd5a97 100644 --- a/crates/typst/src/layout/container.rs +++ b/crates/typst/src/layout/container.rs @@ -190,7 +190,7 @@ impl Packed { // Assign label to the frame. if let Some(label) = self.label() { - frame.group(|group| group.label = Some(label)) + frame.label(label); } // Apply baseline shift. Do this after setting the size and applying the @@ -562,7 +562,7 @@ impl Packed { // Assign label to each frame in the fragment. if let Some(label) = self.label() { - frame.group(|group| group.label = Some(label)); + frame.label(label); } Ok(frame) @@ -723,7 +723,7 @@ impl Packed { // Assign label to each frame in the fragment. if let Some(label) = self.label() { for frame in fragment.iter_mut() { - frame.group(|group| group.label = Some(label)) + frame.label(label); } } diff --git a/crates/typst/src/layout/flow/collect.rs b/crates/typst/src/layout/flow/collect.rs index efb16427f4..ffb45fda2a 100644 --- a/crates/typst/src/layout/flow/collect.rs +++ b/crates/typst/src/layout/flow/collect.rs @@ -11,7 +11,7 @@ use crate::diag::{bail, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{Packed, Resolve, Smart, StyleChain}; use crate::introspection::{ - Introspector, Locator, LocatorLink, SplitLocator, Tag, TagElem, + Introspector, Location, Locator, LocatorLink, SplitLocator, Tag, TagElem, }; use crate::layout::{ layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, ColbreakElem, @@ -62,7 +62,7 @@ struct Collector<'a, 'x, 'y> { impl<'a> Collector<'a, '_, '_> { /// Perform the collection. fn run(mut self) -> SourceResult>> { - for (idx, &(child, styles)) in self.children.iter().enumerate() { + for &(child, styles) in self.children { if let Some(elem) = child.to_packed::() { self.output.push(Child::Tag(&elem.tag)); } else if let Some(elem) = child.to_packed::() { @@ -72,7 +72,7 @@ impl<'a> Collector<'a, '_, '_> { } else if let Some(elem) = child.to_packed::() { self.block(elem, styles); } else if let Some(elem) = child.to_packed::() { - self.place(idx, elem, styles)?; + self.place(elem, styles)?; } else if child.is::() { self.output.push(Child::Flush); } else if let Some(elem) = child.to_packed::() { @@ -220,7 +220,6 @@ impl<'a> Collector<'a, '_, '_> { /// Collects a placed element into a [`PlacedChild`]. fn place( &mut self, - idx: usize, elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { @@ -257,7 +256,6 @@ impl<'a> Collector<'a, '_, '_> { let clearance = elem.clearance(styles); let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); self.output.push(Child::Placed(self.boxed(PlacedChild { - idx, align_x, align_y, scope, @@ -553,7 +551,6 @@ impl MultiSpill<'_, '_> { /// A child that encapsulates a prepared placed element. #[derive(Debug)] pub struct PlacedChild<'a> { - pub idx: usize, pub align_x: FixedAlignment, pub align_y: Smart>, pub scope: PlacementScope, @@ -573,16 +570,27 @@ impl PlacedChild<'_> { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); - layout_frame( + + let mut frame = layout_frame( engine, &self.elem.body, self.locator.relayout(), self.styles.chain(&aligned), Region::new(base, Axes::splat(false)), - ) - .map(|frame| frame.post_processed(self.styles)) + )?; + + if self.float { + frame.set_parent(self.elem.location().unwrap()); + } + + Ok(frame.post_processed(self.styles)) }) } + + /// The element's location. + pub fn location(&self) -> Location { + self.elem.location().unwrap() + } } /// Wraps a parameterized computation and caches its latest output. diff --git a/crates/typst/src/layout/flow/compose.rs b/crates/typst/src/layout/flow/compose.rs index 6f14618ee5..3c52af3822 100644 --- a/crates/typst/src/layout/flow/compose.rs +++ b/crates/typst/src/layout/flow/compose.rs @@ -1,18 +1,16 @@ use std::num::NonZeroUsize; -use super::{ - distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Skip, Stop, Work, -}; +use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{Content, NativeElement, Packed, Resolve, Smart}; use crate::introspection::{ - Counter, CounterDisplayElem, CounterState, CounterUpdate, Locator, SplitLocator, - TagKind, + Counter, CounterDisplayElem, CounterState, CounterUpdate, Location, Locator, + SplitLocator, Tag, }; use crate::layout::{ - layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Frame, FrameItem, - OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size, + layout_fragment, layout_frame, Abs, Axes, Dir, FixedAlignment, Fragment, Frame, + FrameItem, OuterHAlignment, PlacementScope, Point, Region, Regions, Rel, Size, }; use crate::model::{ FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker, @@ -246,7 +244,8 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { clearance: bool, ) -> FlowResult<()> { // If the float is already processed, skip it. - if self.skipped(Skip::Placed(placed.idx)) { + let loc = placed.location(); + if self.skipped(loc) { return Ok(()); } @@ -317,7 +316,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { // Put the float there. area.push_float(placed, frame, align_y); - area.skips.push(Skip::Placed(placed.idx)); + area.skips.push(loc); // Trigger relayout. Err(Stop::Relayout(placed.scope)) @@ -391,7 +390,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { ) -> FlowResult<()> { // Ignore reference footnotes and already processed ones. let loc = elem.location().unwrap(); - if elem.is_ref() || self.skipped(Skip::Footnote(loc)) { + if elem.is_ref() || self.skipped(loc) { return Ok(()); } @@ -420,14 +419,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { pod.size.y -= flow_need + separator_need + self.config.footnote.gap; // Layout the footnote entry. - let frames = layout_fragment( - self.engine, - &FootnoteEntry::new(elem.clone()).pack(), - Locator::synthesize(elem.location().unwrap()), - self.config.shared, - pod, - )? - .into_frames(); + let frames = layout_footnote(self.engine, self.config, &elem, pod)?.into_frames(); // Find nested footnotes in the entry. let nested = find_in_frames::(&frames); @@ -458,7 +450,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { // Save the footnote's frame. area.push_footnote(self.config, first); - area.skips.push(Skip::Footnote(loc)); + area.skips.push(loc); regions.size.y -= note_need; // Save the spill. @@ -501,10 +493,10 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { /// Checks whether an insertion was already processed and doesn't need to be /// handled again. - fn skipped(&self, skip: Skip) -> bool { - self.work.skips.contains(&skip) - || self.page_insertions.skips.contains(&skip) - || self.column_insertions.skips.contains(&skip) + fn skipped(&self, loc: Location) -> bool { + self.work.skips.contains(&loc) + || self.page_insertions.skips.contains(&loc) + || self.column_insertions.skips.contains(&loc) } /// The amount of width needed by insertions. @@ -528,6 +520,29 @@ fn layout_footnote_separator( ) } +/// Lay out a footnote. +fn layout_footnote( + engine: &mut Engine, + config: &Config, + elem: &Packed, + pod: Regions, +) -> SourceResult { + let loc = elem.location().unwrap(); + layout_fragment( + engine, + &FootnoteEntry::new(elem.clone()).pack(), + Locator::synthesize(loc), + config.shared, + pod, + ) + .map(|mut fragment| { + for frame in &mut fragment { + frame.set_parent(loc); + } + fragment + }) +} + /// An additive list of insertions. #[derive(Default)] struct Insertions<'a, 'b> { @@ -538,7 +553,7 @@ struct Insertions<'a, 'b> { top_size: Abs, bottom_size: Abs, width: Abs, - skips: Vec, + skips: Vec, } impl<'a, 'b> Insertions<'a, 'b> { @@ -836,8 +851,8 @@ fn find_in_frame_impl( let y = y_offset + pos.y; match item { FrameItem::Group(group) => find_in_frame_impl(output, &group.frame, y), - FrameItem::Tag(tag) if tag.kind() == TagKind::Start => { - if let Some(elem) = tag.elem().to_packed::() { + FrameItem::Tag(Tag::Start(elem)) => { + if let Some(elem) = elem.to_packed::() { output.push((y, elem.clone())); } } diff --git a/crates/typst/src/layout/flow/distribute.rs b/crates/typst/src/layout/flow/distribute.rs index a738b3e67d..eeb4e76f2f 100644 --- a/crates/typst/src/layout/flow/distribute.rs +++ b/crates/typst/src/layout/flow/distribute.rs @@ -20,7 +20,7 @@ pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult true, + Ok(()) => distributor.composer.work.done(), Err(Stop::Finish(forced)) => forced, Err(err) => return Err(err), }; diff --git a/crates/typst/src/layout/flow/mod.rs b/crates/typst/src/layout/flow/mod.rs index 5db70ecb28..66ec8e97c4 100644 --- a/crates/typst/src/layout/flow/mod.rs +++ b/crates/typst/src/layout/flow/mod.rs @@ -255,18 +255,7 @@ struct Work<'a, 'b> { /// Identifies floats and footnotes that can be skipped if visited because /// they were already handled and incorporated as column or page level /// insertions. - skips: Rc>, -} - -/// Identifies an element that that can be skipped if visited because it was -/// already processed. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum Skip { - /// Uniquely identifies a placed elements. We can't use a [`Location`] - /// because `PlaceElem` is not currently locatable. - Placed(usize), - /// Uniquely identifies a footnote. - Footnote(Location), + skips: Rc>, } impl<'a, 'b> Work<'a, 'b> { @@ -304,7 +293,7 @@ impl<'a, 'b> Work<'a, 'b> { /// Add skipped floats and footnotes from the insertion areas to the skip /// set. - fn extend_skips(&mut self, skips: &[Skip]) { + fn extend_skips(&mut self, skips: &[Location]) { if !skips.is_empty() { Rc::make_mut(&mut self.skips).extend(skips.iter().copied()); } diff --git a/crates/typst/src/layout/frame.rs b/crates/typst/src/layout/frame.rs index 2f68e9361c..cf4153d6d1 100644 --- a/crates/typst/src/layout/frame.rs +++ b/crates/typst/src/layout/frame.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use smallvec::SmallVec; use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; -use crate::introspection::Tag; +use crate::introspection::{Location, Tag}; use crate::layout::{ Abs, Axes, Corners, FixedAlignment, HideElem, Length, Point, Rel, Sides, Size, Transform, @@ -407,8 +407,21 @@ impl Frame { } } + /// Add a label to the frame. + pub fn label(&mut self, label: Label) { + self.group(|g| g.label = Some(label)); + } + + /// Set a parent for the frame. As a result, all elements in the frame + /// become logically ordered immediately after the given location. + pub fn set_parent(&mut self, parent: Location) { + if !self.is_empty() { + self.group(|g| g.parent = Some(parent)); + } + } + /// Wrap the frame's contents in a group and modify that group with `f`. - pub fn group(&mut self, f: F) + fn group(&mut self, f: F) where F: FnOnce(&mut GroupItem), { @@ -557,6 +570,9 @@ pub struct GroupItem { pub clip_path: Option, /// The group's label. pub label: Option