From b7a4382a73e495cf56350c1ba4f216d51b1864c7 Mon Sep 17 00:00:00 2001 From: Philipp Niedermayer Date: Fri, 28 Mar 2025 16:28:03 +0100 Subject: [PATCH 001/162] Fix typo (#6104) --- crates/typst-library/src/visualize/shape.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index 439b4cd98..ff05be2be 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -106,7 +106,7 @@ pub struct RectElem { pub radius: Corners>>, /// How much to pad the rectangle's content. - /// See the [box's documentation]($box.outset) for more details. + /// See the [box's documentation]($box.inset) for more details. #[resolve] #[fold] #[default(Sides::splat(Some(Abs::pt(5.0).into())))] From 20ee446ebab2fbb23246026301e26b82647369a2 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:30:30 +0100 Subject: [PATCH 002/162] Fix descriptions of color maps (#6096) --- crates/typst-library/src/visualize/color.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index 20b0f5719..24d8305cd 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -148,11 +148,11 @@ static TO_SRGB: LazyLock = LazyLock::new(|| { /// | `magma` | A black to purple to yellow color map. | /// | `plasma` | A purple to pink to yellow color map. | /// | `rocket` | A black to red to white color map. | -/// | `mako` | A black to teal to yellow color map. | +/// | `mako` | A black to teal to white color map. | /// | `vlag` | A light blue to white to red color map. | -/// | `icefire` | A light teal to black to yellow color map. | +/// | `icefire` | A light teal to black to orange color map. | /// | `flare` | A orange to purple color map that is perceptually uniform. | -/// | `crest` | A blue to white to red color map. | +/// | `crest` | A light green to blue color map. | /// /// Some popular presets are not included because they are not available under a /// free licence. Others, like From efdb75558f20543af39f75fb88b3bae59b20e2e8 Mon Sep 17 00:00:00 2001 From: Matt Fellenz Date: Fri, 28 Mar 2025 18:33:16 +0100 Subject: [PATCH 003/162] IDE: complete jump-to-cursor impl (#6037) --- crates/typst-ide/src/jump.rs | 152 ++++++++++++++++++-- crates/typst-library/src/visualize/curve.rs | 64 +++++++++ 2 files changed, 206 insertions(+), 10 deletions(-) diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index 428335426..b29bc4a48 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -3,7 +3,7 @@ use std::num::NonZeroUsize; use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size}; use typst::model::{Destination, Url}; use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind}; -use typst::visualize::Geometry; +use typst::visualize::{Curve, CurveItem, FillRule, Geometry}; use typst::WorldExt; use crate::IdeWorld; @@ -53,10 +53,20 @@ pub fn jump_from_click( for (mut pos, item) in frame.items().rev() { match item { FrameItem::Group(group) => { - // TODO: Handle transformation. - if let Some(span) = - jump_from_click(world, document, &group.frame, click - pos) - { + let pos = click - pos; + if let Some(clip) = &group.clip { + if !clip.contains(FillRule::NonZero, pos) { + continue; + } + } + // Realistic transforms should always be invertible. + // An example of one that isn't is a scale of 0, which would + // not be clickable anyway. + let Some(inv_transform) = group.transform.invert() else { + continue; + }; + let pos = pos.transform_inf(inv_transform); + if let Some(span) = jump_from_click(world, document, &group.frame, pos) { return Some(span); } } @@ -94,9 +104,32 @@ pub fn jump_from_click( } FrameItem::Shape(shape, span) => { - let Geometry::Rect(size) = shape.geometry else { continue }; - if is_in_rect(pos, size, click) { - return Jump::from_span(world, *span); + if shape.fill.is_some() { + let within = match &shape.geometry { + Geometry::Line(..) => false, + Geometry::Rect(size) => is_in_rect(pos, *size, click), + Geometry::Curve(curve) => { + curve.contains(shape.fill_rule, click - pos) + } + }; + if within { + return Jump::from_span(world, *span); + } + } + + if let Some(stroke) = &shape.stroke { + let within = !stroke.thickness.approx_empty() && { + // This curve is rooted at (0, 0), not `pos`. + let base_curve = match &shape.geometry { + Geometry::Line(to) => &Curve(vec![CurveItem::Line(*to)]), + Geometry::Rect(size) => &Curve::rect(*size), + Geometry::Curve(curve) => curve, + }; + base_curve.stroke_contains(stroke, click - pos) + }; + if within { + return Jump::from_span(world, *span); + } } } @@ -146,9 +179,8 @@ pub fn jump_from_cursor( fn find_in_frame(frame: &Frame, span: Span) -> Option { for (mut pos, item) in frame.items() { if let FrameItem::Group(group) = item { - // TODO: Handle transformation. if let Some(point) = find_in_frame(&group.frame, span) { - return Some(point + pos); + return Some(pos + point.transform(group.transform)); } } @@ -269,6 +301,97 @@ mod tests { test_click("$a + b$", point(28.0, 14.0), cursor(5)); } + #[test] + fn test_jump_from_click_transform_clip() { + let margin = point(10.0, 10.0); + test_click( + "#rect(width: 20pt, height: 20pt, fill: black)", + point(10.0, 10.0) + margin, + cursor(1), + ); + test_click( + "#rect(width: 60pt, height: 10pt, fill: black)", + point(5.0, 30.0) + margin, + None, + ); + test_click( + "#rotate(90deg, origin: bottom + left, rect(width: 60pt, height: 10pt, fill: black))", + point(5.0, 30.0) + margin, + cursor(38), + ); + test_click( + "#scale(x: 300%, y: 300%, origin: top + left, rect(width: 10pt, height: 10pt, fill: black))", + point(20.0, 20.0) + margin, + cursor(45), + ); + test_click( + "#box(width: 10pt, height: 10pt, clip: true, scale(x: 300%, y: 300%, \ + origin: top + left, rect(width: 10pt, height: 10pt, fill: black)))", + point(20.0, 20.0) + margin, + None, + ); + test_click( + "#box(width: 10pt, height: 10pt, clip: false, rect(width: 30pt, height: 30pt, fill: black))", + point(20.0, 20.0) + margin, + cursor(45), + ); + test_click( + "#box(width: 10pt, height: 10pt, clip: true, rect(width: 30pt, height: 30pt, fill: black))", + point(20.0, 20.0) + margin, + None, + ); + test_click( + "#rotate(90deg, origin: bottom + left)[hello world]", + point(5.0, 15.0) + margin, + cursor(40), + ); + } + + #[test] + fn test_jump_from_click_shapes() { + let margin = point(10.0, 10.0); + + test_click( + "#rect(width: 30pt, height: 30pt, fill: black)", + point(15.0, 15.0) + margin, + cursor(1), + ); + + let circle = "#circle(width: 30pt, height: 30pt, fill: black)"; + test_click(circle, point(15.0, 15.0) + margin, cursor(1)); + test_click(circle, point(1.0, 1.0) + margin, None); + + let bowtie = + "#polygon(fill: black, (0pt, 0pt), (20pt, 20pt), (20pt, 0pt), (0pt, 20pt))"; + test_click(bowtie, point(1.0, 2.0) + margin, cursor(1)); + test_click(bowtie, point(2.0, 1.0) + margin, None); + test_click(bowtie, point(19.0, 10.0) + margin, cursor(1)); + + let evenodd = r#"#polygon(fill: black, fill-rule: "even-odd", + (0pt, 10pt), (30pt, 10pt), (30pt, 20pt), (20pt, 20pt), + (20pt, 0pt), (10pt, 0pt), (10pt, 30pt), (20pt, 30pt), + (20pt, 20pt), (0pt, 20pt))"#; + test_click(evenodd, point(15.0, 15.0) + margin, None); + test_click(evenodd, point(5.0, 15.0) + margin, cursor(1)); + test_click(evenodd, point(15.0, 5.0) + margin, cursor(1)); + } + + #[test] + fn test_jump_from_click_shapes_stroke() { + let margin = point(10.0, 10.0); + + let rect = + "#place(dx: 10pt, dy: 10pt, rect(width: 10pt, height: 10pt, stroke: 5pt))"; + test_click(rect, point(15.0, 15.0) + margin, None); + test_click(rect, point(10.0, 15.0) + margin, cursor(27)); + + test_click( + "#line(angle: 45deg, length: 10pt, stroke: 2pt)", + point(2.0, 2.0) + margin, + cursor(1), + ); + } + #[test] fn test_jump_from_cursor() { let s = "*Hello* #box[ABC] World"; @@ -281,6 +404,15 @@ mod tests { test_cursor("$a + b$", -3, pos(1, 27.51, 16.83)); } + #[test] + fn test_jump_from_cursor_transform() { + test_cursor( + r#"#rotate(90deg, origin: bottom + left, [hello world])"#, + -5, + pos(1, 10.0, 16.58), + ); + } + #[test] fn test_backlink() { let s = "#footnote[Hi]"; diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index fb5151e8f..50944a516 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -10,6 +10,8 @@ use crate::foundations::{ use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; +use super::FixedStroke; + /// A curve consisting of movements, lines, and Bézier segments. /// /// At any point in time, there is a conceptual pen or cursor. @@ -530,3 +532,65 @@ impl Curve { Size::new(max_x - min_x, max_y - min_y) } } + +impl Curve { + fn to_kurbo(&self) -> impl Iterator + '_ { + use kurbo::PathEl; + + self.0.iter().map(|item| match *item { + CurveItem::Move(point) => PathEl::MoveTo(point_to_kurbo(point)), + CurveItem::Line(point) => PathEl::LineTo(point_to_kurbo(point)), + CurveItem::Cubic(point, point1, point2) => PathEl::CurveTo( + point_to_kurbo(point), + point_to_kurbo(point1), + point_to_kurbo(point2), + ), + CurveItem::Close => PathEl::ClosePath, + }) + } + + /// When this curve is interpreted as a clip mask, would it contain `point`? + pub fn contains(&self, fill_rule: FillRule, needle: Point) -> bool { + let kurbo = kurbo::BezPath::from_vec(self.to_kurbo().collect()); + let windings = kurbo::Shape::winding(&kurbo, point_to_kurbo(needle)); + match fill_rule { + FillRule::NonZero => windings != 0, + FillRule::EvenOdd => windings % 2 != 0, + } + } + + /// When this curve is stroked with `stroke`, would the stroke contain + /// `point`? + pub fn stroke_contains(&self, stroke: &FixedStroke, needle: Point) -> bool { + let width = stroke.thickness.to_raw(); + let cap = match stroke.cap { + super::LineCap::Butt => kurbo::Cap::Butt, + super::LineCap::Round => kurbo::Cap::Round, + super::LineCap::Square => kurbo::Cap::Square, + }; + let join = match stroke.join { + super::LineJoin::Miter => kurbo::Join::Miter, + super::LineJoin::Round => kurbo::Join::Round, + super::LineJoin::Bevel => kurbo::Join::Bevel, + }; + let miter_limit = stroke.miter_limit.get(); + let mut style = kurbo::Stroke::new(width) + .with_caps(cap) + .with_join(join) + .with_miter_limit(miter_limit); + if let Some(dash) = &stroke.dash { + style = style.with_dashes( + dash.phase.to_raw(), + dash.array.iter().copied().map(Abs::to_raw), + ); + } + let opts = kurbo::StrokeOpts::default(); + let tolerance = 0.01; + let expanded = kurbo::stroke(self.to_kurbo(), &style, &opts, tolerance); + kurbo::Shape::contains(&expanded, point_to_kurbo(needle)) + } +} + +fn point_to_kurbo(point: Point) -> kurbo::Point { + kurbo::Point::new(point.x.to_raw(), point.y.to_raw()) +} From 758ee78ef57ebbaadacc50817620a540bcf8beeb Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:08:55 +0800 Subject: [PATCH 004/162] Make `World::font` implementations safe (#6117) --- crates/typst-cli/src/world.rs | 4 +++- crates/typst-ide/src/tests.rs | 2 +- docs/src/html.rs | 2 +- tests/src/world.rs | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 12e80d273..2da03d4d5 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -210,7 +210,9 @@ impl World for SystemWorld { } fn font(&self, index: usize) -> Option { - self.fonts[index].get() + // comemo's validation may invoke this function with an invalid index. This is + // impossible in typst-cli but possible if a custom tool mutates the fonts. + self.fonts.get(index)?.get() } fn today(&self, offset: Option) -> Option { diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index 6678ab841..c6d733ca9 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -97,7 +97,7 @@ impl World for TestWorld { } fn font(&self, index: usize) -> Option { - Some(self.base.fonts[index].clone()) + self.base.fonts.get(index).cloned() } fn today(&self, _: Option) -> Option { diff --git a/docs/src/html.rs b/docs/src/html.rs index 9077d5c47..9c02f08e9 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -498,7 +498,7 @@ impl World for DocWorld { } fn font(&self, index: usize) -> Option { - Some(FONTS.1[index].clone()) + FONTS.1.get(index).cloned() } fn today(&self, _: Option) -> Option { diff --git a/tests/src/world.rs b/tests/src/world.rs index 9e0e91ad7..fe2bd45ea 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -67,7 +67,7 @@ impl World for TestWorld { } fn font(&self, index: usize) -> Option { - Some(self.base.fonts[index].clone()) + self.base.fonts.get(index).cloned() } fn today(&self, _: Option) -> Option { From 326bec1f0d0fc65fb26ae4c797487d82d2b18b81 Mon Sep 17 00:00:00 2001 From: Astra3 Date: Mon, 31 Mar 2025 10:16:47 +0200 Subject: [PATCH 005/162] Correcting Czech translation in `typst-library` (#6101) --- crates/typst-library/translations/cs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/translations/cs.txt b/crates/typst-library/translations/cs.txt index d4688ffee..e21ca3520 100644 --- a/crates/typst-library/translations/cs.txt +++ b/crates/typst-library/translations/cs.txt @@ -4,5 +4,5 @@ equation = Rovnice bibliography = Bibliografie heading = Kapitola outline = Obsah -raw = Seznam +raw = Výpis page = strana \ No newline at end of file From e60d3021a782c5977cf7de726682e19ae89abeb3 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 31 Mar 2025 04:17:37 -0400 Subject: [PATCH 006/162] Add env setting for ignore_system_fonts (#6092) --- crates/typst-cli/src/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index d6855d100..76f647276 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -361,7 +361,7 @@ pub struct FontArgs { /// Ensures system fonts won't be searched, unless explicitly included via /// `--font-path`. - #[arg(long)] + #[arg(long, env = "TYPST_IGNORE_SYSTEM_FONTS")] pub ignore_system_fonts: bool, } From 1082181a6f789b73fbc64c4ff5bc1401ad081e76 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:01:01 +0200 Subject: [PATCH 007/162] Improve french smartquotes (#5976) --- crates/typst-library/src/text/smartquote.rs | 2 +- tests/ref/smartquote-disabled-temporarily.png | Bin 2781 -> 2782 bytes tests/ref/smartquote-fr.png | Bin 2344 -> 2334 bytes tests/ref/smartquote-with-embedding-chars.png | Bin 573 -> 568 bytes tests/suite/text/smartquote.typ | 2 +- 5 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index f457a6371..4dda689df 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -238,7 +238,7 @@ impl<'s> SmartQuotes<'s> { "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, "da" => ("‘", "’", "“", "”"), "fr" | "ru" if alternative => default, - "fr" => ("‹\u{00A0}", "\u{00A0}›", "«\u{00A0}", "\u{00A0}»"), + "fr" => ("“", "”", "«\u{202F}", "\u{202F}»"), "fi" | "sv" if alternative => ("’", "’", "»", "»"), "bs" | "fi" | "sv" => ("’", "’", "”", "”"), "it" if alternative => default, diff --git a/tests/ref/smartquote-disabled-temporarily.png b/tests/ref/smartquote-disabled-temporarily.png index 4c565c01ce1d91bfbd8b443f410c56e0b9b55360..f4d08c4d12fd5a8ecbc19d2d3d5bb252b2011386 100644 GIT binary patch delta 2774 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 delta 2773 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 b{6PN!WoyUfJv0%b00000NkvXXu0mjf8my2m diff --git a/tests/ref/smartquote-fr.png b/tests/ref/smartquote-fr.png index e28184226ac174455d34de80f4ab329c305b7bfa..6b7de7abeecd29b66aaad7467f6a787d7fb7d7a7 100644 GIT binary patch delta 2323 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 t&81yhJU5m3P_CFiB(~{ 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 diff --git a/tests/suite/text/smartquote.typ b/tests/suite/text/smartquote.typ index 4940d11b2..f2af93ceb 100644 --- a/tests/suite/text/smartquote.typ +++ b/tests/suite/text/smartquote.typ @@ -99,7 +99,7 @@ He's told some books contain questionable "example text". --- smartquote-disabled-temporarily --- // Test changing properties within text. -"She suddenly started speaking french: #text(lang: "fr")['Je suis une banane.']" Roman told me. +"She suddenly started speaking french: #text(lang: "fr", region: "CH")['Je suis une banane.']" Roman told me. Some people's thought on this would be #[#set smartquote(enabled: false); "strange."] From a64af130dc84c84442d59f322b705bded28201de Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 31 Mar 2025 05:06:18 -0400 Subject: [PATCH 008/162] Add default parameter for array.{first, last} (#5970) --- crates/typst-library/src/foundations/array.rs | 24 ++++++++++++++----- tests/suite/foundations/array.typ | 4 ++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index e81b9e645..b647473ab 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -172,17 +172,29 @@ impl Array { } /// Returns the first item in the array. May be used on the left-hand side - /// of an assignment. Fails with an error if the array is empty. + /// an assignment. Returns the default value if the array is empty + /// or fails with an error is no default value was specified. #[func] - pub fn first(&self) -> StrResult { - self.0.first().cloned().ok_or_else(array_is_empty) + pub fn first( + &self, + /// A default value to return if the array is empty. + #[named] + default: Option, + ) -> StrResult { + self.0.first().cloned().or(default).ok_or_else(array_is_empty) } /// Returns the last item in the array. May be used on the left-hand side of - /// an assignment. Fails with an error if the array is empty. + /// an assignment. Returns the default value if the array is empty + /// or fails with an error is no default value was specified. #[func] - pub fn last(&self) -> StrResult { - self.0.last().cloned().ok_or_else(array_is_empty) + pub fn last( + &self, + /// A default value to return if the array is empty. + #[named] + default: Option, + ) -> StrResult { + self.0.last().cloned().or(default).ok_or_else(array_is_empty) } /// Returns the item at the specified index in the array. May be used on the diff --git a/tests/suite/foundations/array.typ b/tests/suite/foundations/array.typ index 6228f471b..61b5decb3 100644 --- a/tests/suite/foundations/array.typ +++ b/tests/suite/foundations/array.typ @@ -179,6 +179,10 @@ #test((2,).last(), 2) #test((1, 2, 3).first(), 1) #test((1, 2, 3).last(), 3) +#test((1, 2).first(default: 99), 1) +#test(().first(default: 99), 99) +#test((1, 2).last(default: 99), 2) +#test(().last(default: 99), 99) --- array-first-empty --- // Error: 2-12 array is empty From 4f0fbfb7e003f6ae88c1b210fdb7b38f795fc9e4 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 31 Mar 2025 09:17:49 +0000 Subject: [PATCH 009/162] Add dotless parameter to `math.accent` (#5939) Co-authored-by: Laurenz --- crates/typst-layout/src/math/accent.rs | 6 +++-- crates/typst-library/src/math/accent.rs | 26 +++++++++++++++++++-- tests/ref/math-accent-dotless-disabled.png | Bin 0 -> 311 bytes tests/ref/math-accent-dotless-set-rule.png | Bin 0 -> 147 bytes tests/suite/math/accent.typ | 8 +++++++ 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 tests/ref/math-accent-dotless-disabled.png create mode 100644 tests/ref/math-accent-dotless-set-rule.png diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index f2dfa2c45..73d821019 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -19,8 +19,10 @@ pub fn layout_accent( let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; // Try to replace a glyph with its dotless variant. - if let MathFragment::Glyph(glyph) = &mut base { - glyph.make_dotless_form(ctx); + if elem.dotless(styles) { + if let MathFragment::Glyph(glyph) = &mut base { + glyph.make_dotless_form(ctx); + } } // Preserve class to preserve automatic spacing. diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index b162c52b1..e62b63872 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -13,8 +13,8 @@ use crate::math::Mathy; /// ``` #[elem(Mathy)] pub struct AccentElem { - /// The base to which the accent is applied. - /// May consist of multiple letters. + /// The base to which the accent is applied. May consist of multiple + /// letters. /// /// ```example /// $arrow(A B C)$ @@ -51,9 +51,24 @@ pub struct AccentElem { pub accent: Accent, /// The size of the accent, relative to the width of the base. + /// + /// ```example + /// $dash(A, size: #150%)$ + /// ``` #[resolve] #[default(Rel::one())] pub size: Rel, + + /// Whether to remove the dot on top of lowercase i and j when adding a top + /// accent. + /// + /// This enables the `dtls` OpenType feature. + /// + /// ```example + /// $hat(dotless: #false, i)$ + /// ``` + #[default(true)] + pub dotless: bool, } /// An accent character. @@ -103,11 +118,18 @@ macro_rules! accents { /// The size of the accent, relative to the width of the base. #[named] size: Option>, + /// Whether to remove the dot on top of lowercase i and j when + /// adding a top accent. + #[named] + dotless: Option, ) -> Content { let mut accent = AccentElem::new(base, Accent::new($primary)); if let Some(size) = size { accent = accent.with_size(size); } + if let Some(dotless) = dotless { + accent = accent.with_dotless(dotless); + } accent.pack() } )+ diff --git a/tests/ref/math-accent-dotless-disabled.png b/tests/ref/math-accent-dotless-disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..d75ec4580253f4b791384ba7e98bf209eab338c6 GIT binary patch literal 311 zcmV-70m%M|P)w)T5`r&Y`IJ`TyHLT3Y-rdKZLobSId4 z-n?iFMBq;YO@n&RpTm$K{`C_~z5IXo4^-+iO)dUB?-`iB2#e6G3nBF5`HyI7@rvb3 z!1T(HZ(#cNiR{N<#{B)gG`09&$Qwvp2SE6L|GPmMt$S%|@rU3O|6hPSxa1g+1_^}z ze+5p?KHq6-@tdMO{{yf7|G%c^;eX3_K!Ht@{x^X_|F+?005R4eF=Tc4^998002ov JPDHLkV1kDTnQ{OC literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-dotless-set-rule.png b/tests/ref/math-accent-dotless-set-rule.png new file mode 100644 index 0000000000000000000000000000000000000000..ae5ef017aaedc711d30182cd57d05de08e9f9397 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS@0VEh)%)UPdQc<2Rjv*Ddl7HAcG$dYm6xi*q zE4Q^mLFeDx!h{5!dmrnL|8C@bb+w*NqtBl|Kt{gaz(6eCR>a;wY=5+2azm5Vk88gh oH(J#EV)*&v_4zopr0MHjU3IG5A literal 0 HcmV?d00001 diff --git a/tests/suite/math/accent.typ b/tests/suite/math/accent.typ index 5be4f576f..ab0078a5f 100644 --- a/tests/suite/math/accent.typ +++ b/tests/suite/math/accent.typ @@ -42,3 +42,11 @@ $tilde(U, size: #1.1em), x^tilde(U, size: #1.1em), sscript(tilde(U, size: #1.1em macron(bb(#c)), dot(cal(#c)), diaer(upright(#c)), breve(bold(#c)), circle(bold(upright(#c))), caron(upright(sans(#c))), arrow(bold(frak(#c)))$ $test(i) \ test(j)$ + +--- math-accent-dotless-disabled --- +// Test disabling the dotless glyph variants. +$hat(i), hat(i, dotless: #false), accent(j, tilde), accent(j, tilde, dotless: #false)$ + +--- math-accent-dotless-set-rule --- +#set math.accent(dotless: false) +$ hat(i) $ From 012e14d40cb44997630cf6469a446f217f2e9057 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 31 Mar 2025 09:38:04 +0000 Subject: [PATCH 010/162] Unify layout of `vec` and `cases` with `mat` (#5934) --- crates/typst-layout/src/math/mat.rs | 195 +++++++++++----------- crates/typst-layout/src/math/shared.rs | 16 +- crates/typst-layout/src/math/underover.rs | 10 +- tests/ref/math-cases-linebreaks.png | Bin 570 -> 506 bytes tests/ref/math-equation-font.png | Bin 984 -> 1032 bytes tests/ref/math-mat-vec-cases-unity.png | Bin 0 -> 1202 bytes tests/ref/math-vec-linebreaks.png | Bin 856 -> 651 bytes tests/suite/math/cases.typ | 4 +- tests/suite/math/mat.typ | 11 +- tests/suite/math/vec.typ | 4 +- 10 files changed, 118 insertions(+), 122 deletions(-) create mode 100644 tests/ref/math-mat-vec-cases-unity.png diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index bf4929026..d678f8658 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -1,4 +1,4 @@ -use typst_library::diag::{bail, SourceResult}; +use typst_library::diag::{bail, warning, SourceResult}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::layout::{ Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, @@ -9,7 +9,7 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; use typst_syntax::Span; use super::{ - alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult, + alignments, delimiter_alignment, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; @@ -23,67 +23,23 @@ pub fn layout_vec( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let delim = elem.delim(styles); - let frame = layout_vec_body( + let span = elem.span(); + + let column: Vec<&Content> = elem.children.iter().collect(); + let frame = layout_body( ctx, styles, - &elem.children, + &[column], elem.align(styles), - elem.gap(styles), LeftRightAlternator::Right, + None, + Axes::with_y(elem.gap(styles)), + span, + "elements", )?; - layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span()) -} - -/// Lays out a [`MatElem`]. -#[typst_macros::time(name = "math.mat", span = elem.span())] -pub fn layout_mat( - elem: &Packed, - ctx: &mut MathContext, - styles: StyleChain, -) -> SourceResult<()> { - let augment = elem.augment(styles); - let rows = &elem.rows; - - if let Some(aug) = &augment { - for &offset in &aug.hline.0 { - if offset == 0 || offset.unsigned_abs() >= rows.len() { - bail!( - elem.span(), - "cannot draw a horizontal line after row {} of a matrix with {} rows", - if offset < 0 { rows.len() as isize + offset } else { offset }, - rows.len() - ); - } - } - - let ncols = rows.first().map_or(0, |row| row.len()); - - for &offset in &aug.vline.0 { - if offset == 0 || offset.unsigned_abs() >= ncols { - bail!( - elem.span(), - "cannot draw a vertical line after column {} of a matrix with {} columns", - if offset < 0 { ncols as isize + offset } else { offset }, - ncols - ); - } - } - } - let delim = elem.delim(styles); - let frame = layout_mat_body( - ctx, - styles, - rows, - elem.align(styles), - augment, - Axes::new(elem.column_gap(styles), elem.row_gap(styles)), - elem.span(), - )?; - - layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span()) + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } /// Lays out a [`CasesElem`]. @@ -93,60 +49,100 @@ pub fn layout_cases( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let delim = elem.delim(styles); - let frame = layout_vec_body( + let span = elem.span(); + + let column: Vec<&Content> = elem.children.iter().collect(); + let frame = layout_body( ctx, styles, - &elem.children, + &[column], FixedAlignment::Start, - elem.gap(styles), LeftRightAlternator::None, + None, + Axes::with_y(elem.gap(styles)), + span, + "branches", )?; + let delim = elem.delim(styles); let (open, close) = if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; - - layout_delimiters(ctx, styles, frame, open, close, elem.span()) + layout_delimiters(ctx, styles, frame, open, close, span) } -/// Layout the inner contents of a vector. -fn layout_vec_body( +/// Lays out a [`MatElem`]. +#[typst_macros::time(name = "math.mat", span = elem.span())] +pub fn layout_mat( + elem: &Packed, ctx: &mut MathContext, styles: StyleChain, - column: &[Content], - align: FixedAlignment, - row_gap: Rel, - alternator: LeftRightAlternator, -) -> SourceResult { - let gap = row_gap.relative_to(ctx.region.size.y); +) -> SourceResult<()> { + let span = elem.span(); + let rows = &elem.rows; + let ncols = rows.first().map_or(0, |row| row.len()); - let denom_style = style_for_denominator(styles); - let mut flat = vec![]; - for child in column { - // We allow linebreaks in cases and vectors, which are functionally - // identical to commas. - flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows()); + let augment = elem.augment(styles); + if let Some(aug) = &augment { + for &offset in &aug.hline.0 { + if offset == 0 || offset.unsigned_abs() >= rows.len() { + bail!( + span, + "cannot draw a horizontal line after row {} of a matrix with {} rows", + if offset < 0 { rows.len() as isize + offset } else { offset }, + rows.len() + ); + } + } + + for &offset in &aug.vline.0 { + if offset == 0 || offset.unsigned_abs() >= ncols { + bail!( + span, + "cannot draw a vertical line after column {} of a matrix with {} columns", + if offset < 0 { ncols as isize + offset } else { offset }, + ncols + ); + } + } } - // We pad ascent and descent with the ascent and descent of the paren - // to ensure that normal vectors are aligned with others unless they are - // way too big. - let paren = - GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); - Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent)))) + + // Transpose rows of the matrix into columns. + let mut row_iters: Vec<_> = rows.iter().map(|i| i.iter()).collect(); + let columns: Vec> = (0..ncols) + .map(|_| row_iters.iter_mut().map(|i| i.next().unwrap()).collect()) + .collect(); + + let frame = layout_body( + ctx, + styles, + &columns, + elem.align(styles), + LeftRightAlternator::Right, + augment, + Axes::new(elem.column_gap(styles), elem.row_gap(styles)), + span, + "cells", + )?; + + let delim = elem.delim(styles); + layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } -/// Layout the inner contents of a matrix. -fn layout_mat_body( +/// Layout the inner contents of a matrix, vector, or cases. +#[allow(clippy::too_many_arguments)] +fn layout_body( ctx: &mut MathContext, styles: StyleChain, - rows: &[Vec], + columns: &[Vec<&Content>], align: FixedAlignment, + alternator: LeftRightAlternator, augment: Option>, gap: Axes>, span: Span, + children: &str, ) -> SourceResult { - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); + let nrows = columns.first().map_or(0, |col| col.len()); + let ncols = columns.len(); if ncols == 0 || nrows == 0 { return Ok(Frame::soft(Size::zero())); } @@ -178,16 +174,11 @@ fn layout_mat_body( // Before the full matrix body can be laid out, the // individual cells must first be independently laid out // so we can ensure alignment across rows and columns. + let mut cols = vec![vec![]; ncols]; // This variable stores the maximum ascent and descent for each row. let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; - // We want to transpose our data layout to columns - // before final layout. For efficiency, the columns - // variable is set up here and newly generated - // individual cells are then added to it. - let mut cols = vec![vec![]; ncols]; - let denom_style = style_for_denominator(styles); // We pad ascent and descent with the ascent and descent of the paren // to ensure that normal matrices are aligned with others unless they are @@ -195,10 +186,22 @@ fn layout_mat_body( let paren = GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); - for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { - for (cell, col) in row.iter().zip(&mut cols) { + for (column, col) in columns.iter().zip(&mut cols) { + for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { + let cell_span = cell.span(); let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?; + // We ignore linebreaks in the cells as we can't differentiate + // alignment points for the whole body from ones for a specific + // cell, and multiline cells don't quite make sense at the moment. + if cell.is_multiline() { + ctx.engine.sink.warn(warning!( + cell_span, + "linebreaks are ignored in {}", children; + hint: "use commas instead to separate each line" + )); + } + ascent.set_max(cell.ascent().max(paren.ascent)); descent.set_max(cell.descent().max(paren.descent)); @@ -222,7 +225,7 @@ fn layout_mat_body( let mut y = Abs::zero(); for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { - let cell = cell.into_line_frame(&points, LeftRightAlternator::Right); + let cell = cell.into_line_frame(&points, alternator); let pos = Point::new( if points.is_empty() { x + align.position(rcol - cell.width()) diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 5aebdacac..600c130d4 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -117,7 +117,6 @@ pub fn stack( gap: Abs, baseline: usize, alternator: LeftRightAlternator, - minimum_ascent_descent: Option<(Abs, Abs)>, ) -> Frame { let AlignmentResult { points, width } = alignments(&rows); let rows: Vec<_> = rows @@ -125,13 +124,9 @@ pub fn stack( .map(|row| row.into_line_frame(&points, alternator)) .collect(); - let padded_height = |height: Abs| { - height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d)) - }; - let mut frame = Frame::soft(Size::new( width, - rows.iter().map(|row| padded_height(row.height())).sum::() + rows.iter().map(|row| row.height()).sum::() + rows.len().saturating_sub(1) as f64 * gap, )); @@ -142,14 +137,11 @@ pub fn stack( } else { Abs::zero() }; - let ascent_padded_part = minimum_ascent_descent - .map_or(Abs::zero(), |(a, _)| (a - row.ascent())) - .max(Abs::zero()); - let pos = Point::new(x, y + ascent_padded_part); + let pos = Point::new(x, y); if i == baseline { - frame.set_baseline(y + row.baseline() + ascent_padded_part); + frame.set_baseline(y + row.baseline()); } - y += padded_height(row.height()) + gap; + y += row.height() + gap; frame.push_frame(pos, row); } diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index 7b3617c3e..5b6bd40eb 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -312,14 +312,8 @@ fn layout_underoverspreader( } }; - let frame = stack( - rows, - FixedAlignment::Center, - gap, - baseline, - LeftRightAlternator::Right, - None, - ); + let frame = + stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right); ctx.push(FrameFragment::new(styles, frame).with_class(body_class)); Ok(()) diff --git a/tests/ref/math-cases-linebreaks.png b/tests/ref/math-cases-linebreaks.png index 543d5384c11a270a8a56f95e91e4f5ec7ac64d3f..eb4971c46fb2d2a36a8b95324d3d1e08b7d99319 100644 GIT binary patch delta 481 zcmV<70UrLk1o{JzBYy$GNklVP4Bt@3Ph~{FOY>~-6-Ym~#Fk@(%=gn7d8s##_|N9-h|Ka!jzuw_< z_^dgPGQ4DaHy6DPTURhgFj~rC+X$d z*Zcb|6!U@Jf`%y^^#b7U_EF#qbX=t@;hh41RPS2=ydL}DDms+_!Qf*H+cWR{nR?pA zq`(??MII3`luOhtmxVHZX&+S@xjHmcBS8ICMZZCNQe*yTvW{Iao?K$2M~*ie0O?xd zfm<&i8Ue;I67~?JUDnVX5>L}krf~T9e>)`Nb!69DT3Yx)FDVSiZpn3kTjVGv*L@b; z9);nE831eTfQMp?-OV?nMup+DZMjN3pfFt9L$sWP6f+Hsn<5lQAABqA!TMHEz9cRe8q zCP#{hjKH8aMcJl}i-<6+$P9wogh3C29$-Qlq}1S~m52`p2z#sqtf$p4{Ie*W^1g3T14$a%T?k;imS}Xu*AxThl5vuxwIhJd)kH^6!2pL7V0t2{+Kd2Rt|zK1 zybSY`$6yGPRKJY2V+%JoeQ>bv>DrQZjCWjjc|X8&ESNzC7Y7YIRV(jmr_iptIA~YB z-6rqaw$XMR?0-%H7M)9VA)QztpfLkw!h6e6LH9V$hchRamc!{O9Vd7bc z>fVN>mR(XB%vvNLd=(mCZyh=!Qns)PgX2aQ^RQ|gV6G-xUS(@I1Tf56%Ny$~&Kro0$&|(nW5as&2^UIJ kA*{Z0sT*Jwtb$SJ2Wbl+-x_NR>i_@%07*qoM6N<$f>qD?{Qv*} diff --git a/tests/ref/math-equation-font.png b/tests/ref/math-equation-font.png index eb84634e5a8f2c5e42a606d12505d505d63c3294..ec3c72311b4a40abad4b53074e7870c2042087db 100644 GIT binary patch delta 1023 zcmVpswqi8`9zd6D=hw z&|!kNALGGLKYx)u#{${2<{~E8OHF~Hh)8x1A0P4^A|f%tf|o>1->Is;qN2jUT%|uI zxYswk(@hUVu-?i3_I3~=__4tLR@1c(4}5N6D`WoxJg_W~5xtjaMn2t2bJ;WHIv!YZ zh>^-XxUiBcs|05D`W(jti_bS*^;)WwAO28E?v~wn;D1@E>}e1_W3*hW)&2x9jWrxh zBJIu)Jn*zM_5>Xvni(7%TmW#*HQqCzg2~}q@WA7*5#1)IyW*4-@2v*L>G4!Tvij zhxf(^Re#&TFyxzQOc`@y@{$ya)1>LD1N$#tNc5GYDP^_;yfj-CNN!@`jnq^%`39Ze z0R2Ad$g8xZbCVg#*goO#kM@EO#=94jyXUDwCX+L!v2#AX>z#whyJLxoQyF=A+%2QF zz2MAQ685S~p549sn7D3v^qv?A^izt6z7KEzr+=l6_+C=cwpBI zi_xb8xK#pt@>px&M%e~D@boJdlQZ_SQOLTZ5!@KI6%Xu|!Z}6EWf?$`3FJGtDkKyS zJbyodbCokUJ^?s0!$bUqt2WEiUZ zIbj=Ns+ONk4<5MpO=e}@Y$71{0^T>uvhf(D!Qiam?h!x>4SdkK57p0_bhy18>h;pSkF<}O^BW4*AmC1+<`9m+ahSrjT zGw9TzHYC#fa%vv8yL&os_uc2%?*8HSo}0VJf3Lrue|)yjKY#BzJRoyo;aE5pj)fKJ z@aNwVPW6yD!>fhEBRMctUW}p@XJpzTUO3!Zi}!g;7RtY8=q!NT zLD_IoA7BrJ@zy}VsZ~&SKsLN^3O($!FLL}3KyDxOzbYEGCxIUR;m!GmVSw5y;MRP< zWVp?!!g!VdP=8+mK(bvjT(b`qW&*(W=*pFAfwkR|;leHeJScv*+dU1K8YRO^&k{+~ zX36lp2_osv6%5~ajEDxG77U+FP*1G*0nq09p5xMYN9|17EY=02AVzb$t;7!mz3?vJza^d3r z0REB|fYWBP*#X^2jXA~RKw-07cy%-2H?)9q?Zk<~N)33UK`xy4DbTwxJ$)Ua>bd|* z>*T^Ov=dob4a%ie?XwRLd2wAN)bg*0?7eNEJD;__VfNwl^O%}}tK)YxO#?{jA~I7M zz(6km{(pT@gtv6uPPhx~YHY~Q&)0y2<3v`y0hnyA8V4Yn@L}`8Jpg}QyzX!~d_a5; zq5c@SZyW+Xcpa!pOWqiDc(@?d1+c%h#b7YFfW%{;{newuwm7o%fT)6OKl-jSsKbkW_&})ry;gSXj7&{6XPPqEYh6#e!w8(|uu|U9Y zi^rUq0350$D8F4UY|<;V?(NzI!ya~TCn)P1xo}evLC)t4=iq31TEPv1;?Bv1dzYhI z6MxT4zPUH$!lO^28|i1JD^DU9_B=98BtNGLhKc)|P~W3`StJ>Lu^(kq*|IjZ>fe_P zzilOy)dM`ODvH2Zn{ormvvJZBvL zk#T`=ARGt>!v9J*>l_A-A2C>K$KZSpGF7?gJ^J2XxL^}ru5CnT-D$jJQqsA?-QNJvNZ zs~J^(D+V7opg7?*2REV$t3%=3GdNl}Ee1tZt+O4yvW2J?7NW?#+v-9uHVq)hdtSc{ zh3mCv0R4N1nnnvRPX)NV_1mbyi?*^q(R9CoYDzu8QFD};f~!TnXbvw0j+mp8|CtL% z=IjU*&680iZT{NwxZp~$EF5wipbv+elI24tG_Dt(0}PM1i}M~jGY*yKqbd6kF?VES zp0%Jk^%07*@1p3(PUGrZaZe=Spc*ts&y^gwhh}mcK*vNpqMaKqplO&2!gtQ~f1s(K zhT>2t9;;c|BneNtgQnDZz0{qVeGt`*YJl2lc$}p8#*;0T`6#+380V^dbQd+RLgTV> zr1NI-X`5GgLMH}?|1zU`=R81l5JvGmJ5hAMWSk%F68Cu-#g-_H;;mQwgg0Myjum#t zHlT%Nvph^o`fbJz50^h@tgyWnS-7W;vBS+s!gBI3NW&lrOX$L{w4q}=UI@Z;8^FbB z=#}nkKxm%`!sC)1576A1gd%wr9!v3&A6~4$ZN-D9+7BC$dixjy#Ak zkZozivEvp({S;Jh-a%;1v9{Bg*M^X`9Ke=a($A?2+6ccyqG*|p>ST5e>}~Imgm=Xt zEWT^qKzPI=gu-xCUlyXWhYg!fcg~6U2_Yr}u&p83Q88yp7Mkeos9t{xkecV5MRHj9 z4N189JybOjq?}^JmYt|}uSb=%pN0L52)PM_U1J^wbDB|oQ3kkYy6a6SBw>OEP*_u$ zS8vB-_H9(RgHf5T^v$oq<7p7UTz74>ZiL<$2zwHM{!q*o+i2m$92EWAj+t^~PoD*y zD6Stk8hr+j-(vxaESYOBp>rY*q4#}&y(d#r?vEC>e~QGFp_9-lGDtAGC{c8XR6@&w zB3QQ48A}g}S`?Zq2AQdpA-F(8@h}obL~bT61Y6V&wl$2HZaQ@>Hn6R^Z-?Juy|=>c z@!;=!d7j&c_c^=;Y*9N3wkRFeVIAIPu>Za`I~Qf)NK1pZG=JXxDGE3B@6wi?&Fux7 zj(={VBfGavv&sGgK9vAi`3|s9E(ben0mdGU8UQNh#9*Txh!^wP0$^(vgZC!@w~=P| z3J@rkf&+yB@f+TwwkW{fA42eW82~YVJP1IkQwV-|5v8*>A=uoClBq)ou6c~oZIcjO z-H+0p>i~pN3xDSvE~*D0czO*0s>=Z0`T=033N2?KUoSrP_lr|KXvNNemNifCQq9Lq z7Ph|u=I1lKw0s8Ud|+A34?KE#*_4HwBGCKdxrxSZ_Y=(1bwMV^at!aQ0ie#8F49hZ z0H}pfDZ_&t&v-74mjm2QA)7pnmFYhv*5Er3HQegV`v#Nc$v24I<;90F1&5QDMW0m4au zz7at*X7QFz>CPW%z<5%=m~ltqx>imX#p_tLd-~>#z<3e*pLZl?g+Rl0X0e N002ovPDHLkV1gW(A;bUx delta 834 zcmV-I1HJr<1=t3VBYy)QNklcHB<5?*i@o@c^IbymBXCA^i@1L`WLh=a zgU^jTb#dE4$bXZqC~OqQm3$Ew-hkXWuaCnxN=03KIU7<@`y!?cMnklSqpBe@HF;?- zVO-0hEiSIuro|u97R=L`pIQG*hV{y#uvIaNeZ9SP|Y{X{S2<~uHaTO2m z637y7d<>?bw0amLGBfQbmKmYOl2KwS2L4!7?~jkF2PnhD2f9Yi+RdowDKo;`>7%O*OBzw zDnwhnrIR@4Jm#Qgm)i;4vX#0xqJ}sJ{KQ`Uo4}>}sEg0s5nJ2LK`BGTGxVd_eQmgM zQ>lxq;&3T$H_D+)vP7I!fGfKuzZp8Iin@3@0DsaHgt%!Khu(>} Date: Tue, 1 Apr 2025 16:42:52 +0200 Subject: [PATCH 011/162] Switch PDF backend to `krilla` (#5420) Co-authored-by: Laurenz --- Cargo.lock | 200 ++++- Cargo.toml | 5 +- crates/typst-cli/src/args.rs | 32 +- crates/typst-cli/src/compile.rs | 39 +- crates/typst-layout/src/inline/shaping.rs | 40 +- crates/typst-library/src/layout/transform.rs | 14 + .../src/visualize/image/raster.rs | 27 +- crates/typst-pdf/Cargo.toml | 11 +- crates/typst-pdf/src/catalog.rs | 385 -------- crates/typst-pdf/src/color.rs | 394 --------- crates/typst-pdf/src/color_font.rs | 344 -------- crates/typst-pdf/src/content.rs | 823 ------------------ crates/typst-pdf/src/convert.rs | 661 ++++++++++++++ crates/typst-pdf/src/embed.rs | 150 +--- crates/typst-pdf/src/extg.rs | 53 -- crates/typst-pdf/src/font.rs | 278 ------ crates/typst-pdf/src/gradient.rs | 512 ----------- crates/typst-pdf/src/image.rs | 453 +++++----- crates/typst-pdf/src/lib.rs | 751 +++------------- crates/typst-pdf/src/link.rs | 94 ++ crates/typst-pdf/src/metadata.rs | 184 ++++ crates/typst-pdf/src/named_destination.rs | 86 -- crates/typst-pdf/src/outline.rs | 145 +-- crates/typst-pdf/src/page.rs | 348 ++------ crates/typst-pdf/src/paint.rs | 379 ++++++++ crates/typst-pdf/src/resources.rs | 349 -------- crates/typst-pdf/src/shape.rs | 106 +++ crates/typst-pdf/src/text.rs | 135 +++ crates/typst-pdf/src/tiling.rs | 184 ---- crates/typst-pdf/src/util.rs | 120 +++ 30 files changed, 2426 insertions(+), 4876 deletions(-) delete mode 100644 crates/typst-pdf/src/catalog.rs delete mode 100644 crates/typst-pdf/src/color.rs delete mode 100644 crates/typst-pdf/src/color_font.rs delete mode 100644 crates/typst-pdf/src/content.rs create mode 100644 crates/typst-pdf/src/convert.rs delete mode 100644 crates/typst-pdf/src/extg.rs delete mode 100644 crates/typst-pdf/src/font.rs delete mode 100644 crates/typst-pdf/src/gradient.rs create mode 100644 crates/typst-pdf/src/link.rs create mode 100644 crates/typst-pdf/src/metadata.rs delete mode 100644 crates/typst-pdf/src/named_destination.rs create mode 100644 crates/typst-pdf/src/paint.rs delete mode 100644 crates/typst-pdf/src/resources.rs create mode 100644 crates/typst-pdf/src/shape.rs create mode 100644 crates/typst-pdf/src/text.rs delete mode 100644 crates/typst-pdf/src/tiling.rs create mode 100644 crates/typst-pdf/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 630eade2f..c13c64819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,20 @@ name = "bytemuck" version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -735,11 +749,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -749,6 +764,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -761,6 +785,15 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +[[package]] +name = "font-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d868ec188a98bb014c606072edd47e52e7ab7297db943b0b28503121e1d037bd" +dependencies = [ + "bytemuck", +] + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -829,6 +862,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "getopts" version = "0.2.21" @@ -966,7 +1008,7 @@ checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "serde", - "yoke", + "yoke 0.7.5", "zerofrom", "zerovec", ] @@ -1064,7 +1106,7 @@ dependencies = [ "stable_deref_trait", "tinystr", "writeable", - "yoke", + "yoke 0.7.5", "zerofrom", "zerovec", ] @@ -1310,6 +1352,48 @@ dependencies = [ "libc", ] +[[package]] +name = "krilla" +version = "0.3.0" +source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +dependencies = [ + "base64", + "bumpalo", + "comemo", + "flate2", + "float-cmp 0.10.0", + "fxhash", + "gif", + "image-webp", + "imagesize", + "once_cell", + "pdf-writer", + "png", + "rayon", + "rustybuzz", + "siphasher", + "skrifa", + "subsetter", + "tiny-skia-path", + "xmp-writer", + "yoke 0.8.0", + "zune-jpeg", +] + +[[package]] +name = "krilla-svg" +version = "0.3.0" +source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +dependencies = [ + "flate2", + "fontdb", + "krilla", + "png", + "resvg", + "tiny-skia", + "usvg", +] + [[package]] name = "kurbo" version = "0.11.1" @@ -1371,6 +1455,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-rs-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1458,9 +1551,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", "simd-adler32", @@ -1739,8 +1832,7 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" +source = "git+https://github.com/typst/pdf-writer?rev=0d513b9#0d513b9050d2f1a0507cabb4898aca971af6da98" dependencies = [ "bitflags 2.8.0", "itoa", @@ -1997,6 +2089,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "read-fonts" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f004ee5c610b8beb5f33273246893ac6258ec22185a6eb8ee132bccdb904cdaa" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -2315,6 +2417,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skrifa" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e7936ca3627fdb516e97aca3c8ab5103f94ae32fe5ce80a0a02edcbacb7b53" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -2361,7 +2473,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp", + "float-cmp 0.9.0", ] [[package]] @@ -2405,27 +2517,9 @@ dependencies = [ [[package]] name = "subsetter" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f98178f34057d4d4de93d68104007c6dea4dfac930204a69ab4622daefa648" - -[[package]] -name = "svg2pdf" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50dc062439cc1a396181059c80932a6e6bd731b130e674c597c0c8874b6df22" +source = "git+https://github.com/typst/subsetter?rev=460fdb6#460fdb66d6e0138b721b1ca9882faf15ce003246" dependencies = [ - "fontdb", - "image", - "log", - "miniz_oxide", - "once_cell", - "pdf-writer", - "resvg", - "siphasher", - "subsetter", - "tiny-skia", - "ttf-parser", - "usvg", + "fxhash", ] [[package]] @@ -3018,26 +3112,19 @@ dependencies = [ name = "typst-pdf" version = "0.13.1" dependencies = [ - "arrayvec", - "base64", "bytemuck", "comemo", "ecow", "image", - "indexmap 2.7.1", - "miniz_oxide", - "pdf-writer", + "krilla", + "krilla-svg", "serde", - "subsetter", - "svg2pdf", - "ttf-parser", "typst-assets", "typst-library", "typst-macros", "typst-syntax", "typst-timing", "typst-utils", - "xmp-writer", ] [[package]] @@ -3662,8 +3749,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487" +source = "git+https://github.com/LaurenzV/xmp-writer?rev=a1cbb887#a1cbb887a84376fea4d7590d41c194a332840549" [[package]] name = "xz2" @@ -3701,7 +3787,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive", + "yoke-derive 0.7.5", + "zerofrom", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive 0.8.0", "zerofrom", ] @@ -3717,6 +3815,18 @@ dependencies = [ "synstructure", ] +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -3778,7 +3888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "serde", - "yoke", + "yoke 0.7.5", "zerofrom", "zerovec-derive", ] @@ -3809,6 +3919,12 @@ dependencies = [ "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05" + [[package]] name = "zopfli" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index a73241832..cbe69a05d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,11 +71,12 @@ if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.6" +krilla = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7", default-features = false, features = ["raster-images", "comemo", "rayon"] } +krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7" } kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" memchr = "2" -miniz_oxide = "0.8" native-tls = "0.2" notify = "8" once_cell = "1" @@ -113,7 +114,6 @@ siphasher = "1" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } stacker = "0.1.15" subsetter = "0.2" -svg2pdf = "0.13" syn = { version = "2", features = ["full", "extra-traits"] } syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] } tar = "0.4" @@ -140,7 +140,6 @@ wasmi = "0.40.0" web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" -xmp-writer = "0.3.1" xz2 = { version = "0.1", features = ["static"] } yaml-front-matter = "0.1" zip = { version = "2.5", default-features = false, features = ["deflate"] } diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 76f647276..fd0eb5f05 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -467,15 +467,45 @@ display_possible_values!(Feature); #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[allow(non_camel_case_types)] pub enum PdfStandard { + /// PDF 1.4. + #[value(name = "1.4")] + V_1_4, + /// PDF 1.5. + #[value(name = "1.5")] + V_1_5, + /// PDF 1.5. + #[value(name = "1.6")] + V_1_6, /// PDF 1.7. #[value(name = "1.7")] V_1_7, + /// PDF 2.0. + #[value(name = "2.0")] + V_2_0, + /// PDF/A-1b. + #[value(name = "a-1b")] + A_1b, /// PDF/A-2b. #[value(name = "a-2b")] A_2b, - /// PDF/A-3b. + /// PDF/A-2u. + #[value(name = "a-2u")] + A_2u, + /// PDF/A-3u. #[value(name = "a-3b")] A_3b, + /// PDF/A-3u. + #[value(name = "a-3u")] + A_3u, + /// PDF/A-4. + #[value(name = "a-4")] + A_4, + /// PDF/A-4f. + #[value(name = "a-4f")] + A_4f, + /// PDF/A-4e. + #[value(name = "a-4e")] + A_4e, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index ae71e298c..4edb4c323 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -63,8 +63,7 @@ pub struct CompileConfig { /// Opens the output file with the default viewer or a specific program after /// compilation. pub open: Option>, - /// One (or multiple comma-separated) PDF standards that Typst will enforce - /// conformance with. + /// A list of standards the PDF should conform to. pub pdf_standards: PdfStandards, /// A path to write a Makefile rule describing the current compilation. pub make_deps: Option, @@ -130,18 +129,9 @@ impl CompileConfig { PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) }); - let pdf_standards = { - let list = args - .pdf_standard - .iter() - .map(|standard| match standard { - PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, - PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, - PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, - }) - .collect::>(); - PdfStandards::new(&list)? - }; + let pdf_standards = PdfStandards::new( + &args.pdf_standard.iter().copied().map(Into::into).collect::>(), + )?; #[cfg(feature = "http-server")] let server = match watch { @@ -295,6 +285,7 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult< }) } }; + let options = PdfOptions { ident: Smart::Auto, timestamp, @@ -765,3 +756,23 @@ impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { }) } } + +impl From for typst_pdf::PdfStandard { + fn from(standard: PdfStandard) -> Self { + match standard { + PdfStandard::V_1_4 => typst_pdf::PdfStandard::V_1_4, + PdfStandard::V_1_5 => typst_pdf::PdfStandard::V_1_5, + PdfStandard::V_1_6 => typst_pdf::PdfStandard::V_1_6, + PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, + PdfStandard::V_2_0 => typst_pdf::PdfStandard::V_2_0, + PdfStandard::A_1b => typst_pdf::PdfStandard::A_1b, + PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, + PdfStandard::A_2u => typst_pdf::PdfStandard::A_2u, + PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, + PdfStandard::A_3u => typst_pdf::PdfStandard::A_3u, + PdfStandard::A_4 => typst_pdf::PdfStandard::A_4, + PdfStandard::A_4f => typst_pdf::PdfStandard::A_4f, + PdfStandard::A_4e => typst_pdf::PdfStandard::A_4e, + } + } +} diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 159619eb3..8236d1e36 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -824,12 +824,42 @@ fn shape_segment<'a>( // Add the glyph to the shaped output. if info.glyph_id != 0 && is_covered(cluster) { - // Determine the text range of the glyph. + // Assume we have the following sequence of (glyph_id, cluster): + // [(120, 0), (80, 0), (3, 3), (755, 4), (69, 4), (424, 13), + // (63, 13), (193, 25), (80, 25), (3, 31) + // + // We then want the sequence of (glyph_id, text_range) to look as follows: + // [(120, 0..3), (80, 0..3), (3, 3..4), (755, 4..13), (69, 4..13), + // (424, 13..25), (63, 13..25), (193, 25..31), (80, 25..31), (3, 31..x)] + // + // Each glyph in the same cluster should be assigned the full text + // range. This is necessary because only this way krilla can + // properly assign `ActualText` attributes in complex shaping + // scenarios. + + // The start of the glyph's text range. let start = base + cluster; - let end = base - + if ltr { i.checked_add(1) } else { i.checked_sub(1) } - .and_then(|last| infos.get(last)) - .map_or(text.len(), |info| info.cluster as usize); + + // Determine the end of the glyph's text range. + let mut k = i; + let step: isize = if ltr { 1 } else { -1 }; + let end = loop { + // If we've reached the end of the glyphs, the `end` of the + // range should be the end of the full text. + let Some((next, next_info)) = k + .checked_add_signed(step) + .and_then(|n| infos.get(n).map(|info| (n, info))) + else { + break base + text.len(); + }; + + // If the cluster doesn't match anymore, we've reached the end. + if next_info.cluster != info.cluster { + break base + next_info.cluster as usize; + } + + k = next; + }; let c = text[cluster..].chars().next().unwrap(); let script = c.script(); diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs index 183df6098..d153d97db 100644 --- a/crates/typst-library/src/layout/transform.rs +++ b/crates/typst-library/src/layout/transform.rs @@ -307,6 +307,20 @@ impl Transform { Self { sx, sy, ..Self::identity() } } + /// A scale transform at a specific position. + pub fn scale_at(sx: Ratio, sy: Ratio, px: Abs, py: Abs) -> Self { + Self::translate(px, py) + .pre_concat(Self::scale(sx, sy)) + .pre_concat(Self::translate(-px, -py)) + } + + /// A rotate transform at a specific position. + pub fn rotate_at(angle: Angle, px: Abs, py: Abs) -> Self { + Self::translate(px, py) + .pre_concat(Self::rotate(angle)) + .pre_concat(Self::translate(-px, -py)) + } + /// A rotate transform. pub fn rotate(angle: Angle) -> Self { let cos = Ratio::new(angle.cos()); diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 453b94066..21d5b18fc 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -3,6 +3,8 @@ use std::hash::{Hash, Hasher}; use std::io; use std::sync::Arc; +use crate::diag::{bail, StrResult}; +use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; @@ -11,9 +13,6 @@ use image::{ guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, }; -use crate::diag::{bail, StrResult}; -use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; - /// A decoded raster image. #[derive(Clone, Hash)] pub struct RasterImage(Arc); @@ -22,7 +21,8 @@ pub struct RasterImage(Arc); struct Repr { data: Bytes, format: RasterFormat, - dynamic: image::DynamicImage, + dynamic: Arc, + exif_rotation: Option, icc: Option, dpi: Option, } @@ -50,6 +50,8 @@ impl RasterImage { format: RasterFormat, icc: Smart, ) -> StrResult { + let mut exif_rot = None; + let (dynamic, icc, dpi) = match format { RasterFormat::Exchange(format) => { fn decode( @@ -85,6 +87,7 @@ impl RasterImage { // Apply rotation from EXIF metadata. if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { apply_rotation(&mut dynamic, rotation); + exif_rot = Some(rotation); } // Extract pixel density. @@ -136,7 +139,14 @@ impl RasterImage { } }; - Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi }))) + Ok(Self(Arc::new(Repr { + data, + format, + exif_rotation: exif_rot, + dynamic: Arc::new(dynamic), + icc, + dpi, + }))) } /// The raw image data. @@ -159,6 +169,11 @@ impl RasterImage { self.dynamic().height() } + /// TODO. + pub fn exif_rotation(&self) -> Option { + self.0.exif_rotation + } + /// The image's pixel density in pixels per inch, if known. /// /// This is guaranteed to be positive. @@ -167,7 +182,7 @@ impl RasterImage { } /// Access the underlying dynamic image. - pub fn dynamic(&self) -> &image::DynamicImage { + pub fn dynamic(&self) -> &Arc { &self.0.dynamic } diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index bc0da06c3..f6f08b5bc 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -19,20 +19,13 @@ typst-macros = { workspace = true } typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } -arrayvec = { workspace = true } -base64 = { workspace = true } bytemuck = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } -indexmap = { workspace = true } -miniz_oxide = { workspace = true } -pdf-writer = { workspace = true } +krilla = { workspace = true } +krilla-svg = { workspace = true } serde = { workspace = true } -subsetter = { workspace = true } -svg2pdf = { workspace = true } -ttf-parser = { workspace = true } -xmp-writer = { workspace = true } [lints] workspace = true diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs deleted file mode 100644 index 709b01553..000000000 --- a/crates/typst-pdf/src/catalog.rs +++ /dev/null @@ -1,385 +0,0 @@ -use std::num::NonZeroUsize; - -use ecow::eco_format; -use pdf_writer::types::Direction; -use pdf_writer::writers::PageLabel; -use pdf_writer::{Finish, Name, Pdf, Ref, Str, TextStr}; -use typst_library::diag::{bail, SourceResult}; -use typst_library::foundations::{Datetime, Smart}; -use typst_library::layout::Dir; -use typst_library::text::Lang; -use typst_syntax::Span; -use xmp_writer::{DateTime, LangId, RenditionClass, XmpWriter}; - -use crate::page::PdfPageLabel; -use crate::{hash_base64, outline, TextStrExt, Timestamp, Timezone, WithEverything}; - -/// Write the document catalog. -pub fn write_catalog( - ctx: WithEverything, - pdf: &mut Pdf, - alloc: &mut Ref, -) -> SourceResult<()> { - let lang = ctx - .resources - .languages - .iter() - .max_by_key(|(_, &count)| count) - .map(|(&l, _)| l); - - let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { - Direction::R2L - } else { - Direction::L2R - }; - - // Write the outline tree. - let outline_root_id = outline::write_outline(pdf, alloc, &ctx); - - // Write the page labels. - let page_labels = write_page_labels(pdf, alloc, &ctx); - - // Write the document information. - let info_ref = alloc.bump(); - let mut info = pdf.document_info(info_ref); - let mut xmp = XmpWriter::new(); - if let Some(title) = &ctx.document.info.title { - info.title(TextStr::trimmed(title)); - xmp.title([(None, title.as_str())]); - } - - if let Some(description) = &ctx.document.info.description { - info.subject(TextStr::trimmed(description)); - xmp.description([(None, description.as_str())]); - } - - let authors = &ctx.document.info.author; - if !authors.is_empty() { - // Turns out that if the authors are given in both the document - // information dictionary and the XMP metadata, Acrobat takes a little - // bit of both: The first author from the document information - // dictionary and the remaining authors from the XMP metadata. - // - // To fix this for Acrobat, we could omit the remaining authors or all - // metadata from the document information catalog (it is optional) and - // only write XMP. However, not all other tools (including Apple - // Preview) read the XMP data. This means we do want to include all - // authors in the document information dictionary. - // - // Thus, the only alternative is to fold all authors into a single - // `` in the XMP metadata. This is, in fact, exactly what the - // PDF/A spec Part 1 section 6.7.3 has to say about the matter. It's a - // bit weird to not use the array (and it makes Acrobat show the author - // list in quotes), but there's not much we can do about that. - let joined = authors.join(", "); - info.author(TextStr::trimmed(&joined)); - xmp.creator([joined.as_str()]); - } - - let creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION")); - info.creator(TextStr(&creator)); - xmp.creator_tool(&creator); - - let keywords = &ctx.document.info.keywords; - if !keywords.is_empty() { - let joined = keywords.join(", "); - info.keywords(TextStr::trimmed(&joined)); - xmp.pdf_keywords(&joined); - } - let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); - if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { - info.creation_date(pdf_date); - info.modified_date(pdf_date); - } - - info.finish(); - - // A unique ID for this instance of the document. Changes if anything - // changes in the frames. - let instance_id = hash_base64(&pdf.as_bytes()); - - // Determine the document's ID. It should be as stable as possible. - const PDF_VERSION: &str = "PDF-1.7"; - let doc_id = if let Smart::Custom(ident) = ctx.options.ident { - // We were provided with a stable ID. Yay! - hash_base64(&(PDF_VERSION, ident)) - } else if ctx.document.info.title.is_some() && !ctx.document.info.author.is_empty() { - // If not provided from the outside, but title and author were given, we - // compute a hash of them, which should be reasonably stable and unique. - hash_base64(&(PDF_VERSION, &ctx.document.info.title, &ctx.document.info.author)) - } else { - // The user provided no usable metadata which we can use as an `/ID`. - instance_id.clone() - }; - - xmp.document_id(&doc_id); - xmp.instance_id(&instance_id); - xmp.format("application/pdf"); - xmp.pdf_version("1.7"); - xmp.language(ctx.resources.languages.keys().map(|lang| LangId(lang.as_str()))); - xmp.num_pages(ctx.document.pages.len() as u32); - xmp.rendition_class(RenditionClass::Proof); - - if let Some(xmp_date) = date.and_then(|date| xmp_date(date, tz)) { - xmp.create_date(xmp_date); - xmp.modify_date(xmp_date); - - if ctx.options.standards.pdfa { - let mut history = xmp.history(); - history - .add_event() - .action(xmp_writer::ResourceEventAction::Saved) - .when(xmp_date) - .instance_id(&eco_format!("{instance_id}_source")); - history - .add_event() - .action(xmp_writer::ResourceEventAction::Converted) - .when(xmp_date) - .instance_id(&instance_id) - .software_agent(&creator); - } - } - - // Assert dominance. - if let Some((part, conformance)) = ctx.options.standards.pdfa_part { - let mut extension_schemas = xmp.extension_schemas(); - extension_schemas - .xmp_media_management() - .properties() - .describe_instance_id(); - extension_schemas.pdf().properties().describe_all(); - extension_schemas.finish(); - xmp.pdfa_part(part); - xmp.pdfa_conformance(conformance); - } - - let xmp_buf = xmp.finish(None); - let meta_ref = alloc.bump(); - pdf.stream(meta_ref, xmp_buf.as_bytes()) - .pair(Name(b"Type"), Name(b"Metadata")) - .pair(Name(b"Subtype"), Name(b"XML")); - - // Set IDs only now, so that we don't need to clone them. - pdf.set_file_id((doc_id.into_bytes(), instance_id.into_bytes())); - - // Write the document catalog. - let catalog_ref = alloc.bump(); - let mut catalog = pdf.catalog(catalog_ref); - catalog.pages(ctx.page_tree_ref); - catalog.viewer_preferences().direction(dir); - catalog.metadata(meta_ref); - - let has_dests = !ctx.references.named_destinations.dests.is_empty(); - let has_embeddings = !ctx.references.embedded_files.is_empty(); - - // Write the `/Names` dictionary. - if has_dests || has_embeddings { - // Write the named destination tree if there are any entries. - let mut name_dict = catalog.names(); - if has_dests { - let mut dests_name_tree = name_dict.destinations(); - let mut names = dests_name_tree.names(); - for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { - names.insert(Str(name.resolve().as_bytes()), dest_ref); - } - } - - if has_embeddings { - let mut embedded_files = name_dict.embedded_files(); - let mut names = embedded_files.names(); - for (name, file_ref) in &ctx.references.embedded_files { - names.insert(Str(name.as_bytes()), *file_ref); - } - } - } - - if has_embeddings && ctx.options.standards.pdfa { - // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. - let mut associated_files = catalog.insert(Name(b"AF")).array().typed(); - for (_, file_ref) in ctx.references.embedded_files { - associated_files.item(file_ref).finish(); - } - } - - // Insert the page labels. - if !page_labels.is_empty() { - let mut num_tree = catalog.page_labels(); - let mut entries = num_tree.nums(); - for (n, r) in &page_labels { - entries.insert(n.get() as i32 - 1, *r); - } - } - - if let Some(outline_root_id) = outline_root_id { - catalog.outlines(outline_root_id); - } - - if let Some(lang) = lang { - catalog.lang(TextStr(lang.as_str())); - } - - if ctx.options.standards.pdfa { - catalog - .output_intents() - .push() - .subtype(pdf_writer::types::OutputIntentSubtype::PDFA) - .output_condition(TextStr("sRGB")) - .output_condition_identifier(TextStr("Custom")) - .info(TextStr("sRGB IEC61966-2.1")) - .dest_output_profile(ctx.globals.color_functions.srgb.unwrap()); - } - - catalog.finish(); - - if ctx.options.standards.pdfa && pdf.refs().count() > 8388607 { - bail!(Span::detached(), "too many PDF objects"); - } - - Ok(()) -} - -/// Write the page labels. -pub(crate) fn write_page_labels( - chunk: &mut Pdf, - alloc: &mut Ref, - ctx: &WithEverything, -) -> Vec<(NonZeroUsize, Ref)> { - // If there is no exported page labeled, we skip the writing - if !ctx.pages.iter().filter_map(Option::as_ref).any(|p| { - p.label - .as_ref() - .is_some_and(|l| l.prefix.is_some() || l.style.is_some()) - }) { - return Vec::new(); - } - - let empty_label = PdfPageLabel::default(); - let mut result = vec![]; - let mut prev: Option<&PdfPageLabel> = None; - - // Skip non-exported pages for numbering. - for (i, page) in ctx.pages.iter().filter_map(Option::as_ref).enumerate() { - let nr = NonZeroUsize::new(1 + i).unwrap(); - // If there are pages with empty labels between labeled pages, we must - // write empty PageLabel entries. - let label = page.label.as_ref().unwrap_or(&empty_label); - - if let Some(pre) = prev { - if label.prefix == pre.prefix - && label.style == pre.style - && label.offset == pre.offset.map(|n| n.saturating_add(1)) - { - prev = Some(label); - continue; - } - } - - let id = alloc.bump(); - let mut entry = chunk.indirect(id).start::(); - - // Only add what is actually provided. Don't add empty prefix string if - // it wasn't given for example. - if let Some(prefix) = &label.prefix { - entry.prefix(TextStr::trimmed(prefix)); - } - - if let Some(style) = label.style { - entry.style(style.to_pdf_numbering_style()); - } - - if let Some(offset) = label.offset { - entry.offset(offset.get() as i32); - } - - result.push((nr, id)); - prev = Some(label); - } - - result -} - -/// Resolve the document date. -/// -/// (1) If the `document.date` is set to specific `datetime` or `none`, use it. -/// (2) If the `document.date` is set to `auto` or not set, try to use the -/// date from the options. -/// (3) Otherwise, we don't write date metadata. -pub fn document_date( - document_date: Smart>, - timestamp: Option, -) -> (Option, Option) { - match (document_date, timestamp) { - (Smart::Custom(date), _) => (date, None), - (Smart::Auto, Some(timestamp)) => { - (Some(timestamp.datetime), Some(timestamp.timezone)) - } - _ => (None, None), - } -} - -/// Converts a datetime to a pdf-writer date. -pub fn pdf_date(datetime: Datetime, tz: Option) -> Option { - let year = datetime.year().filter(|&y| y >= 0)? as u16; - - let mut pdf_date = pdf_writer::Date::new(year); - - if let Some(month) = datetime.month() { - pdf_date = pdf_date.month(month); - } - - if let Some(day) = datetime.day() { - pdf_date = pdf_date.day(day); - } - - if let Some(h) = datetime.hour() { - pdf_date = pdf_date.hour(h); - } - - if let Some(m) = datetime.minute() { - pdf_date = pdf_date.minute(m); - } - - if let Some(s) = datetime.second() { - pdf_date = pdf_date.second(s); - } - - match tz { - Some(Timezone::UTC) => { - pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0) - } - Some(Timezone::Local { hour_offset, minute_offset }) => { - pdf_date = - pdf_date.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset) - } - None => {} - } - - Some(pdf_date) -} - -/// Converts a datetime to an xmp-writer datetime. -fn xmp_date( - datetime: Datetime, - timezone: Option, -) -> Option { - let year = datetime.year().filter(|&y| y >= 0)? as u16; - let timezone = timezone.map(|tz| match tz { - Timezone::UTC => xmp_writer::Timezone::Utc, - Timezone::Local { hour_offset, minute_offset } => { - // The xmp-writer use signed integers for the minute offset, which - // can be buggy if the minute offset is negative. And because our - // minute_offset is ensured to be `0 <= minute_offset < 60`, we can - // safely cast it to a signed integer. - xmp_writer::Timezone::Local { hour: hour_offset, minute: minute_offset as i8 } - } - }); - Some(DateTime { - year, - month: datetime.month(), - day: datetime.day(), - hour: datetime.hour(), - minute: datetime.minute(), - second: datetime.second(), - timezone, - }) -} diff --git a/crates/typst-pdf/src/color.rs b/crates/typst-pdf/src/color.rs deleted file mode 100644 index 412afca9a..000000000 --- a/crates/typst-pdf/src/color.rs +++ /dev/null @@ -1,394 +0,0 @@ -use std::sync::LazyLock; - -use arrayvec::ArrayVec; -use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref}; -use typst_library::diag::{bail, SourceResult}; -use typst_library::visualize::{Color, ColorSpace, Paint}; -use typst_syntax::Span; - -use crate::{content, deflate, PdfChunk, PdfOptions, Renumber, WithResources}; - -// The names of the color spaces. -pub const SRGB: Name<'static> = Name(b"srgb"); -pub const D65_GRAY: Name<'static> = Name(b"d65gray"); -pub const LINEAR_SRGB: Name<'static> = Name(b"linearrgb"); - -// The ICC profiles. -static SRGB_ICC_DEFLATED: LazyLock> = - LazyLock::new(|| deflate(typst_assets::icc::S_RGB_V4)); -static GRAY_ICC_DEFLATED: LazyLock> = - LazyLock::new(|| deflate(typst_assets::icc::S_GREY_V4)); - -/// The color spaces present in the PDF document -#[derive(Default)] -pub struct ColorSpaces { - use_srgb: bool, - use_d65_gray: bool, - use_linear_rgb: bool, -} - -impl ColorSpaces { - /// Mark a color space as used. - pub fn mark_as_used(&mut self, color_space: ColorSpace) { - match color_space { - ColorSpace::Oklch - | ColorSpace::Oklab - | ColorSpace::Hsl - | ColorSpace::Hsv - | ColorSpace::Srgb => { - self.use_srgb = true; - } - ColorSpace::D65Gray => { - self.use_d65_gray = true; - } - ColorSpace::LinearRgb => { - self.use_linear_rgb = true; - } - ColorSpace::Cmyk => {} - } - } - - /// Write the color spaces to the PDF file. - pub fn write_color_spaces(&self, mut spaces: Dict, refs: &ColorFunctionRefs) { - if self.use_srgb { - write(ColorSpace::Srgb, spaces.insert(SRGB).start(), refs); - } - - if self.use_d65_gray { - write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), refs); - } - - if self.use_linear_rgb { - write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), refs); - } - } - - /// Write the necessary color spaces functions and ICC profiles to the - /// PDF file. - pub fn write_functions(&self, chunk: &mut Chunk, refs: &ColorFunctionRefs) { - // Write the sRGB color space. - if let Some(id) = refs.srgb { - chunk - .icc_profile(id, &SRGB_ICC_DEFLATED) - .n(3) - .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) - .filter(Filter::FlateDecode); - } - - // Write the gray color space. - if let Some(id) = refs.d65_gray { - chunk - .icc_profile(id, &GRAY_ICC_DEFLATED) - .n(1) - .range([0.0, 1.0]) - .filter(Filter::FlateDecode); - } - } - - /// Merge two color space usage information together: a given color space is - /// considered to be used if it is used on either side. - pub fn merge(&mut self, other: &Self) { - self.use_d65_gray |= other.use_d65_gray; - self.use_linear_rgb |= other.use_linear_rgb; - self.use_srgb |= other.use_srgb; - } -} - -/// Write the color space. -pub fn write( - color_space: ColorSpace, - writer: writers::ColorSpace, - refs: &ColorFunctionRefs, -) { - match color_space { - ColorSpace::Srgb - | ColorSpace::Oklab - | ColorSpace::Hsl - | ColorSpace::Hsv - | ColorSpace::Oklch => writer.icc_based(refs.srgb.unwrap()), - ColorSpace::D65Gray => writer.icc_based(refs.d65_gray.unwrap()), - ColorSpace::LinearRgb => { - writer.cal_rgb( - [0.9505, 1.0, 1.0888], - None, - Some([1.0, 1.0, 1.0]), - Some([ - 0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722, 0.9505, - ]), - ); - } - ColorSpace::Cmyk => writer.device_cmyk(), - } -} - -/// Global references for color conversion functions. -/// -/// These functions are only written once (at most, they are not written if not -/// needed) in the final document, and be shared by all color space -/// dictionaries. -pub struct ColorFunctionRefs { - pub srgb: Option, - d65_gray: Option, -} - -impl Renumber for ColorFunctionRefs { - fn renumber(&mut self, offset: i32) { - if let Some(r) = &mut self.srgb { - r.renumber(offset); - } - if let Some(r) = &mut self.d65_gray { - r.renumber(offset); - } - } -} - -/// Allocate all necessary [`ColorFunctionRefs`]. -pub fn alloc_color_functions_refs( - context: &WithResources, -) -> SourceResult<(PdfChunk, ColorFunctionRefs)> { - let mut chunk = PdfChunk::new(); - let mut used_color_spaces = ColorSpaces::default(); - - if context.options.standards.pdfa { - used_color_spaces.mark_as_used(ColorSpace::Srgb); - } - - context.resources.traverse(&mut |r| { - used_color_spaces.merge(&r.colors); - Ok(()) - })?; - - let refs = ColorFunctionRefs { - srgb: if used_color_spaces.use_srgb { Some(chunk.alloc()) } else { None }, - d65_gray: if used_color_spaces.use_d65_gray { Some(chunk.alloc()) } else { None }, - }; - - Ok((chunk, refs)) -} - -/// Encodes the color into four f32s, which can be used in a PDF file. -/// Ensures that the values are in the range [0.0, 1.0]. -/// -/// # Why? -/// - Oklab: The a and b components are in the range [-0.5, 0.5] and the PDF -/// specifies (and some readers enforce) that all color values be in the range -/// [0.0, 1.0]. This means that the PostScript function and the encoded color -/// must be offset by 0.5. -/// - HSV/HSL: The hue component is in the range [0.0, 360.0] and the PDF format -/// specifies that it must be in the range [0.0, 1.0]. This means that the -/// PostScript function and the encoded color must be divided by 360.0. -pub trait ColorEncode { - /// Performs the color to PDF f32 array conversion. - fn encode(&self, color: Color) -> [f32; 4]; -} - -impl ColorEncode for ColorSpace { - fn encode(&self, color: Color) -> [f32; 4] { - match self { - ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::Hsl | ColorSpace::Hsv => { - color.to_space(ColorSpace::Srgb).to_vec4() - } - _ => color.to_space(*self).to_vec4(), - } - } -} - -/// Encodes a paint into either a fill or stroke color. -pub(super) trait PaintEncode { - /// Set the paint as the fill color. - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()>; - - /// Set the paint as the stroke color. - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()>; -} - -impl PaintEncode for Paint { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - match self { - Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms), - Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms), - Self::Tiling(tiling) => tiling.set_as_fill(ctx, on_text, transforms), - } - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - match self { - Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms), - Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms), - Self::Tiling(tiling) => tiling.set_as_stroke(ctx, on_text, transforms), - } - } -} - -impl PaintEncode for Color { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - _: bool, - _: content::Transforms, - ) -> SourceResult<()> { - match self { - Color::Luma(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::D65Gray); - ctx.set_fill_color_space(D65_GRAY); - - let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); - ctx.content.set_fill_color([l]); - } - Color::LinearRgb(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb); - ctx.set_fill_color_space(LINEAR_SRGB); - - let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); - ctx.content.set_fill_color([r, g, b]); - } - // Oklab & friends are encoded as RGB. - Color::Rgb(_) - | Color::Oklab(_) - | Color::Oklch(_) - | Color::Hsl(_) - | Color::Hsv(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::Srgb); - ctx.set_fill_color_space(SRGB); - - let [r, g, b, _] = ColorSpace::Srgb.encode(*self); - ctx.content.set_fill_color([r, g, b]); - } - Color::Cmyk(_) => { - check_cmyk_allowed(ctx.options)?; - ctx.reset_fill_color_space(); - - let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); - ctx.content.set_fill_cmyk(c, m, y, k); - } - } - Ok(()) - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - _: bool, - _: content::Transforms, - ) -> SourceResult<()> { - match self { - Color::Luma(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::D65Gray); - ctx.set_stroke_color_space(D65_GRAY); - - let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); - ctx.content.set_stroke_color([l]); - } - Color::LinearRgb(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb); - ctx.set_stroke_color_space(LINEAR_SRGB); - - let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); - ctx.content.set_stroke_color([r, g, b]); - } - // Oklab & friends are encoded as RGB. - Color::Rgb(_) - | Color::Oklab(_) - | Color::Oklch(_) - | Color::Hsl(_) - | Color::Hsv(_) => { - ctx.resources.colors.mark_as_used(ColorSpace::Srgb); - ctx.set_stroke_color_space(SRGB); - - let [r, g, b, _] = ColorSpace::Srgb.encode(*self); - ctx.content.set_stroke_color([r, g, b]); - } - Color::Cmyk(_) => { - check_cmyk_allowed(ctx.options)?; - ctx.reset_stroke_color_space(); - - let [c, m, y, k] = ColorSpace::Cmyk.encode(*self); - ctx.content.set_stroke_cmyk(c, m, y, k); - } - } - Ok(()) - } -} - -/// Extra color space functions. -pub(super) trait ColorSpaceExt { - /// Returns the range of the color space. - fn range(self) -> &'static [f32]; - - /// Converts a color to the color space. - fn convert(self, color: Color) -> ArrayVec; -} - -impl ColorSpaceExt for ColorSpace { - fn range(self) -> &'static [f32] { - match self { - ColorSpace::D65Gray => &[0.0, 1.0], - ColorSpace::Oklab => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Oklch => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::LinearRgb => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Srgb => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Cmyk => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Hsl => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - ColorSpace::Hsv => &[0.0, 1.0, 0.0, 1.0, 0.0, 1.0], - } - } - - fn convert(self, color: Color) -> ArrayVec { - let components = self.encode(color); - - self.range() - .chunks(2) - .zip(components) - .map(|(range, component)| U::quantize(component, [range[0], range[1]])) - .collect() - } -} - -/// Quantizes a color component to a specific type. -pub(super) trait QuantizedColor { - fn quantize(color: f32, range: [f32; 2]) -> Self; -} - -impl QuantizedColor for u16 { - fn quantize(color: f32, [min, max]: [f32; 2]) -> Self { - let value = (color - min) / (max - min); - (value * Self::MAX as f32).round().clamp(0.0, Self::MAX as f32) as Self - } -} - -impl QuantizedColor for f32 { - fn quantize(color: f32, [min, max]: [f32; 2]) -> Self { - color.clamp(min, max) - } -} - -/// Fails with an error if PDF/A processing is enabled. -pub(super) fn check_cmyk_allowed(options: &PdfOptions) -> SourceResult<()> { - if options.standards.pdfa { - bail!( - Span::detached(), - "cmyk colors are not currently supported by PDF/A export" - ); - } - Ok(()) -} diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs deleted file mode 100644 index 1183e966e..000000000 --- a/crates/typst-pdf/src/color_font.rs +++ /dev/null @@ -1,344 +0,0 @@ -//! OpenType fonts generally define monochrome glyphs, but they can also define -//! glyphs with colors. This is how emojis are generally implemented for -//! example. -//! -//! There are various standards to represent color glyphs, but PDF readers don't -//! support any of them natively, so Typst has to handle them manually. - -use std::collections::HashMap; - -use ecow::eco_format; -use indexmap::IndexMap; -use pdf_writer::types::UnicodeCmap; -use pdf_writer::writers::WMode; -use pdf_writer::{Filter, Finish, Name, Rect, Ref}; -use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; -use typst_library::foundations::Repr; -use typst_library::layout::Em; -use typst_library::text::color::glyph_frame; -use typst_library::text::{Font, Glyph, TextItemView}; - -use crate::font::{base_font_name, write_font_descriptor, CMAP_NAME, SYSTEM_INFO}; -use crate::resources::{Resources, ResourcesRefs}; -use crate::{content, EmExt, PdfChunk, PdfOptions, WithGlobalRefs}; - -/// Write color fonts in the PDF document. -/// -/// They are written as Type3 fonts, which map glyph IDs to arbitrary PDF -/// instructions. -pub fn write_color_fonts( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut out = HashMap::new(); - let mut chunk = PdfChunk::new(); - context.resources.traverse(&mut |resources: &Resources| { - let Some(color_fonts) = &resources.color_fonts else { - return Ok(()); - }; - - for (color_font, font_slice) in color_fonts.iter() { - if out.contains_key(&font_slice) { - continue; - } - - // Allocate some IDs. - let subfont_id = chunk.alloc(); - let cmap_ref = chunk.alloc(); - let descriptor_ref = chunk.alloc(); - let widths_ref = chunk.alloc(); - - // And a map between glyph IDs and the instructions to draw this - // glyph. - let mut glyphs_to_instructions = Vec::new(); - - let start = font_slice.subfont * 256; - let end = (start + 256).min(color_font.glyphs.len()); - let glyph_count = end - start; - let subset = &color_font.glyphs[start..end]; - let mut widths = Vec::new(); - let mut gids = Vec::new(); - - let scale_factor = font_slice.font.ttf().units_per_em() as f32; - - // Write the instructions for each glyph. - for color_glyph in subset { - let instructions_stream_ref = chunk.alloc(); - let width = font_slice - .font - .advance(color_glyph.gid) - .unwrap_or(Em::new(0.0)) - .get() as f32 - * scale_factor; - widths.push(width); - chunk - .stream( - instructions_stream_ref, - color_glyph.instructions.content.wait(), - ) - .filter(Filter::FlateDecode); - - // Use this stream as instructions to draw the glyph. - glyphs_to_instructions.push(instructions_stream_ref); - gids.push(color_glyph.gid); - } - - // Determine the base font name. - gids.sort(); - let base_font = base_font_name(&font_slice.font, &gids); - - // Write the Type3 font object. - let mut pdf_font = chunk.type3_font(subfont_id); - pdf_font.name(Name(base_font.as_bytes())); - pdf_font.pair(Name(b"Resources"), color_fonts.resources.reference); - pdf_font.bbox(color_font.bbox); - pdf_font.matrix([1.0 / scale_factor, 0.0, 0.0, 1.0 / scale_factor, 0.0, 0.0]); - pdf_font.first_char(0); - pdf_font.last_char((glyph_count - 1) as u8); - pdf_font.pair(Name(b"Widths"), widths_ref); - pdf_font.to_unicode(cmap_ref); - pdf_font.font_descriptor(descriptor_ref); - - // Write the /CharProcs dictionary, that maps glyph names to - // drawing instructions. - let mut char_procs = pdf_font.char_procs(); - for (gid, instructions_ref) in glyphs_to_instructions.iter().enumerate() { - char_procs - .pair(Name(eco_format!("glyph{gid}").as_bytes()), *instructions_ref); - } - char_procs.finish(); - - // Write the /Encoding dictionary. - let names = (0..glyph_count) - .map(|gid| eco_format!("glyph{gid}")) - .collect::>(); - pdf_font - .encoding_custom() - .differences() - .consecutive(0, names.iter().map(|name| Name(name.as_bytes()))); - pdf_font.finish(); - - // Encode a CMAP to make it possible to search or copy glyphs. - let glyph_set = resources.color_glyph_sets.get(&font_slice.font).unwrap(); - let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); - for (index, glyph) in subset.iter().enumerate() { - let Some(text) = glyph_set.get(&glyph.gid) else { - continue; - }; - - if !text.is_empty() { - cmap.pair_with_multiple(index as u8, text.chars()); - } - } - chunk.cmap(cmap_ref, &cmap.finish()).writing_mode(WMode::Horizontal); - - // Write the font descriptor. - write_font_descriptor( - &mut chunk, - descriptor_ref, - &font_slice.font, - &base_font, - ); - - // Write the widths array - chunk.indirect(widths_ref).array().items(widths); - - out.insert(font_slice, subfont_id); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// A mapping between `Font`s and all the corresponding `ColorFont`s. -/// -/// This mapping is one-to-many because there can only be 256 glyphs in a Type 3 -/// font, and fonts generally have more color glyphs than that. -pub struct ColorFontMap { - /// The mapping itself. - map: IndexMap, - /// The resources required to render the fonts in this map. - /// - /// For example, this can be the images for glyphs based on bitmaps or SVG. - pub resources: Resources, - /// The number of font slices (groups of 256 color glyphs), across all color - /// fonts. - total_slice_count: usize, -} - -/// A collection of Type3 font, belonging to the same TTF font. -pub struct ColorFont { - /// The IDs of each sub-slice of this font. They are the numbers after "Cf" - /// in the Resources dictionaries. - slice_ids: Vec, - /// The list of all color glyphs in this family. - /// - /// The index in this vector modulo 256 corresponds to the index in one of - /// the Type3 fonts in `refs` (the `n`-th in the vector, where `n` is the - /// quotient of the index divided by 256). - pub glyphs: Vec, - /// The global bounding box of the font. - pub bbox: Rect, - /// A mapping between glyph IDs and character indices in the `glyphs` - /// vector. - glyph_indices: HashMap, -} - -/// A single color glyph. -pub struct ColorGlyph { - /// The ID of the glyph. - pub gid: u16, - /// Instructions to draw the glyph. - pub instructions: content::Encoded, -} - -impl ColorFontMap<()> { - /// Creates a new empty mapping - pub fn new() -> Self { - Self { - map: IndexMap::new(), - total_slice_count: 0, - resources: Resources::default(), - } - } - - /// For a given glyph in a TTF font, give the ID of the Type3 font and the - /// index of the glyph inside of this Type3 font. - /// - /// If this is the first occurrence of this glyph in this font, it will - /// start its encoding and add it to the list of known glyphs. - pub fn get( - &mut self, - options: &PdfOptions, - 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( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - ColorFont { - bbox, - slice_ids: Vec::new(), - glyphs: Vec::new(), - glyph_indices: HashMap::new(), - } - }); - - 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 { - // Otherwise, allocate a new ColorGlyph in the font, and a new Type3 font - // if needed - let index = color_font.glyphs.len(); - if index % 256 == 0 { - color_font.slice_ids.push(self.total_slice_count); - self.total_slice_count += 1; - } - - 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, - &frame, - None, - Some(width as f32), - )?; - 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) - }) - } - - /// Assign references to the resource dictionary used by this set of color - /// fonts. - pub fn with_refs(self, refs: &ResourcesRefs) -> ColorFontMap { - ColorFontMap { - map: self.map, - resources: self.resources.with_refs(refs), - total_slice_count: self.total_slice_count, - } - } -} - -impl ColorFontMap { - /// Iterate over all Type3 fonts. - /// - /// Each item of this iterator maps to a Type3 font: it contains - /// at most 256 glyphs. A same TTF font can yield multiple Type3 fonts. - pub fn iter(&self) -> ColorFontMapIter<'_, R> { - ColorFontMapIter { map: self, font_index: 0, slice_index: 0 } - } -} - -/// Iterator over a [`ColorFontMap`]. -/// -/// See [`ColorFontMap::iter`]. -pub struct ColorFontMapIter<'a, R> { - /// The map over which to iterate - map: &'a ColorFontMap, - /// The index of TTF font on which we currently iterate - font_index: usize, - /// The sub-font (slice of at most 256 glyphs) at which we currently are. - slice_index: usize, -} - -impl<'a, R> Iterator for ColorFontMapIter<'a, R> { - type Item = (&'a ColorFont, ColorFontSlice); - - fn next(&mut self) -> Option { - let (font, color_font) = self.map.map.get_index(self.font_index)?; - let slice_count = (color_font.glyphs.len() / 256) + 1; - - if self.slice_index >= slice_count { - self.font_index += 1; - self.slice_index = 0; - return self.next(); - } - - let slice = ColorFontSlice { font: font.clone(), subfont: self.slice_index }; - self.slice_index += 1; - Some((color_font, slice)) - } -} - -/// A set of at most 256 glyphs (a limit imposed on Type3 fonts by the PDF -/// specification) that represents a part of a TTF font. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct ColorFontSlice { - /// The original TTF font. - pub font: Font, - /// The index of the Type3 font, among all those that are necessary to - /// 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 deleted file mode 100644 index 8b7517f51..000000000 --- a/crates/typst-pdf/src/content.rs +++ /dev/null @@ -1,823 +0,0 @@ -//! Generic writer for PDF content. -//! -//! It is used to write page contents, color glyph instructions, and tilings. -//! -//! See also [`pdf_writer::Content`]. - -use ecow::eco_format; -use pdf_writer::types::{ - ColorSpaceOperand, LineCapStyle, LineJoinStyle, TextRenderingMode, -}; -use pdf_writer::writers::PositionedItems; -use pdf_writer::{Content, Finish, Name, Rect, Str}; -use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; -use typst_library::foundations::Repr; -use typst_library::layout::{ - Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform, -}; -use typst_library::model::Destination; -use typst_library::text::color::should_outline; -use typst_library::text::{Font, Glyph, TextItem, TextItemView}; -use typst_library::visualize::{ - Curve, CurveItem, FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, - Shape, -}; -use typst_syntax::Span; -use typst_utils::{Deferred, Numeric, SliceExt}; - -use crate::color::PaintEncode; -use crate::color_font::ColorFontMap; -use crate::extg::ExtGState; -use crate::image::deferred_image; -use crate::resources::Resources; -use crate::{deflate_deferred, AbsExt, ContentExt, EmExt, PdfOptions, StrExt}; - -/// Encode a [`Frame`] into a content stream. -/// -/// The resources that were used in the stream will be added to `resources`. -/// -/// `color_glyph_width` should be `None` unless the `Frame` represents a [color -/// glyph]. -/// -/// [color glyph]: `crate::color_font` -pub fn build( - options: &PdfOptions, - resources: &mut Resources<()>, - frame: &Frame, - fill: Option, - color_glyph_width: Option, -) -> SourceResult { - let size = frame.size(); - let mut ctx = Builder::new(options, resources, size); - - if let Some(width) = color_glyph_width { - ctx.content.start_color_glyph(width); - } - - // Make the coordinate system start at the top-left. - ctx.transform( - // Make the Y axis go upwards - Transform::scale(Ratio::one(), -Ratio::one()) - // Also move the origin to the top left corner - .post_concat(Transform::translate(Abs::zero(), size.y)), - ); - - if let Some(fill) = fill { - let shape = Geometry::Rect(frame.size()).filled(fill); - write_shape(&mut ctx, Point::zero(), &shape)?; - } - - // Encode the frame into the content stream. - write_frame(&mut ctx, frame)?; - - Ok(Encoded { - size, - content: deflate_deferred(ctx.content.finish()), - uses_opacities: ctx.uses_opacities, - links: ctx.links, - }) -} - -/// An encoded content stream. -pub struct Encoded { - /// The dimensions of the content. - pub size: Size, - /// The actual content stream. - pub content: Deferred>, - /// Whether the content opacities. - pub uses_opacities: bool, - /// Links in the PDF coordinate system. - pub links: Vec<(Destination, Rect)>, -} - -/// An exporter for a single PDF content stream. -/// -/// Content streams are a series of PDF commands. They can reference external -/// objects only through resources. -/// -/// Content streams can be used for page contents, but also to describe color -/// glyphs and tilings. -pub struct Builder<'a, R = ()> { - /// Settings for PDF export. - pub(crate) options: &'a PdfOptions<'a>, - /// A list of all resources that are used in the content stream. - pub(crate) resources: &'a mut Resources, - /// The PDF content stream that is being built. - pub content: Content, - /// Current graphic state. - state: State, - /// Stack of saved graphic states. - saves: Vec, - /// Whether any stroke or fill was not totally opaque. - uses_opacities: bool, - /// All clickable links that are present in this content. - links: Vec<(Destination, Rect)>, -} - -impl<'a, R> Builder<'a, R> { - /// Create a new content builder. - pub fn new( - options: &'a PdfOptions<'a>, - resources: &'a mut Resources, - size: Size, - ) -> Self { - Builder { - options, - resources, - uses_opacities: false, - content: Content::new(), - state: State::new(size), - saves: vec![], - links: vec![], - } - } -} - -/// A simulated graphics state used to deduplicate graphics state changes and -/// keep track of the current transformation matrix for link annotations. -#[derive(Debug, Clone)] -struct State { - /// The transform of the current item. - transform: Transform, - /// The transform of first hard frame in the hierarchy. - container_transform: Transform, - /// The size of the first hard frame in the hierarchy. - size: Size, - /// The current font. - font: Option<(Font, Abs)>, - /// The current fill paint. - fill: Option, - /// The color space of the current fill paint. - fill_space: Option>, - /// The current external graphic state. - external_graphics_state: ExtGState, - /// The current stroke paint. - stroke: Option, - /// The color space of the current stroke paint. - stroke_space: Option>, - /// The current text rendering mode. - text_rendering_mode: TextRenderingMode, -} - -impl State { - /// Creates a new, clean state for a given `size`. - pub fn new(size: Size) -> Self { - Self { - transform: Transform::identity(), - container_transform: Transform::identity(), - size, - font: None, - fill: None, - fill_space: None, - external_graphics_state: ExtGState::default(), - stroke: None, - stroke_space: None, - text_rendering_mode: TextRenderingMode::Fill, - } - } - - /// Creates the [`Transforms`] structure for the current item. - pub fn transforms(&self, size: Size, pos: Point) -> Transforms { - Transforms { - transform: self.transform.pre_concat(Transform::translate(pos.x, pos.y)), - container_transform: self.container_transform, - container_size: self.size, - size, - } - } -} - -/// Subset of the state used to calculate the transform of gradients and tilings. -#[derive(Debug, Clone, Copy)] -pub(super) struct Transforms { - /// The transform of the current item. - pub transform: Transform, - /// The transform of first hard frame in the hierarchy. - pub container_transform: Transform, - /// The size of the first hard frame in the hierarchy. - pub container_size: Size, - /// The size of the item. - pub size: Size, -} - -impl Builder<'_, ()> { - fn save_state(&mut self) -> SourceResult<()> { - self.saves.push(self.state.clone()); - self.content.save_state_checked() - } - - fn restore_state(&mut self) { - self.content.restore_state(); - self.state = self.saves.pop().expect("missing state save"); - } - - fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) { - let current_state = &self.state.external_graphics_state; - if current_state != graphics_state { - let index = self.resources.ext_gs.insert(*graphics_state); - let name = eco_format!("Gs{index}"); - self.content.set_parameters(Name(name.as_bytes())); - - self.state.external_graphics_state = *graphics_state; - if graphics_state.uses_opacities() { - self.uses_opacities = true; - } - } - } - - fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) { - let get_opacity = |paint: &Paint| { - let color = match paint { - Paint::Solid(color) => *color, - Paint::Gradient(_) | Paint::Tiling(_) => return 255, - }; - - color.alpha().map_or(255, |v| (v * 255.0).round() as u8) - }; - - let stroke_opacity = stroke.map_or(255, |stroke| get_opacity(&stroke.paint)); - let fill_opacity = fill.map_or(255, get_opacity); - self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); - } - - fn reset_opacities(&mut self) { - self.set_external_graphics_state(&ExtGState { - stroke_opacity: 255, - fill_opacity: 255, - }); - } - - pub fn transform(&mut self, transform: Transform) { - let Transform { sx, ky, kx, sy, tx, ty } = transform; - self.state.transform = self.state.transform.pre_concat(transform); - if self.state.container_transform.is_identity() { - self.state.container_transform = self.state.transform; - } - self.content.transform([ - sx.get() as _, - ky.get() as _, - kx.get() as _, - sy.get() as _, - tx.to_f32(), - ty.to_f32(), - ]); - } - - fn group_transform(&mut self, transform: Transform) { - self.state.container_transform = - self.state.container_transform.pre_concat(transform); - } - - fn set_font(&mut self, font: &Font, size: Abs) { - if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) { - let index = self.resources.fonts.insert(font.clone()); - let name = eco_format!("F{index}"); - self.content.set_font(Name(name.as_bytes()), size.to_f32()); - self.state.font = Some((font.clone(), size)); - } - } - - fn size(&mut self, size: Size) { - self.state.size = size; - } - - fn set_fill( - &mut self, - fill: &Paint, - on_text: bool, - transforms: Transforms, - ) -> SourceResult<()> { - if self.state.fill.as_ref() != Some(fill) - || matches!(self.state.fill, Some(Paint::Gradient(_))) - { - fill.set_as_fill(self, on_text, transforms)?; - self.state.fill = Some(fill.clone()); - } - Ok(()) - } - - pub fn set_fill_color_space(&mut self, space: Name<'static>) { - if self.state.fill_space != Some(space) { - self.content.set_fill_color_space(ColorSpaceOperand::Named(space)); - self.state.fill_space = Some(space); - } - } - - pub fn reset_fill_color_space(&mut self) { - self.state.fill_space = None; - } - - fn set_stroke( - &mut self, - stroke: &FixedStroke, - on_text: bool, - transforms: Transforms, - ) -> SourceResult<()> { - if self.state.stroke.as_ref() != Some(stroke) - || matches!( - self.state.stroke.as_ref().map(|s| &s.paint), - Some(Paint::Gradient(_)) - ) - { - let FixedStroke { paint, thickness, cap, join, dash, miter_limit } = stroke; - paint.set_as_stroke(self, on_text, transforms)?; - - self.content.set_line_width(thickness.to_f32()); - if self.state.stroke.as_ref().map(|s| &s.cap) != Some(cap) { - self.content.set_line_cap(to_pdf_line_cap(*cap)); - } - if self.state.stroke.as_ref().map(|s| &s.join) != Some(join) { - self.content.set_line_join(to_pdf_line_join(*join)); - } - if self.state.stroke.as_ref().map(|s| &s.dash) != Some(dash) { - if let Some(dash) = dash { - self.content.set_dash_pattern( - dash.array.iter().map(|l| l.to_f32()), - dash.phase.to_f32(), - ); - } else { - self.content.set_dash_pattern([], 0.0); - } - } - if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { - self.content.set_miter_limit(miter_limit.get() as f32); - } - self.state.stroke = Some(stroke.clone()); - } - - Ok(()) - } - - pub fn set_stroke_color_space(&mut self, space: Name<'static>) { - if self.state.stroke_space != Some(space) { - self.content.set_stroke_color_space(ColorSpaceOperand::Named(space)); - self.state.stroke_space = Some(space); - } - } - - pub fn reset_stroke_color_space(&mut self) { - self.state.stroke_space = None; - } - - fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) { - if self.state.text_rendering_mode != mode { - self.content.set_text_rendering_mode(mode); - self.state.text_rendering_mode = mode; - } - } -} - -/// Encode a frame into the content stream. -pub(crate) fn write_frame(ctx: &mut Builder, frame: &Frame) -> SourceResult<()> { - for &(pos, ref item) in frame.items() { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - match item { - FrameItem::Group(group) => write_group(ctx, pos, group)?, - FrameItem::Text(text) => write_text(ctx, pos, text)?, - FrameItem::Shape(shape, _) => write_shape(ctx, pos, shape)?, - FrameItem::Image(image, size, span) => { - write_image(ctx, x, y, image, *size, *span)? - } - FrameItem::Link(dest, size) => write_link(ctx, pos, dest, *size), - FrameItem::Tag(_) => {} - } - } - Ok(()) -} - -/// Encode a group into the content stream. -fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) -> SourceResult<()> { - let translation = Transform::translate(pos.x, pos.y); - - ctx.save_state()?; - - if group.frame.kind().is_hard() { - ctx.group_transform( - ctx.state - .transform - .post_concat(ctx.state.container_transform.invert().unwrap()) - .pre_concat(translation) - .pre_concat(group.transform), - ); - ctx.size(group.frame.size()); - } - - ctx.transform(translation.pre_concat(group.transform)); - if let Some(clip_curve) = &group.clip { - write_curve(ctx, 0.0, 0.0, clip_curve); - ctx.content.clip_nonzero(); - ctx.content.end_path(); - } - - write_frame(ctx, &group.frame)?; - ctx.restore_state(); - - Ok(()) -} - -/// Encode a text run into the content stream. -fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) -> SourceResult<()> { - 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 outline_glyphs = - text.glyphs.iter().filter(|g| should_outline(&text.font, g)).count(); - - 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 into smaller text runs. - let mut offset = 0; - let mut position_in_run = Abs::zero(); - for (should_outline, sub_run) in - text.glyphs.group_by_key(|g| should_outline(&text.font, g)) - { - let end = offset + sub_run.len(); - - // Build a sub text-run - let text_item_view = TextItemView::from_glyph_range(text, offset..end); - - // Adjust the position of the run on the line - 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 should_outline { - write_normal_text(ctx, pos, text_item_view)?; - } else { - write_complex_glyphs(ctx, pos, text_item_view)?; - } - } - } - - Ok(()) -} - -/// Encodes a text run (without any color glyph) into the content stream. -fn write_normal_text( - ctx: &mut Builder, - pos: Point, - text: TextItemView, -) -> SourceResult<()> { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - *ctx.resources.languages.entry(text.item.lang).or_insert(0) += text.glyph_range.len(); - - let glyph_set = ctx.resources.glyph_sets.entry(text.item.font.clone()).or_default(); - for g in text.glyphs() { - glyph_set.entry(g.id).or_insert_with(|| text.glyph_text(g)); - } - - let fill_transform = ctx.state.transforms(Size::zero(), pos); - ctx.set_fill(&text.item.fill, true, fill_transform)?; - - let stroke = text.item.stroke.as_ref().and_then(|stroke| { - if stroke.thickness.to_f32() > 0.0 { - Some(stroke) - } else { - None - } - }); - - if let Some(stroke) = stroke { - ctx.set_stroke(stroke, true, fill_transform)?; - ctx.set_text_rendering_mode(TextRenderingMode::FillStroke); - } else { - ctx.set_text_rendering_mode(TextRenderingMode::Fill); - } - - ctx.set_font(&text.item.font, text.item.size); - ctx.set_opacities(text.item.stroke.as_ref(), Some(&text.item.fill)); - ctx.content.begin_text(); - - // Position the text. - ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); - - let mut positioned = ctx.content.show_positioned(); - let mut items = positioned.items(); - let mut adjustment = Em::zero(); - let mut encoded = vec![]; - - let glyph_remapper = ctx - .resources - .glyph_remappers - .entry(text.item.font.clone()) - .or_default(); - - // 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() { - if !encoded.is_empty() { - show_text(&mut items, &encoded); - encoded.clear(); - } - - items.adjust(-adjustment.to_font_units()); - adjustment = Em::zero(); - } - - // In PDF, we use CIDs to index the glyphs in a font, not GIDs. What a - // CID actually refers to depends on the type of font we are embedding: - // - // - For TrueType fonts, the CIDs are defined by an external mapping. - // - For SID-keyed CFF fonts, the CID is the same as the GID in the font. - // - For CID-keyed CFF fonts, the CID refers to the CID in the font. - // - // (See in the PDF-spec for more details on this.) - // - // However, in our case: - // - We use the identity-mapping for TrueType fonts. - // - SID-keyed fonts will get converted into CID-keyed fonts by the - // subsetter. - // - CID-keyed fonts will be rewritten in a way so that the mapping - // between CID and GID is always the identity mapping, regardless of - // the mapping before. - // - // Because of this, we can always use the remapped GID as the CID, - // regardless of which type of font we are actually embedding. - let cid = glyph_remapper.remap(glyph.id); - encoded.push((cid >> 8) as u8); - encoded.push((cid & 0xff) as u8); - - if let Some(advance) = text.item.font.advance(glyph.id) { - adjustment += glyph.x_advance - advance; - } - - adjustment -= glyph.x_offset; - } - - if !encoded.is_empty() { - show_text(&mut items, &encoded); - } - - items.finish(); - positioned.finish(); - ctx.content.end_text(); - - Ok(()) -} - -/// Shows text, ensuring that each individual string doesn't exceed the -/// implementation limits. -fn show_text(items: &mut PositionedItems, encoded: &[u8]) { - for chunk in encoded.chunks(Str::PDFA_LIMIT) { - items.show(Str(chunk)); - } -} - -/// Encodes a text run made only of color glyphs into the content stream -fn write_complex_glyphs( - ctx: &mut Builder, - pos: Point, - text: TextItemView, -) -> SourceResult<()> { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - let mut last_font = None; - - ctx.reset_opacities(); - - ctx.content.begin_text(); - ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); - // So that the next call to ctx.set_font() will change the font to one that - // displays regular glyphs and not color glyphs. - ctx.state.font = None; - - let glyph_set = ctx - .resources - .color_glyph_sets - .entry(text.item.font.clone()) - .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, glyph)?; - - if last_font != Some(font) { - ctx.content.set_font( - Name(eco_format!("Cf{}", font).as_bytes()), - text.item.size.to_f32(), - ); - last_font = Some(font); - } - - ctx.content.show(Str(&[index])); - - glyph_set.entry(glyph.id).or_insert_with(|| text.glyph_text(glyph)); - } - ctx.content.end_text(); - - Ok(()) -} - -/// Encode a geometrical shape into the content stream. -fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) -> SourceResult<()> { - let x = pos.x.to_f32(); - let y = pos.y.to_f32(); - - let stroke = shape.stroke.as_ref().and_then(|stroke| { - if stroke.thickness.to_f32() > 0.0 { - Some(stroke) - } else { - None - } - }); - - if shape.fill.is_none() && stroke.is_none() { - return Ok(()); - } - - if let Some(fill) = &shape.fill { - ctx.set_fill(fill, false, ctx.state.transforms(shape.geometry.bbox_size(), pos))?; - } - - if let Some(stroke) = stroke { - ctx.set_stroke( - stroke, - false, - ctx.state.transforms(shape.geometry.bbox_size(), pos), - )?; - } - - ctx.set_opacities(stroke, shape.fill.as_ref()); - - match &shape.geometry { - Geometry::Line(target) => { - let dx = target.x.to_f32(); - let dy = target.y.to_f32(); - ctx.content.move_to(x, y); - ctx.content.line_to(x + dx, y + dy); - } - Geometry::Rect(size) => { - let w = size.x.to_f32(); - let h = size.y.to_f32(); - if w.abs() > f32::EPSILON && h.abs() > f32::EPSILON { - ctx.content.rect(x, y, w, h); - } - } - Geometry::Curve(curve) => { - write_curve(ctx, x, y, curve); - } - } - - match (&shape.fill, &shape.fill_rule, stroke) { - (None, _, None) => unreachable!(), - (Some(_), FillRule::NonZero, None) => ctx.content.fill_nonzero(), - (Some(_), FillRule::EvenOdd, None) => ctx.content.fill_even_odd(), - (None, _, Some(_)) => ctx.content.stroke(), - (Some(_), FillRule::NonZero, Some(_)) => ctx.content.fill_nonzero_and_stroke(), - (Some(_), FillRule::EvenOdd, Some(_)) => ctx.content.fill_even_odd_and_stroke(), - }; - - Ok(()) -} - -/// Encode a curve into the content stream. -fn write_curve(ctx: &mut Builder, x: f32, y: f32, curve: &Curve) { - for elem in &curve.0 { - match elem { - CurveItem::Move(p) => ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()), - CurveItem::Line(p) => ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()), - CurveItem::Cubic(p1, p2, p3) => ctx.content.cubic_to( - x + p1.x.to_f32(), - y + p1.y.to_f32(), - x + p2.x.to_f32(), - y + p2.y.to_f32(), - x + p3.x.to_f32(), - y + p3.y.to_f32(), - ), - CurveItem::Close => ctx.content.close_path(), - }; - } -} - -/// Encode a vector or raster image into the content stream. -fn write_image( - ctx: &mut Builder, - x: f32, - y: f32, - image: &Image, - size: Size, - span: Span, -) -> SourceResult<()> { - let index = ctx.resources.images.insert(image.clone()); - ctx.resources.deferred_images.entry(index).or_insert_with(|| { - let (image, color_space) = - deferred_image(image.clone(), ctx.options.standards.pdfa); - if let Some(color_space) = color_space { - ctx.resources.colors.mark_as_used(color_space); - } - (image, span) - }); - - ctx.reset_opacities(); - - let name = eco_format!("Im{index}"); - let w = size.x.to_f32(); - let h = size.y.to_f32(); - ctx.content.save_state_checked()?; - ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]); - - if let Some(alt) = image.alt() { - if ctx.options.standards.pdfa && alt.len() > Str::PDFA_LIMIT { - bail!(span, "the image's alt text is too long"); - } - - let mut image_span = - ctx.content.begin_marked_content_with_properties(Name(b"Span")); - let mut image_alt = image_span.properties(); - image_alt.pair(Name(b"Alt"), Str(alt.as_bytes())); - image_alt.finish(); - image_span.finish(); - - ctx.content.x_object(Name(name.as_bytes())); - ctx.content.end_marked_content(); - } else { - ctx.content.x_object(Name(name.as_bytes())); - } - - ctx.content.restore_state(); - Ok(()) -} - -/// Save a link for later writing in the annotations dictionary. -fn write_link(ctx: &mut Builder, pos: Point, dest: &Destination, size: Size) { - let mut min_x = Abs::inf(); - let mut min_y = Abs::inf(); - let mut max_x = -Abs::inf(); - let mut max_y = -Abs::inf(); - - // Compute the bounding box of the transformed link. - for point in [ - pos, - pos + Point::with_x(size.x), - pos + Point::with_y(size.y), - pos + size.to_point(), - ] { - let t = point.transform(ctx.state.transform); - min_x.set_min(t.x); - min_y.set_min(t.y); - max_x.set_max(t.x); - max_y.set_max(t.y); - } - - let x1 = min_x.to_f32(); - let x2 = max_x.to_f32(); - let y1 = max_y.to_f32(); - let y2 = min_y.to_f32(); - let rect = Rect::new(x1, y1, x2, y2); - - ctx.links.push((dest.clone(), rect)); -} - -fn to_pdf_line_cap(cap: LineCap) -> LineCapStyle { - match cap { - LineCap::Butt => LineCapStyle::ButtCap, - LineCap::Round => LineCapStyle::RoundCap, - LineCap::Square => LineCapStyle::ProjectingSquareCap, - } -} - -fn to_pdf_line_join(join: LineJoin) -> LineJoinStyle { - match join { - LineJoin::Miter => LineJoinStyle::MiterJoin, - LineJoin::Round => LineJoinStyle::RoundJoin, - 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-pdf/src/convert.rs b/crates/typst-pdf/src/convert.rs new file mode 100644 index 000000000..f5ca31730 --- /dev/null +++ b/crates/typst-pdf/src/convert.rs @@ -0,0 +1,661 @@ +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::num::NonZeroU64; + +use ecow::{eco_format, EcoVec}; +use krilla::annotation::Annotation; +use krilla::configure::{Configuration, ValidationError, Validator}; +use krilla::destination::{NamedDestination, XyzDestination}; +use krilla::embed::EmbedError; +use krilla::error::KrillaError; +use krilla::geom::PathBuilder; +use krilla::page::{PageLabel, PageSettings}; +use krilla::surface::Surface; +use krilla::{Document, SerializeSettings}; +use krilla_svg::render_svg_glyph; +use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; +use typst_library::foundations::NativeElement; +use typst_library::introspection::Location; +use typst_library::layout::{ + Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform, +}; +use typst_library::model::HeadingElem; +use typst_library::text::{Font, Lang}; +use typst_library::visualize::{Geometry, Paint}; +use typst_syntax::Span; + +use crate::embed::embed_files; +use crate::image::handle_image; +use crate::link::handle_link; +use crate::metadata::build_metadata; +use crate::outline::build_outline; +use crate::page::PageLabelExt; +use crate::shape::handle_shape; +use crate::text::handle_text; +use crate::util::{convert_path, display_font, AbsExt, TransformExt}; +use crate::PdfOptions; + +#[typst_macros::time(name = "convert document")] +pub fn convert( + typst_document: &PagedDocument, + options: &PdfOptions, +) -> SourceResult> { + let settings = SerializeSettings { + compress_content_streams: true, + no_device_cs: true, + ascii_compatible: false, + xmp_metadata: true, + cmyk_profile: None, + configuration: options.standards.config, + enable_tagging: false, + render_svg_glyph_fn: render_svg_glyph, + }; + + let mut document = Document::new_with(settings); + let page_index_converter = PageIndexConverter::new(typst_document, options); + let named_destinations = + collect_named_destinations(typst_document, &page_index_converter); + let mut gc = GlobalContext::new( + typst_document, + options, + named_destinations, + page_index_converter, + ); + + convert_pages(&mut gc, &mut document)?; + embed_files(typst_document, &mut document)?; + + document.set_outline(build_outline(&gc)); + document.set_metadata(build_metadata(&gc)); + + finish(document, gc, options.standards.config) +} + +fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResult<()> { + for (i, typst_page) in gc.document.pages.iter().enumerate() { + if gc.page_index_converter.pdf_page_index(i).is_none() { + // Don't export this page. + continue; + } else { + let mut settings = PageSettings::new( + typst_page.frame.width().to_f32(), + typst_page.frame.height().to_f32(), + ); + + if let Some(label) = typst_page + .numbering + .as_ref() + .and_then(|num| PageLabel::generate(num, typst_page.number)) + .or_else(|| { + // When some pages were ignored from export, we show a page label with + // the correct real (not logical) page number. + // This is for consistency with normal output when pages have no numbering + // and all are exported: the final PDF page numbers always correspond to + // the real (not logical) page numbers. Here, the final PDF page number + // will differ, but we can at least use labels to indicate what was + // the corresponding real page number in the Typst document. + gc.page_index_converter + .has_skipped_pages() + .then(|| PageLabel::arabic((i + 1) as u64)) + }) + { + settings = settings.with_page_label(label); + } + + let mut page = document.start_page_with(settings); + let mut surface = page.surface(); + let mut fc = FrameContext::new(typst_page.frame.size()); + + handle_frame( + &mut fc, + &typst_page.frame, + typst_page.fill_or_transparent(), + &mut surface, + gc, + )?; + + surface.finish(); + + for annotation in fc.annotations { + page.add_annotation(annotation); + } + } + } + + Ok(()) +} + +/// A state allowing us to keep track of transforms and container sizes, +/// which is mainly needed to resolve gradients and patterns correctly. +#[derive(Debug, Clone)] +pub(crate) struct State { + /// The current transform. + transform: Transform, + /// The transform of first hard frame in the hierarchy. + container_transform: Transform, + /// The size of the first hard frame in the hierarchy. + container_size: Size, +} + +impl State { + /// Creates a new, clean state for a given `size`. + fn new(size: Size) -> Self { + Self { + transform: Transform::identity(), + container_transform: Transform::identity(), + container_size: size, + } + } + + pub(crate) fn register_container(&mut self, size: Size) { + self.container_transform = self.transform; + self.container_size = size; + } + + pub(crate) fn pre_concat(&mut self, transform: Transform) { + self.transform = self.transform.pre_concat(transform); + } + + pub(crate) fn transform(&self) -> Transform { + self.transform + } + + pub(crate) fn container_transform(&self) -> Transform { + self.container_transform + } + + pub(crate) fn container_size(&self) -> Size { + self.container_size + } +} + +/// Context needed for converting a single frame. +pub(crate) struct FrameContext { + states: Vec, + annotations: Vec, +} + +impl FrameContext { + pub(crate) fn new(size: Size) -> Self { + Self { + states: vec![State::new(size)], + annotations: vec![], + } + } + + pub(crate) fn push(&mut self) { + self.states.push(self.states.last().unwrap().clone()); + } + + pub(crate) fn pop(&mut self) { + self.states.pop(); + } + + pub(crate) fn state(&self) -> &State { + self.states.last().unwrap() + } + + pub(crate) fn state_mut(&mut self) -> &mut State { + self.states.last_mut().unwrap() + } + + pub(crate) fn push_annotation(&mut self, annotation: Annotation) { + self.annotations.push(annotation); + } +} + +/// Globally needed context for converting a typst document. +pub(crate) struct GlobalContext<'a> { + /// Cache the conversion between krilla and Typst fonts (forward and backward). + pub(crate) fonts_forward: HashMap, + pub(crate) fonts_backward: HashMap, + /// Mapping between images and their span. + // Note: In theory, the same image can have multiple spans + // if it appears in the document multiple times. We just store the + // first appearance, though. + pub(crate) image_to_spans: HashMap, + /// The spans of all images that appear in the document. We use this so + /// we can give more accurate error messages. + pub(crate) image_spans: HashSet, + /// The document to convert. + pub(crate) document: &'a PagedDocument, + /// Options for PDF export. + pub(crate) options: &'a PdfOptions<'a>, + /// Mapping between locations in the document and named destinations. + pub(crate) loc_to_names: HashMap, + /// The languages used throughout the document. + pub(crate) languages: BTreeMap, + pub(crate) page_index_converter: PageIndexConverter, +} + +impl<'a> GlobalContext<'a> { + pub(crate) fn new( + document: &'a PagedDocument, + options: &'a PdfOptions, + loc_to_names: HashMap, + page_index_converter: PageIndexConverter, + ) -> GlobalContext<'a> { + Self { + fonts_forward: HashMap::new(), + fonts_backward: HashMap::new(), + document, + options, + loc_to_names, + image_to_spans: HashMap::new(), + image_spans: HashSet::new(), + languages: BTreeMap::new(), + page_index_converter, + } + } +} + +#[typst_macros::time(name = "handle page")] +pub(crate) fn handle_frame( + fc: &mut FrameContext, + frame: &Frame, + fill: Option, + surface: &mut Surface, + gc: &mut GlobalContext, +) -> SourceResult<()> { + fc.push(); + + if frame.kind().is_hard() { + fc.state_mut().register_container(frame.size()); + } + + if let Some(fill) = fill { + let shape = Geometry::Rect(frame.size()).filled(fill); + handle_shape(fc, &shape, surface, gc, Span::detached())?; + } + + for (point, item) in frame.items() { + fc.push(); + fc.state_mut().pre_concat(Transform::translate(point.x, point.y)); + + match item { + FrameItem::Group(g) => handle_group(fc, g, surface, gc)?, + FrameItem::Text(t) => handle_text(fc, t, surface, gc)?, + FrameItem::Shape(s, span) => handle_shape(fc, s, surface, gc, *span)?, + FrameItem::Image(image, size, span) => { + handle_image(gc, fc, image, *size, surface, *span)? + } + FrameItem::Link(d, s) => handle_link(fc, gc, d, *s), + FrameItem::Tag(_) => {} + } + + fc.pop(); + } + + fc.pop(); + + Ok(()) +} + +pub(crate) fn handle_group( + fc: &mut FrameContext, + group: &GroupItem, + surface: &mut Surface, + context: &mut GlobalContext, +) -> SourceResult<()> { + fc.push(); + fc.state_mut().pre_concat(group.transform); + + let clip_path = group + .clip + .as_ref() + .and_then(|p| { + let mut builder = PathBuilder::new(); + convert_path(p, &mut builder); + builder.finish() + }) + .and_then(|p| p.transform(fc.state().transform.to_krilla())); + + if let Some(clip_path) = &clip_path { + surface.push_clip_path(clip_path, &krilla::paint::FillRule::NonZero); + } + + handle_frame(fc, &group.frame, None, surface, context)?; + + if clip_path.is_some() { + surface.pop(); + } + + fc.pop(); + + Ok(()) +} + +#[typst_macros::time(name = "finish export")] +/// Finish a krilla document and handle export errors. +fn finish( + document: Document, + gc: GlobalContext, + configuration: Configuration, +) -> SourceResult> { + let validator = configuration.validator(); + + match document.finish() { + Ok(r) => Ok(r), + Err(e) => match e { + KrillaError::Font(f, s) => { + let font_str = display_font(gc.fonts_backward.get(&f).unwrap()); + bail!( + Span::detached(), + "failed to process font {font_str}: {s}"; + hint: "make sure the font is valid"; + hint: "the used font might be unsupported by Typst" + ); + } + KrillaError::Validation(ve) => { + let errors = ve + .iter() + .map(|e| convert_error(&gc, validator, e)) + .collect::>(); + Err(errors) + } + KrillaError::Image(_, loc) => { + let span = to_span(loc); + bail!(span, "failed to process image"); + } + KrillaError::SixteenBitImage(image, _) => { + let span = gc.image_to_spans.get(&image).unwrap(); + bail!( + *span, "16 bit images are not supported in this export mode"; + hint: "convert the image to 8 bit instead" + ) + } + }, + } +} + +/// Converts a krilla error into a Typst error. +fn convert_error( + gc: &GlobalContext, + validator: Validator, + error: &ValidationError, +) -> SourceDiagnostic { + let prefix = eco_format!("{} error:", validator.as_str()); + match error { + ValidationError::TooLongString => error!( + Span::detached(), + "{prefix} a PDF string is longer than 32767 characters"; + hint: "ensure title and author names are short enough" + ), + // Should in theory never occur, as krilla always trims font names. + ValidationError::TooLongName => error!( + Span::detached(), + "{prefix} a PDF name is longer than 127 characters"; + hint: "perhaps a font name is too long" + ), + + ValidationError::TooLongArray => error!( + Span::detached(), + "{prefix} a PDF array is longer than 8191 elements"; + hint: "this can happen if you have a very long text in a single line" + ), + ValidationError::TooLongDictionary => error!( + Span::detached(), + "{prefix} a PDF dictionary has more than 4095 entries"; + hint: "try reducing the complexity of your document" + ), + ValidationError::TooLargeFloat => error!( + Span::detached(), + "{prefix} a PDF floating point number is larger than the allowed limit"; + hint: "try exporting with a higher PDF version" + ), + ValidationError::TooManyIndirectObjects => error!( + Span::detached(), + "{prefix} the PDF has too many indirect objects"; + hint: "reduce the size of your document" + ), + // Can only occur if we have 27+ nested clip paths + ValidationError::TooHighQNestingLevel => error!( + Span::detached(), + "{prefix} the PDF has too high q nesting"; + hint: "reduce the number of nested containers" + ), + ValidationError::ContainsPostScript(loc) => error!( + to_span(*loc), + "{prefix} the PDF contains PostScript code"; + hint: "conic gradients are not supported in this PDF standard" + ), + ValidationError::MissingCMYKProfile => error!( + Span::detached(), + "{prefix} the PDF is missing a CMYK profile"; + hint: "CMYK colors are not yet supported in this export mode" + ), + ValidationError::ContainsNotDefGlyph(f, loc, text) => error!( + to_span(*loc), + "{prefix} the text '{text}' cannot be displayed using {}", + display_font(gc.fonts_backward.get(f).unwrap()); + hint: "try using a different font" + ), + ValidationError::InvalidCodepointMapping(_, _, cp, loc) => { + if let Some(c) = cp.map(|c| eco_format!("{:#06x}", c as u32)) { + let msg = if loc.is_some() { + "the PDF contains text with" + } else { + "the text contains" + }; + error!(to_span(*loc), "{prefix} {msg} the disallowed codepoint {c}") + } else { + // I think this code path is in theory unreachable, + // but just to be safe. + let msg = if loc.is_some() { + "the PDF contains text with missing codepoints" + } else { + "the text was not mapped to a code point" + }; + error!( + to_span(*loc), + "{prefix} {msg}"; + hint: "for complex scripts like Arabic, it might not be \ + possible to produce a compliant document" + ) + } + } + ValidationError::UnicodePrivateArea(_, _, c, loc) => { + let code_point = eco_format!("{:#06x}", *c as u32); + let msg = if loc.is_some() { "the PDF" } else { "the text" }; + error!( + to_span(*loc), + "{prefix} {msg} contains the codepoint {code_point}"; + hint: "codepoints from the Unicode private area are \ + forbidden in this export mode" + ) + } + ValidationError::Transparency(loc) => { + let span = to_span(*loc); + let hint1 = "try exporting with a different standard that \ + supports transparency"; + if loc.is_some() { + if gc.image_spans.contains(&span) { + error!( + span, "{prefix} the image contains transparency"; + hint: "{hint1}"; + hint: "or convert the image to a non-transparent one"; + hint: "you might have to convert SVGs into \ + non-transparent bitmap images" + ) + } else { + error!( + span, "{prefix} the used fill or stroke has transparency"; + hint: "{hint1}"; + hint: "or don't use colors with transparency in \ + this export mode" + ) + } + } else { + error!( + span, "{prefix} the PDF contains transparency"; + hint: "{hint1}" + ) + } + } + ValidationError::ImageInterpolation(loc) => { + let span = to_span(*loc); + if loc.is_some() { + error!( + span, "{prefix} the image has smooth scaling"; + hint: "set the `scaling` attribute to `pixelated`" + ) + } else { + error!( + span, "{prefix} an image in the PDF has smooth scaling"; + hint: "set the `scaling` attribute of all images to `pixelated`" + ) + } + } + ValidationError::EmbeddedFile(e, s) => { + // We always set the span for embedded files, so it cannot be detached. + let span = to_span(*s); + match e { + EmbedError::Existence => { + error!( + span, "{prefix} document contains an embedded file"; + hint: "embedded files are not supported in this export mode" + ) + } + EmbedError::MissingDate => { + error!( + span, "{prefix} document date is missing"; + hint: "the document must have a date when embedding files"; + hint: "`set document(date: none)` must not be used in this case" + ) + } + EmbedError::MissingDescription => { + error!(span, "{prefix} the file description is missing") + } + EmbedError::MissingMimeType => { + error!(span, "{prefix} the file mime type is missing") + } + } + } + // The below errors cannot occur yet, only once Typst supports full PDF/A + // and PDF/UA. But let's still add a message just to be on the safe side. + ValidationError::MissingAnnotationAltText => error!( + Span::detached(), + "{prefix} missing annotation alt text"; + hint: "please report this as a bug" + ), + ValidationError::MissingAltText => error!( + Span::detached(), + "{prefix} missing alt text"; + hint: "make sure your images and equations have alt text" + ), + ValidationError::NoDocumentLanguage => error!( + Span::detached(), + "{prefix} missing document language"; + hint: "set the language of the document" + ), + // Needs to be set by typst-pdf. + ValidationError::MissingHeadingTitle => error!( + Span::detached(), + "{prefix} missing heading title"; + hint: "please report this as a bug" + ), + ValidationError::MissingDocumentOutline => error!( + Span::detached(), + "{prefix} missing document outline"; + hint: "please report this as a bug" + ), + ValidationError::MissingTagging => error!( + Span::detached(), + "{prefix} missing document tags"; + hint: "please report this as a bug" + ), + ValidationError::NoDocumentTitle => error!( + Span::detached(), + "{prefix} missing document title"; + hint: "set the title of the document" + ), + ValidationError::MissingDocumentDate => error!( + Span::detached(), + "{prefix} missing document date"; + hint: "set the date of the document" + ), + } +} + +/// Convert a krilla location to a span. +fn to_span(loc: Option) -> Span { + loc.map(|l| Span::from_raw(NonZeroU64::new(l).unwrap())) + .unwrap_or(Span::detached()) +} + +fn collect_named_destinations( + document: &PagedDocument, + pic: &PageIndexConverter, +) -> HashMap { + let mut locs_to_names = HashMap::new(); + + // Find all headings that have a label and are the first among other + // headings with the same label. + let matches: Vec<_> = { + let mut seen = HashSet::new(); + document + .introspector + .query(&HeadingElem::elem().select()) + .iter() + .filter_map(|elem| elem.location().zip(elem.label())) + .filter(|&(_, label)| seen.insert(label)) + .collect() + }; + + for (loc, label) in matches { + let pos = document.introspector.position(loc); + let index = pos.page.get() - 1; + // We are subtracting 10 because the position of links e.g. to headings is always at the + // baseline and if you link directly to it, the text will not be visible + // because it is right above. + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + + // Only add named destination if page belonging to the position is exported. + if let Some(index) = pic.pdf_page_index(index) { + let named = NamedDestination::new( + label.resolve().to_string(), + XyzDestination::new( + index, + krilla::geom::Point::from_xy(pos.point.x.to_f32(), y.to_f32()), + ), + ); + locs_to_names.insert(loc, named); + } + } + + locs_to_names +} + +pub(crate) struct PageIndexConverter { + page_indices: HashMap, + skipped_pages: usize, +} + +impl PageIndexConverter { + pub fn new(document: &PagedDocument, options: &PdfOptions) -> Self { + let mut page_indices = HashMap::new(); + let mut skipped_pages = 0; + + for i in 0..document.pages.len() { + if options + .page_ranges + .as_ref() + .is_some_and(|ranges| !ranges.includes_page_index(i)) + { + skipped_pages += 1; + } else { + page_indices.insert(i, i - skipped_pages); + } + } + + Self { page_indices, skipped_pages } + } + + pub(crate) fn has_skipped_pages(&self) -> bool { + self.skipped_pages > 0 + } + + /// Get the PDF page index of a page index, if it's not excluded. + pub(crate) fn pdf_page_index(&self, page_index: usize) -> Option { + self.page_indices.get(&page_index).copied() + } +} diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs index 597638f4b..6ed65a2b6 100644 --- a/crates/typst-pdf/src/embed.rs +++ b/crates/typst-pdf/src/embed.rs @@ -1,122 +1,54 @@ -use std::collections::BTreeMap; +use std::sync::Arc; -use ecow::EcoString; -use pdf_writer::types::AssociationKind; -use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr}; +use krilla::embed::{AssociationKind, EmbeddedFile}; +use krilla::Document; use typst_library::diag::{bail, SourceResult}; -use typst_library::foundations::{NativeElement, Packed, StyleChain}; +use typst_library::foundations::{NativeElement, StyleChain}; +use typst_library::layout::PagedDocument; use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; -use crate::catalog::{document_date, pdf_date}; -use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs}; +pub(crate) fn embed_files( + typst_doc: &PagedDocument, + document: &mut Document, +) -> SourceResult<()> { + let elements = typst_doc.introspector.query(&EmbedElem::elem().select()); -/// Query for all [`EmbedElem`] and write them and their file specifications. -/// -/// This returns a map of embedding names and references so that we can later -/// add them to the catalog's `/Names` dictionary. -pub fn write_embedded_files( - ctx: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, BTreeMap)> { - let mut chunk = PdfChunk::new(); - let mut embedded_files = BTreeMap::default(); - - let elements = ctx.document.introspector.query(&EmbedElem::elem().select()); for elem in &elements { - if !ctx.options.standards.embedded_files { - // PDF/A-2 requires embedded files to be PDF/A-1 or PDF/A-2, - // which we don't currently check. - bail!( - elem.span(), - "file embeddings are not currently supported for PDF/A-2"; - hint: "PDF/A-3 supports arbitrary embedded files" - ); - } - let embed = elem.to_packed::().unwrap(); - if embed.path.derived.len() > Str::PDFA_LIMIT { - bail!(embed.span(), "embedded file path is too long"); - } - - let id = embed_file(ctx, &mut chunk, embed)?; - if embedded_files.insert(embed.path.derived.clone(), id).is_some() { - bail!( - elem.span(), - "duplicate embedded file for path `{}`", embed.path.derived; - hint: "embedded file paths must be unique", - ); - } - } - - Ok((chunk, embedded_files)) -} - -/// Write the embedded file stream and its file specification. -fn embed_file( - ctx: &WithGlobalRefs, - chunk: &mut PdfChunk, - embed: &Packed, -) -> SourceResult { - let embedded_file_stream_ref = chunk.alloc.bump(); - let file_spec_dict_ref = chunk.alloc.bump(); - - let data = embed.data.as_slice(); - let compressed = deflate(data); - - let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); - embedded_file.filter(Filter::FlateDecode); - - if let Some(mime_type) = embed.mime_type(StyleChain::default()) { - if mime_type.len() > Name::PDFA_LIMIT { - bail!(embed.span(), "embedded file MIME type is too long"); - } - embedded_file.subtype(Name(mime_type.as_bytes())); - } else if ctx.options.standards.pdfa { - bail!(embed.span(), "embedded files must have a MIME type in PDF/A-3"); - } - - let mut params = embedded_file.params(); - params.size(data.len() as i32); - - let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); - if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { - params.modification_date(pdf_date); - } else if ctx.options.standards.pdfa { - bail!( - embed.span(), - "the document must have a date when embedding files in PDF/A-3"; - hint: "`set document(date: none)` must not be used in this case" - ); - } - - params.finish(); - embedded_file.finish(); - - let mut file_spec = chunk.file_spec(file_spec_dict_ref); - file_spec.path(Str(embed.path.derived.as_bytes())); - file_spec.unic_file(TextStr(&embed.path.derived)); - file_spec - .insert(Name(b"EF")) - .dict() - .pair(Name(b"F"), embedded_file_stream_ref) - .pair(Name(b"UF"), embedded_file_stream_ref); - - if ctx.options.standards.pdfa { - // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. - file_spec.association_kind(match embed.relationship(StyleChain::default()) { - Some(EmbeddedFileRelationship::Source) => AssociationKind::Source, - Some(EmbeddedFileRelationship::Data) => AssociationKind::Data, - Some(EmbeddedFileRelationship::Alternative) => AssociationKind::Alternative, - Some(EmbeddedFileRelationship::Supplement) => AssociationKind::Supplement, + let span = embed.span(); + let derived_path = &embed.path.derived; + let path = derived_path.to_string(); + let mime_type = + embed.mime_type(StyleChain::default()).clone().map(|s| s.to_string()); + let description = embed + .description(StyleChain::default()) + .clone() + .map(|s| s.to_string()); + let association_kind = match embed.relationship(StyleChain::default()) { None => AssociationKind::Unspecified, - }); - } + Some(e) => match e { + EmbeddedFileRelationship::Source => AssociationKind::Source, + EmbeddedFileRelationship::Data => AssociationKind::Data, + EmbeddedFileRelationship::Alternative => AssociationKind::Alternative, + EmbeddedFileRelationship::Supplement => AssociationKind::Supplement, + }, + }; + let data: Arc + Send + Sync> = Arc::new(embed.data.clone()); - if let Some(description) = embed.description(StyleChain::default()) { - if description.len() > Str::PDFA_LIMIT { - bail!(embed.span(), "embedded file description is too long"); + let file = EmbeddedFile { + path, + mime_type, + description, + association_kind, + data: data.into(), + compress: true, + location: Some(span.into_raw().get()), + }; + + if document.embed_file(file).is_none() { + bail!(span, "attempted to embed file {derived_path} twice"); } - file_spec.description(TextStr(description)); } - Ok(file_spec_dict_ref) + Ok(()) } diff --git a/crates/typst-pdf/src/extg.rs b/crates/typst-pdf/src/extg.rs deleted file mode 100644 index 06617d8d2..000000000 --- a/crates/typst-pdf/src/extg.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::collections::HashMap; - -use pdf_writer::Ref; -use typst_library::diag::SourceResult; - -use crate::{PdfChunk, WithGlobalRefs}; - -/// A PDF external graphics state. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub struct ExtGState { - // In the range 0-255, needs to be divided before being written into the graphics state! - pub stroke_opacity: u8, - // In the range 0-255, needs to be divided before being written into the graphics state! - pub fill_opacity: u8, -} - -impl Default for ExtGState { - fn default() -> Self { - Self { stroke_opacity: 255, fill_opacity: 255 } - } -} - -impl ExtGState { - pub fn uses_opacities(&self) -> bool { - self.stroke_opacity != 255 || self.fill_opacity != 255 - } -} - -/// Embed all used external graphics states into the PDF. -pub fn write_graphic_states( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for external_gs in resources.ext_gs.items() { - if out.contains_key(external_gs) { - continue; - } - - let id = chunk.alloc(); - out.insert(*external_gs, id); - chunk - .ext_graphics(id) - .non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0) - .stroking_alpha(external_gs.stroke_opacity as f32 / 255.0); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs deleted file mode 100644 index f2df2ac92..000000000 --- a/crates/typst-pdf/src/font.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; -use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; -use pdf_writer::writers::{FontDescriptor, WMode}; -use pdf_writer::{Chunk, Filter, Finish, Name, Rect, Ref, Str}; -use subsetter::GlyphRemapper; -use ttf_parser::{name_id, GlyphId, Tag}; -use typst_library::diag::{At, SourceResult}; -use typst_library::text::Font; -use typst_syntax::Span; -use typst_utils::SliceExt; - -use crate::{deflate, EmExt, NameExt, PdfChunk, WithGlobalRefs}; - -const CFF: Tag = Tag::from_bytes(b"CFF "); -const CFF2: Tag = Tag::from_bytes(b"CFF2"); - -const SUBSET_TAG_LEN: usize = 6; -const IDENTITY_H: &str = "Identity-H"; - -pub(crate) const CMAP_NAME: Name = Name(b"Custom"); -pub(crate) const SYSTEM_INFO: SystemInfo = SystemInfo { - registry: Str(b"Adobe"), - ordering: Str(b"Identity"), - supplement: 0, -}; - -/// Embed all used fonts into the PDF. -#[typst_macros::time(name = "write fonts")] -pub fn write_fonts( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for font in resources.fonts.items() { - if out.contains_key(font) { - continue; - } - - let type0_ref = chunk.alloc(); - let cid_ref = chunk.alloc(); - let descriptor_ref = chunk.alloc(); - let cmap_ref = chunk.alloc(); - let data_ref = chunk.alloc(); - out.insert(font.clone(), type0_ref); - - let glyph_set = resources.glyph_sets.get(font).unwrap(); - let glyph_remapper = resources.glyph_remappers.get(font).unwrap(); - let ttf = font.ttf(); - - // Do we have a TrueType or CFF font? - // - // FIXME: CFF2 must be handled differently and requires PDF 2.0 - // (or we have to convert it to CFF). - let is_cff = ttf - .raw_face() - .table(CFF) - .or_else(|| ttf.raw_face().table(CFF2)) - .is_some(); - - let base_font = base_font_name(font, glyph_set); - let base_font_type0 = if is_cff { - eco_format!("{base_font}-{IDENTITY_H}") - } else { - base_font.clone() - }; - - // Write the base font object referencing the CID font. - chunk - .type0_font(type0_ref) - .base_font(Name(base_font_type0.as_bytes())) - .encoding_predefined(Name(IDENTITY_H.as_bytes())) - .descendant_font(cid_ref) - .to_unicode(cmap_ref); - - // Write the CID font referencing the font descriptor. - let mut cid = chunk.cid_font(cid_ref); - cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 }); - cid.base_font(Name(base_font.as_bytes())); - cid.system_info(SYSTEM_INFO); - cid.font_descriptor(descriptor_ref); - cid.default_width(0.0); - if !is_cff { - cid.cid_to_gid_map_predefined(Name(b"Identity")); - } - - // Extract the widths of all glyphs. - // `remapped_gids` returns an iterator over the old GIDs in their new sorted - // order, so we can append the widths as is. - let widths = glyph_remapper - .remapped_gids() - .map(|gid| { - let width = ttf.glyph_hor_advance(GlyphId(gid)).unwrap_or(0); - font.to_em(width).to_font_units() - }) - .collect::>(); - - // Write all non-zero glyph widths. - let mut first = 0; - let mut width_writer = cid.widths(); - for (w, group) in widths.group_by_key(|&w| w) { - let end = first + group.len(); - if w != 0.0 { - let last = end - 1; - width_writer.same(first as u16, last as u16, w); - } - first = end; - } - - width_writer.finish(); - cid.finish(); - - // Write the /ToUnicode character map, which maps glyph ids back to - // unicode codepoints to enable copying out of the PDF. - let cmap = create_cmap(glyph_set, glyph_remapper); - chunk - .cmap(cmap_ref, &cmap) - .writing_mode(WMode::Horizontal) - .filter(Filter::FlateDecode); - - let subset = subset_font(font, glyph_remapper) - .map_err(|err| { - let postscript_name = font.find_name(name_id::POST_SCRIPT_NAME); - let name = postscript_name.as_deref().unwrap_or(&font.info().family); - eco_format!("failed to process font {name}: {err}") - }) - .at(Span::detached())?; - - let mut stream = chunk.stream(data_ref, &subset); - stream.filter(Filter::FlateDecode); - if is_cff { - stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C")); - } - stream.finish(); - - let mut font_descriptor = - write_font_descriptor(&mut chunk, descriptor_ref, font, &base_font); - if is_cff { - font_descriptor.font_file3(data_ref); - } else { - font_descriptor.font_file2(data_ref); - } - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// Writes a FontDescriptor dictionary. -pub fn write_font_descriptor<'a>( - pdf: &'a mut Chunk, - descriptor_ref: Ref, - font: &'a Font, - base_font: &str, -) -> FontDescriptor<'a> { - let ttf = font.ttf(); - let metrics = font.metrics(); - let serif = font - .find_name(name_id::POST_SCRIPT_NAME) - .is_some_and(|name| name.contains("Serif")); - - let mut flags = FontFlags::empty(); - flags.set(FontFlags::SERIF, serif); - flags.set(FontFlags::FIXED_PITCH, ttf.is_monospaced()); - flags.set(FontFlags::ITALIC, ttf.is_italic()); - flags.insert(FontFlags::SYMBOLIC); - flags.insert(FontFlags::SMALL_CAP); - - let global_bbox = ttf.global_bounding_box(); - let bbox = Rect::new( - font.to_em(global_bbox.x_min).to_font_units(), - font.to_em(global_bbox.y_min).to_font_units(), - font.to_em(global_bbox.x_max).to_font_units(), - font.to_em(global_bbox.y_max).to_font_units(), - ); - - let italic_angle = ttf.italic_angle(); - let ascender = metrics.ascender.to_font_units(); - let descender = metrics.descender.to_font_units(); - let cap_height = metrics.cap_height.to_font_units(); - let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); - - // Write the font descriptor (contains metrics about the font). - let mut font_descriptor = pdf.font_descriptor(descriptor_ref); - font_descriptor - .name(Name(base_font.as_bytes())) - .flags(flags) - .bbox(bbox) - .italic_angle(italic_angle) - .ascent(ascender) - .descent(descender) - .cap_height(cap_height) - .stem_v(stem_v); - - font_descriptor -} - -/// Subset a font to the given glyphs. -/// -/// - For a font with TrueType outlines, this produces the whole OpenType font. -/// - For a font with CFF outlines, this produces just the CFF font program. -/// -/// In both cases, this returns the already compressed data. -#[comemo::memoize] -#[typst_macros::time(name = "subset font")] -fn subset_font( - font: &Font, - glyph_remapper: &GlyphRemapper, -) -> Result>, subsetter::Error> { - let data = font.data(); - let subset = subsetter::subset(data, font.index(), glyph_remapper)?; - let mut data = subset.as_ref(); - - // Extract the standalone CFF font program if applicable. - let raw = ttf_parser::RawFace::parse(data, 0).unwrap(); - if let Some(cff) = raw.table(CFF) { - data = cff; - } - - Ok(Arc::new(deflate(data))) -} - -/// Creates the base font name for a font with a specific glyph subset. -/// Consists of a subset tag and the PostScript name of the font. -/// -/// Returns a string of length maximum 116, so that even with `-Identity-H` -/// added it does not exceed the maximum PDF/A name length of 127. -pub(crate) fn base_font_name(font: &Font, glyphs: &T) -> EcoString { - const MAX_LEN: usize = Name::PDFA_LIMIT - REST_LEN; - const REST_LEN: usize = SUBSET_TAG_LEN + 1 + 1 + IDENTITY_H.len(); - - let postscript_name = font.find_name(name_id::POST_SCRIPT_NAME); - let name = postscript_name.as_deref().unwrap_or("unknown"); - let trimmed = &name[..name.len().min(MAX_LEN)]; - - // Hash the full name (we might have trimmed) and the glyphs to produce - // a fairly unique subset tag. - let subset_tag = subset_tag(&(name, glyphs)); - - eco_format!("{subset_tag}+{trimmed}") -} - -/// Produce a unique 6 letter tag for a glyph set. -pub(crate) fn subset_tag(glyphs: &T) -> EcoString { - const BASE: u128 = 26; - let mut hash = typst_utils::hash128(&glyphs); - let mut letter = [b'A'; SUBSET_TAG_LEN]; - for l in letter.iter_mut() { - *l = b'A' + (hash % BASE) as u8; - hash /= BASE; - } - std::str::from_utf8(&letter).unwrap().into() -} - -/// Create a compressed `/ToUnicode` CMap. -#[comemo::memoize] -#[typst_macros::time(name = "create cmap")] -fn create_cmap( - glyph_set: &BTreeMap, - glyph_remapper: &GlyphRemapper, -) -> Arc> { - // Produce a reverse mapping from glyphs' CIDs to unicode strings. - let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); - for (&g, text) in glyph_set.iter() { - // See commend in `write_normal_text` for why we can choose the CID this way. - let cid = glyph_remapper.get(g).unwrap(); - if !text.is_empty() { - cmap.pair_with_multiple(cid, text.chars()); - } - } - Arc::new(deflate(&cmap.finish())) -} diff --git a/crates/typst-pdf/src/gradient.rs b/crates/typst-pdf/src/gradient.rs deleted file mode 100644 index 6cd4c1ae8..000000000 --- a/crates/typst-pdf/src/gradient.rs +++ /dev/null @@ -1,512 +0,0 @@ -use std::collections::HashMap; -use std::f32::consts::{PI, TAU}; -use std::sync::Arc; - -use ecow::eco_format; -use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType}; -use pdf_writer::writers::StreamShadingType; -use pdf_writer::{Filter, Finish, Name, Ref}; -use typst_library::diag::SourceResult; -use typst_library::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; -use typst_library::visualize::{ - Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor, -}; -use typst_utils::Numeric; - -use crate::color::{ - self, check_cmyk_allowed, ColorSpaceExt, PaintEncode, QuantizedColor, -}; -use crate::{content, deflate, transform_to_array, AbsExt, PdfChunk, WithGlobalRefs}; - -/// A unique-transform-aspect-ratio combination that will be encoded into the -/// PDF. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct PdfGradient { - /// The transform to apply to the gradient. - pub transform: Transform, - /// The aspect ratio of the gradient. - /// Required for aspect ratio correction. - pub aspect_ratio: Ratio, - /// The gradient. - pub gradient: Gradient, - /// The corrected angle of the gradient. - pub angle: Angle, -} - -/// Writes the actual gradients (shading patterns) to the PDF. -/// This is performed once after writing all pages. -pub fn write_gradients( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for pdf_gradient in resources.gradients.items() { - if out.contains_key(pdf_gradient) { - continue; - } - - let shading = chunk.alloc(); - out.insert(pdf_gradient.clone(), shading); - - let PdfGradient { transform, aspect_ratio, gradient, angle } = pdf_gradient; - - let color_space = if gradient.space().hue_index().is_some() { - ColorSpace::Oklab - } else { - gradient.space() - }; - - if color_space == ColorSpace::Cmyk { - check_cmyk_allowed(context.options)?; - } - - let mut shading_pattern = match &gradient { - Gradient::Linear(_) => { - let shading_function = - shading_function(gradient, &mut chunk, color_space); - let mut shading_pattern = chunk.chunk.shading_pattern(shading); - let mut shading = shading_pattern.function_shading(); - shading.shading_type(FunctionShadingType::Axial); - - color::write( - color_space, - shading.color_space(), - &context.globals.color_functions, - ); - - let (mut sin, mut cos) = (angle.sin(), angle.cos()); - - // Scale to edges of unit square. - let factor = cos.abs() + sin.abs(); - sin *= factor; - cos *= factor; - - let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() { - Quadrant::First => (0.0, 0.0, cos, sin), - Quadrant::Second => (1.0, 0.0, cos + 1.0, sin), - Quadrant::Third => (1.0, 1.0, cos + 1.0, sin + 1.0), - Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0), - }; - - shading - .anti_alias(gradient.anti_alias()) - .function(shading_function) - .coords([x1 as f32, y1 as f32, x2 as f32, y2 as f32]) - .extend([true; 2]); - - shading.finish(); - - shading_pattern - } - Gradient::Radial(radial) => { - let shading_function = - shading_function(gradient, &mut chunk, color_space_of(gradient)); - let mut shading_pattern = chunk.chunk.shading_pattern(shading); - let mut shading = shading_pattern.function_shading(); - shading.shading_type(FunctionShadingType::Radial); - - color::write( - color_space, - shading.color_space(), - &context.globals.color_functions, - ); - - shading - .anti_alias(gradient.anti_alias()) - .function(shading_function) - .coords([ - radial.focal_center.x.get() as f32, - radial.focal_center.y.get() as f32, - radial.focal_radius.get() as f32, - radial.center.x.get() as f32, - radial.center.y.get() as f32, - radial.radius.get() as f32, - ]) - .extend([true; 2]); - - shading.finish(); - - shading_pattern - } - Gradient::Conic(_) => { - let vertices = compute_vertex_stream(gradient, *aspect_ratio); - - let stream_shading_id = chunk.alloc(); - let mut stream_shading = - chunk.chunk.stream_shading(stream_shading_id, &vertices); - - color::write( - color_space, - stream_shading.color_space(), - &context.globals.color_functions, - ); - - let range = color_space.range(); - stream_shading - .bits_per_coordinate(16) - .bits_per_component(16) - .bits_per_flag(8) - .shading_type(StreamShadingType::CoonsPatch) - .decode( - [0.0, 1.0, 0.0, 1.0].into_iter().chain(range.iter().copied()), - ) - .anti_alias(gradient.anti_alias()) - .filter(Filter::FlateDecode); - - stream_shading.finish(); - - let mut shading_pattern = chunk.shading_pattern(shading); - shading_pattern.shading_ref(stream_shading_id); - shading_pattern - } - }; - - shading_pattern.matrix(transform_to_array(*transform)); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// Writes an exponential or stitched function that expresses the gradient. -fn shading_function( - gradient: &Gradient, - chunk: &mut PdfChunk, - color_space: ColorSpace, -) -> Ref { - let function = chunk.alloc(); - let mut functions = vec![]; - let mut bounds = vec![]; - let mut encode = vec![]; - - // Create the individual gradient functions for each pair of stops. - for window in gradient.stops_ref().windows(2) { - let (first, second) = (window[0], window[1]); - - // If we have a hue index or are using Oklab, we will create several - // stops in-between to make the gradient smoother without interpolation - // issues with native color spaces. - let mut last_c = first.0; - if gradient.space().hue_index().is_some() { - for i in 0..=32 { - let t = i as f64 / 32.0; - let real_t = first.1.get() * (1.0 - t) + second.1.get() * t; - - let c = gradient.sample(RatioOrAngle::Ratio(Ratio::new(real_t))); - functions.push(single_gradient(chunk, last_c, c, color_space)); - bounds.push(real_t as f32); - encode.extend([0.0, 1.0]); - last_c = c; - } - } - - bounds.push(second.1.get() as f32); - functions.push(single_gradient(chunk, first.0, second.0, color_space)); - encode.extend([0.0, 1.0]); - } - - // Special case for gradients with only two stops. - if functions.len() == 1 { - return functions[0]; - } - - // Remove the last bound, since it's not needed for the stitching function. - bounds.pop(); - - // Create the stitching function. - chunk - .stitching_function(function) - .domain([0.0, 1.0]) - .range(color_space.range().iter().copied()) - .functions(functions) - .bounds(bounds) - .encode(encode); - - function -} - -/// Writes an exponential function that expresses a single segment (between two -/// stops) of a gradient. -fn single_gradient( - chunk: &mut PdfChunk, - first_color: Color, - second_color: Color, - color_space: ColorSpace, -) -> Ref { - let reference = chunk.alloc(); - chunk - .exponential_function(reference) - .range(color_space.range().iter().copied()) - .c0(color_space.convert(first_color)) - .c1(color_space.convert(second_color)) - .domain([0.0, 1.0]) - .n(1.0); - - reference -} - -impl PaintEncode for Gradient { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_fill_color_space(); - - let index = register_gradient(ctx, self, on_text, transforms); - let id = eco_format!("Gr{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_fill_pattern(None, name); - Ok(()) - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_stroke_color_space(); - - let index = register_gradient(ctx, self, on_text, transforms); - let id = eco_format!("Gr{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_stroke_pattern(None, name); - Ok(()) - } -} - -/// Deduplicates a gradient to a named PDF resource. -fn register_gradient( - ctx: &mut content::Builder, - gradient: &Gradient, - on_text: bool, - mut transforms: content::Transforms, -) -> usize { - // Edge cases for strokes. - if transforms.size.x.is_zero() { - transforms.size.x = Abs::pt(1.0); - } - - if transforms.size.y.is_zero() { - transforms.size.y = Abs::pt(1.0); - } - let size = match gradient.unwrap_relative(on_text) { - RelativeTo::Self_ => transforms.size, - RelativeTo::Parent => transforms.container_size, - }; - - let (offset_x, offset_y) = match gradient { - Gradient::Conic(conic) => ( - -size.x * (1.0 - conic.center.x.get() / 2.0) / 2.0, - -size.y * (1.0 - conic.center.y.get() / 2.0) / 2.0, - ), - _ => (Abs::zero(), Abs::zero()), - }; - - let rotation = gradient.angle().unwrap_or_else(Angle::zero); - - let transform = match gradient.unwrap_relative(on_text) { - RelativeTo::Self_ => transforms.transform, - RelativeTo::Parent => transforms.container_transform, - }; - - let scale_offset = match gradient { - Gradient::Conic(_) => 4.0_f64, - _ => 1.0, - }; - - let pdf_gradient = PdfGradient { - aspect_ratio: size.aspect_ratio(), - transform: transform - .pre_concat(Transform::translate( - offset_x * scale_offset, - offset_y * scale_offset, - )) - .pre_concat(Transform::scale( - Ratio::new(size.x.to_pt() * scale_offset), - Ratio::new(size.y.to_pt() * scale_offset), - )), - gradient: gradient.clone(), - angle: Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()), - }; - - ctx.resources.colors.mark_as_used(color_space_of(gradient)); - - ctx.resources.gradients.insert(pdf_gradient) -} - -/// Writes a single Coons Patch as defined in the PDF specification -/// to a binary vec. -/// -/// Structure: -/// - flag: `u8` -/// - points: `[u16; 24]` -/// - colors: `[u16; 4*N]` (N = number of components) -fn write_patch( - target: &mut Vec, - t: f32, - t1: f32, - c0: &[u16], - c1: &[u16], - angle: Angle, -) { - let theta = -TAU * t + angle.to_rad() as f32 + PI; - let theta1 = -TAU * t1 + angle.to_rad() as f32 + PI; - - let (cp1, cp2) = - control_point(Point::new(Abs::pt(0.5), Abs::pt(0.5)), 0.5, theta, theta1); - - // Push the flag - target.push(0); - - let p1 = - [u16::quantize(0.5, [0.0, 1.0]).to_be(), u16::quantize(0.5, [0.0, 1.0]).to_be()]; - - let p2 = [ - u16::quantize(theta.cos(), [-1.0, 1.0]).to_be(), - u16::quantize(theta.sin(), [-1.0, 1.0]).to_be(), - ]; - - let p3 = [ - u16::quantize(theta1.cos(), [-1.0, 1.0]).to_be(), - u16::quantize(theta1.sin(), [-1.0, 1.0]).to_be(), - ]; - - let cp1 = [ - u16::quantize(cp1.x.to_f32(), [0.0, 1.0]).to_be(), - u16::quantize(cp1.y.to_f32(), [0.0, 1.0]).to_be(), - ]; - - let cp2 = [ - u16::quantize(cp2.x.to_f32(), [0.0, 1.0]).to_be(), - u16::quantize(cp2.y.to_f32(), [0.0, 1.0]).to_be(), - ]; - - // Push the points - target.extend_from_slice(bytemuck::cast_slice(&[ - p1, p1, p2, p2, cp1, cp2, p3, p3, p1, p1, p1, p1, - ])); - - // Push the colors. - let colors = [c0, c0, c1, c1] - .into_iter() - .flat_map(|c| c.iter().copied().map(u16::to_be_bytes)) - .flatten(); - - target.extend(colors); -} - -fn control_point(c: Point, r: f32, angle_start: f32, angle_end: f32) -> (Point, Point) { - let n = (TAU / (angle_end - angle_start)).abs(); - let f = ((angle_end - angle_start) / n).tan() * 4.0 / 3.0; - - let p1 = c + Point::new( - Abs::pt((r * angle_start.cos() - f * r * angle_start.sin()) as f64), - Abs::pt((r * angle_start.sin() + f * r * angle_start.cos()) as f64), - ); - - let p2 = c + Point::new( - Abs::pt((r * angle_end.cos() + f * r * angle_end.sin()) as f64), - Abs::pt((r * angle_end.sin() - f * r * angle_end.cos()) as f64), - ); - - (p1, p2) -} - -#[comemo::memoize] -fn compute_vertex_stream(gradient: &Gradient, aspect_ratio: Ratio) -> Arc> { - let Gradient::Conic(conic) = gradient else { unreachable!() }; - - // Generated vertices for the Coons patches - let mut vertices = Vec::new(); - - // Correct the gradient's angle - let angle = Gradient::correct_aspect_ratio(conic.angle, aspect_ratio); - - for window in conic.stops.windows(2) { - let ((c0, t0), (c1, t1)) = (window[0], window[1]); - - // Precision: - // - On an even color, insert a stop every 90deg - // - For a hue-based color space, insert 200 stops minimum - // - On any other, insert 20 stops minimum - let max_dt = if c0 == c1 { - 0.25 - } else if conic.space.hue_index().is_some() { - 0.005 - } else { - 0.05 - }; - let encode_space = conic - .space - .hue_index() - .map(|_| ColorSpace::Oklab) - .unwrap_or(conic.space); - let mut t_x = t0.get(); - let dt = (t1.get() - t0.get()).min(max_dt); - - // Special casing for sharp gradients. - if t0 == t1 { - write_patch( - &mut vertices, - t0.get() as f32, - t1.get() as f32, - &encode_space.convert(c0), - &encode_space.convert(c1), - angle, - ); - continue; - } - - while t_x < t1.get() { - let t_next = (t_x + dt).min(t1.get()); - - // The current progress in the current window. - let t = |t| (t - t0.get()) / (t1.get() - t0.get()); - let c = Color::mix_iter( - [WeightedColor::new(c0, 1.0 - t(t_x)), WeightedColor::new(c1, t(t_x))], - conic.space, - ) - .unwrap(); - - let c_next = Color::mix_iter( - [ - WeightedColor::new(c0, 1.0 - t(t_next)), - WeightedColor::new(c1, t(t_next)), - ], - conic.space, - ) - .unwrap(); - - write_patch( - &mut vertices, - t_x as f32, - t_next as f32, - &encode_space.convert(c), - &encode_space.convert(c_next), - angle, - ); - - t_x = t_next; - } - } - - Arc::new(deflate(&vertices)) -} - -fn color_space_of(gradient: &Gradient) -> ColorSpace { - if gradient.space().hue_index().is_some() { - ColorSpace::Oklab - } else { - gradient.space() - } -} diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index fa326e3e0..93bdb1950 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -1,249 +1,244 @@ -use std::collections::HashMap; -use std::io::Cursor; +use std::hash::{Hash, Hasher}; +use std::sync::{Arc, OnceLock}; -use ecow::eco_format; -use image::{DynamicImage, GenericImageView, Rgba}; -use pdf_writer::{Chunk, Filter, Finish, Ref}; -use typst_library::diag::{At, SourceResult, StrResult}; +use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba}; +use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; +use krilla::surface::Surface; +use krilla_svg::{SurfaceExt, SvgSettings}; +use typst_library::diag::{bail, SourceResult}; use typst_library::foundations::Smart; +use typst_library::layout::{Abs, Angle, Ratio, Size, Transform}; use typst_library::visualize::{ - ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, - RasterImage, SvgImage, + ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, RasterImage, }; -use typst_utils::Deferred; +use typst_syntax::Span; -use crate::{color, deflate, PdfChunk, WithGlobalRefs}; +use crate::convert::{FrameContext, GlobalContext}; +use crate::util::{SizeExt, TransformExt}; -/// Embed all used images into the PDF. -#[typst_macros::time(name = "write images")] -pub fn write_images( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - for (i, image) in resources.images.items().enumerate() { - if out.contains_key(image) { - continue; +#[typst_macros::time(name = "handle image")] +pub(crate) fn handle_image( + gc: &mut GlobalContext, + fc: &mut FrameContext, + image: &Image, + size: Size, + surface: &mut Surface, + span: Span, +) -> SourceResult<()> { + surface.push_transform(&fc.state().transform().to_krilla()); + surface.set_location(span.into_raw().get()); + + let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth); + + if let Some(alt) = image.alt() { + surface.start_alt_text(alt); + } + + gc.image_spans.insert(span); + + match image.kind() { + ImageKind::Raster(raster) => { + let (exif_transform, new_size) = exif_transform(raster, size); + surface.push_transform(&exif_transform.to_krilla()); + + let image = match convert_raster(raster.clone(), interpolate) { + None => bail!(span, "failed to process image"), + Some(i) => i, + }; + + if !gc.image_to_spans.contains_key(&image) { + gc.image_to_spans.insert(image.clone(), span); } - let (handle, span) = resources.deferred_images.get(&i).unwrap(); - let encoded = handle.wait().as_ref().map_err(Clone::clone).at(*span)?; - - match encoded { - EncodedImage::Raster { - data, - filter, - color_space, - bits_per_component, - width, - height, - compressed_icc, - alpha, - interpolate, - } => { - let image_ref = chunk.alloc(); - out.insert(image.clone(), image_ref); - - let mut image = chunk.chunk.image_xobject(image_ref, data); - image.filter(*filter); - image.width(*width as i32); - image.height(*height as i32); - image.bits_per_component(i32::from(*bits_per_component)); - image.interpolate(*interpolate); - - let mut icc_ref = None; - let space = image.color_space(); - if compressed_icc.is_some() { - let id = chunk.alloc.bump(); - space.icc_based(id); - icc_ref = Some(id); - } else { - color::write( - *color_space, - space, - &context.globals.color_functions, - ); - } - - // Add a second gray-scale image containing the alpha values if - // this image has an alpha channel. - if let Some((alpha_data, alpha_filter)) = alpha { - let mask_ref = chunk.alloc.bump(); - image.s_mask(mask_ref); - image.finish(); - - let mut mask = chunk.image_xobject(mask_ref, alpha_data); - mask.filter(*alpha_filter); - mask.width(*width as i32); - mask.height(*height as i32); - mask.color_space().device_gray(); - mask.bits_per_component(i32::from(*bits_per_component)); - mask.interpolate(*interpolate); - } else { - image.finish(); - } - - if let (Some(compressed_icc), Some(icc_ref)) = - (compressed_icc, icc_ref) - { - let mut stream = chunk.icc_profile(icc_ref, compressed_icc); - stream.filter(Filter::FlateDecode); - match color_space { - ColorSpace::Srgb => { - stream.n(3); - stream.alternate().srgb(); - } - ColorSpace::D65Gray => { - stream.n(1); - stream.alternate().d65_gray(); - } - _ => unimplemented!(), - } - } - } - EncodedImage::Svg(svg_chunk, id) => { - let mut map = HashMap::new(); - svg_chunk.renumber_into(&mut chunk.chunk, |old| { - *map.entry(old).or_insert_with(|| chunk.alloc.bump()) - }); - out.insert(image.clone(), map[id]); - } - } + surface.draw_image(image, new_size.to_krilla()); + surface.pop(); } + ImageKind::Svg(svg) => { + surface.draw_svg( + svg.tree(), + size.to_krilla(), + SvgSettings { embed_text: true, ..Default::default() }, + ); + } + } - Ok(()) - })?; + if image.alt().is_some() { + surface.end_alt_text(); + } - Ok((chunk, out)) + surface.pop(); + surface.reset_location(); + + Ok(()) } -/// Creates a new PDF image from the given image. -/// -/// Also starts the deferred encoding of the image. -#[comemo::memoize] -pub fn deferred_image( - image: Image, - pdfa: bool, -) -> (Deferred>, Option) { - let color_space = match image.kind() { - ImageKind::Raster(raster) if raster.icc().is_none() => { - Some(to_color_space(raster.dynamic().color())) +struct Repr { + /// The original, underlying raster image. + raster: RasterImage, + /// The alpha channel of the raster image, if existing. + alpha_channel: OnceLock>>, + /// A (potentially) converted version of the dynamic image stored `raster` that is + /// guaranteed to either be in luma8 or rgb8, and thus can be used for the + /// `color_channel` method of `CustomImage`. + actual_dynamic: OnceLock>, +} + +/// A wrapper around `RasterImage` so that we can implement `CustomImage`. +#[derive(Clone)] +struct PdfImage(Arc); + +impl PdfImage { + pub fn new(raster: RasterImage) -> Self { + Self(Arc::new(Repr { + raster, + alpha_channel: OnceLock::new(), + actual_dynamic: OnceLock::new(), + })) + } +} + +impl Hash for PdfImage { + fn hash(&self, state: &mut H) { + // `alpha_channel` and `actual_dynamic` are generated from the underlying `RasterImage`, + // so this is enough. Since `raster` is prehashed, this is also very cheap. + self.0.raster.hash(state); + } +} + +impl CustomImage for PdfImage { + fn color_channel(&self) -> &[u8] { + self.0 + .actual_dynamic + .get_or_init(|| { + let dynamic = self.0.raster.dynamic(); + let channel_count = dynamic.color().channel_count(); + + match (dynamic.as_ref(), channel_count) { + // Pure luma8 or rgb8 image, can use it directly. + (DynamicImage::ImageLuma8(_), _) => dynamic.clone(), + (DynamicImage::ImageRgb8(_), _) => dynamic.clone(), + // Grey-scale image, convert to luma8. + (_, 1 | 2) => Arc::new(DynamicImage::ImageLuma8(dynamic.to_luma8())), + // Anything else, convert to rgb8. + _ => Arc::new(DynamicImage::ImageRgb8(dynamic.to_rgb8())), + } + }) + .as_bytes() + } + + fn alpha_channel(&self) -> Option<&[u8]> { + self.0 + .alpha_channel + .get_or_init(|| { + self.0.raster.dynamic().color().has_alpha().then(|| { + self.0 + .raster + .dynamic() + .pixels() + .map(|(_, _, Rgba([_, _, _, a]))| a) + .collect() + }) + }) + .as_ref() + .map(|v| &**v) + } + + fn bits_per_component(&self) -> BitsPerComponent { + BitsPerComponent::Eight + } + + fn size(&self) -> (u32, u32) { + (self.0.raster.width(), self.0.raster.height()) + } + + fn icc_profile(&self) -> Option<&[u8]> { + if matches!( + self.0.raster.dynamic().as_ref(), + DynamicImage::ImageLuma8(_) + | DynamicImage::ImageLumaA8(_) + | DynamicImage::ImageRgb8(_) + | DynamicImage::ImageRgba8(_) + ) { + self.0.raster.icc().map(|b| b.as_bytes()) + } else { + // In all other cases, the dynamic will be converted into RGB8 or LUMA8, so the ICC + // profile may become invalid, and thus we don't include it. + None } - _ => None, + } + + fn color_space(&self) -> ImageColorspace { + // Remember that we convert all images to either RGB or luma. + if self.0.raster.dynamic().color().has_color() { + ImageColorspace::Rgb + } else { + ImageColorspace::Luma + } + } +} + +#[comemo::memoize] +fn convert_raster( + raster: RasterImage, + interpolate: bool, +) -> Option { + if let RasterFormat::Exchange(ExchangeFormat::Jpg) = raster.format() { + let image_data: Arc + Send + Sync> = + Arc::new(raster.data().clone()); + let icc_profile = raster.icc().map(|i| { + let i: Arc + Send + Sync> = Arc::new(i.clone()); + i + }); + + krilla::image::Image::from_jpeg_with_icc( + image_data.into(), + icc_profile.map(|i| i.into()), + interpolate, + ) + } else { + krilla::image::Image::from_custom(PdfImage::new(raster), interpolate) + } +} + +fn exif_transform(image: &RasterImage, size: Size) -> (Transform, Size) { + let base = |hp: bool, vp: bool, mut base_ts: Transform, size: Size| { + if hp { + // Flip horizontally in-place. + base_ts = base_ts.pre_concat( + Transform::scale(-Ratio::one(), Ratio::one()) + .pre_concat(Transform::translate(-size.x, Abs::zero())), + ) + } + + if vp { + // Flip vertically in-place. + base_ts = base_ts.pre_concat( + Transform::scale(Ratio::one(), -Ratio::one()) + .pre_concat(Transform::translate(Abs::zero(), -size.y)), + ) + } + + base_ts }; - // PDF/A does not appear to allow interpolation. - // See https://github.com/typst/typst/issues/2942. - let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); + let no_flipping = + |hp: bool, vp: bool| (base(hp, vp, Transform::identity(), size), size); - let deferred = Deferred::new(move || match image.kind() { - ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)), - ImageKind::Svg(svg) => { - let (chunk, id) = encode_svg(svg, pdfa) - .map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?; - Ok(EncodedImage::Svg(chunk, id)) - } - }); + let with_flipping = |hp: bool, vp: bool| { + let base_ts = Transform::rotate_at(Angle::deg(90.0), Abs::zero(), Abs::zero()) + .pre_concat(Transform::scale(Ratio::one(), -Ratio::one())); + let inv_size = Size::new(size.y, size.x); + (base(hp, vp, base_ts, inv_size), inv_size) + }; - (deferred, color_space) -} - -/// Encode an image with a suitable filter. -#[typst_macros::time(name = "encode raster image")] -fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage { - let dynamic = image.dynamic(); - let color_space = to_color_space(dynamic.color()); - - let (filter, data, bits_per_component) = - if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (Filter::DctDecode, data.into_inner(), 8) - } else { - // TODO: Encode flate streams with PNG-predictor? - let (data, bits_per_component) = match (dynamic, color_space) { - // RGB image. - (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8), - // Grayscale image - (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8), - (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8), - // Anything else - _ => (deflate(dynamic.to_rgb8().as_raw()), 8), - }; - (Filter::FlateDecode, data, bits_per_component) - }; - - let compressed_icc = image.icc().map(|data| deflate(data)); - let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic)); - - EncodedImage::Raster { - data, - filter, - color_space, - bits_per_component, - width: image.width(), - height: image.height(), - compressed_icc, - alpha, - interpolate, - } -} - -/// Encode an image's alpha channel if present. -#[typst_macros::time(name = "encode alpha")] -fn encode_alpha(image: &DynamicImage) -> (Vec, Filter) { - let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); - (deflate(&pixels), Filter::FlateDecode) -} - -/// Encode an SVG into a chunk of PDF objects. -#[typst_macros::time(name = "encode svg")] -fn encode_svg( - svg: &SvgImage, - pdfa: bool, -) -> Result<(Chunk, Ref), svg2pdf::ConversionError> { - svg2pdf::to_chunk( - svg.tree(), - svg2pdf::ConversionOptions { pdfa, ..Default::default() }, - ) -} - -/// A pre-encoded image. -pub enum EncodedImage { - /// A pre-encoded rasterized image. - Raster { - /// The raw, pre-deflated image data. - data: Vec, - /// The filter to use for the image. - filter: Filter, - /// Which color space this image is encoded in. - color_space: ColorSpace, - /// How many bits of each color component are stored. - bits_per_component: u8, - /// The image's width. - width: u32, - /// The image's height. - height: u32, - /// The image's ICC profile, deflated, if any. - compressed_icc: Option>, - /// The alpha channel of the image, pre-deflated, if any. - alpha: Option<(Vec, Filter)>, - /// Whether image interpolation should be enabled. - interpolate: bool, - }, - /// A vector graphic. - /// - /// The chunk is the SVG converted to PDF objects. - Svg(Chunk, Ref), -} - -/// Matches an [`image::ColorType`] to [`ColorSpace`]. -fn to_color_space(color: image::ColorType) -> ColorSpace { - use image::ColorType::*; - match color { - L8 | La8 | L16 | La16 => ColorSpace::D65Gray, - Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, - _ => unimplemented!(), + match image.exif_rotation() { + Some(2) => no_flipping(true, false), + Some(3) => no_flipping(true, true), + Some(4) => no_flipping(false, true), + Some(5) => with_flipping(false, false), + Some(6) => with_flipping(true, false), + Some(7) => with_flipping(true, true), + Some(8) => with_flipping(false, true), + _ => no_flipping(false, false), } } diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index 88e62389c..4e0b74308 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -1,81 +1,33 @@ -//! Exporting of Typst documents into PDFs. +//! Exporting Typst documents to PDF. -mod catalog; -mod color; -mod color_font; -mod content; +mod convert; mod embed; -mod extg; -mod font; -mod gradient; mod image; -mod named_destination; +mod link; +mod metadata; mod outline; mod page; -mod resources; -mod tiling; +mod paint; +mod shape; +mod text; +mod util; + +pub use self::metadata::{Timestamp, Timezone}; -use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; -use std::ops::{Deref, DerefMut}; -use base64::Engine; -use ecow::EcoString; -use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; +use ecow::eco_format; use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; -use typst_library::foundations::{Datetime, Smart}; -use typst_library::layout::{Abs, Em, PageRanges, PagedDocument, Transform}; -use typst_library::text::Font; -use typst_library::visualize::Image; -use typst_syntax::Span; -use typst_utils::Deferred; - -use crate::catalog::write_catalog; -use crate::color::{alloc_color_functions_refs, ColorFunctionRefs}; -use crate::color_font::{write_color_fonts, ColorFontSlice}; -use crate::embed::write_embedded_files; -use crate::extg::{write_graphic_states, ExtGState}; -use crate::font::write_fonts; -use crate::gradient::{write_gradients, PdfGradient}; -use crate::image::write_images; -use crate::named_destination::{write_named_destinations, NamedDestinations}; -use crate::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage}; -use crate::resources::{ - alloc_resources_refs, write_resource_dictionaries, Resources, ResourcesRefs, -}; -use crate::tiling::{write_tilings, PdfTiling}; +use typst_library::foundations::Smart; +use typst_library::layout::{PageRanges, PagedDocument}; /// Export a document into a PDF file. /// /// Returns the raw bytes making up the PDF file. #[typst_macros::time(name = "pdf")] pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult> { - PdfBuilder::new(document, options) - .phase(|builder| builder.run(traverse_pages))? - .phase(|builder| { - Ok(GlobalRefs { - color_functions: builder.run(alloc_color_functions_refs)?, - pages: builder.run(alloc_page_refs)?, - resources: builder.run(alloc_resources_refs)?, - }) - })? - .phase(|builder| { - Ok(References { - named_destinations: builder.run(write_named_destinations)?, - fonts: builder.run(write_fonts)?, - color_fonts: builder.run(write_color_fonts)?, - images: builder.run(write_images)?, - gradients: builder.run(write_gradients)?, - tilings: builder.run(write_tilings)?, - ext_gs: builder.run(write_graphic_states)?, - embedded_files: builder.run(write_embedded_files)?, - }) - })? - .phase(|builder| builder.run(write_page_tree))? - .phase(|builder| builder.run(write_resource_dictionaries))? - .export_with(write_catalog) + convert::convert(document, options) } /// Settings for PDF export. @@ -103,82 +55,74 @@ pub struct PdfOptions<'a> { pub standards: PdfStandards, } -/// A timestamp with timezone information. -#[derive(Debug, Clone, Copy)] -pub struct Timestamp { - /// The datetime of the timestamp. - pub(crate) datetime: Datetime, - /// The timezone of the timestamp. - pub(crate) timezone: Timezone, -} - -impl Timestamp { - /// Create a new timestamp with a given datetime and UTC suffix. - pub fn new_utc(datetime: Datetime) -> Self { - Self { datetime, timezone: Timezone::UTC } - } - - /// Create a new timestamp with a given datetime, and a local timezone offset. - pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option { - let hour_offset = (whole_minute_offset / 60).try_into().ok()?; - // Note: the `%` operator in Rust is the remainder operator, not the - // modulo operator. The remainder operator can return negative results. - // We can simply apply `abs` here because we assume the `minute_offset` - // will have the same sign as `hour_offset`. - let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?; - match (hour_offset, minute_offset) { - // Only accept valid timezone offsets with `-23 <= hours <= 23`, - // and `0 <= minutes <= 59`. - (-23..=23, 0..=59) => Some(Self { - datetime, - timezone: Timezone::Local { hour_offset, minute_offset }, - }), - _ => None, - } - } -} - -/// A timezone. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Timezone { - /// The UTC timezone. - UTC, - /// The local timezone offset from UTC. And the `minute_offset` will have - /// same sign as `hour_offset`. - Local { hour_offset: i8, minute_offset: u8 }, -} - /// Encapsulates a list of compatible PDF standards. #[derive(Clone)] pub struct PdfStandards { - /// For now, we simplify to just PDF/A. But it can be more fine-grained in - /// the future. - pub(crate) pdfa: bool, - /// Whether the standard allows for embedding any kind of file into the PDF. - /// We disallow this for PDF/A-2, since it only allows embedding - /// PDF/A-1 and PDF/A-2 documents. - pub(crate) embedded_files: bool, - /// Part of the PDF/A standard. - pub(crate) pdfa_part: Option<(i32, &'static str)>, + pub(crate) config: krilla::configure::Configuration, } impl PdfStandards { /// Validates a list of PDF standards for compatibility and returns their /// encapsulated representation. pub fn new(list: &[PdfStandard]) -> StrResult { - let a2b = list.contains(&PdfStandard::A_2b); - let a3b = list.contains(&PdfStandard::A_3b); + use krilla::configure::{Configuration, PdfVersion, Validator}; - if a2b && a3b { - bail!("PDF cannot conform to A-2B and A-3B at the same time") + let mut version: Option = None; + let mut set_version = |v: PdfVersion| -> StrResult<()> { + if let Some(prev) = version { + bail!( + "PDF cannot conform to {} and {} at the same time", + prev.as_str(), + v.as_str() + ); + } + version = Some(v); + Ok(()) + }; + + let mut validator = None; + let mut set_validator = |v: Validator| -> StrResult<()> { + if validator.is_some() { + bail!("Typst currently only supports one PDF substandard at a time"); + } + validator = Some(v); + Ok(()) + }; + + for standard in list { + match standard { + PdfStandard::V_1_4 => set_version(PdfVersion::Pdf14)?, + PdfStandard::V_1_5 => set_version(PdfVersion::Pdf15)?, + PdfStandard::V_1_6 => set_version(PdfVersion::Pdf16)?, + PdfStandard::V_1_7 => set_version(PdfVersion::Pdf17)?, + PdfStandard::V_2_0 => set_version(PdfVersion::Pdf20)?, + PdfStandard::A_1b => set_validator(Validator::A1_B)?, + PdfStandard::A_2b => set_validator(Validator::A2_B)?, + PdfStandard::A_2u => set_validator(Validator::A2_U)?, + PdfStandard::A_3b => set_validator(Validator::A3_B)?, + PdfStandard::A_3u => set_validator(Validator::A3_U)?, + PdfStandard::A_4 => set_validator(Validator::A4)?, + PdfStandard::A_4f => set_validator(Validator::A4F)?, + PdfStandard::A_4e => set_validator(Validator::A4E)?, + } } - let pdfa = a2b || a3b; - Ok(Self { - pdfa, - embedded_files: !a2b, - pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")), - }) + let config = match (version, validator) { + (Some(version), Some(validator)) => { + Configuration::new_with(validator, version).ok_or_else(|| { + eco_format!( + "{} is not compatible with {}", + version.as_str(), + validator.as_str() + ) + })? + } + (Some(version), None) => Configuration::new_with_version(version), + (None, Some(validator)) => Configuration::new_with_validator(validator), + (None, None) => Configuration::new_with_version(PdfVersion::Pdf17), + }; + + Ok(Self { config }) } } @@ -190,7 +134,10 @@ impl Debug for PdfStandards { impl Default for PdfStandards { fn default() -> Self { - Self { pdfa: false, embedded_files: true, pdfa_part: None } + use krilla::configure::{Configuration, PdfVersion}; + Self { + config: Configuration::new_with_version(PdfVersion::Pdf17), + } } } @@ -201,531 +148,43 @@ impl Default for PdfStandards { #[allow(non_camel_case_types)] #[non_exhaustive] pub enum PdfStandard { + /// PDF 1.4. + #[serde(rename = "1.4")] + V_1_4, + /// PDF 1.5. + #[serde(rename = "1.5")] + V_1_5, + /// PDF 1.5. + #[serde(rename = "1.6")] + V_1_6, /// PDF 1.7. #[serde(rename = "1.7")] V_1_7, + /// PDF 2.0. + #[serde(rename = "2.0")] + V_2_0, + /// PDF/A-1b. + #[serde(rename = "a-1b")] + A_1b, /// PDF/A-2b. #[serde(rename = "a-2b")] A_2b, - /// PDF/A-3b. + /// PDF/A-2u. + #[serde(rename = "a-2u")] + A_2u, + /// PDF/A-3u. #[serde(rename = "a-3b")] A_3b, -} - -/// A struct to build a PDF following a fixed succession of phases. -/// -/// This type uses generics to represent its current state. `S` (for "state") is -/// all data that was produced by the previous phases, that is now read-only. -/// -/// Phase after phase, this state will be transformed. Each phase corresponds to -/// a call to the [eponymous function](`PdfBuilder::phase`) and produces a new -/// part of the state, that will be aggregated with all other information, for -/// consumption during the next phase. -/// -/// In other words: this struct follows the **typestate pattern**. This prevents -/// you from using data that is not yet available, at the type level. -/// -/// Each phase consists of processes, that can read the state of the previous -/// phases, and construct a part of the new state. -/// -/// A final step, that has direct access to the global reference allocator and -/// PDF document, can be run with [`PdfBuilder::export_with`]. -struct PdfBuilder { - /// The context that has been accumulated so far. - state: S, - /// A global bump allocator. - alloc: Ref, - /// The PDF document that is being written. - pdf: Pdf, -} - -/// The initial state: we are exploring the document, collecting all resources -/// that will be necessary later. The content of the pages is also built during -/// this phase. -struct WithDocument<'a> { - /// The Typst document that is exported. - document: &'a PagedDocument, - /// Settings for PDF export. - options: &'a PdfOptions<'a>, -} - -/// At this point, resources were listed, but they don't have any reference -/// associated with them. -/// -/// This phase allocates some global references. -struct WithResources<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - /// The content of the pages encoded as PDF content streams. - /// - /// The pages are at the index corresponding to their page number, but they - /// may be `None` if they are not in the range specified by - /// `exported_pages`. - pages: Vec>, - /// The PDF resources that are used in the content of the pages. - resources: Resources<()>, -} - -/// Global references. -struct GlobalRefs { - /// References for color conversion functions. - color_functions: ColorFunctionRefs, - /// Reference for pages. - /// - /// Items of this vector are `None` if the corresponding page is not - /// exported. - pages: Vec>, - /// References for the resource dictionaries. - resources: ResourcesRefs, -} - -impl<'a> From<(WithDocument<'a>, (Vec>, Resources<()>))> - for WithResources<'a> -{ - fn from( - (previous, (pages, resources)): ( - WithDocument<'a>, - (Vec>, Resources<()>), - ), - ) -> Self { - Self { - document: previous.document, - options: previous.options, - pages, - resources, - } - } -} - -/// At this point, the resources have been collected, and global references have -/// been allocated. -/// -/// We are now writing objects corresponding to resources, and giving them references, -/// that will be collected in [`References`]. -struct WithGlobalRefs<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - pages: Vec>, - /// Resources are the same as in previous phases, but each dictionary now has a reference. - resources: Resources, - /// Global references that were just allocated. - globals: GlobalRefs, -} - -impl<'a> From<(WithResources<'a>, GlobalRefs)> for WithGlobalRefs<'a> { - fn from((previous, globals): (WithResources<'a>, GlobalRefs)) -> Self { - Self { - document: previous.document, - options: previous.options, - pages: previous.pages, - resources: previous.resources.with_refs(&globals.resources), - globals, - } - } -} - -/// The references that have been assigned to each object. -struct References { - /// List of named destinations, each with an ID. - named_destinations: NamedDestinations, - /// The IDs of written fonts. - fonts: HashMap, - /// The IDs of written color fonts. - color_fonts: HashMap, - /// The IDs of written images. - images: HashMap, - /// The IDs of written gradients. - gradients: HashMap, - /// The IDs of written tilings. - tilings: HashMap, - /// The IDs of written external graphics states. - ext_gs: HashMap, - /// The names and references for embedded files. - embedded_files: BTreeMap, -} - -/// At this point, the references have been assigned to all resources. The page -/// tree is going to be written, and given a reference. It is also at this point that -/// the page contents is actually written. -struct WithRefs<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - globals: GlobalRefs, - pages: Vec>, - resources: Resources, - /// References that were allocated for resources. - references: References, -} - -impl<'a> From<(WithGlobalRefs<'a>, References)> for WithRefs<'a> { - fn from((previous, references): (WithGlobalRefs<'a>, References)) -> Self { - Self { - document: previous.document, - options: previous.options, - globals: previous.globals, - pages: previous.pages, - resources: previous.resources, - references, - } - } -} - -/// In this phase, we write resource dictionaries. -/// -/// Each sub-resource gets its own isolated resource dictionary. -struct WithEverything<'a> { - document: &'a PagedDocument, - options: &'a PdfOptions<'a>, - globals: GlobalRefs, - pages: Vec>, - resources: Resources, - references: References, - /// Reference that was allocated for the page tree. - page_tree_ref: Ref, -} - -impl<'a> From<(WithEverything<'a>, ())> for WithEverything<'a> { - fn from((this, _): (WithEverything<'a>, ())) -> Self { - this - } -} - -impl<'a> From<(WithRefs<'a>, Ref)> for WithEverything<'a> { - fn from((previous, page_tree_ref): (WithRefs<'a>, Ref)) -> Self { - Self { - document: previous.document, - options: previous.options, - globals: previous.globals, - resources: previous.resources, - references: previous.references, - pages: previous.pages, - page_tree_ref, - } - } -} - -impl<'a> PdfBuilder> { - /// Start building a PDF for a Typst document. - fn new(document: &'a PagedDocument, options: &'a PdfOptions<'a>) -> Self { - Self { - alloc: Ref::new(1), - pdf: Pdf::new(), - state: WithDocument { document, options }, - } - } -} - -impl PdfBuilder { - /// Start a new phase, and save its output in the global state. - fn phase(mut self, builder: B) -> SourceResult> - where - // New state - NS: From<(S, O)>, - // Builder - B: Fn(&mut Self) -> SourceResult, - { - let output = builder(&mut self)?; - Ok(PdfBuilder { - state: NS::from((self.state, output)), - alloc: self.alloc, - pdf: self.pdf, - }) - } - - /// Run a step with the current state, merges its output into the PDF file, - /// and renumbers any references it returned. - fn run(&mut self, process: P) -> SourceResult - where - // Process - P: Fn(&S) -> SourceResult<(PdfChunk, O)>, - // Output - O: Renumber, - { - let (chunk, mut output) = process(&self.state)?; - // Allocate a final reference for each temporary one - let allocated = chunk.alloc.get() - TEMPORARY_REFS_START; - let offset = TEMPORARY_REFS_START - self.alloc.get(); - - // Merge the chunk into the PDF, using the new references - chunk.renumber_into(&mut self.pdf, |mut r| { - r.renumber(offset); - - r - }); - - // Also update the references in the output - output.renumber(offset); - - self.alloc = Ref::new(self.alloc.get() + allocated); - - Ok(output) - } - - /// Finalize the PDF export and returns the buffer representing the - /// document. - fn export_with

(mut self, process: P) -> SourceResult> - where - P: Fn(S, &mut Pdf, &mut Ref) -> SourceResult<()>, - { - process(self.state, &mut self.pdf, &mut self.alloc)?; - Ok(self.pdf.finish()) - } -} - -/// A reference or collection of references that can be re-numbered, -/// to become valid in a global scope. -trait Renumber { - /// Renumber this value by shifting any references it contains by `offset`. - fn renumber(&mut self, offset: i32); -} - -impl Renumber for () { - fn renumber(&mut self, _offset: i32) {} -} - -impl Renumber for Ref { - fn renumber(&mut self, offset: i32) { - if self.get() >= TEMPORARY_REFS_START { - *self = Ref::new(self.get() - offset); - } - } -} - -impl Renumber for Vec { - fn renumber(&mut self, offset: i32) { - for item in self { - item.renumber(offset); - } - } -} - -impl Renumber for HashMap { - fn renumber(&mut self, offset: i32) { - for v in self.values_mut() { - v.renumber(offset); - } - } -} - -impl Renumber for BTreeMap { - fn renumber(&mut self, offset: i32) { - for v in self.values_mut() { - v.renumber(offset); - } - } -} - -impl Renumber for Option { - fn renumber(&mut self, offset: i32) { - if let Some(r) = self { - r.renumber(offset) - } - } -} - -impl Renumber for (T, R) { - fn renumber(&mut self, offset: i32) { - self.1.renumber(offset) - } -} - -/// A portion of a PDF file. -struct PdfChunk { - /// The actual chunk. - chunk: Chunk, - /// A local allocator. - alloc: Ref, -} - -/// Any reference below that value was already allocated before and -/// should not be rewritten. Anything above was allocated in the current -/// chunk, and should be remapped. -/// -/// This is a constant (large enough to avoid collisions) and not -/// dependent on self.alloc to allow for better memoization of steps, if -/// needed in the future. -const TEMPORARY_REFS_START: i32 = 1_000_000_000; - -/// A part of a PDF document. -impl PdfChunk { - /// Start writing a new part of the document. - fn new() -> Self { - PdfChunk { - chunk: Chunk::new(), - alloc: Ref::new(TEMPORARY_REFS_START), - } - } - - /// Allocate a reference that is valid in the context of this chunk. - /// - /// References allocated with this function should be [renumbered](`Renumber::renumber`) - /// before being used in other chunks. This is done automatically if these - /// references are stored in the global `PdfBuilder` state. - fn alloc(&mut self) -> Ref { - self.alloc.bump() - } -} - -impl Deref for PdfChunk { - type Target = Chunk; - - fn deref(&self) -> &Self::Target { - &self.chunk - } -} - -impl DerefMut for PdfChunk { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.chunk - } -} - -/// Compress data with the DEFLATE algorithm. -fn deflate(data: &[u8]) -> Vec { - const COMPRESSION_LEVEL: u8 = 6; - miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) -} - -/// Memoized and deferred version of [`deflate`] specialized for a page's content -/// stream. -#[comemo::memoize] -fn deflate_deferred(content: Vec) -> Deferred> { - Deferred::new(move || deflate(&content)) -} - -/// Create a base64-encoded hash of the value. -fn hash_base64(value: &T) -> String { - base64::engine::general_purpose::STANDARD - .encode(typst_utils::hash128(value).to_be_bytes()) -} - -/// Additional methods for [`Abs`]. -trait AbsExt { - /// Convert an to a number of points. - fn to_f32(self) -> f32; -} - -impl AbsExt for Abs { - fn to_f32(self) -> f32 { - self.to_pt() as f32 - } -} - -/// Additional methods for [`Em`]. -trait EmExt { - /// Convert an em length to a number of PDF font units. - fn to_font_units(self) -> f32; -} - -impl EmExt for Em { - fn to_font_units(self) -> f32 { - 1000.0 * self.get() as f32 - } -} - -trait NameExt<'a> { - /// The maximum length of a name in PDF/A. - const PDFA_LIMIT: usize = 127; -} - -impl<'a> NameExt<'a> for Name<'a> {} - -/// Additional methods for [`Str`]. -trait StrExt<'a>: Sized { - /// The maximum length of a string in PDF/A. - const PDFA_LIMIT: usize = 32767; - - /// Create a string that satisfies the constraints of PDF/A. - #[allow(unused)] - fn trimmed(string: &'a [u8]) -> Self; -} - -impl<'a> StrExt<'a> for Str<'a> { - fn trimmed(string: &'a [u8]) -> Self { - Self(&string[..string.len().min(Self::PDFA_LIMIT)]) - } -} - -/// Additional methods for [`TextStr`]. -trait TextStrExt<'a>: Sized { - /// The maximum length of a string in PDF/A. - const PDFA_LIMIT: usize = Str::PDFA_LIMIT; - - /// Create a text string that satisfies the constraints of PDF/A. - fn trimmed(string: &'a str) -> Self; -} - -impl<'a> TextStrExt<'a> for TextStr<'a> { - fn trimmed(string: &'a str) -> Self { - Self(&string[..string.len().min(Self::PDFA_LIMIT)]) - } -} - -/// Extension trait for [`Content`](pdf_writer::Content). -trait ContentExt { - fn save_state_checked(&mut self) -> SourceResult<()>; -} - -impl ContentExt for pdf_writer::Content { - fn save_state_checked(&mut self) -> SourceResult<()> { - self.save_state(); - if self.state_nesting_depth() > 28 { - bail!( - Span::detached(), - "maximum PDF grouping depth exceeding"; - hint: "try to avoid excessive nesting of layout containers", - ); - } - Ok(()) - } -} - -/// Convert to an array of floats. -fn transform_to_array(ts: Transform) -> [f32; 6] { - [ - ts.sx.get() as f32, - ts.ky.get() as f32, - ts.kx.get() as f32, - ts.sy.get() as f32, - ts.tx.to_f32(), - ts.ty.to_f32(), - ] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_timestamp_new_local() { - let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap(); - let test = |whole_minute_offset, expect_timezone| { - assert_eq!( - Timestamp::new_local(dummy_datetime, whole_minute_offset) - .unwrap() - .timezone, - expect_timezone - ); - }; - - // Valid timezone offsets - test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 }); - test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 }); - test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 }); - test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 }); - test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 }); - test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE - - // Corner cases - test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 }); - test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 }); - test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 }); - test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 }); - - // Invalid timezone offsets - assert!(Timestamp::new_local(dummy_datetime, 1440).is_none()); - assert!(Timestamp::new_local(dummy_datetime, -1440).is_none()); - assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none()); - assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none()); - } + /// PDF/A-3u. + #[serde(rename = "a-3u")] + A_3u, + /// PDF/A-4. + #[serde(rename = "a-4")] + A_4, + /// PDF/A-4f. + #[serde(rename = "a-4f")] + A_4f, + /// PDF/A-4e. + #[serde(rename = "a-4e")] + A_4e, } diff --git a/crates/typst-pdf/src/link.rs b/crates/typst-pdf/src/link.rs new file mode 100644 index 000000000..64cb8f0a2 --- /dev/null +++ b/crates/typst-pdf/src/link.rs @@ -0,0 +1,94 @@ +use krilla::action::{Action, LinkAction}; +use krilla::annotation::{LinkAnnotation, Target}; +use krilla::destination::XyzDestination; +use krilla::geom::Rect; +use typst_library::layout::{Abs, Point, Size}; +use typst_library::model::Destination; + +use crate::convert::{FrameContext, GlobalContext}; +use crate::util::{AbsExt, PointExt}; + +pub(crate) fn handle_link( + fc: &mut FrameContext, + gc: &mut GlobalContext, + dest: &Destination, + size: Size, +) { + let mut min_x = Abs::inf(); + let mut min_y = Abs::inf(); + let mut max_x = -Abs::inf(); + let mut max_y = -Abs::inf(); + + let pos = Point::zero(); + + // Compute the bounding box of the transformed link. + for point in [ + pos, + pos + Point::with_x(size.x), + pos + Point::with_y(size.y), + pos + size.to_point(), + ] { + let t = point.transform(fc.state().transform()); + min_x.set_min(t.x); + min_y.set_min(t.y); + max_x.set_max(t.x); + max_y.set_max(t.y); + } + + let x1 = min_x.to_f32(); + let x2 = max_x.to_f32(); + let y1 = min_y.to_f32(); + let y2 = max_y.to_f32(); + + let rect = Rect::from_ltrb(x1, y1, x2, y2).unwrap(); + + // TODO: Support quad points. + + let pos = match dest { + Destination::Url(u) => { + fc.push_annotation( + LinkAnnotation::new( + rect, + None, + Target::Action(Action::Link(LinkAction::new(u.to_string()))), + ) + .into(), + ); + return; + } + Destination::Position(p) => *p, + Destination::Location(loc) => { + if let Some(nd) = gc.loc_to_names.get(loc) { + // If a named destination has been registered, it's already guaranteed to + // not point to an excluded page. + fc.push_annotation( + LinkAnnotation::new( + rect, + None, + Target::Destination(krilla::destination::Destination::Named( + nd.clone(), + )), + ) + .into(), + ); + return; + } else { + gc.document.introspector.position(*loc) + } + } + }; + + let page_index = pos.page.get() - 1; + if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { + fc.push_annotation( + LinkAnnotation::new( + rect, + None, + Target::Destination(krilla::destination::Destination::Xyz( + XyzDestination::new(index, pos.point.to_krilla()), + )), + ) + .into(), + ); + } +} diff --git a/crates/typst-pdf/src/metadata.rs b/crates/typst-pdf/src/metadata.rs new file mode 100644 index 000000000..589c5e2fb --- /dev/null +++ b/crates/typst-pdf/src/metadata.rs @@ -0,0 +1,184 @@ +use ecow::EcoString; +use krilla::metadata::{Metadata, TextDirection}; +use typst_library::foundations::{Datetime, Smart}; +use typst_library::layout::Dir; +use typst_library::text::Lang; + +use crate::convert::GlobalContext; + +pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata { + let creator = format!("Typst {}", env!("CARGO_PKG_VERSION")); + + let lang = gc.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l); + + let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + TextDirection::RightToLeft + } else { + TextDirection::LeftToRight + }; + + let mut metadata = Metadata::new() + .creator(creator) + .keywords(gc.document.info.keywords.iter().map(EcoString::to_string).collect()) + .authors(gc.document.info.author.iter().map(EcoString::to_string).collect()); + + let lang = gc.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l); + + if let Some(lang) = lang { + metadata = metadata.language(lang.as_str().to_string()); + } + + if let Some(title) = &gc.document.info.title { + metadata = metadata.title(title.to_string()); + } + + if let Some(subject) = &gc.document.info.description { + metadata = metadata.subject(subject.to_string()); + } + + if let Some(ident) = gc.options.ident.custom() { + metadata = metadata.document_id(ident.to_string()); + } + + // (1) If the `document.date` is set to specific `datetime` or `none`, use it. + // (2) If the `document.date` is set to `auto` or not set, try to use the + // date from the options. + // (3) Otherwise, we don't write date metadata. + let (date, tz) = match (gc.document.info.date, gc.options.timestamp) { + (Smart::Custom(date), _) => (date, None), + (Smart::Auto, Some(timestamp)) => { + (Some(timestamp.datetime), Some(timestamp.timezone)) + } + _ => (None, None), + }; + + if let Some(date) = date.and_then(|d| convert_date(d, tz)) { + metadata = metadata.creation_date(date); + } + + metadata = metadata.text_direction(dir); + + metadata +} + +fn convert_date( + datetime: Datetime, + tz: Option, +) -> Option { + let year = datetime.year().filter(|&y| y >= 0)? as u16; + + let mut kd = krilla::metadata::DateTime::new(year); + + if let Some(month) = datetime.month() { + kd = kd.month(month); + } + + if let Some(day) = datetime.day() { + kd = kd.day(day); + } + + if let Some(h) = datetime.hour() { + kd = kd.hour(h); + } + + if let Some(m) = datetime.minute() { + kd = kd.minute(m); + } + + if let Some(s) = datetime.second() { + kd = kd.second(s); + } + + match tz { + Some(Timezone::UTC) => kd = kd.utc_offset_hour(0).utc_offset_minute(0), + Some(Timezone::Local { hour_offset, minute_offset }) => { + kd = kd.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset) + } + None => {} + } + + Some(kd) +} + +/// A timestamp with timezone information. +#[derive(Debug, Clone, Copy)] +pub struct Timestamp { + /// The datetime of the timestamp. + pub(crate) datetime: Datetime, + /// The timezone of the timestamp. + pub(crate) timezone: Timezone, +} + +impl Timestamp { + /// Create a new timestamp with a given datetime and UTC suffix. + pub fn new_utc(datetime: Datetime) -> Self { + Self { datetime, timezone: Timezone::UTC } + } + + /// Create a new timestamp with a given datetime, and a local timezone offset. + pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option { + let hour_offset = (whole_minute_offset / 60).try_into().ok()?; + // Note: the `%` operator in Rust is the remainder operator, not the + // modulo operator. The remainder operator can return negative results. + // We can simply apply `abs` here because we assume the `minute_offset` + // will have the same sign as `hour_offset`. + let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?; + match (hour_offset, minute_offset) { + // Only accept valid timezone offsets with `-23 <= hours <= 23`, + // and `0 <= minutes <= 59`. + (-23..=23, 0..=59) => Some(Self { + datetime, + timezone: Timezone::Local { hour_offset, minute_offset }, + }), + _ => None, + } + } +} + +/// A timezone. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Timezone { + /// The UTC timezone. + UTC, + /// The local timezone offset from UTC. And the `minute_offset` will have + /// same sign as `hour_offset`. + Local { hour_offset: i8, minute_offset: u8 }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timestamp_new_local() { + let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap(); + let test = |whole_minute_offset, expect_timezone| { + assert_eq!( + Timestamp::new_local(dummy_datetime, whole_minute_offset) + .unwrap() + .timezone, + expect_timezone + ); + }; + + // Valid timezone offsets + test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 }); + test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 }); + test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 }); + test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 }); + test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 }); + test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE + + // Corner cases + test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 }); + test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 }); + test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 }); + test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 }); + + // Invalid timezone offsets + assert!(Timestamp::new_local(dummy_datetime, 1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, -1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none()); + } +} diff --git a/crates/typst-pdf/src/named_destination.rs b/crates/typst-pdf/src/named_destination.rs deleted file mode 100644 index 7ae2c5e6f..000000000 --- a/crates/typst-pdf/src/named_destination.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use pdf_writer::writers::Destination; -use pdf_writer::{Ref, Str}; -use typst_library::diag::SourceResult; -use typst_library::foundations::{Label, NativeElement}; -use typst_library::introspection::Location; -use typst_library::layout::Abs; -use typst_library::model::HeadingElem; - -use crate::{AbsExt, PdfChunk, Renumber, StrExt, WithGlobalRefs}; - -/// A list of destinations in the PDF document (a specific point on a specific -/// page), that have a name associated with them. -/// -/// Typst creates a named destination for each heading in the document, that -/// will then be written in the document catalog. PDF readers can then display -/// them to show a clickable outline of the document. -#[derive(Default)] -pub struct NamedDestinations { - /// A map between elements and their associated labels - pub loc_to_dest: HashMap, - /// A sorted list of all named destinations. - pub dests: Vec<(Label, Ref)>, -} - -impl Renumber for NamedDestinations { - fn renumber(&mut self, offset: i32) { - for (_, reference) in &mut self.dests { - reference.renumber(offset); - } - } -} - -/// Fills in the map and vector for named destinations and writes the indirect -/// destination objects. -pub fn write_named_destinations( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, NamedDestinations)> { - let mut chunk = PdfChunk::new(); - let mut out = NamedDestinations::default(); - let mut seen = HashSet::new(); - - // Find all headings that have a label and are the first among other - // headings with the same label. - let mut matches: Vec<_> = context - .document - .introspector - .query(&HeadingElem::elem().select()) - .iter() - .filter_map(|elem| elem.location().zip(elem.label())) - .filter(|&(_, label)| seen.insert(label)) - .collect(); - - // Named destinations must be sorted by key. - matches.sort_by_key(|&(_, label)| label.resolve()); - - for (loc, label) in matches { - // Don't encode named destinations that would exceed the limit. Those - // will instead be encoded as normal links. - if label.resolve().len() > Str::PDFA_LIMIT { - continue; - } - - let pos = context.document.introspector.position(loc); - let index = pos.page.get() - 1; - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - - if let Some((Some(page), Some(page_ref))) = - context.pages.get(index).zip(context.globals.pages.get(index)) - { - let dest_ref = chunk.alloc(); - let x = pos.point.x.to_f32(); - let y = (page.content.size.y - y).to_f32(); - out.dests.push((label, dest_ref)); - out.loc_to_dest.insert(loc, label); - chunk - .indirect(dest_ref) - .start::() - .page(*page_ref) - .xyz(x, y, None); - } - } - - Ok((chunk, out)) -} diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index eff1182c1..e6324309f 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -1,18 +1,15 @@ use std::num::NonZeroUsize; -use pdf_writer::{Finish, Pdf, Ref, TextStr}; +use krilla::destination::XyzDestination; +use krilla::outline::{Outline, OutlineNode}; use typst_library::foundations::{NativeElement, Packed, StyleChain}; use typst_library::layout::Abs; use typst_library::model::HeadingElem; -use crate::{AbsExt, TextStrExt, WithEverything}; +use crate::convert::GlobalContext; +use crate::util::AbsExt; -/// Construct the outline for the document. -pub(crate) fn write_outline( - chunk: &mut Pdf, - alloc: &mut Ref, - ctx: &WithEverything, -) -> Option { +pub(crate) fn build_outline(gc: &GlobalContext) -> Outline { let mut tree: Vec = vec![]; // Stores the level of the topmost skipped ancestor of the next bookmarked @@ -21,14 +18,14 @@ pub(crate) fn write_outline( // Therefore, its next descendant must be added at its level, which is // enforced in the manner shown below. let mut last_skipped_level = None; - let elements = ctx.document.introspector.query(&HeadingElem::elem().select()); + let elements = &gc.document.introspector.query(&HeadingElem::elem().select()); for elem in elements.iter() { - if let Some(page_ranges) = &ctx.options.page_ranges { + if let Some(page_ranges) = &gc.options.page_ranges { if !page_ranges - .includes_page(ctx.document.introspector.page(elem.location().unwrap())) + .includes_page(gc.document.introspector.page(elem.location().unwrap())) { - // Don't bookmark headings in non-exported pages + // Don't bookmark headings in non-exported pages. continue; } } @@ -95,39 +92,15 @@ pub(crate) fn write_outline( } } - if tree.is_empty() { - return None; + let mut outline = Outline::new(); + + for child in convert_nodes(&tree, gc) { + outline.push_child(child); } - let root_id = alloc.bump(); - let start_ref = *alloc; - let len = tree.len(); - - let mut prev_ref = None; - for (i, node) in tree.iter().enumerate() { - prev_ref = Some(write_outline_item( - ctx, - chunk, - alloc, - node, - root_id, - prev_ref, - i + 1 == len, - )); - } - - chunk - .outline(root_id) - .first(start_ref) - .last(Ref::new( - alloc.get() - tree.last().map(|child| child.len() as i32).unwrap_or(1), - )) - .count(tree.len() as i32); - - Some(root_id) + outline } -/// A heading in the outline panel. #[derive(Debug)] struct HeadingNode<'a> { element: &'a Packed, @@ -149,73 +122,31 @@ impl<'a> HeadingNode<'a> { } } - fn len(&self) -> usize { - 1 + self.children.iter().map(Self::len).sum::() + fn to_krilla(&self, gc: &GlobalContext) -> Option { + let loc = self.element.location().unwrap(); + let title = self.element.body.plain_text().to_string(); + let pos = gc.document.introspector.position(loc); + let page_index = pos.page.get() - 1; + + if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { + let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); + let dest = XyzDestination::new( + index, + krilla::geom::Point::from_xy(pos.point.x.to_f32(), y.to_f32()), + ); + + let mut outline_node = OutlineNode::new(title, dest); + for child in convert_nodes(&self.children, gc) { + outline_node.push_child(child); + } + + return Some(outline_node); + } + + None } } -/// Write an outline item and all its children. -fn write_outline_item( - ctx: &WithEverything, - chunk: &mut Pdf, - alloc: &mut Ref, - node: &HeadingNode, - parent_ref: Ref, - prev_ref: Option, - is_last: bool, -) -> Ref { - let id = alloc.bump(); - let next_ref = Ref::new(id.get() + node.len() as i32); - - let mut outline = chunk.outline_item(id); - outline.parent(parent_ref); - - if !is_last { - outline.next(next_ref); - } - - if let Some(prev_rev) = prev_ref { - outline.prev(prev_rev); - } - - if let Some(last_immediate_child) = node.children.last() { - outline.first(Ref::new(id.get() + 1)); - outline.last(Ref::new(next_ref.get() - last_immediate_child.len() as i32)); - outline.count(-(node.children.len() as i32)); - } - - outline.title(TextStr::trimmed(node.element.body.plain_text().trim())); - - let loc = node.element.location().unwrap(); - let pos = ctx.document.introspector.position(loc); - let index = pos.page.get() - 1; - - // Don't link to non-exported pages. - if let Some((Some(page), Some(page_ref))) = - ctx.pages.get(index).zip(ctx.globals.pages.get(index)) - { - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - outline.dest().page(*page_ref).xyz( - pos.point.x.to_f32(), - (page.content.size.y - y).to_f32(), - None, - ); - } - - outline.finish(); - - let mut prev_ref = None; - for (i, child) in node.children.iter().enumerate() { - prev_ref = Some(write_outline_item( - ctx, - chunk, - alloc, - child, - id, - prev_ref, - i + 1 == node.children.len(), - )); - } - - id +fn convert_nodes(nodes: &[HeadingNode], gc: &GlobalContext) -> Vec { + nodes.iter().flat_map(|node| node.to_krilla(gc)).collect() } diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 68125d29a..aa34400ef 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -1,310 +1,64 @@ -use std::collections::HashMap; -use std::num::NonZeroU64; +use std::num::NonZeroUsize; -use ecow::EcoString; -use pdf_writer::types::{ActionType, AnnotationFlags, AnnotationType, NumberingStyle}; -use pdf_writer::{Filter, Finish, Name, Rect, Ref, Str}; -use typst_library::diag::SourceResult; -use typst_library::foundations::Label; -use typst_library::introspection::Location; -use typst_library::layout::{Abs, Page}; -use typst_library::model::{Destination, Numbering}; +use krilla::page::{NumberingStyle, PageLabel}; +use typst_library::model::Numbering; -use crate::{ - content, AbsExt, PdfChunk, PdfOptions, Resources, WithDocument, WithRefs, - WithResources, -}; - -/// Construct page objects. -#[typst_macros::time(name = "construct pages")] -#[allow(clippy::type_complexity)] -pub fn traverse_pages( - state: &WithDocument, -) -> SourceResult<(PdfChunk, (Vec>, Resources<()>))> { - let mut resources = Resources::default(); - let mut pages = Vec::with_capacity(state.document.pages.len()); - let mut skipped_pages = 0; - for (i, page) in state.document.pages.iter().enumerate() { - if state - .options - .page_ranges - .as_ref() - .is_some_and(|ranges| !ranges.includes_page_index(i)) - { - // Don't export this page. - pages.push(None); - skipped_pages += 1; - } else { - let mut encoded = construct_page(state.options, &mut resources, page)?; - encoded.label = page - .numbering - .as_ref() - .and_then(|num| PdfPageLabel::generate(num, page.number)) - .or_else(|| { - // When some pages were ignored from export, we show a page label with - // the correct real (not logical) page number. - // This is for consistency with normal output when pages have no numbering - // and all are exported: the final PDF page numbers always correspond to - // the real (not logical) page numbers. Here, the final PDF page number - // will differ, but we can at least use labels to indicate what was - // the corresponding real page number in the Typst document. - (skipped_pages > 0).then(|| PdfPageLabel::arabic((i + 1) as u64)) - }); - pages.push(Some(encoded)); - } - } - - Ok((PdfChunk::new(), (pages, resources))) -} - -/// Construct a page object. -#[typst_macros::time(name = "construct page")] -fn construct_page( - options: &PdfOptions, - out: &mut Resources<()>, - page: &Page, -) -> SourceResult { - Ok(EncodedPage { - content: content::build( - options, - out, - &page.frame, - page.fill_or_transparent(), - None, - )?, - label: None, - }) -} - -/// Allocate a reference for each exported page. -pub fn alloc_page_refs( - context: &WithResources, -) -> SourceResult<(PdfChunk, Vec>)> { - let mut chunk = PdfChunk::new(); - let page_refs = context - .pages - .iter() - .map(|p| p.as_ref().map(|_| chunk.alloc())) - .collect(); - Ok((chunk, page_refs)) -} - -/// Write the page tree. -pub fn write_page_tree(ctx: &WithRefs) -> SourceResult<(PdfChunk, Ref)> { - let mut chunk = PdfChunk::new(); - let page_tree_ref = chunk.alloc.bump(); - - for i in 0..ctx.pages.len() { - let content_id = chunk.alloc.bump(); - write_page( - &mut chunk, - ctx, - content_id, - page_tree_ref, - &ctx.references.named_destinations.loc_to_dest, - i, - ); - } - - let page_kids = ctx.globals.pages.iter().filter_map(Option::as_ref).copied(); - - chunk - .pages(page_tree_ref) - .count(page_kids.clone().count() as i32) - .kids(page_kids); - - Ok((chunk, page_tree_ref)) -} - -/// Write a page tree node. -fn write_page( - chunk: &mut PdfChunk, - ctx: &WithRefs, - content_id: Ref, - page_tree_ref: Ref, - loc_to_dest: &HashMap, - i: usize, -) { - let Some((page, page_ref)) = ctx.pages[i].as_ref().zip(ctx.globals.pages[i]) else { - // Page excluded from export. - return; - }; - - let mut annotations = Vec::with_capacity(page.content.links.len()); - for (dest, rect) in &page.content.links { - let id = chunk.alloc(); - annotations.push(id); - - let mut annotation = chunk.annotation(id); - annotation.subtype(AnnotationType::Link).rect(*rect); - annotation.border(0.0, 0.0, 0.0, None).flags(AnnotationFlags::PRINT); - - let pos = match dest { - Destination::Url(uri) => { - annotation - .action() - .action_type(ActionType::Uri) - .uri(Str(uri.as_bytes())); - continue; - } - Destination::Position(pos) => *pos, - Destination::Location(loc) => { - if let Some(key) = loc_to_dest.get(loc) { - annotation - .action() - .action_type(ActionType::GoTo) - // `key` must be a `Str`, not a `Name`. - .pair(Name(b"D"), Str(key.resolve().as_bytes())); - continue; - } else { - ctx.document.introspector.position(*loc) - } - } - }; - - let index = pos.page.get() - 1; - let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); - - // Don't add links to non-exported pages. - if let Some((Some(page), Some(page_ref))) = - ctx.pages.get(index).zip(ctx.globals.pages.get(index)) - { - annotation - .action() - .action_type(ActionType::GoTo) - .destination() - .page(*page_ref) - .xyz(pos.point.x.to_f32(), (page.content.size.y - y).to_f32(), None); - } - } - - let mut page_writer = chunk.page(page_ref); - page_writer.parent(page_tree_ref); - - let w = page.content.size.x.to_f32(); - let h = page.content.size.y.to_f32(); - page_writer.media_box(Rect::new(0.0, 0.0, w, h)); - page_writer.contents(content_id); - page_writer.pair(Name(b"Resources"), ctx.resources.reference); - - if page.content.uses_opacities { - page_writer - .group() - .transparency() - .isolated(false) - .knockout(false) - .color_space() - .srgb(); - } - - page_writer.annotations(annotations); - - page_writer.finish(); - - chunk - .stream(content_id, page.content.content.wait()) - .filter(Filter::FlateDecode); -} - -/// Specification for a PDF page label. -#[derive(Debug, Clone, PartialEq, Hash, Default)] -pub(crate) struct PdfPageLabel { - /// Can be any string or none. Will always be prepended to the numbering style. - pub prefix: Option, - /// Based on the numbering pattern. - /// - /// If `None` or numbering is a function, the field will be empty. - pub style: Option, - /// Offset for the page label start. - /// - /// Describes where to start counting from when setting a style. - /// (Has to be greater or equal than 1) - pub offset: Option, -} - -/// A PDF page label number style. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum PdfPageLabelStyle { - /// Decimal arabic numerals (1, 2, 3). - Arabic, - /// Lowercase roman numerals (i, ii, iii). - LowerRoman, - /// Uppercase roman numerals (I, II, III). - UpperRoman, - /// Lowercase letters (`a` to `z` for the first 26 pages, - /// `aa` to `zz` and so on for the next). - LowerAlpha, - /// Uppercase letters (`A` to `Z` for the first 26 pages, - /// `AA` to `ZZ` and so on for the next). - UpperAlpha, -} - -impl PdfPageLabel { - /// Create a new `PdfNumbering` from a `Numbering` applied to a page +pub(crate) trait PageLabelExt { + /// Create a new `PageLabel` from a `Numbering` applied to a page /// number. - fn generate(numbering: &Numbering, number: u64) -> Option { - let Numbering::Pattern(pat) = numbering else { - return None; - }; - - let (prefix, kind) = pat.pieces.first()?; - - // If there is a suffix, we cannot use the common style optimisation, - // since PDF does not provide a suffix field. - let style = if pat.suffix.is_empty() { - use typst_library::model::NumberingKind as Kind; - use PdfPageLabelStyle as Style; - match kind { - Kind::Arabic => Some(Style::Arabic), - Kind::LowerRoman => Some(Style::LowerRoman), - Kind::UpperRoman => Some(Style::UpperRoman), - Kind::LowerLatin if number <= 26 => Some(Style::LowerAlpha), - Kind::LowerLatin if number <= 26 => Some(Style::UpperAlpha), - _ => None, - } - } else { - None - }; - - // Prefix and offset depend on the style: If it is supported by the PDF - // spec, we use the given prefix and an offset. Otherwise, everything - // goes into prefix. - let prefix = if style.is_none() { - Some(pat.apply(&[number])) - } else { - (!prefix.is_empty()).then(|| prefix.clone()) - }; - - let offset = style.and(NonZeroU64::new(number)); - Some(PdfPageLabel { prefix, style, offset }) - } + fn generate(numbering: &Numbering, number: u64) -> Option; /// Creates an arabic page label with the specified page number. /// For example, this will display page label `11` when given the page /// number 11. - fn arabic(number: u64) -> PdfPageLabel { - PdfPageLabel { - prefix: None, - style: Some(PdfPageLabelStyle::Arabic), - offset: NonZeroU64::new(number), - } - } + fn arabic(number: u64) -> PageLabel; } -impl PdfPageLabelStyle { - pub fn to_pdf_numbering_style(self) -> NumberingStyle { - match self { - PdfPageLabelStyle::Arabic => NumberingStyle::Arabic, - PdfPageLabelStyle::LowerRoman => NumberingStyle::LowerRoman, - PdfPageLabelStyle::UpperRoman => NumberingStyle::UpperRoman, - PdfPageLabelStyle::LowerAlpha => NumberingStyle::LowerAlpha, - PdfPageLabelStyle::UpperAlpha => NumberingStyle::UpperAlpha, +impl PageLabelExt for PageLabel { + fn generate(numbering: &Numbering, number: u64) -> Option { + { + let Numbering::Pattern(pat) = numbering else { + return None; + }; + + let (prefix, kind) = pat.pieces.first()?; + + // If there is a suffix, we cannot use the common style optimisation, + // since PDF does not provide a suffix field. + let style = if pat.suffix.is_empty() { + use krilla::page::NumberingStyle as Style; + use typst_library::model::NumberingKind as Kind; + match kind { + Kind::Arabic => Some(Style::Arabic), + Kind::LowerRoman => Some(Style::LowerRoman), + Kind::UpperRoman => Some(Style::UpperRoman), + Kind::LowerLatin if number <= 26 => Some(Style::LowerAlpha), + Kind::LowerLatin if number <= 26 => Some(Style::UpperAlpha), + _ => None, + } + } else { + None + }; + + // Prefix and offset depend on the style: If it is supported by the PDF + // spec, we use the given prefix and an offset. Otherwise, everything + // goes into prefix. + let prefix = if style.is_none() { + Some(pat.apply(&[number])) + } else { + (!prefix.is_empty()).then(|| prefix.clone()) + }; + + let offset = style.and(number.try_into().ok().and_then(NonZeroUsize::new)); + Some(PageLabel::new(style, prefix.map(|s| s.to_string()), offset)) } } -} -/// Data for an exported page. -pub struct EncodedPage { - pub content: content::Encoded, - pub label: Option, + fn arabic(number: u64) -> PageLabel { + PageLabel::new( + Some(NumberingStyle::Arabic), + None, + number.try_into().ok().and_then(NonZeroUsize::new), + ) + } } diff --git a/crates/typst-pdf/src/paint.rs b/crates/typst-pdf/src/paint.rs new file mode 100644 index 000000000..5224464ab --- /dev/null +++ b/crates/typst-pdf/src/paint.rs @@ -0,0 +1,379 @@ +//! Convert paint types from typst to krilla. + +use krilla::color::{self, cmyk, luma, rgb}; +use krilla::num::NormalizedF32; +use krilla::paint::{ + Fill, LinearGradient, Pattern, RadialGradient, SpreadMethod, Stop, Stroke, + StrokeDash, SweepGradient, +}; +use krilla::surface::Surface; +use typst_library::diag::SourceResult; +use typst_library::layout::{Abs, Angle, Quadrant, Ratio, Size, Transform}; +use typst_library::visualize::{ + Color, ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, RatioOrAngle, + RelativeTo, Tiling, WeightedColor, +}; +use typst_utils::Numeric; + +use crate::convert::{handle_frame, FrameContext, GlobalContext, State}; +use crate::util::{AbsExt, FillRuleExt, LineCapExt, LineJoinExt, TransformExt}; + +pub(crate) fn convert_fill( + gc: &mut GlobalContext, + paint_: &Paint, + fill_rule_: FillRule, + on_text: bool, + surface: &mut Surface, + state: &State, + size: Size, +) -> SourceResult { + let (paint, opacity) = convert_paint(gc, paint_, on_text, surface, state, size)?; + + Ok(Fill { + paint, + rule: fill_rule_.to_krilla(), + opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), + }) +} + +pub(crate) fn convert_stroke( + fc: &mut GlobalContext, + stroke: &FixedStroke, + on_text: bool, + surface: &mut Surface, + state: &State, + size: Size, +) -> SourceResult { + let (paint, opacity) = + convert_paint(fc, &stroke.paint, on_text, surface, state, size)?; + + Ok(Stroke { + paint, + width: stroke.thickness.to_f32(), + miter_limit: stroke.miter_limit.get() as f32, + line_join: stroke.join.to_krilla(), + line_cap: stroke.cap.to_krilla(), + opacity: NormalizedF32::new(opacity as f32 / 255.0).unwrap(), + dash: stroke.dash.as_ref().map(convert_dash), + }) +} + +fn convert_paint( + gc: &mut GlobalContext, + paint: &Paint, + on_text: bool, + surface: &mut Surface, + state: &State, + mut size: Size, +) -> SourceResult<(krilla::paint::Paint, u8)> { + // Edge cases for strokes. + if size.x.is_zero() { + size.x = Abs::pt(1.0); + } + + if size.y.is_zero() { + size.y = Abs::pt(1.0); + } + + match paint { + Paint::Solid(c) => { + let (c, a) = convert_solid(c); + Ok((c.into(), a)) + } + Paint::Gradient(g) => Ok(convert_gradient(g, on_text, state, size)), + Paint::Tiling(p) => convert_pattern(gc, p, on_text, surface, state), + } +} + +fn convert_solid(color: &Color) -> (color::Color, u8) { + match color.space() { + ColorSpace::D65Gray => { + let (c, a) = convert_luma(color); + (c.into(), a) + } + ColorSpace::Cmyk => (convert_cmyk(color).into(), 255), + // Convert all other colors in different colors spaces into RGB. + _ => { + let (c, a) = convert_rgb(color); + (c.into(), a) + } + } +} + +fn convert_cmyk(color: &Color) -> cmyk::Color { + let components = color.to_space(ColorSpace::Cmyk).to_vec4_u8(); + + cmyk::Color::new(components[0], components[1], components[2], components[3]) +} + +fn convert_rgb(color: &Color) -> (rgb::Color, u8) { + let components = color.to_space(ColorSpace::Srgb).to_vec4_u8(); + (rgb::Color::new(components[0], components[1], components[2]), components[3]) +} + +fn convert_luma(color: &Color) -> (luma::Color, u8) { + let components = color.to_space(ColorSpace::D65Gray).to_vec4_u8(); + (luma::Color::new(components[0]), components[3]) +} + +fn convert_pattern( + gc: &mut GlobalContext, + pattern: &Tiling, + on_text: bool, + surface: &mut Surface, + state: &State, +) -> SourceResult<(krilla::paint::Paint, u8)> { + let transform = correct_transform(state, pattern.unwrap_relative(on_text)); + + let mut stream_builder = surface.stream_builder(); + let mut surface = stream_builder.surface(); + let mut fc = FrameContext::new(pattern.frame().size()); + handle_frame(&mut fc, pattern.frame(), None, &mut surface, gc)?; + surface.finish(); + let stream = stream_builder.finish(); + let pattern = Pattern { + stream, + transform: transform.to_krilla(), + width: (pattern.size().x + pattern.spacing().x).to_pt() as _, + height: (pattern.size().y + pattern.spacing().y).to_pt() as _, + }; + + Ok((pattern.into(), 255)) +} + +fn convert_gradient( + gradient: &Gradient, + on_text: bool, + state: &State, + size: Size, +) -> (krilla::paint::Paint, u8) { + let size = match gradient.unwrap_relative(on_text) { + RelativeTo::Self_ => size, + RelativeTo::Parent => state.container_size(), + }; + + let angle = gradient.angle().unwrap_or_else(Angle::zero); + let base_transform = correct_transform(state, gradient.unwrap_relative(on_text)); + let stops = convert_gradient_stops(gradient); + match &gradient { + Gradient::Linear(_) => { + let (x1, y1, x2, y2) = { + let (mut sin, mut cos) = (angle.sin(), angle.cos()); + + // Scale to edges of unit square. + let factor = cos.abs() + sin.abs(); + sin *= factor; + cos *= factor; + + match angle.quadrant() { + Quadrant::First => (0.0, 0.0, cos as f32, sin as f32), + Quadrant::Second => (1.0, 0.0, cos as f32 + 1.0, sin as f32), + Quadrant::Third => (1.0, 1.0, cos as f32 + 1.0, sin as f32 + 1.0), + Quadrant::Fourth => (0.0, 1.0, cos as f32, sin as f32 + 1.0), + } + }; + + let linear = LinearGradient { + x1, + y1, + x2, + y2, + // x and y coordinates are normalized, so need to scale by the size. + transform: base_transform + .pre_concat(Transform::scale( + Ratio::new(size.x.to_f32() as f64), + Ratio::new(size.y.to_f32() as f64), + )) + .to_krilla(), + spread_method: SpreadMethod::Pad, + stops, + anti_alias: gradient.anti_alias(), + }; + + (linear.into(), 255) + } + Gradient::Radial(radial) => { + let radial = RadialGradient { + fx: radial.focal_center.x.get() as f32, + fy: radial.focal_center.y.get() as f32, + fr: radial.focal_radius.get() as f32, + cx: radial.center.x.get() as f32, + cy: radial.center.y.get() as f32, + cr: radial.radius.get() as f32, + transform: base_transform + .pre_concat(Transform::scale( + Ratio::new(size.x.to_f32() as f64), + Ratio::new(size.y.to_f32() as f64), + )) + .to_krilla(), + spread_method: SpreadMethod::Pad, + stops, + anti_alias: gradient.anti_alias(), + }; + + (radial.into(), 255) + } + Gradient::Conic(conic) => { + // Correct the gradient's angle. + let cx = size.x.to_f32() * conic.center.x.get() as f32; + let cy = size.y.to_f32() * conic.center.y.get() as f32; + let actual_transform = base_transform + // Adjust for the angle. + .pre_concat(Transform::rotate_at( + angle, + Abs::pt(cx as f64), + Abs::pt(cy as f64), + )) + // Default start point in krilla and typst are at the opposite side, so we need + // to flip it horizontally. + .pre_concat(Transform::scale_at( + -Ratio::one(), + Ratio::one(), + Abs::pt(cx as f64), + Abs::pt(cy as f64), + )); + + let sweep = SweepGradient { + cx, + cy, + start_angle: 0.0, + end_angle: 360.0, + transform: actual_transform.to_krilla(), + spread_method: SpreadMethod::Pad, + stops, + anti_alias: gradient.anti_alias(), + }; + + (sweep.into(), 255) + } + } +} + +fn convert_gradient_stops(gradient: &Gradient) -> Vec { + let mut stops = vec![]; + + let use_cmyk = gradient.stops().iter().all(|s| s.color.space() == ColorSpace::Cmyk); + + let mut add_single = |color: &Color, offset: Ratio| { + let (color, opacity) = if use_cmyk { + (convert_cmyk(color).into(), 255) + } else { + let (c, a) = convert_rgb(color); + (c.into(), a) + }; + + let opacity = NormalizedF32::new((opacity as f32) / 255.0).unwrap(); + let offset = NormalizedF32::new(offset.get() as f32).unwrap(); + let stop = Stop { offset, color, opacity }; + stops.push(stop); + }; + + // Convert stops. + match &gradient { + Gradient::Linear(_) | Gradient::Radial(_) => { + if let Some(s) = gradient.stops().first() { + add_single(&s.color, s.offset.unwrap()); + } + + // Create the individual gradient functions for each pair of stops. + for window in gradient.stops().windows(2) { + let (first, second) = (window[0], window[1]); + + // If we have a hue index or are using Oklab, we will create several + // stops in-between to make the gradient smoother without interpolation + // issues with native color spaces. + if gradient.space().hue_index().is_some() { + for i in 0..=32 { + let t = i as f64 / 32.0; + let real_t = Ratio::new( + first.offset.unwrap().get() * (1.0 - t) + + second.offset.unwrap().get() * t, + ); + + let c = gradient.sample(RatioOrAngle::Ratio(real_t)); + add_single(&c, real_t); + } + } + + add_single(&second.color, second.offset.unwrap()); + } + } + Gradient::Conic(conic) => { + if let Some((c, t)) = conic.stops.first() { + add_single(c, *t); + } + + for window in conic.stops.windows(2) { + let ((c0, t0), (c1, t1)) = (window[0], window[1]); + + // Precision: + // - On an even color, insert a stop every 90deg. + // - For a hue-based color space, insert 200 stops minimum. + // - On any other, insert 20 stops minimum. + let max_dt = if c0 == c1 { + 0.25 + } else if conic.space.hue_index().is_some() { + 0.005 + } else { + 0.05 + }; + + let mut t_x = t0.get(); + let dt = (t1.get() - t0.get()).min(max_dt); + + // Special casing for sharp gradients. + if t0 == t1 { + add_single(&c1, t1); + continue; + } + + while t_x < t1.get() { + let t_next = (t_x + dt).min(t1.get()); + + // The current progress in the current window. + let t = |t| (t - t0.get()) / (t1.get() - t0.get()); + + let c_next = Color::mix_iter( + [ + WeightedColor::new(c0, 1.0 - t(t_next)), + WeightedColor::new(c1, t(t_next)), + ], + conic.space, + ) + .unwrap(); + + add_single(&c_next, Ratio::new(t_next)); + t_x = t_next; + } + + add_single(&c1, t1); + } + } + } + + stops +} + +fn convert_dash(dash: &DashPattern) -> StrokeDash { + StrokeDash { + array: dash.array.iter().map(|e| e.to_f32()).collect(), + offset: dash.phase.to_f32(), + } +} + +fn correct_transform(state: &State, relative: RelativeTo) -> Transform { + // In krilla, if we have a shape with a transform and a complex paint, + // then the paint will inherit the transform of the shape. + match relative { + // Because of the above, we don't need to apply an additional transform here. + RelativeTo::Self_ => Transform::identity(), + // Because of the above, we need to first reverse the transform that will be + // applied from the shape, and then re-apply the transform that is used for + // the next parent container. + RelativeTo::Parent => state + .transform() + .invert() + .unwrap() + .pre_concat(state.container_transform()), + } +} diff --git a/crates/typst-pdf/src/resources.rs b/crates/typst-pdf/src/resources.rs deleted file mode 100644 index bdbf2f1e4..000000000 --- a/crates/typst-pdf/src/resources.rs +++ /dev/null @@ -1,349 +0,0 @@ -//! PDF resources. -//! -//! Resources are defined in dictionaries. They map identifiers such as `Im0` to -//! a PDF reference. Each [content stream] is associated with a resource dictionary. -//! The identifiers defined in the resources can then be used in content streams. -//! -//! [content stream]: `crate::content` - -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; - -use ecow::{eco_format, EcoString}; -use pdf_writer::{Dict, Finish, Name, Ref}; -use subsetter::GlyphRemapper; -use typst_library::diag::{SourceResult, StrResult}; -use typst_library::text::{Font, Lang}; -use typst_library::visualize::Image; -use typst_syntax::Span; -use typst_utils::Deferred; - -use crate::color::ColorSpaces; -use crate::color_font::ColorFontMap; -use crate::extg::ExtGState; -use crate::gradient::PdfGradient; -use crate::image::EncodedImage; -use crate::tiling::TilingRemapper; -use crate::{PdfChunk, Renumber, WithEverything, WithResources}; - -/// All the resources that have been collected when traversing the document. -/// -/// This does not allocate references to resources, only track what was used -/// and deduplicate what can be deduplicated. -/// -/// You may notice that this structure is a tree: [`TilingRemapper`] and -/// [`ColorFontMap`] (that are present in the fields of [`Resources`]), -/// themselves contain [`Resources`] (that will be called "sub-resources" from -/// now on). Because color glyphs and tilings are defined using content -/// streams, just like pages, they can refer to resources too, which are tracked -/// by the respective sub-resources. -/// -/// Each instance of this structure will become a `/Resources` dictionary in -/// the final PDF. It is not possible to use a single shared dictionary for all -/// pages, tilings and color fonts, because if a resource is listed in its own -/// `/Resources` dictionary, some PDF readers will fail to open the document. -/// -/// Because we need to lazily initialize sub-resources (we don't know how deep -/// the tree will be before reading the document), and that this is done in a -/// context where no PDF reference allocator is available, `Resources` are -/// originally created with the type parameter `R = ()`. The reference for each -/// dictionary will only be allocated in the next phase, once we know the shape -/// of the tree, at which point `R` becomes `Ref`. No other value of `R` should -/// ever exist. -pub struct Resources { - /// The global reference to this resource dictionary, or `()` if it has not - /// been allocated yet. - pub reference: R, - - /// Handles color space writing. - pub colors: ColorSpaces, - - /// Deduplicates fonts used across the document. - pub fonts: Remapper, - /// Deduplicates images used across the document. - pub images: Remapper, - /// Handles to deferred image conversions. - pub deferred_images: HashMap>, Span)>, - /// Deduplicates gradients used across the document. - pub gradients: Remapper, - /// Deduplicates tilings used across the document. - pub tilings: Option>>, - /// Deduplicates external graphics states used across the document. - pub ext_gs: Remapper, - /// Deduplicates color glyphs. - pub color_fonts: Option>>, - - // The fields below do not correspond to actual resources that will be - // written in a dictionary, but are more meta-data about resources that - // can't really live somewhere else. - /// The number of glyphs for all referenced languages in the content stream. - /// We keep track of this to determine the main document language. - /// BTreeMap is used to write sorted list of languages to metadata. - pub languages: BTreeMap, - - /// For each font a mapping from used glyphs to their text representation. - /// This is used for the PDF's /ToUnicode map, and important for copy-paste - /// and searching. - /// - /// Note that the text representation may contain multiple chars in case of - /// ligatures or similar things, and it may have no entry in the font's cmap - /// (or only a private-use codepoint), like the “Th” in Linux Libertine. - /// - /// A glyph may have multiple entries in the font's cmap, and even the same - /// glyph can have a different text representation within one document. - /// But /ToUnicode does not support that, so we just save the first occurrence. - pub glyph_sets: HashMap>, - /// Same as `glyph_sets`, but for color fonts. - pub color_glyph_sets: HashMap>, - /// Stores the glyph remapper for each font for the subsetter. - pub glyph_remappers: HashMap, -} - -impl Renumber for Resources { - fn renumber(&mut self, offset: i32) { - self.reference.renumber(offset); - - if let Some(color_fonts) = &mut self.color_fonts { - color_fonts.resources.renumber(offset); - } - - if let Some(tilings) = &mut self.tilings { - tilings.resources.renumber(offset); - } - } -} - -impl Default for Resources<()> { - fn default() -> Self { - Resources { - reference: (), - colors: ColorSpaces::default(), - fonts: Remapper::new("F"), - images: Remapper::new("Im"), - deferred_images: HashMap::new(), - gradients: Remapper::new("Gr"), - tilings: None, - ext_gs: Remapper::new("Gs"), - color_fonts: None, - languages: BTreeMap::new(), - glyph_sets: HashMap::new(), - color_glyph_sets: HashMap::new(), - glyph_remappers: HashMap::new(), - } - } -} - -impl Resources<()> { - /// Associate a reference with this resource dictionary (and do so - /// recursively for sub-resources). - pub fn with_refs(self, refs: &ResourcesRefs) -> Resources { - Resources { - reference: refs.reference, - colors: self.colors, - fonts: self.fonts, - images: self.images, - deferred_images: self.deferred_images, - gradients: self.gradients, - tilings: self - .tilings - .zip(refs.tilings.as_ref()) - .map(|(p, r)| Box::new(p.with_refs(r))), - ext_gs: self.ext_gs, - color_fonts: self - .color_fonts - .zip(refs.color_fonts.as_ref()) - .map(|(c, r)| Box::new(c.with_refs(r))), - languages: self.languages, - glyph_sets: self.glyph_sets, - color_glyph_sets: self.color_glyph_sets, - glyph_remappers: self.glyph_remappers, - } - } -} - -impl Resources { - /// Run a function on this resource dictionary and all - /// of its sub-resources. - pub fn traverse

(&self, process: &mut P) -> SourceResult<()> - where - P: FnMut(&Self) -> SourceResult<()>, - { - process(self)?; - if let Some(color_fonts) = &self.color_fonts { - color_fonts.resources.traverse(process)?; - } - if let Some(tilings) = &self.tilings { - tilings.resources.traverse(process)?; - } - Ok(()) - } -} - -/// References for a resource tree. -/// -/// This structure is a tree too, that should have the same structure as the -/// corresponding `Resources`. -pub struct ResourcesRefs { - pub reference: Ref, - pub color_fonts: Option>, - pub tilings: Option>, -} - -impl Renumber for ResourcesRefs { - fn renumber(&mut self, offset: i32) { - self.reference.renumber(offset); - if let Some(color_fonts) = &mut self.color_fonts { - color_fonts.renumber(offset); - } - if let Some(tilings) = &mut self.tilings { - tilings.renumber(offset); - } - } -} - -/// Allocate references for all resource dictionaries. -pub fn alloc_resources_refs( - context: &WithResources, -) -> SourceResult<(PdfChunk, ResourcesRefs)> { - let mut chunk = PdfChunk::new(); - /// Recursively explore resource dictionaries and assign them references. - fn refs_for(resources: &Resources<()>, chunk: &mut PdfChunk) -> ResourcesRefs { - ResourcesRefs { - reference: chunk.alloc(), - color_fonts: resources - .color_fonts - .as_ref() - .map(|c| Box::new(refs_for(&c.resources, chunk))), - tilings: resources - .tilings - .as_ref() - .map(|p| Box::new(refs_for(&p.resources, chunk))), - } - } - - let refs = refs_for(&context.resources, &mut chunk); - Ok((chunk, refs)) -} - -/// Write the resource dictionaries that will be referenced by all pages. -/// -/// We add a reference to this dictionary to each page individually instead of -/// to the root node of the page tree because using the resource inheritance -/// feature breaks PDF merging with Apple Preview. -/// -/// Also write resource dictionaries for Type3 fonts and PDF patterns. -pub fn write_resource_dictionaries(ctx: &WithEverything) -> SourceResult<(PdfChunk, ())> { - let mut chunk = PdfChunk::new(); - let mut used_color_spaces = ColorSpaces::default(); - - ctx.resources.traverse(&mut |resources| { - used_color_spaces.merge(&resources.colors); - - let images_ref = chunk.alloc.bump(); - let patterns_ref = chunk.alloc.bump(); - let ext_gs_states_ref = chunk.alloc.bump(); - let color_spaces_ref = chunk.alloc.bump(); - - let mut color_font_slices = Vec::new(); - let mut color_font_numbers = HashMap::new(); - if let Some(color_fonts) = &resources.color_fonts { - for (_, font_slice) in color_fonts.iter() { - color_font_numbers.insert(font_slice.clone(), color_font_slices.len()); - color_font_slices.push(font_slice); - } - } - let color_font_remapper = Remapper { - prefix: "Cf", - to_pdf: color_font_numbers, - to_items: color_font_slices, - }; - - resources - .images - .write(&ctx.references.images, &mut chunk.indirect(images_ref).dict()); - - let mut patterns_dict = chunk.indirect(patterns_ref).dict(); - resources - .gradients - .write(&ctx.references.gradients, &mut patterns_dict); - if let Some(p) = &resources.tilings { - p.remapper.write(&ctx.references.tilings, &mut patterns_dict); - } - patterns_dict.finish(); - - resources - .ext_gs - .write(&ctx.references.ext_gs, &mut chunk.indirect(ext_gs_states_ref).dict()); - - let mut res_dict = chunk - .indirect(resources.reference) - .start::(); - res_dict.pair(Name(b"XObject"), images_ref); - res_dict.pair(Name(b"Pattern"), patterns_ref); - res_dict.pair(Name(b"ExtGState"), ext_gs_states_ref); - res_dict.pair(Name(b"ColorSpace"), color_spaces_ref); - - // TODO: can't this be an indirect reference too? - let mut fonts_dict = res_dict.fonts(); - resources.fonts.write(&ctx.references.fonts, &mut fonts_dict); - color_font_remapper.write(&ctx.references.color_fonts, &mut fonts_dict); - fonts_dict.finish(); - - res_dict.finish(); - - let color_spaces = chunk.indirect(color_spaces_ref).dict(); - resources - .colors - .write_color_spaces(color_spaces, &ctx.globals.color_functions); - - Ok(()) - })?; - - used_color_spaces.write_functions(&mut chunk, &ctx.globals.color_functions); - - Ok((chunk, ())) -} - -/// Assigns new, consecutive PDF-internal indices to items. -pub struct Remapper { - /// The prefix to use when naming these resources. - prefix: &'static str, - /// Forwards from the items to the pdf indices. - to_pdf: HashMap, - /// Backwards from the pdf indices to the items. - to_items: Vec, -} - -impl Remapper -where - T: Eq + Hash + Clone, -{ - /// Create an empty mapping. - pub fn new(prefix: &'static str) -> Self { - Self { prefix, to_pdf: HashMap::new(), to_items: vec![] } - } - - /// Insert an item in the mapping if it was not already present. - pub fn insert(&mut self, item: T) -> usize { - let to_layout = &mut self.to_items; - *self.to_pdf.entry(item.clone()).or_insert_with(|| { - let pdf_index = to_layout.len(); - to_layout.push(item); - pdf_index - }) - } - - /// All items in this - pub fn items(&self) -> impl Iterator + '_ { - self.to_items.iter() - } - - /// Write this list of items in a Resource dictionary. - fn write(&self, mapping: &HashMap, dict: &mut Dict) { - for (number, item) in self.items().enumerate() { - let name = eco_format!("{}{}", self.prefix, number); - let reference = mapping[item]; - dict.pair(Name(name.as_bytes()), reference); - } - } -} diff --git a/crates/typst-pdf/src/shape.rs b/crates/typst-pdf/src/shape.rs new file mode 100644 index 000000000..5b9232dbe --- /dev/null +++ b/crates/typst-pdf/src/shape.rs @@ -0,0 +1,106 @@ +use krilla::geom::{Path, PathBuilder, Rect}; +use krilla::surface::Surface; +use typst_library::diag::SourceResult; +use typst_library::visualize::{Geometry, Shape}; +use typst_syntax::Span; + +use crate::convert::{FrameContext, GlobalContext}; +use crate::paint; +use crate::util::{convert_path, AbsExt, TransformExt}; + +#[typst_macros::time(name = "handle shape")] +pub(crate) fn handle_shape( + fc: &mut FrameContext, + shape: &Shape, + surface: &mut Surface, + gc: &mut GlobalContext, + span: Span, +) -> SourceResult<()> { + surface.set_location(span.into_raw().get()); + surface.push_transform(&fc.state().transform().to_krilla()); + + if let Some(path) = convert_geometry(&shape.geometry) { + let fill = if let Some(paint) = &shape.fill { + Some(paint::convert_fill( + gc, + paint, + shape.fill_rule, + false, + surface, + fc.state(), + shape.geometry.bbox_size(), + )?) + } else { + None + }; + + let stroke = shape.stroke.as_ref().and_then(|stroke| { + if stroke.thickness.to_f32() > 0.0 { + Some(stroke) + } else { + None + } + }); + + let stroke = if let Some(stroke) = &stroke { + let stroke = paint::convert_stroke( + gc, + stroke, + false, + surface, + fc.state(), + shape.geometry.bbox_size(), + )?; + + Some(stroke) + } else { + None + }; + + // Otherwise, krilla will by default fill with a black paint. + if fill.is_some() || stroke.is_some() { + surface.set_fill(fill); + surface.set_stroke(stroke); + surface.draw_path(&path); + } + } + + surface.pop(); + surface.reset_location(); + + Ok(()) +} + +fn convert_geometry(geometry: &Geometry) -> Option { + let mut path_builder = PathBuilder::new(); + + match geometry { + Geometry::Line(l) => { + path_builder.move_to(0.0, 0.0); + path_builder.line_to(l.x.to_f32(), l.y.to_f32()); + } + Geometry::Rect(size) => { + let w = size.x.to_f32(); + let h = size.y.to_f32(); + let rect = if w < 0.0 || h < 0.0 { + // krilla doesn't normally allow for negative dimensions, but + // Typst supports them, so we apply a transform if needed. + let transform = + krilla::geom::Transform::from_scale(w.signum(), h.signum()); + Rect::from_xywh(0.0, 0.0, w.abs(), h.abs()) + .and_then(|rect| rect.transform(transform)) + } else { + Rect::from_xywh(0.0, 0.0, w, h) + }; + + if let Some(rect) = rect { + path_builder.push_rect(rect); + } + } + Geometry::Curve(c) => { + convert_path(c, &mut path_builder); + } + } + + path_builder.finish() +} diff --git a/crates/typst-pdf/src/text.rs b/crates/typst-pdf/src/text.rs new file mode 100644 index 000000000..8d532e08c --- /dev/null +++ b/crates/typst-pdf/src/text.rs @@ -0,0 +1,135 @@ +use std::ops::Range; +use std::sync::Arc; + +use bytemuck::TransparentWrapper; +use krilla::surface::{Location, Surface}; +use krilla::text::GlyphId; +use typst_library::diag::{bail, SourceResult}; +use typst_library::layout::Size; +use typst_library::text::{Font, Glyph, TextItem}; +use typst_library::visualize::FillRule; +use typst_syntax::Span; + +use crate::convert::{FrameContext, GlobalContext}; +use crate::paint; +use crate::util::{display_font, AbsExt, TransformExt}; + +#[typst_macros::time(name = "handle text")] +pub(crate) fn handle_text( + fc: &mut FrameContext, + t: &TextItem, + surface: &mut Surface, + gc: &mut GlobalContext, +) -> SourceResult<()> { + *gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len(); + + let font = convert_font(gc, t.font.clone())?; + let fill = paint::convert_fill( + gc, + &t.fill, + FillRule::NonZero, + true, + surface, + fc.state(), + Size::zero(), + )?; + let stroke = + if let Some(stroke) = t.stroke.as_ref().map(|s| { + paint::convert_stroke(gc, s, true, surface, fc.state(), Size::zero()) + }) { + Some(stroke?) + } else { + None + }; + let text = t.text.as_str(); + let size = t.size; + let glyphs: &[PdfGlyph] = TransparentWrapper::wrap_slice(t.glyphs.as_slice()); + + surface.push_transform(&fc.state().transform().to_krilla()); + surface.set_fill(Some(fill)); + surface.set_stroke(stroke); + surface.draw_glyphs( + krilla::geom::Point::from_xy(0.0, 0.0), + glyphs, + font.clone(), + text, + size.to_f32(), + false, + ); + + surface.pop(); + + Ok(()) +} + +fn convert_font( + gc: &mut GlobalContext, + typst_font: Font, +) -> SourceResult { + if let Some(font) = gc.fonts_forward.get(&typst_font) { + Ok(font.clone()) + } else { + let font = build_font(typst_font.clone())?; + + gc.fonts_forward.insert(typst_font.clone(), font.clone()); + gc.fonts_backward.insert(font.clone(), typst_font.clone()); + + Ok(font) + } +} + +#[comemo::memoize] +fn build_font(typst_font: Font) -> SourceResult { + let font_data: Arc + Send + Sync> = + Arc::new(typst_font.data().clone()); + + match krilla::text::Font::new(font_data.into(), typst_font.index()) { + None => { + let font_str = display_font(&typst_font); + bail!(Span::detached(), "failed to process font {font_str}"); + } + Some(f) => Ok(f), + } +} + +#[derive(TransparentWrapper, Debug)] +#[repr(transparent)] +struct PdfGlyph(Glyph); + +impl krilla::text::Glyph for PdfGlyph { + #[inline(always)] + fn glyph_id(&self) -> GlyphId { + GlyphId::new(self.0.id as u32) + } + + #[inline(always)] + fn text_range(&self) -> Range { + self.0.range.start as usize..self.0.range.end as usize + } + + #[inline(always)] + fn x_advance(&self, size: f32) -> f32 { + // Don't use `Em::at`, because it contains an expensive check whether the result is finite. + self.0.x_advance.get() as f32 * size + } + + #[inline(always)] + fn x_offset(&self, size: f32) -> f32 { + // Don't use `Em::at`, because it contains an expensive check whether the result is finite. + self.0.x_offset.get() as f32 * size + } + + #[inline(always)] + fn y_offset(&self, _: f32) -> f32 { + 0.0 + } + + #[inline(always)] + fn y_advance(&self, _: f32) -> f32 { + 0.0 + } + + fn location(&self) -> Option { + Some(self.0.span.0.into_raw().get()) + } +} diff --git a/crates/typst-pdf/src/tiling.rs b/crates/typst-pdf/src/tiling.rs deleted file mode 100644 index f8950f344..000000000 --- a/crates/typst-pdf/src/tiling.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::collections::HashMap; - -use ecow::eco_format; -use pdf_writer::types::{ColorSpaceOperand, PaintType, TilingType}; -use pdf_writer::{Filter, Name, Rect, Ref}; -use typst_library::diag::SourceResult; -use typst_library::layout::{Abs, Ratio, Transform}; -use typst_library::visualize::{RelativeTo, Tiling}; -use typst_utils::Numeric; - -use crate::color::PaintEncode; -use crate::resources::{Remapper, ResourcesRefs}; -use crate::{content, transform_to_array, PdfChunk, Resources, WithGlobalRefs}; - -/// Writes the actual patterns (tiling patterns) to the PDF. -/// This is performed once after writing all pages. -pub fn write_tilings( - context: &WithGlobalRefs, -) -> SourceResult<(PdfChunk, HashMap)> { - let mut chunk = PdfChunk::new(); - let mut out = HashMap::new(); - context.resources.traverse(&mut |resources| { - let Some(patterns) = &resources.tilings else { - return Ok(()); - }; - - for pdf_pattern in patterns.remapper.items() { - let PdfTiling { transform, pattern, content, .. } = pdf_pattern; - if out.contains_key(pdf_pattern) { - continue; - } - - let tiling = chunk.alloc(); - out.insert(pdf_pattern.clone(), tiling); - - let mut tiling_pattern = chunk.tiling_pattern(tiling, content); - tiling_pattern - .tiling_type(TilingType::ConstantSpacing) - .paint_type(PaintType::Colored) - .bbox(Rect::new( - 0.0, - 0.0, - pattern.size().x.to_pt() as _, - pattern.size().y.to_pt() as _, - )) - .x_step((pattern.size().x + pattern.spacing().x).to_pt() as _) - .y_step((pattern.size().y + pattern.spacing().y).to_pt() as _); - - // The actual resource dict will be written in a later step - tiling_pattern.pair(Name(b"Resources"), patterns.resources.reference); - - tiling_pattern - .matrix(transform_to_array( - transform - .pre_concat(Transform::scale(Ratio::one(), -Ratio::one())) - .post_concat(Transform::translate( - Abs::zero(), - pattern.spacing().y, - )), - )) - .filter(Filter::FlateDecode); - } - - Ok(()) - })?; - - Ok((chunk, out)) -} - -/// A pattern and its transform. -#[derive(Clone, PartialEq, Eq, Hash, Debug)] -pub struct PdfTiling { - /// The transform to apply to the pattern. - pub transform: Transform, - /// The pattern to paint. - pub pattern: Tiling, - /// The rendered pattern. - pub content: Vec, -} - -/// Registers a pattern with the PDF. -fn register_pattern( - ctx: &mut content::Builder, - pattern: &Tiling, - on_text: bool, - mut transforms: content::Transforms, -) -> SourceResult { - let patterns = ctx - .resources - .tilings - .get_or_insert_with(|| Box::new(TilingRemapper::new())); - - // Edge cases for strokes. - if transforms.size.x.is_zero() { - transforms.size.x = Abs::pt(1.0); - } - - if transforms.size.y.is_zero() { - transforms.size.y = Abs::pt(1.0); - } - - let transform = match pattern.unwrap_relative(on_text) { - RelativeTo::Self_ => transforms.transform, - RelativeTo::Parent => transforms.container_transform, - }; - - // Render the body. - let content = content::build( - ctx.options, - &mut patterns.resources, - pattern.frame(), - None, - None, - )?; - - let pdf_pattern = PdfTiling { - transform, - pattern: pattern.clone(), - content: content.content.wait().clone(), - }; - - Ok(patterns.remapper.insert(pdf_pattern)) -} - -impl PaintEncode for Tiling { - fn set_as_fill( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_fill_color_space(); - - let index = register_pattern(ctx, self, on_text, transforms)?; - let id = eco_format!("P{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_fill_pattern(None, name); - Ok(()) - } - - fn set_as_stroke( - &self, - ctx: &mut content::Builder, - on_text: bool, - transforms: content::Transforms, - ) -> SourceResult<()> { - ctx.reset_stroke_color_space(); - - let index = register_pattern(ctx, self, on_text, transforms)?; - let id = eco_format!("P{index}"); - let name = Name(id.as_bytes()); - - ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); - ctx.content.set_stroke_pattern(None, name); - Ok(()) - } -} - -/// De-duplicate patterns and the resources they require to be drawn. -pub struct TilingRemapper { - /// Pattern de-duplicator. - pub remapper: Remapper, - /// PDF resources that are used by these patterns. - pub resources: Resources, -} - -impl TilingRemapper<()> { - pub fn new() -> Self { - Self { - remapper: Remapper::new("P"), - resources: Resources::default(), - } - } - - /// Allocate a reference to the resource dictionary of these patterns. - pub fn with_refs(self, refs: &ResourcesRefs) -> TilingRemapper { - TilingRemapper { - remapper: self.remapper, - resources: self.resources.with_refs(refs), - } - } -} diff --git a/crates/typst-pdf/src/util.rs b/crates/typst-pdf/src/util.rs new file mode 100644 index 000000000..3b85d0b8a --- /dev/null +++ b/crates/typst-pdf/src/util.rs @@ -0,0 +1,120 @@ +//! Basic utilities for converting typst types to krilla. + +use krilla::geom as kg; +use krilla::geom::PathBuilder; +use krilla::paint as kp; +use typst_library::layout::{Abs, Point, Size, Transform}; +use typst_library::text::Font; +use typst_library::visualize::{Curve, CurveItem, FillRule, LineCap, LineJoin}; + +pub(crate) trait SizeExt { + fn to_krilla(&self) -> kg::Size; +} + +impl SizeExt for Size { + fn to_krilla(&self) -> kg::Size { + kg::Size::from_wh(self.x.to_f32(), self.y.to_f32()).unwrap() + } +} + +pub(crate) trait PointExt { + fn to_krilla(&self) -> kg::Point; +} + +impl PointExt for Point { + fn to_krilla(&self) -> kg::Point { + kg::Point::from_xy(self.x.to_f32(), self.y.to_f32()) + } +} + +pub(crate) trait LineCapExt { + fn to_krilla(&self) -> kp::LineCap; +} + +impl LineCapExt for LineCap { + fn to_krilla(&self) -> kp::LineCap { + match self { + LineCap::Butt => kp::LineCap::Butt, + LineCap::Round => kp::LineCap::Round, + LineCap::Square => kp::LineCap::Square, + } + } +} + +pub(crate) trait LineJoinExt { + fn to_krilla(&self) -> kp::LineJoin; +} + +impl LineJoinExt for LineJoin { + fn to_krilla(&self) -> kp::LineJoin { + match self { + LineJoin::Miter => kp::LineJoin::Miter, + LineJoin::Round => kp::LineJoin::Round, + LineJoin::Bevel => kp::LineJoin::Bevel, + } + } +} + +pub(crate) trait TransformExt { + fn to_krilla(&self) -> kg::Transform; +} + +impl TransformExt for Transform { + fn to_krilla(&self) -> kg::Transform { + kg::Transform::from_row( + self.sx.get() as f32, + self.ky.get() as f32, + self.kx.get() as f32, + self.sy.get() as f32, + self.tx.to_f32(), + self.ty.to_f32(), + ) + } +} + +pub(crate) trait FillRuleExt { + fn to_krilla(&self) -> kp::FillRule; +} + +impl FillRuleExt for FillRule { + fn to_krilla(&self) -> kp::FillRule { + match self { + FillRule::NonZero => kp::FillRule::NonZero, + FillRule::EvenOdd => kp::FillRule::EvenOdd, + } + } +} + +pub(crate) trait AbsExt { + fn to_f32(self) -> f32; +} + +impl AbsExt for Abs { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} + +/// Display the font family of a font. +pub(crate) fn display_font(font: &Font) -> &str { + &font.info().family +} + +/// Convert a typst path to a krilla path. +pub(crate) fn convert_path(path: &Curve, builder: &mut PathBuilder) { + for item in &path.0 { + match item { + CurveItem::Move(p) => builder.move_to(p.x.to_f32(), p.y.to_f32()), + CurveItem::Line(p) => builder.line_to(p.x.to_f32(), p.y.to_f32()), + CurveItem::Cubic(p1, p2, p3) => builder.cubic_to( + p1.x.to_f32(), + p1.y.to_f32(), + p2.x.to_f32(), + p2.y.to_f32(), + p3.x.to_f32(), + p3.y.to_f32(), + ), + CurveItem::Close => builder.close(), + } + } +} From 12699eb7f415bdba6797c84e3e7bf44dde75bdf9 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 2 Apr 2025 05:30:04 -0400 Subject: [PATCH 012/162] Parse multi-character numbers consistently in math (#5996) Co-authored-by: Laurenz --- crates/typst-syntax/src/parser.rs | 8 +++----- .../ref/issue-4828-math-number-multi-char.png | Bin 0 -> 465 bytes tests/ref/math-frac-precedence.png | Bin 5504 -> 3586 bytes tests/suite/math/frac.typ | 4 ++-- tests/suite/math/syntax.typ | 4 ++++ 5 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 tests/ref/issue-4828-math-number-multi-char.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index c5d13c8b3..ecd0d78a5 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -271,11 +271,9 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { - continuable = !p.at(SyntaxKind::MathShorthand) - && matches!( - math_class(p.current_text()), - None | Some(MathClass::Alphabetic) - ); + // `a(b)/c` parses as `(a(b))/c` if `a` is continuable. + continuable = math_class(p.current_text()) == Some(MathClass::Alphabetic) + || p.current_text().chars().all(char::is_alphabetic); if !maybe_delimited(p) { p.eat(); } diff --git a/tests/ref/issue-4828-math-number-multi-char.png b/tests/ref/issue-4828-math-number-multi-char.png new file mode 100644 index 0000000000000000000000000000000000000000..ff0a9bab97de1957f3ea99de261346c14f8ccd9d GIT binary patch literal 465 zcmV;?0WSWDP)|ER(H<~| zY15fS>tQtA{}XtTVCdm*c-i;*JA9768pl*k6|TZn_hfVZBb`zLU zu}s@P^Y-#l!KFB0edr+gWfqVu9ueHV4bU7M05<^?+C#&I=3AT8;l;r86B6U-X$jKF za=L2`P;6~j!>cYf`lp^vQWqM|=kr@!WGr_t9x0mZz~X6-bI-fy7r^iQUO)9tl5%1- z#^oR^9F0bMsV8vm3s&yKnOscn)a^gub#$+3kwt>F^Kh*g7B-VXEpJLEO)(aS^x2H5 z;KpF747*FrtuipA^fu91SfEx|IQ_Y;k4A3B?xZqAmSN`-9qo4u4+mkcQ8L^4%lZSK z8oHWdWg0`nTS2m)v3YDC_`}G^hC`YiT`~W@dW%)K3RmbaR+3c8Kj6b400000NkvXX Hu0mjf!|2wg literal 0 HcmV?d00001 diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png index 973c433e2c0d267aa57d746e586736c2b77b1a96..fd16f2e6bf3e043a81e4e6e3146a6b15c05fc559 100644 GIT binary patch literal 3586 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 IM6N<$f*h$L1poj5 literal 5504 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 diff --git a/tests/suite/math/frac.typ b/tests/suite/math/frac.typ index 7f513930a..3bd00eab2 100644 --- a/tests/suite/math/frac.typ +++ b/tests/suite/math/frac.typ @@ -37,8 +37,8 @@ $ 1/2/3 = (1/2)/3 = 1/(2/3) $ // Test precedence. $ a_1/b_2, 1/f(x), zeta(x)/2, "foo"[|x|]/2 \ 1.2/3.7, 2.3^3.4 \ - 🏳️‍🌈[x]/2, f [x]/2, phi [x]/2, 🏳️‍🌈 [x]/2 \ - +[x]/2, 1(x)/2, 2[x]/2 \ + f [x]/2, phi [x]/2 \ + +[x]/2, 1(x)/2, 2[x]/2, 🏳️‍🌈[x]/2 \ (a)b/2, b(a)[b]/2 \ n!/2, 5!/2, n !/2, 1/n!, 1/5! $ diff --git a/tests/suite/math/syntax.typ b/tests/suite/math/syntax.typ index 7091d908c..32b9c098c 100644 --- a/tests/suite/math/syntax.typ +++ b/tests/suite/math/syntax.typ @@ -28,6 +28,10 @@ $ dot \ dots \ ast \ tilde \ star $ $floor(phi.alt.)$ $floor(phi.alt. )$ +--- issue-4828-math-number-multi-char --- +// Numbers should parse the same regardless of number of characters. +$1/2(x)$ vs. $1/10(x)$ + --- math-unclosed --- // Error: 1-2 unclosed delimiter $a From 417f5846b68777b8a4d3b9336761bd23c48a26b5 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:41:45 +0200 Subject: [PATCH 013/162] Support comparison functions in `array.sorted` (#5627) Co-authored-by: +merlan #flirora Co-authored-by: Laurenz --- Cargo.lock | 7 + Cargo.toml | 1 + crates/typst-library/Cargo.toml | 1 + crates/typst-library/src/foundations/array.rs | 149 +++++++++++++++--- tests/suite/foundations/array.typ | 6 + 5 files changed, 140 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c13c64819..f9c0cb189 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -913,6 +913,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "glidesort" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0" + [[package]] name = "half" version = "2.4.1" @@ -3052,6 +3058,7 @@ dependencies = [ "ecow", "flate2", "fontdb", + "glidesort", "hayagriva", "icu_properties", "icu_provider", diff --git a/Cargo.toml b/Cargo.toml index cbe69a05d..b9ec25054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ fastrand = "2.3" flate2 = "1" fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" +glidesort = "0.1.2" hayagriva = "0.8.1" heck = "0.5" hypher = "0.1.4" diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index 71729b63a..b210637a8 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -29,6 +29,7 @@ csv = { workspace = true } ecow = { workspace = true } flate2 = { workspace = true } fontdb = { workspace = true } +glidesort = { workspace = true } hayagriva = { workspace = true } icu_properties = { workspace = true } icu_provider = { workspace = true } diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index b647473ab..c1fcb6b49 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -808,7 +808,7 @@ impl Array { /// function. The sorting algorithm used is stable. /// /// Returns an error if two values could not be compared or if the key - /// function (if given) yields an error. + /// or comparison function (if given) yields an error. /// /// To sort according to multiple criteria at once, e.g. in case of equality /// between some criteria, the key function can return an array. The results @@ -832,33 +832,134 @@ impl Array { /// determine the keys to sort by. #[named] key: Option, + /// If given, uses this function to compare elements in the array. + /// + /// This function should return a boolean: `{true}` indicates that the + /// elements are in order, while `{false}` indicates that they should be + /// swapped. To keep the sort stable, if the two elements are equal, the + /// function should return `{true}`. + /// + /// If this function does not order the elements properly (e.g., by + /// returning `{false}` for both `{(x, y)}` and `{(y, x)}`, or for + /// `{(x, x)}`), the resulting array will be in unspecified order. + /// + /// When used together with `key`, `by` will be passed the keys instead + /// of the elements. + /// + /// ```example + /// #( + /// "sorted", + /// "by", + /// "decreasing", + /// "length", + /// ).sorted( + /// key: s => s.len(), + /// by: (l, r) => l >= r, + /// ) + /// ``` + #[named] + by: Option, ) -> SourceResult { - let mut result = Ok(()); - let mut vec = self.0; - let mut key_of = |x: Value| match &key { - // NOTE: We are relying on `comemo`'s memoization of function - // evaluation to not excessively reevaluate the `key`. - Some(f) => f.call(engine, context, [x]), - None => Ok(x), - }; - vec.make_mut().sort_by(|a, b| { - // Until we get `try` blocks :) - match (key_of(a.clone()), key_of(b.clone())) { - (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { - if result.is_ok() { - result = Err(err).at(span); + match by { + Some(by) => { + let mut are_in_order = |mut x, mut y| { + if let Some(f) = &key { + // We rely on `comemo`'s memoization of function + // evaluation to not excessively reevaluate the key. + x = f.call(engine, context, [x])?; + y = f.call(engine, context, [y])?; } - Ordering::Equal - }), - (Err(e), _) | (_, Err(e)) => { - if result.is_ok() { - result = Err(e); + match by.call(engine, context, [x, y])? { + Value::Bool(b) => Ok(b), + x => { + bail!( + span, + "expected boolean from `by` function, got {}", + x.ty(), + ) + } } - Ordering::Equal - } + }; + // If a comparison function is provided, we use `glidesort` + // instead of the standard library sorting algorithm to prevent + // panics in case the comparison function does not define a + // valid order (see https://github.com/typst/typst/pull/5627). + let mut result = Ok(()); + let mut vec = self.0.into_iter().enumerate().collect::>(); + glidesort::sort_by(&mut vec, |(i, x), (j, y)| { + // Because we use booleans for the comparison function, in + // order to keep the sort stable, we need to compare in the + // right order. + if i < j { + // If `x` and `y` appear in this order in the original + // array, then we should change their order (i.e., + // return `Ordering::Greater`) iff `y` is strictly less + // than `x` (i.e., `compare(x, y)` returns `false`). + // Otherwise, we should keep them in the same order + // (i.e., return `Ordering::Less`). + match are_in_order(x.clone(), y.clone()) { + Ok(false) => Ordering::Greater, + Ok(true) => Ordering::Less, + Err(err) => { + if result.is_ok() { + result = Err(err); + } + Ordering::Equal + } + } + } else { + // If `x` and `y` appear in the opposite order in the + // original array, then we should change their order + // (i.e., return `Ordering::Less`) iff `x` is strictly + // less than `y` (i.e., `compare(y, x)` returns + // `false`). Otherwise, we should keep them in the same + // order (i.e., return `Ordering::Less`). + match are_in_order(y.clone(), x.clone()) { + Ok(false) => Ordering::Less, + Ok(true) => Ordering::Greater, + Err(err) => { + if result.is_ok() { + result = Err(err); + } + Ordering::Equal + } + } + } + }); + result.map(|()| vec.into_iter().map(|(_, x)| x).collect()) } - }); - result.map(|_| vec.into()) + + None => { + let mut key_of = |x: Value| match &key { + // We rely on `comemo`'s memoization of function evaluation + // to not excessively reevaluate the key. + Some(f) => f.call(engine, context, [x]), + None => Ok(x), + }; + // If no comparison function is provided, we know the order is + // valid, so we can use the standard library sort and prevent an + // extra allocation. + let mut result = Ok(()); + let mut vec = self.0; + vec.make_mut().sort_by(|a, b| { + match (key_of(a.clone()), key_of(b.clone())) { + (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { + if result.is_ok() { + result = Err(err).at(span); + } + Ordering::Equal + }), + (Err(e), _) | (_, Err(e)) => { + if result.is_ok() { + result = Err(e); + } + Ordering::Equal + } + } + }); + result.map(|()| vec.into()) + } + } } /// Deduplicates all items in the array. diff --git a/tests/suite/foundations/array.typ b/tests/suite/foundations/array.typ index 61b5decb3..0c820d7f2 100644 --- a/tests/suite/foundations/array.typ +++ b/tests/suite/foundations/array.typ @@ -359,6 +359,12 @@ #test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10)) #test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x), (-10, -7, -5, 1, 2, 2, 3, 6, 8)) #test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x * x), (1, 2, 2, 3, -5, 6, -7, 8, -10)) +#test(("I", "the", "hi", "text").sorted(by: (x, y) => x.len() < y.len()), ("I", "hi", "the", "text")) +#test(("I", "the", "hi", "text").sorted(key: x => x.len(), by: (x, y) => y < x), ("text", "the", "hi", "I")) + +--- array-sorted-invalid-by-function --- +// Error: 2-39 expected boolean from `by` function, got string +#(1, 2, 3).sorted(by: (_, _) => "hmm") --- array-sorted-key-function-positional-1 --- // Error: 12-18 unexpected argument From ed2106e28d4b0cc213a4789d5e59c59ad08e9f29 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:47:42 +0200 Subject: [PATCH 014/162] Disallow empty font lists (#6049) --- crates/typst-library/src/text/mod.rs | 16 ++++++++++++++-- tests/suite/text/font.typ | 4 ++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 3aac15ba5..462d16060 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -42,7 +42,7 @@ use ttf_parser::Tag; use typst_syntax::Spanned; use typst_utils::singleton; -use crate::diag::{bail, warning, HintedStrResult, SourceResult}; +use crate::diag::{bail, warning, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue, @@ -891,9 +891,21 @@ cast! { } /// Font family fallback list. +/// +/// Must contain at least one font. #[derive(Debug, Default, Clone, PartialEq, Hash)] pub struct FontList(pub Vec); +impl FontList { + pub fn new(fonts: Vec) -> StrResult { + if fonts.is_empty() { + bail!("font fallback list must not be empty") + } else { + Ok(Self(fonts)) + } + } +} + impl<'a> IntoIterator for &'a FontList { type IntoIter = std::slice::Iter<'a, FontFamily>; type Item = &'a FontFamily; @@ -911,7 +923,7 @@ cast! { self.0.into_value() }, family: FontFamily => Self(vec![family]), - values: Array => Self(values.into_iter().map(|v| v.cast()).collect::>()?), + values: Array => Self::new(values.into_iter().map(|v| v.cast()).collect::>()?)?, } /// Resolve a prioritized iterator over the font families. diff --git a/tests/suite/text/font.typ b/tests/suite/text/font.typ index 60a1cd94d..6e21dfd23 100644 --- a/tests/suite/text/font.typ +++ b/tests/suite/text/font.typ @@ -149,3 +149,7 @@ The number 123. #set text(-1pt) a + +--- empty-text-font-array --- +// Error: 17-19 font fallback list must not be empty +#set text(font: ()) From bf8751c06352c305a8132a2bd0a06ced557a3819 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 4 Apr 2025 10:35:51 +0200 Subject: [PATCH 015/162] Switch to released `krilla` version (#6137) --- Cargo.lock | 37 +++++++++++++++++++++---------------- Cargo.toml | 6 ++---- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f9c0cb189..8c485ea7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -787,9 +787,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "font-types" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d868ec188a98bb014c606072edd47e52e7ab7297db943b0b28503121e1d037bd" +checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf" dependencies = [ "bytemuck", ] @@ -1360,8 +1360,9 @@ dependencies = [ [[package]] name = "krilla" -version = "0.3.0" -source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9" dependencies = [ "base64", "bumpalo", @@ -1388,8 +1389,9 @@ dependencies = [ [[package]] name = "krilla-svg" -version = "0.3.0" -source = "git+https://github.com/LaurenzV/krilla?rev=14756f7#14756f7067cb1a80b73b712cae9f98597153e623" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e" dependencies = [ "flate2", "fontdb", @@ -1837,8 +1839,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" -version = "0.12.1" -source = "git+https://github.com/typst/pdf-writer?rev=0d513b9#0d513b9050d2f1a0507cabb4898aca971af6da98" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc" dependencies = [ "bitflags 2.8.0", "itoa", @@ -2097,9 +2100,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f004ee5c610b8beb5f33273246893ac6258ec22185a6eb8ee132bccdb904cdaa" +checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288" dependencies = [ "bytemuck", "font-types", @@ -2425,9 +2428,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.28.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e7936ca3627fdb516e97aca3c8ab5103f94ae32fe5ce80a0a02edcbacb7b53" +checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8" dependencies = [ "bytemuck", "read-fonts", @@ -2522,8 +2525,9 @@ dependencies = [ [[package]] name = "subsetter" -version = "0.2.0" -source = "git+https://github.com/typst/subsetter?rev=460fdb6#460fdb66d6e0138b721b1ca9882faf15ce003246" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35539e8de3dcce8dd0c01f3575f85db1e5ac1aea1b996d2d09d89f148bc91497" dependencies = [ "fxhash", ] @@ -3755,8 +3759,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" -version = "0.3.1" -source = "git+https://github.com/LaurenzV/xmp-writer?rev=a1cbb887#a1cbb887a84376fea4d7590d41c194a332840549" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7" [[package]] name = "xz2" diff --git a/Cargo.toml b/Cargo.toml index b9ec25054..16c6a7d63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,8 +72,8 @@ if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.6" -krilla = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7", default-features = false, features = ["raster-images", "comemo", "rayon"] } -krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "14756f7" } +krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] } +krilla-svg = "0.1.0" kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" @@ -87,7 +87,6 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" pathdiff = "0.2" -pdf-writer = "0.12.1" phf = { version = "0.11", features = ["macros"] } pixglyph = "0.6" png = "0.17" @@ -114,7 +113,6 @@ sigpipe = "0.1" siphasher = "1" smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } stacker = "0.1.15" -subsetter = "0.2" syn = { version = "2", features = ["full", "extra-traits"] } syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] } tar = "0.4" From 387a8b48951b0e7e283c81557852e3eba3afb446 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:53:14 +0200 Subject: [PATCH 016/162] Display color spaces in the order in which they are presented in the doc (#6140) --- crates/typst-library/src/visualize/gradient.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index d59175a4e..45f388ccd 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -120,12 +120,12 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// #let spaces = ( /// ("Oklab", color.oklab), /// ("Oklch", color.oklch), -/// ("linear-RGB", color.linear-rgb), /// ("sRGB", color.rgb), +/// ("linear-RGB", color.linear-rgb), /// ("CMYK", color.cmyk), +/// ("Grayscale", color.luma), /// ("HSL", color.hsl), /// ("HSV", color.hsv), -/// ("Grayscale", color.luma), /// ) /// /// #for (name, space) in spaces { From ea336a6ac71ba9d84da6caa5d64291c87b0bca44 Mon Sep 17 00:00:00 2001 From: Markus Langgeng Iman Saputra Date: Fri, 4 Apr 2025 15:50:13 +0000 Subject: [PATCH 017/162] Add Indonesian translation (#6108) Co-authored-by: Malo <57839069+MDLC01@users.noreply.github.com> --- crates/typst-library/src/text/lang.rs | 4 +++- crates/typst-library/translations/id.txt | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 crates/typst-library/translations/id.txt diff --git a/crates/typst-library/src/text/lang.rs b/crates/typst-library/src/text/lang.rs index c75e5225f..2cc66a261 100644 --- a/crates/typst-library/src/text/lang.rs +++ b/crates/typst-library/src/text/lang.rs @@ -14,7 +14,7 @@ macro_rules! translation { }; } -const TRANSLATIONS: [(&str, &str); 38] = [ +const TRANSLATIONS: [(&str, &str); 39] = [ translation!("ar"), translation!("bg"), translation!("ca"), @@ -31,6 +31,7 @@ const TRANSLATIONS: [(&str, &str); 38] = [ translation!("el"), translation!("he"), translation!("hu"), + translation!("id"), translation!("is"), translation!("it"), translation!("ja"), @@ -82,6 +83,7 @@ impl Lang { pub const HEBREW: Self = Self(*b"he ", 2); pub const HUNGARIAN: Self = Self(*b"hu ", 2); pub const ICELANDIC: Self = Self(*b"is ", 2); + pub const INDONESIAN: Self = Self(*b"id ", 2); pub const ITALIAN: Self = Self(*b"it ", 2); pub const JAPANESE: Self = Self(*b"ja ", 2); pub const LATIN: Self = Self(*b"la ", 2); diff --git a/crates/typst-library/translations/id.txt b/crates/typst-library/translations/id.txt new file mode 100644 index 000000000..bea5ee18c --- /dev/null +++ b/crates/typst-library/translations/id.txt @@ -0,0 +1,8 @@ +figure = Gambar +table = Tabel +equation = Persamaan +bibliography = Daftar Pustaka +heading = Bagian +outline = Daftar Isi +raw = Kode +page = halaman From d55abf084263c15b4eac8efcf4faa3aafdd3af11 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 7 Apr 2025 19:46:46 +0200 Subject: [PATCH 018/162] Update community section in README (#6150) --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 41f465152..9526f3df4 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Typst's CLI is available from different sources: - You can install Typst through different package managers. Note that the versions in the package managers might lag behind the latest release. - - Linux: + - Linux: - View [Typst on Repology][repology] - View [Typst's Snap][snap] - macOS: `brew install typst` @@ -177,22 +177,22 @@ If you prefer an integrated IDE-like experience with autocompletion and instant preview, you can also check out [Typst's free web app][app]. ## Community -The main place where the community gathers is our [Discord server][discord]. -Feel free to join there to ask questions, help out others, share cool things -you created with Typst, or just to chat. +The main places where the community gathers are our [Forum][forum] and our +[Discord server][discord]. The Forum is a great place to ask questions, help +others, and share cool things you created with Typst. The Discord server is more +suitable for quicker questions, discussions about contributing, or just to chat. +We'd be happy to see you there! -Aside from that there are a few places where you can find things built by -the community: - -- The official [package list](https://typst.app/docs/packages) -- The [Awesome Typst](https://github.com/qjcg/awesome-typst) repository +[Typst Universe][universe] is where the community shares templates and packages. +If you want to share your own creations, you can submit them to our +[package repository][packages]. If you had a bad experience in our community, please [reach out to us][contact]. ## Contributing -We would love to see contributions from the community. If you experience bugs, -feel free to open an issue. If you would like to implement a new feature or bug -fix, please follow the steps outlined in the [contribution guide][contributing]. +We love to see contributions from the community. If you experience bugs, feel +free to open an issue. If you would like to implement a new feature or bug fix, +please follow the steps outlined in the [contribution guide][contributing]. To build Typst yourself, first ensure that you have the [latest stable Rust][rust] installed. Then, clone this repository and build the @@ -243,6 +243,8 @@ instant preview. To achieve these goals, we follow three core design principles: [docs]: https://typst.app/docs/ [app]: https://typst.app/ [discord]: https://discord.gg/2uDybryKPe +[forum]: https://forum.typst.app/ +[universe]: https://typst.app/universe/ [tutorial]: https://typst.app/docs/tutorial/ [show]: https://typst.app/docs/reference/styling/#show-rules [math]: https://typst.app/docs/reference/math/ From 14928ef9628d084af370463ccbf2f3bae3f70794 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:47:29 +0300 Subject: [PATCH 019/162] Fix typo in module docs (#6146) Co-authored-by: Alberto Corbi --- crates/typst-library/src/foundations/module.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 8d9626a1a..d6d5e831d 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -7,7 +7,7 @@ use typst_syntax::FileId; use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{repr, ty, Content, Scope, Value}; -/// An module of definitions. +/// A module of definitions. /// /// A module /// - be built-in From bd2e76e11d487d1e825217db155e45d3fb6f6584 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 7 Apr 2025 20:20:27 +0200 Subject: [PATCH 020/162] Bump OpenSSL (#6153) --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c485ea7d..ab2d2cc83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,9 +1702,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.70" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags 2.8.0", "cfg-if", @@ -1743,9 +1743,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.105" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", diff --git a/Cargo.toml b/Cargo.toml index 16c6a7d63..12870b809 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ native-tls = "0.2" notify = "8" once_cell = "1" open = "5.0.1" -openssl = "0.10" +openssl = "0.10.72" oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] } palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" From 14a0565d95b40bb58a07da554b7d05d4712efcd7 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Mon, 7 Apr 2025 14:42:29 -0400 Subject: [PATCH 021/162] Show warnings from eval (#6100) Co-authored-by: Laurenz --- crates/typst-cli/src/query.rs | 3 +++ crates/typst-eval/src/lib.rs | 4 ++-- crates/typst-library/src/foundations/mod.rs | 12 +++++++++++- crates/typst-library/src/model/bibliography.rs | 6 ++++-- crates/typst-library/src/routines.rs | 1 + tests/suite/foundations/eval.typ | 6 ++++++ 6 files changed, 27 insertions(+), 5 deletions(-) diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs index 610f23cd4..7806e456f 100644 --- a/crates/typst-cli/src/query.rs +++ b/crates/typst-cli/src/query.rs @@ -2,6 +2,7 @@ use comemo::Track; use ecow::{eco_format, EcoString}; use serde::Serialize; use typst::diag::{bail, HintedStrResult, StrResult, Warned}; +use typst::engine::Sink; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::layout::PagedDocument; use typst::syntax::Span; @@ -58,6 +59,8 @@ fn retrieve( let selector = eval_string( &typst::ROUTINES, world.track(), + // TODO: propagate warnings + Sink::new().track_mut(), &command.selector, Span::detached(), EvalMode::Code, diff --git a/crates/typst-eval/src/lib.rs b/crates/typst-eval/src/lib.rs index 5eae7c1df..586da26be 100644 --- a/crates/typst-eval/src/lib.rs +++ b/crates/typst-eval/src/lib.rs @@ -101,6 +101,7 @@ pub fn eval( pub fn eval_string( routines: &Routines, world: Tracked, + sink: TrackedMut, string: &str, span: Span, mode: EvalMode, @@ -121,7 +122,6 @@ pub fn eval_string( } // Prepare the engine. - let mut sink = Sink::new(); let introspector = Introspector::default(); let traced = Traced::default(); let engine = Engine { @@ -129,7 +129,7 @@ pub fn eval_string( world, introspector: introspector.track(), traced: traced.track(), - sink: sink.track_mut(), + sink, route: Route::default(), }; diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 8e3aa060d..d42be15b1 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -77,6 +77,7 @@ pub use { indexmap::IndexMap, }; +use comemo::TrackedMut; use ecow::EcoString; use typst_syntax::Spanned; @@ -297,5 +298,14 @@ pub fn eval( for (key, value) in dict { scope.bind(key.into(), Binding::new(value, span)); } - (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) + + (engine.routines.eval_string)( + engine.routines, + engine.world, + TrackedMut::reborrow_mut(&mut engine.sink), + &text, + span, + mode, + scope, + ) } diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index b11c61789..51e3b03b0 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -6,7 +6,7 @@ use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; -use comemo::Tracked; +use comemo::{Track, Tracked}; use ecow::{eco_format, EcoString, EcoVec}; use hayagriva::archive::ArchivedStyle; use hayagriva::io::BibLaTeXError; @@ -20,7 +20,7 @@ use typst_syntax::{Span, Spanned}; use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; -use crate::engine::Engine; +use crate::engine::{Engine, Sink}; use crate::foundations::{ elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, @@ -999,6 +999,8 @@ impl ElemRenderer<'_> { (self.routines.eval_string)( self.routines, self.world, + // TODO: propagate warnings + Sink::new().track_mut(), math, self.span, EvalMode::Math, diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index b283052a4..6f0cb32b1 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -55,6 +55,7 @@ routines! { fn eval_string( routines: &Routines, world: Tracked, + sink: TrackedMut, string: &str, span: Span, mode: EvalMode, diff --git a/tests/suite/foundations/eval.typ b/tests/suite/foundations/eval.typ index f85146b23..85f43911c 100644 --- a/tests/suite/foundations/eval.typ +++ b/tests/suite/foundations/eval.typ @@ -52,3 +52,9 @@ _Tiger!_ #eval(mode: "math", "f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)") $f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)$ + +--- issue-6067-eval-warnings --- +// Test that eval shows warnings from the executed code. +// Warning: 7-11 no text within stars +// Hint: 7-11 using multiple consecutive stars (e.g. **) has no additional effect +#eval("**", mode: "markup") From 43c3d5d3afc945639a576535e48806112c161743 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:47:02 +0300 Subject: [PATCH 022/162] Improved ratio and relative length docs (#5750) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: Laurenz --- crates/typst-library/src/layout/ratio.rs | 32 ++++++++++++--- crates/typst-library/src/layout/rel.rs | 51 +++++++++++++++++++++--- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/crates/typst-library/src/layout/ratio.rs b/crates/typst-library/src/layout/ratio.rs index 1c0dcd298..cf826c2b5 100644 --- a/crates/typst-library/src/layout/ratio.rs +++ b/crates/typst-library/src/layout/ratio.rs @@ -8,15 +8,35 @@ use crate::foundations::{repr, ty, Repr}; /// A ratio of a whole. /// -/// Written as a number, followed by a percent sign. +/// A ratio is written as a number, followed by a percent sign. Ratios most +/// often appear as part of a [relative length]($relative), to specify the size +/// of some layout element relative to the page or some container. /// -/// # Example /// ```example -/// #set align(center) -/// #scale(x: 150%)[ -/// Scaled apart. -/// ] +/// #rect(width: 25%) /// ``` +/// +/// However, they can also describe any other property that is relative to some +/// base, e.g. an amount of [horizontal scaling]($scale.x) or the +/// [height of parentheses]($math.lr.size) relative to the height of the content +/// they enclose. +/// +/// # Scripting +/// Within your own code, you can use ratios as you like. You can multiply them +/// with various other types as shown below: +/// +/// | Multiply by | Example | Result | +/// |-----------------|-------------------------|-----------------| +/// | [`ratio`] | `{27% * 10%}` | `{2.7%}` | +/// | [`length`] | `{27% * 100pt}` | `{27pt}` | +/// | [`relative`] | `{27% * (10% + 100pt)}` | `{2.7% + 27pt}` | +/// | [`angle`] | `{27% * 100deg}` | `{27deg}` | +/// | [`int`] | `{27% * 2}` | `{54%}` | +/// | [`float`] | `{27% * 0.37037}` | `{10%}` | +/// | [`fraction`] | `{27% * 3fr}` | `{0.81fr}` | +/// +/// When ratios are displayed in the document, they are rounded to two +/// significant digits for readability. #[ty(cast)] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Ratio(Scalar); diff --git a/crates/typst-library/src/layout/rel.rs b/crates/typst-library/src/layout/rel.rs index 76d736785..7fe5d9c05 100644 --- a/crates/typst-library/src/layout/rel.rs +++ b/crates/typst-library/src/layout/rel.rs @@ -14,17 +14,58 @@ use crate::layout::{Abs, Em, Length, Ratio}; /// addition and subtraction of a length and a ratio. Wherever a relative length /// is expected, you can also use a bare length or ratio. /// -/// # Example -/// ```example -/// #rect(width: 100% - 50pt) +/// # Relative to the page +/// A common use case is setting the width or height of a layout element (e.g., +/// [block], [rect], etc.) as a certain percentage of the width of the page. +/// Here, the rectangle's width is set to `{25%}`, so it takes up one fourth of +/// the page's _inner_ width (the width minus margins). /// -/// #(100% - 50pt).length \ -/// #(100% - 50pt).ratio +/// ```example +/// #rect(width: 25%) /// ``` /// +/// Bare lengths or ratios are always valid where relative lengths are expected, +/// but the two can also be freely mixed: +/// ```example +/// #rect(width: 25% + 1cm) +/// ``` +/// +/// If you're trying to size an element so that it takes up the page's _full_ +/// width, you have a few options (this highly depends on your exact use case): +/// +/// 1. Set page margins to `{0pt}` (`[#set page(margin: 0pt)]`) +/// 2. Multiply the ratio by the known full page width (`{21cm * 69%}`) +/// 3. Use padding which will negate the margins (`[#pad(x: -2.5cm, ...)]`) +/// 4. Use the page [background](page.background) or +/// [foreground](page.foreground) field as those don't take margins into +/// account (note that it will render the content outside of the document +/// flow, see [place] to control the content position) +/// +/// # Relative to a container +/// When a layout element (e.g. a [rect]) is nested in another layout container +/// (e.g. a [block]) instead of being a direct descendant of the page, relative +/// widths become relative to the container: +/// +/// ```example +/// #block( +/// width: 100pt, +/// fill: aqua, +/// rect(width: 50%), +/// ) +/// ``` +/// +/// # Scripting +/// You can multiply relative lengths by [ratios]($ratio), [integers]($int), and +/// [floats]($float). +/// /// A relative length has the following fields: /// - `length`: Its length component. /// - `ratio`: Its ratio component. +/// +/// ```example +/// #(100% - 50pt).length \ +/// #(100% - 50pt).ratio +/// ``` #[ty(cast, name = "relative", title = "Relative Length")] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] pub struct Rel { From 9829bd8326fc67ebf78593bf4e860390c5ae8804 Mon Sep 17 00:00:00 2001 From: alluring-mushroom <86041465+alluring-mushroom@users.noreply.github.com> Date: Tue, 8 Apr 2025 05:56:20 +1000 Subject: [PATCH 023/162] Document exceptions and alternatives to using `type` (#6027) Co-authored-by: Zedd Serjeant Co-authored-by: Laurenz --- crates/typst-library/src/foundations/ty.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 40f7003c3..9d7690283 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -39,11 +39,25 @@ use crate::foundations::{ /// #type(image("glacier.jpg")). /// ``` /// -/// The type of `10` is `int`. Now, what is the type of `int` or even `type`? +/// The type of `{10}` is `int`. Now, what is the type of `int` or even `type`? /// ```example /// #type(int) \ /// #type(type) /// ``` +/// +/// Unlike other types like `int`, [none] and [auto] do not have a name +/// representing them. To test if a value is one of these, compare your value to +/// them directly, e.g: +/// ```example +/// #let val = none +/// #if val == none [ +/// Yep, it's none. +/// ] +/// ``` +/// +/// Note that `type` will return [`content`] for all document elements. To +/// programmatically determine which kind of content you are dealing with, see +/// [`content.func`]. #[ty(scope, cast)] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Type(Static); From 94a497a01ffd60743b0a2ae67367be168bbde076 Mon Sep 17 00:00:00 2001 From: Approximately Equal Date: Mon, 7 Apr 2025 13:18:52 -0700 Subject: [PATCH 024/162] Add HTML meta tags for document authors and keywords (#6134) --- crates/typst-html/src/lib.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index aa769976e..7d78a5da4 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -263,13 +263,13 @@ fn handle( /// Wrap the nodes in `` and `` if they are not yet rooted, /// supplying a suitable ``. fn root_element(output: Vec, info: &DocumentInfo) -> SourceResult { + let head = head_element(info); let body = match classify_output(output)? { OutputKind::Html(element) => return Ok(element), OutputKind::Body(body) => body, OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), }; - Ok(HtmlElement::new(tag::html) - .with_children(vec![head_element(info).into(), body.into()])) + Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()])) } /// Generate a `` element. @@ -302,6 +302,24 @@ fn head_element(info: &DocumentInfo) -> HtmlElement { ); } + if !info.author.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "authors") + .with_attr(attr::content, info.author.join(", ")) + .into(), + ) + } + + if !info.keywords.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "keywords") + .with_attr(attr::content, info.keywords.join(", ")) + .into(), + ) + } + HtmlElement::new(tag::head).with_children(children) } From c21c1c391b48f843c8671993a28eaf1fe0d40b89 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:27:42 +0200 Subject: [PATCH 025/162] Use `measure` `width` argument in `layout` doc (#6160) --- crates/typst-library/src/layout/layout.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index cde3187d3..88252e5e3 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -22,7 +22,8 @@ use crate::layout::{BlockElem, Size}; /// #let text = lorem(30) /// #layout(size => [ /// #let (height,) = measure( -/// block(width: size.width, text), +/// width: size.width, +/// text, /// ) /// This text is #height high with /// the current page width: \ From 7e072e24930d8a7524f700b62cabd97ceb4f45e6 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 17 Apr 2025 14:10:27 +0000 Subject: [PATCH 026/162] Add test for flattened accents in math (#6188) --- tests/ref/math-accent-flattened.png | Bin 0 -> 464 bytes tests/suite/math/accent.typ | 8 ++++++++ 2 files changed, 8 insertions(+) create mode 100644 tests/ref/math-accent-flattened.png diff --git a/tests/ref/math-accent-flattened.png b/tests/ref/math-accent-flattened.png new file mode 100644 index 0000000000000000000000000000000000000000..f7764cb74144af9ca15f4a817511bfc5b2b291cd GIT binary patch literal 464 zcmV;>0WbcEP)8mP!bM$Vnv$5`(6eC>tD#OcbrCA+=02DzbvC49pjT#BwPi zkRnWQ(CWaeG`cvo!I#UsF7Ny_-~N=#^Zf@t+wYfWjmit*5Dwvg3BM|zlhfGEmr-)- z>S6x$VnFaWSbd%xlGDg&cLprZlKKvG|04b(82It=_t4%3q^fHka%@+mXI-!RCDZ&Y z<9AsCp9>C8xwpO}7V<^H?69mznqk2gZpM3k1G~eZ-|9wf<~LH0+Jm zgG4b(<}g)=s{TOW2iHAnG8WqlJVDrVrLls}Z>pJX&zO&dL3k2@0n%Q?F zauVA|&|TNRXQDaAjzK4yfaaE{cqvqNdXg>{2)uBny{GNkm5ev1u41XacjSB=PZ?MN z2Ai@5TJg5JHQjpa!+%_^m6_VFYciSfnn-vOAsoUX4EzOh@I@3%PB~xz0000 Date: Fri, 18 Apr 2025 17:27:07 +0300 Subject: [PATCH 027/162] Fix frac syntax section typo (#6193) --- crates/typst-library/src/math/frac.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/math/frac.rs b/crates/typst-library/src/math/frac.rs index f5c4514d6..dd5986b5f 100644 --- a/crates/typst-library/src/math/frac.rs +++ b/crates/typst-library/src/math/frac.rs @@ -15,7 +15,7 @@ use crate::math::Mathy; /// # Syntax /// This function also has dedicated syntax: Use a slash to turn neighbouring /// expressions into a fraction. Multiple atoms can be grouped into a single -/// expression using round grouping parenthesis. Such parentheses are removed +/// expression using round grouping parentheses. Such parentheses are removed /// from the output, but you can nest multiple to force them. #[elem(title = "Fraction", Mathy)] pub struct FracElem { From 14241ec1aae43ce3bff96411f62af76a01c7f709 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 1 May 2025 17:43:07 +0200 Subject: [PATCH 028/162] Use the right field name for `figure.caption.position` (#6226) --- crates/typst-library/src/model/figure.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 78a79a8e2..5a137edbd 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -457,7 +457,7 @@ impl Outlinable for Packed { /// customize the appearance of captions for all figures or figures of a /// specific kind. /// -/// In addition to its `pos` and `body`, the `caption` also provides the +/// In addition to its `position` and `body`, the `caption` also provides the /// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These /// parts can be used in [`where`]($function.where) selectors and show rules to /// build a completely custom caption. From b322da930fe35ee3d19896de6ab653e2f321e301 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Tue, 6 May 2025 10:26:55 +0200 Subject: [PATCH 029/162] Respect RTL cell layouting order in grid layout (#6232) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> --- crates/typst-layout/src/grid/layouter.rs | 60 +++----- crates/typst-layout/src/grid/rowspans.rs | 16 +- tests/ref/grid-rtl-counter.png | Bin 0 -> 272 bytes tests/ref/grid-rtl-rowspan-counter-equal.png | Bin 0 -> 272 bytes .../ref/grid-rtl-rowspan-counter-mixed-1.png | Bin 0 -> 360 bytes .../ref/grid-rtl-rowspan-counter-mixed-2.png | Bin 0 -> 361 bytes .../grid-rtl-rowspan-counter-unequal-1.png | Bin 0 -> 361 bytes .../grid-rtl-rowspan-counter-unequal-2.png | Bin 0 -> 360 bytes tests/suite/layout/grid/rtl.typ | 140 ++++++++++++++++++ 9 files changed, 173 insertions(+), 43 deletions(-) create mode 100644 tests/ref/grid-rtl-counter.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-equal.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-mixed-1.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-mixed-2.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-unequal-1.png create mode 100644 tests/ref/grid-rtl-rowspan-counter-unequal-2.png diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index dc9e2238d..99b85eddb 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -11,7 +11,7 @@ use typst_library::layout::{ use typst_library::text::TextElem; use typst_library::visualize::Geometry; use typst_syntax::Span; -use typst_utils::{MaybeReverseIter, Numeric}; +use typst_utils::Numeric; use super::{ generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, @@ -574,7 +574,7 @@ impl<'a> GridLayouter<'a> { // Reverse with RTL so that later columns start first. let mut dx = Abs::zero(); - for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + for (x, &col) in self.rcols.iter().enumerate() { let mut dy = Abs::zero(); for row in rows { // We want to only draw the fill starting at the parent @@ -643,18 +643,13 @@ impl<'a> GridLayouter<'a> { .sum() }; let width = self.cell_spanned_width(cell, x); - // In the grid, cell colspans expand to the right, - // so we're at the leftmost (lowest 'x') column - // spanned by the cell. However, in RTL, cells - // expand to the left. Therefore, without the - // offset below, cell fills would start at the - // rightmost visual position of a cell and extend - // over to unrelated columns to the right in RTL. - // We avoid this by ensuring the fill starts at the - // very left of the cell, even with colspan > 1. - let offset = - if self.is_rtl { -width + col } else { Abs::zero() }; - let pos = Point::new(dx + offset, dy); + let mut pos = Point::new(dx, dy); + if self.is_rtl { + // In RTL cells expand to the left, thus the + // position must additionally be offset by the + // cell's width. + pos.x = self.width - (dx + width); + } let size = Size::new(width, height); let rect = Geometry::Rect(size).filled(fill); fills.push((pos, FrameItem::Shape(rect, self.span))); @@ -1236,10 +1231,9 @@ impl<'a> GridLayouter<'a> { } let mut output = Frame::soft(Size::new(self.width, height)); - let mut pos = Point::zero(); + let mut offset = Point::zero(); - // Reverse the column order when using RTL. - for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(cell) = self.grid.cell(x, y) { // Rowspans have a separate layout step if cell.rowspan.get() == 1 { @@ -1257,25 +1251,17 @@ impl<'a> GridLayouter<'a> { let frame = layout_cell(cell, engine, disambiguator, self.styles, pod)? .into_frame(); - let mut pos = pos; + let mut pos = offset; if self.is_rtl { - // In the grid, cell colspans expand to the right, - // so we're at the leftmost (lowest 'x') column - // spanned by the cell. However, in RTL, cells - // expand to the left. Therefore, without the - // offset below, the cell's contents would be laid out - // starting at its rightmost visual position and extend - // over to unrelated cells to its right in RTL. - // We avoid this by ensuring the rendered cell starts at - // the very left of the cell, even with colspan > 1. - let offset = -width + rcol; - pos.x += offset; + // In RTL cells expand to the left, thus the position + // must additionally be offset by the cell's width. + pos.x = self.width - (pos.x + width); } output.push_frame(pos, frame); } } - pos.x += rcol; + offset.x += rcol; } Ok(output) @@ -1302,8 +1288,8 @@ impl<'a> GridLayouter<'a> { pod.backlog = &heights[1..]; // Layout the row. - let mut pos = Point::zero(); - for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { + let mut offset = Point::zero(); + for (x, &rcol) in self.rcols.iter().enumerate() { if let Some(cell) = self.grid.cell(x, y) { // Rowspans have a separate layout step if cell.rowspan.get() == 1 { @@ -1314,17 +1300,19 @@ impl<'a> GridLayouter<'a> { let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; for (output, frame) in outputs.iter_mut().zip(fragment) { - let mut pos = pos; + let mut pos = offset; if self.is_rtl { - let offset = -width + rcol; - pos.x += offset; + // In RTL cells expand to the left, thus the + // position must additionally be offset by the + // cell's width. + pos.x = self.width - (offset.x + width); } output.push_frame(pos, frame); } } } - pos.x += rcol; + offset.x += rcol; } Ok(Fragment::frames(outputs)) diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 21992ed02..5ab0417d8 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -3,7 +3,6 @@ use typst_library::engine::Engine; use typst_library::foundations::Resolve; use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; -use typst_utils::MaybeReverseIter; use super::layouter::{in_last_with_offset, points, Row, RowPiece}; use super::{layout_cell, Cell, GridLayouter}; @@ -23,6 +22,10 @@ pub struct Rowspan { /// specified for the parent cell's `breakable` field. pub is_effectively_unbreakable: bool, /// The horizontal offset of this rowspan in all regions. + /// + /// This is the offset from the text direction start, meaning that, on RTL + /// grids, this is the offset from the right of the grid, whereas, on LTR + /// grids, it is the offset from the left. pub dx: Abs, /// The vertical offset of this rowspan in the first region. pub dy: Abs, @@ -118,10 +121,11 @@ impl GridLayouter<'_> { // Nothing to layout. return Ok(()); }; - let first_column = self.rcols[x]; let cell = self.grid.cell(x, y).unwrap(); let width = self.cell_spanned_width(cell, x); - let dx = if self.is_rtl { dx - width + first_column } else { dx }; + // In RTL cells expand to the left, thus the position + // must additionally be offset by the cell's width. + let dx = if self.is_rtl { self.width - (dx + width) } else { dx }; // Prepare regions. let size = Size::new(width, *first_height); @@ -185,10 +189,8 @@ impl GridLayouter<'_> { /// Checks if a row contains the beginning of one or more rowspan cells. /// If so, adds them to the rowspans vector. pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { - // We will compute the horizontal offset of each rowspan in advance. - // For that reason, we must reverse the column order when using RTL. - let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl)); - for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) { + let offsets = points(self.rcols.iter().copied()); + for (x, dx) in (0..self.rcols.len()).zip(offsets) { let Some(cell) = self.grid.cell(x, y) else { continue; }; diff --git a/tests/ref/grid-rtl-counter.png b/tests/ref/grid-rtl-counter.png new file mode 100644 index 0000000000000000000000000000000000000000..fb0df44ad40da59bfc8ee7d98b1445de8c70d3a3 GIT binary patch literal 272 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=Y0VEjK$QP*tsq3CDjv*Ddl7HAcG$dYm6xi*q zE4Q@*#5lR}$N9$H%qfMZf+Rj4Ul(fcY4Y(nv{N3;QLEU8< zD>nBvMb>BE7Z5$5uXcuyIY=aW&y(;2^ACqNtodcEuP&Tic=DpR>E*9$tG7JM57(nv{N3;QLEU8< zD>nBvMb>BE7Z5$5uXcuyIY=aW&y(;2^ACqNtodcEuP&Tic=DpR>E*9$tG7JM57}9D*9cp;sUck_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&ie+z?dM&b1i$a}Y@h83(4ztZ3oNj}0t@U0HjmUE!Qs3;_5Ce_?o#hv!=wuK zihQl6>8f)7fd@FckmckGXtEc_L7yA(I=im`bB7+_HGM<*t? z%Ra)5fn1g$4O)w}(6^5;`niq?aHm*uAOZ_4u)qTUGx!I#s*xWevRUx}0000{9D*9cp+_JMk_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&U*)ew4R6jg8F`@_x9R8K7MIFfdv*=V1Wfz1KS6Bw_*L(6&%dA)7;&#x&Rk@ z_i(dBu3e#z<)ml$eJxsL`x#(n-yIyDOLKGq)Y*x8hL5i+;~l~)_#eMvn!z{E@Ka)G z1n30L5h?-DioM_&o+zCM*lYkqw%oz1&0+gc1Ex>vDbMg(yb#gzuYfigH*m4SLgI?@ zB`_Nv%gb{KEU>`;5q1{Wrja8>3}Eg9cC=%BY01DJ(1i=b`6LA)Ji; zbg-xpuJvWo3>eUuuLM37!kDKfHo%SM$bkqfu)qQf{O92>(vXoKDr-mq00000NkvXX Hu0mjfwc?u% literal 0 HcmV?d00001 diff --git a/tests/ref/grid-rtl-rowspan-counter-unequal-1.png b/tests/ref/grid-rtl-rowspan-counter-unequal-1.png new file mode 100644 index 0000000000000000000000000000000000000000..c091f3a806bb3bbd5cc37d2e5372c59005093466 GIT binary patch literal 361 zcmV-v0ha!WP){9D*9cp+_JMk_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&U*)ew4R6jg8F`@_x9R8K7MIFfdv*=V1Wfz1KS6Bw_*L(6&%dA)7;&#x&Rk@ z_i(dBu3e#z<)ml$eJxsL`x#(n-yIyDOLKGq)Y*x8hL5i+;~l~)_#eMvn!z{E@Ka)G z1n30L5h?-DioM_&o+zCM*lYkqw%oz1&0+gc1Ex>vDbMg(yb#gzuYfigH*m4SLgI?@ zB`_Nv%gb{KEU>`;5q1{Wrja8>3}Eg9cC=%BY01DJ(1i=b`6LA)Ji; zbg-xpuJvWo3>eUuuLM37!kDKfHo%SM$bkqfu)qQf{O92>(vXoKDr-mq00000NkvXX Hu0mjfwc?u% literal 0 HcmV?d00001 diff --git a/tests/ref/grid-rtl-rowspan-counter-unequal-2.png b/tests/ref/grid-rtl-rowspan-counter-unequal-2.png new file mode 100644 index 0000000000000000000000000000000000000000..fffccc5664edfcd379a237268f14dd21e18fa39a GIT binary patch literal 360 zcmV-u0hj)XP)}9D*9cp;sUck_{3~S`;=krMT5lXeI?g1rAn75JqAY#E}hMh4fD= zC^+cO&ie+z?dM&b1i$a}Y@h83(4ztZ3oNj}0t@U0HjmUE!Qs3;_5Ce_?o#hv!=wuK zihQl6>8f)7fd@FckmckGXtEc_L7yA(I=im`bB7+_HGM<*t? z%Ra)5fn1g$4O)w}(6^5;`niq?aHm*uAOZ_4u)qTUGx!I#s*xWevRUx}0000 ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() ) + +--- grid-rtl-counter --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + [ + a: // should produce 1 + #test.step() + #context test.get().first() + ], + [ + b: // should produce 2 + #test.step() + #context test.get().first() + ], +) + +--- grid-rtl-rowspan-counter-equal --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 2, [ + a: // should produce 1 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 2, [ + b: // should produce 2 + #test.step() + #context test.get().first() + ]), +) + +--- grid-rtl-rowspan-counter-unequal-1 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 5, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 2, [ + a: // will produce 1 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 3, [ + c: // will produce 3 + #test.step() + #context test.get().first() + ]), +) + +--- grid-rtl-rowspan-counter-unequal-2 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 2, [ + a: // will produce 1 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 5, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + grid.cell(rowspan: 3, [ + c: // will produce 3 + #test.step() + #context test.get().first() + ]), +) + +--- grid-rtl-rowspan-counter-mixed-1 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + [ + a: // will produce 1 + #test.step() + #context test.get().first() + ], + grid.cell(rowspan: 2, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + [ + c: // will produce 3 + #test.step() + #context test.get().first() + ], +) + +--- grid-rtl-rowspan-counter-mixed-2 --- +// Test interaction between RTL and counters +#set text(dir: rtl) +#let test = counter("test") +#grid( + columns: (1fr, 1fr), + inset: 5pt, + align: center, + grid.cell(rowspan: 2, [ + b: // will produce 2 + #test.step() + #context test.get().first() + ]), + [ + a: // will produce 1 + #test.step() + #context test.get().first() + ], + [ + c: // will produce 3 + #test.step() + #context test.get().first() + ] +) From 9b09146a6b5e936966ed7ee73bce9dd2df3810ae Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Tue, 6 May 2025 16:03:48 +0200 Subject: [PATCH 030/162] Use list spacing for attach spacing in tight lists (#6242) --- crates/typst-library/src/model/enum.rs | 9 +++++---- crates/typst-library/src/model/list.rs | 9 +++++---- crates/typst-library/src/model/terms.rs | 8 +++++--- .../ref/issue-6242-tight-list-attach-spacing.png | Bin 0 -> 410 bytes tests/suite/model/list.typ | 8 ++++++++ 5 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 tests/ref/issue-6242-tight-list-attach-spacing.png diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index 2d95996ab..f1f93702b 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -259,10 +259,11 @@ impl Show for Packed { .spanned(self.span()); if tight { - let leading = ParElem::leading_in(styles); - let spacing = - VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); - realized = spacing + realized; + let spacing = self + .spacing(styles) + .unwrap_or_else(|| ParElem::leading_in(styles).into()); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; } Ok(realized) diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index d93ec9172..3c3afd338 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -166,10 +166,11 @@ impl Show for Packed { .spanned(self.span()); if tight { - let leading = ParElem::leading_in(styles); - let spacing = - VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); - realized = spacing + realized; + let spacing = self + .spacing(styles) + .unwrap_or_else(|| ParElem::leading_in(styles).into()); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; } Ok(realized) diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index e197ff318..3df74cd9e 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -189,13 +189,15 @@ impl Show for Packed { .styled(TermsElem::set_within(true)); if tight { - let leading = ParElem::leading_in(styles); - let spacing = VElem::new(leading.into()) + let spacing = self + .spacing(styles) + .unwrap_or_else(|| ParElem::leading_in(styles).into()); + let v = VElem::new(spacing.into()) .with_weak(true) .with_attach(true) .pack() .spanned(span); - realized = spacing + realized; + realized = v + realized; } Ok(realized) diff --git a/tests/ref/issue-6242-tight-list-attach-spacing.png b/tests/ref/issue-6242-tight-list-attach-spacing.png new file mode 100644 index 0000000000000000000000000000000000000000..48920008b1350f8b604d4e06842d25b282ae1afd GIT binary patch literal 410 zcmV;L0cHM)P)0004DNkl_mzthIoO<(BZ&hpc2JIEVmAj-D~C-E!p%iy#TpKiDY3^`zc26O_^cr~*i$nC=fnOZLAW@u z4gf5kZj1@SXHC5TQ1Mz;cS#N=Rseud8kRTZGrsj6P+jrmwLlbBSYd_#0i2By8sZG5zg4v90VZUCk*?O0PyXO zLgqz2W6G<6ohvO6g%ws<;l_k>sj><9b^1Iucx(V3zlXQ5Ap8jc^$y{Tv#pZ=l-h;i zo0bs(aAtuYIhc~YlkUsne#XQ*RpXI&Z7zisR=8o|U!MOHPD9hDwg3PC07*qoM6N<$ Eg2vyu4*&oF literal 0 HcmV?d00001 diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 9bed930bb..796a7b069 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -304,3 +304,11 @@ World - C - = D E + +--- issue-6242-tight-list-attach-spacing --- +// Nested tight lists should be uniformly spaced when list spacing is set. +#set list(spacing: 1.2em) +- A + - B + - C +- C From 54c5113a83d317ed9c37a129ad90165c100bd25d Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Mon, 12 May 2025 10:06:18 +0200 Subject: [PATCH 031/162] Catch indefinite loop in realization due to cycle between show and grouping rule (#6259) --- crates/typst-realize/src/lib.rs | 11 +++++++++++ tests/suite/styling/show.typ | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 151ae76ba..7d2460a89 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -655,6 +655,7 @@ fn visit_grouping_rules<'a>( let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind)); // Try to continue or finish an existing grouping. + let mut i = 0; while let Some(active) = s.groupings.last() { // Start a nested group if a rule with higher priority matches. if matching.is_some_and(|rule| rule.priority > active.rule.priority) { @@ -670,6 +671,16 @@ fn visit_grouping_rules<'a>( } finish_innermost_grouping(s)?; + i += 1; + if i > 512 { + // It seems like this case is only hit when there is a cycle between + // a show rule and a grouping rule. The show rule produces content + // that is matched by a grouping rule, which is then again processed + // by the show rule, and so on. The two must be at an equilibrium, + // otherwise either the "maximum show rule depth" or "maximum + // grouping depth" errors are triggered. + bail!(content.span(), "maximum grouping depth exceeded"); + } } // Start a new grouping. diff --git a/tests/suite/styling/show.typ b/tests/suite/styling/show.typ index e8ddf5534..f3d9efd55 100644 --- a/tests/suite/styling/show.typ +++ b/tests/suite/styling/show.typ @@ -258,3 +258,11 @@ I am *strong*, I am _emphasized_, and I am #[special]. = Hello *strong* + +--- issue-5690-oom-par-box --- +// Error: 3:6-5:1 maximum grouping depth exceeded +#show par: box + +Hello + +World From 26c19a49c8a73b1e7f7c299b9e25e57acfcd7eac Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Mon, 12 May 2025 10:07:43 +0200 Subject: [PATCH 032/162] Use the infer crate to determine if pdf embeds should be compressed (#6256) --- Cargo.lock | 7 ++++ Cargo.toml | 1 + crates/typst-pdf/Cargo.toml | 1 + crates/typst-pdf/src/embed.rs | 70 ++++++++++++++++++++++++++++++++++- 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index ab2d2cc83..4b70e06bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1259,6 +1259,12 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" + [[package]] name = "inotify" version = "0.11.0" @@ -3127,6 +3133,7 @@ dependencies = [ "comemo", "ecow", "image", + "infer", "krilla", "krilla-svg", "serde", diff --git a/Cargo.toml b/Cargo.toml index 12870b809..bc563b980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } +infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] } krilla-svg = "0.1.0" diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index f6f08b5bc..5745d0530 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -23,6 +23,7 @@ bytemuck = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } image = { workspace = true } +infer = { workspace = true } krilla = { workspace = true } krilla-svg = { workspace = true } serde = { workspace = true } diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs index 6ed65a2b6..f0cd9060a 100644 --- a/crates/typst-pdf/src/embed.rs +++ b/crates/typst-pdf/src/embed.rs @@ -34,6 +34,8 @@ pub(crate) fn embed_files( }, }; let data: Arc + Send + Sync> = Arc::new(embed.data.clone()); + // TODO: update when new krilla version lands (https://github.com/LaurenzV/krilla/pull/203) + let compress = should_compress(&embed.data).unwrap_or(true); let file = EmbeddedFile { path, @@ -41,7 +43,7 @@ pub(crate) fn embed_files( description, association_kind, data: data.into(), - compress: true, + compress, location: Some(span.into_raw().get()), }; @@ -52,3 +54,69 @@ pub(crate) fn embed_files( Ok(()) } + +fn should_compress(data: &[u8]) -> Option { + let ty = infer::get(data)?; + match ty.matcher_type() { + infer::MatcherType::App => None, + infer::MatcherType::Archive => match ty.mime_type() { + #[rustfmt::skip] + "application/zip" + | "application/vnd.rar" + | "application/gzip" + | "application/x-bzip2" + | "application/vnd.bzip3" + | "application/x-7z-compressed" + | "application/x-xz" + | "application/vnd.ms-cab-compressed" + | "application/vnd.debian.binary-package" + | "application/x-compress" + | "application/x-lzip" + | "application/x-rpm" + | "application/zstd" + | "application/x-lz4" + | "application/x-ole-storage" => Some(false), + _ => None, + }, + infer::MatcherType::Audio => match ty.mime_type() { + #[rustfmt::skip] + "audio/mpeg" + | "audio/m4a" + | "audio/opus" + | "audio/ogg" + | "audio/x-flac" + | "audio/amr" + | "audio/aac" + | "audio/x-ape" => Some(false), + _ => None, + }, + infer::MatcherType::Book => None, + infer::MatcherType::Doc => None, + infer::MatcherType::Font => None, + infer::MatcherType::Image => match ty.mime_type() { + #[rustfmt::skip] + "image/jpeg" + | "image/jp2" + | "image/png" + | "image/webp" + | "image/vnd.ms-photo" + | "image/heif" + | "image/avif" + | "image/jxl" + | "image/vnd.djvu" => None, + _ => None, + }, + infer::MatcherType::Text => None, + infer::MatcherType::Video => match ty.mime_type() { + #[rustfmt::skip] + "video/mp4" + | "video/x-m4v" + | "video/x-matroska" + | "video/webm" + | "video/quicktime" + | "video/x-flv" => Some(false), + _ => None, + }, + infer::MatcherType::Custom => None, + } +} From 22a117a091f2d5936533d361098e7483f2997568 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Mon, 12 May 2025 11:16:38 +0200 Subject: [PATCH 033/162] Prohibit some line break opportunities between LTR-ISOLATE and OBJECT-REPLACEMENT-CHARACTER (#6251) Co-authored-by: Max Co-authored-by: Laurenz --- crates/typst-layout/src/inline/linebreak.rs | 25 ++++++++++++++++-- .../ref/issue-5489-matrix-stray-linebreak.png | Bin 0 -> 644 bytes tests/suite/layout/inline/linebreak.typ | 8 ++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-5489-matrix-stray-linebreak.png diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 31512604f..ada048c7d 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -690,13 +690,34 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { let breakpoint = if point == text.len() { Breakpoint::Mandatory } else { + const OBJ_REPLACE: char = '\u{FFFC}'; match lb.get(c) { - // Fix for: https://github.com/unicode-org/icu4x/issues/4146 - LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue, LineBreak::MandatoryBreak | LineBreak::CarriageReturn | LineBreak::LineFeed | LineBreak::NextLine => Breakpoint::Mandatory, + + // https://github.com/typst/typst/issues/5489 + // + // OBJECT-REPLACEMENT-CHARACTERs provide Contingent Break + // opportunities before and after by default. This behaviour + // is however tailorable, see: + // https://www.unicode.org/reports/tr14/#CB + // https://www.unicode.org/reports/tr14/#TailorableBreakingRules + // https://www.unicode.org/reports/tr14/#LB20 + // + // Don't provide a line breaking opportunity between a LTR- + // ISOLATE (or any other Combining Mark) and an OBJECT- + // REPLACEMENT-CHARACTER representing an inline item, if the + // LTR-ISOLATE could end up as the only character on the + // previous line. + LineBreak::CombiningMark + if text[point..].starts_with(OBJ_REPLACE) + && last + c.len_utf8() == point => + { + continue; + } + _ => Breakpoint::Normal, } }; diff --git a/tests/ref/issue-5489-matrix-stray-linebreak.png b/tests/ref/issue-5489-matrix-stray-linebreak.png new file mode 100644 index 0000000000000000000000000000000000000000..2d278bd5c9cadb4b26a5f26dd0565f6a4bfafedf GIT binary patch literal 644 zcmV-~0(FAT!-&LSWKIn|;D1$L9BQHvo)N3y(o! zS^G3^ywWxJ5Y)`U>Po{Kj5rF?Ij!(01~NLFa2Ngby2A*Mfxxfs9@;=E=}=+VWfyTy zDLf8Rpj);=g+Xcye&U=~c=84WJ<1lSysCzuK@Zu@B(?CoAF_@h2uUI05ri6KJ>i?Z zaPIuG{t1Zu^VGu81pscAW#cuE;v-~jkKzL$T8L5$;~^CL!D~H2uJmddiVWMpND28$Q z9Fi2@V5|9T^`HX94_n7lygY3NE&yP;c+c?b=?&7-`o^8CFR7l>TLXtnp#%lTCG97n eg|)C29=6|_=gtD%*Z=JR0000 Date: Mon, 12 May 2025 20:12:35 +0200 Subject: [PATCH 034/162] Expand text link boxes vertically by half the leading spacing (#6252) --- crates/typst-layout/src/inline/shaping.rs | 4 +- crates/typst-layout/src/modifiers.rs | 48 ++++++++++++++---- tests/ref/bibliography-basic.png | Bin 7552 -> 7676 bytes tests/ref/bibliography-before-content.png | Bin 17122 -> 17010 bytes tests/ref/bibliography-grid-par.png | Bin 8757 -> 8821 bytes tests/ref/bibliography-indent-par.png | Bin 9096 -> 9120 bytes tests/ref/bibliography-math.png | Bin 4610 -> 4567 bytes tests/ref/bibliography-multiple-files.png | Bin 16310 -> 16308 bytes tests/ref/bibliography-ordering.png | Bin 11795 -> 11741 bytes tests/ref/block-consistent-width.png | Bin 920 -> 947 bytes tests/ref/cite-footnote.png | Bin 13532 -> 13383 bytes tests/ref/cite-form.png | Bin 10863 -> 10698 bytes tests/ref/cite-group.png | Bin 4745 -> 4806 bytes tests/ref/cite-grouping-and-ordering.png | Bin 841 -> 869 bytes tests/ref/figure-basic.png | Bin 7911 -> 7850 bytes tests/ref/footnote-basic.png | Bin 395 -> 417 bytes tests/ref/footnote-block-at-end.png | Bin 617 -> 643 bytes tests/ref/footnote-block-fr.png | Bin 833 -> 867 bytes .../ref/footnote-break-across-pages-block.png | Bin 1263 -> 1280 bytes .../ref/footnote-break-across-pages-float.png | Bin 1428 -> 1459 bytes .../footnote-break-across-pages-nested.png | Bin 1315 -> 1342 bytes tests/ref/footnote-break-across-pages.png | Bin 5473 -> 5489 bytes tests/ref/footnote-duplicate.png | Bin 7510 -> 7555 bytes tests/ref/footnote-entry.png | Bin 1793 -> 1831 bytes tests/ref/footnote-float-priority.png | Bin 1433 -> 1450 bytes tests/ref/footnote-in-caption.png | Bin 6154 -> 6044 bytes tests/ref/footnote-in-columns.png | Bin 1248 -> 1283 bytes tests/ref/footnote-in-list.png | Bin 2507 -> 2541 bytes tests/ref/footnote-in-place.png | Bin 1110 -> 1132 bytes tests/ref/footnote-in-table.png | Bin 12727 -> 12817 bytes tests/ref/footnote-invariant.png | Bin 1080 -> 1099 bytes tests/ref/footnote-multiple-in-one-line.png | Bin 699 -> 739 bytes .../footnote-nested-break-across-pages.png | Bin 1324 -> 1369 bytes tests/ref/footnote-nested.png | Bin 2539 -> 2579 bytes tests/ref/footnote-ref-call.png | Bin 515 -> 547 bytes tests/ref/footnote-ref-forward.png | Bin 1202 -> 1227 bytes tests/ref/footnote-ref-in-footnote.png | Bin 2524 -> 2580 bytes tests/ref/footnote-ref-multiple.png | Bin 4407 -> 4425 bytes tests/ref/footnote-ref.png | Bin 1466 -> 1497 bytes tests/ref/footnote-space-collapsing.png | Bin 749 -> 772 bytes tests/ref/footnote-styling.png | Bin 828 -> 850 bytes tests/ref/issue-1433-footnote-in-list.png | Bin 524 -> 558 bytes tests/ref/issue-1597-cite-footnote.png | Bin 508 -> 531 bytes tests/ref/issue-2531-cite-show-set.png | Bin 981 -> 989 bytes tests/ref/issue-3481-cite-location.png | Bin 500 -> 508 bytes tests/ref/issue-3699-cite-twice-et-al.png | Bin 2297 -> 1857 bytes .../ref/issue-4454-footnote-ref-numbering.png | Bin 802 -> 841 bytes ...ue-4618-bibliography-set-heading-level.png | Bin 5129 -> 5175 bytes ...ue-5256-multiple-footnotes-in-footnote.png | Bin 796 -> 820 bytes ...354-footnote-empty-frame-infinite-loop.png | Bin 2342 -> 1105 bytes ...ssue-5435-footnote-migration-in-floats.png | Bin 448 -> 475 bytes ...ssue-5496-footnote-in-float-never-fits.png | Bin 399 -> 409 bytes ...ssue-5496-footnote-never-fits-multiple.png | Bin 1211 -> 1230 bytes tests/ref/issue-5496-footnote-never-fits.png | Bin 399 -> 409 bytes ...sue-5496-footnote-separator-never-fits.png | Bin 226 -> 242 bytes ...03-cite-group-interrupted-by-par-align.png | Bin 1487 -> 1216 bytes tests/ref/issue-5503-cite-in-align.png | Bin 393 -> 396 bytes tests/ref/issue-622-hide-meta-cite.png | Bin 2470 -> 2429 bytes tests/ref/issue-758-link-repeat.png | Bin 1836 -> 1848 bytes tests/ref/issue-785-cite-locate.png | Bin 9441 -> 9284 bytes tests/ref/issue-footnotes-skip-first-page.png | Bin 524 -> 567 bytes tests/ref/linebreak-cite-punctuation.png | Bin 10391 -> 10455 bytes tests/ref/linebreak-link-end.png | Bin 2081 -> 2051 bytes tests/ref/linebreak-link-justify.png | Bin 12210 -> 11661 bytes tests/ref/linebreak-link.png | Bin 6423 -> 3994 bytes tests/ref/link-basic.png | Bin 6240 -> 5991 bytes tests/ref/link-bracket-balanced.png | Bin 3948 -> 2506 bytes tests/ref/link-bracket-unbalanced-closing.png | Bin 2183 -> 1648 bytes tests/ref/link-show.png | Bin 2599 -> 2563 bytes tests/ref/link-to-label.png | Bin 962 -> 1013 bytes tests/ref/link-to-page.png | Bin 981 -> 892 bytes tests/ref/link-trailing-period.png | Bin 2989 -> 2958 bytes tests/ref/link-transformed.png | Bin 1247 -> 1239 bytes tests/ref/math-equation-numbering.png | Bin 4699 -> 4615 bytes tests/ref/measure-citation-deeply-nested.png | Bin 711 -> 711 bytes tests/ref/measure-citation-in-flow.png | Bin 729 -> 726 bytes tests/ref/par-semantic-align.png | Bin 3082 -> 3104 bytes tests/ref/quote-cite-format-author-date.png | Bin 2131 -> 2119 bytes .../quote-cite-format-label-or-numeric.png | Bin 2170 -> 2144 bytes tests/ref/quote-cite-format-note.png | Bin 2889 -> 2800 bytes tests/ref/quote-inline.png | Bin 1437 -> 1476 bytes tests/ref/ref-basic.png | Bin 4001 -> 4006 bytes tests/ref/ref-form-page-unambiguous.png | Bin 2859 -> 2929 bytes tests/ref/ref-form-page.png | Bin 3592 -> 3561 bytes tests/ref/ref-supplements.png | Bin 8266 -> 8167 bytes tests/ref/show-text-citation-smartquote.png | Bin 403 -> 434 bytes tests/ref/show-text-citation.png | Bin 524 -> 496 bytes tests/ref/show-text-in-citation.png | Bin 795 -> 811 bytes tests/ref/table-header-citation.png | Bin 626 -> 632 bytes 89 files changed, 40 insertions(+), 12 deletions(-) diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 8236d1e36..ca723c0a5 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -20,7 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use super::{decorate, Item, Range, SpanMapper}; -use crate::modifiers::{FrameModifiers, FrameModify}; +use crate::modifiers::FrameModifyText; /// The result of shaping text. /// @@ -327,7 +327,7 @@ impl<'a> ShapedText<'a> { offset += width; } - frame.modify(&FrameModifiers::get_in(self.styles)); + frame.modify_text(self.styles); frame } diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs index ac5f40b04..b0371d63e 100644 --- a/crates/typst-layout/src/modifiers.rs +++ b/crates/typst-layout/src/modifiers.rs @@ -1,6 +1,6 @@ use typst_library::foundations::StyleChain; -use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; -use typst_library::model::{Destination, LinkElem}; +use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides}; +use typst_library::model::{Destination, LinkElem, ParElem}; /// Frame-level modifications resulting from styles that do not impose any /// layout structure. @@ -52,14 +52,7 @@ pub trait FrameModify { impl FrameModify for Frame { fn modify(&mut self, modifiers: &FrameModifiers) { - if let Some(dest) = &modifiers.dest { - let size = self.size(); - self.push(Point::zero(), FrameItem::Link(dest.clone(), size)); - } - - if modifiers.hidden { - self.hide(); - } + modify_frame(self, modifiers, None); } } @@ -82,6 +75,41 @@ where } } +pub trait FrameModifyText { + /// Resolve and apply [`FrameModifiers`] for this text frame. + fn modify_text(&mut self, styles: StyleChain); +} + +impl FrameModifyText for Frame { + fn modify_text(&mut self, styles: StyleChain) { + let modifiers = FrameModifiers::get_in(styles); + let expand_y = 0.5 * ParElem::leading_in(styles); + let outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y); + modify_frame(self, &modifiers, Some(outset)); + } +} + +fn modify_frame( + frame: &mut Frame, + modifiers: &FrameModifiers, + link_box_outset: Option>, +) { + if let Some(dest) = &modifiers.dest { + let mut pos = Point::zero(); + let mut size = frame.size(); + if let Some(outset) = link_box_outset { + pos.y -= outset.top; + pos.x -= outset.left; + size += outset.sum_by_axis(); + } + frame.push(pos, FrameItem::Link(dest.clone(), size)); + } + + if modifiers.hidden { + frame.hide(); + } +} + /// Performs layout and modification in one step. /// /// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, diff --git a/tests/ref/bibliography-basic.png b/tests/ref/bibliography-basic.png index 0844eaf81ab407044d1d0b527749d44807d691c4..86d02cc697884337c0bc077f6532359da5451858 100644 GIT binary patch literal 7676 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 literal 17122 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 diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png index 02278124b81abe597746918206f3a211949a7e5c..031e289743ec3bbc86a185baf1725649157f93b2 100644 GIT binary patch delta 8914 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 P_GlU^I?A<*Ht_!e-Ez^E delta 8938 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 x+r1P|($I1Wz diff --git a/tests/ref/bibliography-math.png b/tests/ref/bibliography-math.png index 6b60fef4a2cb9f91c71783df66d09c1999e90ca8..6d6d88d53a0d4ba6dfb5b44789df9eba98dae224 100644 GIT binary patch literal 4567 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 diff --git a/tests/ref/bibliography-multiple-files.png b/tests/ref/bibliography-multiple-files.png index 3be3763f49a222b39e2dfef80b8704ea1fdf3bf3..b2e7b0144ae5cd81de570f3c5fcc29bad80c85cd 100644 GIT binary patch literal 16308 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 iss4kUf0Xfw4uOFlFTOCQ@ozsqn6$V8utwA<SZn diff --git a/tests/ref/bibliography-ordering.png b/tests/ref/bibliography-ordering.png index c19b7e7d00d078cad06db5590149caae7593fe89..ad5e86019490b2e659da36cc79dc572832017499 100644 GIT binary patch literal 11741 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 literal 11795 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 BNqPVP diff --git a/tests/ref/block-consistent-width.png b/tests/ref/block-consistent-width.png index 045603cb800ef6260f520b7bc9ee44093f371877..f181bd335b3231559ef800313c59e6eee46aa452 100644 GIT binary patch delta 866 zcmV-o1D*Vs2eSu|B!AaXOjJex|Nnr1fPa5~_xJhT-{+N=pO=}T?(g&a`~3O&`sU~D zb$5Tj!pgqD$bW%}qou8>tFsmuAlKR9*xKSiLP|eDNl{TzQc_Z3VPQ&2N^x;<%gf6* zH#c*0b8l~NZEbCLcXxq-fq{dIgM)*7e0-mupNor&tgNh~qmyF+6D`}@+s)0*x3{ge}|r+ zsGp&!p`)u}WNe9xlg`lCGBi9oIyw^*6BHB_>+9?I`1n>hJ8(aeSUs^xw*Oh{r$7Ev-$b?m45@$>WZ&(F`()6?|y^rE7o zczAfezP_@uvV??$_4W1e@bK8!*xK6KU0q#NR8*dxo`QmclOF*af0dP$o12@Gl9I;8 z#+jL!prD|Rj*hmrwhs>v00004GM&u;00EatL_t(|+U?k7QyXCvhT*61mI`fYON+Y` zJh($}4esu4!Ly46m*5uovA{ye=bhPonSpcvfotY`ICJKJq9{s%KFMlKIk&=2MkAxj z!FG_*@DRc853Z~zf8Q{cf5ERTEUJ*qf4Rj&g;Z-19 zgHY~p$r=#mi?BV;aHW2I!{qO`85m@u1wydAEO&h}>K%k)^~-92PBx07-mLpKiY8A{ z6n%XZzpKC6+cNMx=)W*q_U{&+SYEQ2tnklYPLkT%(yR)ce~=gnPcLsX08CCT_H0^V zhk!@*w=-k)O{&010^qugr)H+Hz4Z!zW~}hfU&b0$f|CV7a4{lRS73OC&o3?wFEIP> ztR9>qilUcM$jQr}mEipYTozm5e=5SM00xa&*$eY?61=yIs}c^dISqjO;ZgG9K!0r= zc1}-r4A&P{RJglKHCUIP0Z)$u^RYf&TT@-7KZE7=M*9M{cXUcH*9GEG#GY&53}-qz sNp73t53ca|cU#}+>qB23ilRP758}-I)ERzEVgLXD07*qoM6N<$g8n$ea{vGU delta 855 zcmV-d1E~D72bc$tBo50^OjJex|Nnr1fPa5~lMn$Le{*wlH#axS%gb?baY{-`Qc_Y; zQBglZNkBqM*xKUP+2Ix#AgQafrKP1JA|fFnAtxs%A0HnmC@2;d7Hn*6t*x!Vz`$Bs zT0cKOyu7@qsHnfczfMk0k&%&=m6e;Do05`}#>U2(nVF!VppK4?wzjqp4-YyzIx;jo z&d}JBf0LJJX>F{nvF`8lmzklJm!I9==ZcJ!qNJ?%_xbz#{H?CCl$Du_i;JJ1pL~3L zf`Wpco}N@xR9#(N+S=OK*x2y!@b&fegoK3M-QD2e;Ej!qw6wH|iHXO@$MW*>{QUg2 zwY9suyV23nnwpxJn3$lUrJ6`Z*OmHZEbgVcY%R{frE>UkC%vuh+bY^va+(izP@;Pc%q`B^z`)8 z)6>t-&-3&1@$vEH<>lk!$l73e6B82v002XOBKMyF00D?eL_t(|+U?loP6I&{hT#t>?(R-; zDZVhcLvd*F;!@mww|H@v%O$12|4q6>Am_P(H@JKKfr zA;ZHXh5ujJ(P6noyRI@Om0n8c z40m=RedjwIpkO!(z>_I1{^a!RGhA1Xj9m_4T{Hln?w*L_=NDHuH1zd0m^0J9!+H6N zh1D^!@bd1`=~cr97iT9`KQ!4{2B3UsP!Ng=3k0F6vV!XXnI3>u^#!;lhNmVcV4-hq hqghQ?A3_LG)(0tUE3<&}loS8}002ovPDHLkV1h_cz~}$~ diff --git a/tests/ref/cite-footnote.png b/tests/ref/cite-footnote.png index dd2cf8bdb7bcc974a7f0a860e7f745310ff5dd28..e89b1dc123bfb2612189dfce81b1e760c7e5e9a8 100644 GIT binary patch literal 13383 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 uX?$D1Q4?88(r`~F80L`qCvv{u+4=zjq2k!T|T literal 10863 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 diff --git a/tests/ref/cite-group.png b/tests/ref/cite-group.png index 02772f499029d5e93c65e7ac3d62aaf4ca2023ad..d512d07e02f04b19962bb82b11b9b4c8f74244b4 100644 GIT binary patch literal 4806 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 literal 4745 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 delta 818 zcmV-21I_&92FV7HB!8+`}_R*`ug|x`SER z@9**M@AK~N@a^vM>FVz4>gwp}?dj?1=;-L?=k4L);ojfn-QVZj-saof;@aHg+}zyR z+vD5Y+t}LT+S=OK*xT0E+t=6E)z;e7)YQ_`+0W70&(F`y&VSa<&d$rt)y>V#$;;Eo z%F@Tk%*4jd#l^+K#LUCP!@$DIzro4Azr(z~#<#h_xw*NvxWKiyzqhxyw6(jjv%9mi zv#+nOs;soDtgxx8v#6@Fr>Cc-rmCNzsi2^sou8+jo}`?frk$OgoSdAQo1>eXo0plP znVFfEn4p-Ln17X*pO%-KmzS56mY$ZDmXwv5m6er|lbeu|nvjr?kB^Uyj*^Rwm5Pj% zi;IhiiHU}Ykc5VghK7cNg^hxPhl7KIfP#vDfr)l_fOmL&b$5SucYAbod2w@ladLKU zaCmBLbZcyIX=`(6X>Da^Z)0U`VPk7pT4GmOVO3XOR)1DjP*Pe?QCUt{=5Iy^)&G(0deIu;lpKJw%&00046NklcZ8jeRej(gNJKjrLtT@cL7Qzc3dMrJ^MFM{DVH^pC^*GiP z7Pd}uVe^aKD}?b`hiJZ$E*#fP#ULKz_i*M~6@M4Tt*3Fegbo;_567EKCL@EE&9HFa zzVEFCU}#47T9`v8k|YiKvob!Mof7)v+AFpo*4>L2>B6etix4djl7yMO-MxGj zP^yG&LR9prdi5);Ci4fmZO-J0?s7G7)SV|&Bys(yju9OWC?G=A9T*K<*af`GH28rc w04w4wk47F4mjif)jlT}a@REFdWwL+i6P{~FK%j2VrvLx|07*qoM6N<$f&*K|2mk;8 diff --git a/tests/ref/figure-basic.png b/tests/ref/figure-basic.png index 69388755f537155b7bbeef92c17046453f3eeb79..eed77cc3ab5f12182e8d569721b74665e9d667bb 100644 GIT binary patch literal 7850 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 O0000C(~w diff --git a/tests/ref/footnote-block-at-end.png b/tests/ref/footnote-block-at-end.png index 09880aaba44519f4a2cfff67b3e29ad71e9a7efe..15d0bb59325836de2e82028f619f8fa857fb2948 100644 GIT binary patch delta 598 zcmV-c0;&D!1cL>TBqJ|SOjJex|No1Ni}UmI{{H^!>+Adb`&Cs{>gwu3LPEH>xRF5^ ze@IA3G&D5s?(W;$+j)6;Zf{pEWMpK}(9pcRyorg4q@<*^wY6wyXvW6IX=!Pb zlaulB@jgC2Ha0fF!NJwl)s~i)+S=NFe|~;0E-v`^_~qs0b#--ZZEeWN$kWx`Yi)IU ze1y-@*@cIXtgg0%hK`SsnYFjSuCTa;g@vi9sn^%nCMG7>+T!Tw=$Dt5>FMdt(Aa8h zbnNWx`uh4$Pfu1>R{Z?@tgNir+vD@|_WS$%_xJhc=k4zA^WER)0001Wxc3SGe*ggw zNkl41m9i+Qkq%J_+-Q8WNx74uN<^HdMkZ?>wLgq)l=V;E}WOo0V zWI#mJ;JSklv9P%X5s~d3Nbs%i?H)ovWMlu}2#VU#Sd4WKwSs;ay!q#Lab*e}6T@4A+M1y1Xfgi0V>72zQ={h=_=Ys4DFhp~e(9 zTu@ePh;YT8k8dA?9p!a=aw2Rle3wL^ydDiXwz=^A1OC!yr^-ryTA@xG<8$-IzVYw( z+G=8??V}WS2C=wgDjdLUES>;L;l2T^EE@{HzrM^&PmRTf`)#YIH~ReKC^tGz>IX^z kz<-m^aW0?d@lDi7iZe`E7A}veg8%>k07*qoM6N<$f&fuddjJ3c delta 592 zcmV-W0+9<3>bSVLtgNhHU|{w2_4fAm zo}Qj?aBz){jeLB3l$4a|>FJl3mw9=4G&D5s?(W;$+tk$5ySux$x3`9dhPt}Ce}8|N zn3$!drAkUlZf-va+(DpP#C#s)vV%zP`S-wSTpwq@;Cqb>-#dlarHa zX=(BC@jgC2XlQ80#>Umv)xp8RHa0evmX_Mu+J1h1E-o(k`1oyYZOF*T-rwVJadm2J zblKbE>g(^=+Tv?%b<@?|yu7@LiHT%nWYEyi!o$aokCUvfwuFX`kCB;re1xvBxS*h* zg@uKwsj2Mj?0-*BPgYh|*Vor3CML_v%g@o-=;-K3NJ#qn`uzO-LPA1SRaN`@`|j`a z_xJhT-{$}T0C*v>(f|Me_(?=TRCwC$)WwbhQ4odUGjpNo9%Ni#7K;i%=0=UV(VL-OHWWnH*0**b&x-(iyE%w! zj|FhB|IFJaBC2DDN2gG6a(o6A_WS}Wex6cLVXb9YhVT3QDMUon+zLv#c_JbrA|j%y zv{r-~Gh8?c^6zegnerST>W@H5{yv3lc^(LRu3Y%x5g(!X1>=M|oN$|qiODIMu(f!9 zo1IGxwn=oB!rmyZZ=}Lm%p~IppcL*M!PTWu`1k8`dVFj++1KSd-953_mmDOSe;5M* eX`VU})srGA4NMarQ{y-Q0000E-w4~`=FqpZEbBoZh$meX>!HpOoN$hy4f2qH+0GBAUHvI&+$!$oZQ^pwY9aj zwzk5;!g0gn2oDi;$?K@9vZSW2sjIWHw7ig#nr5xhK4^eoqkqL!k+5Z}&SaG4`1tsimX>~ge%jjF zHa0dsK0YTWCx74H-`?KdiHV6yN=j5zROaU9=I8C*-{gwu~laulB@xj5t)z#I; z#>Qx9XpN1H>+9=$e0-FYl=b!XaBy&LaCpYZ&~LorHh)@gG*@ddQeYrAKwF-;jgObP zyTi%L)2^_%@9*#W`ugnb?CtIC&(F`Nr>Cf>sMpum{9dxf0004WNkl#+=>CfWu+xT5QK$Be_=0;@$$BwGF^5=YmQ#WnUl$+qaDwwDnBw9^CVw` WMRaF6&GO{{0000+9={jg9f~@n~ph z_xJg~z{LCe{Pp$s5F8-u?C?H5KI!S{J3Bj2ilOG_=2TQv-{0Tf-rmd0%V}w8#>U2p ziHS-|N}8LWX06e5b$Qj++Hbt#Hd=2qS8G0KfMBD=Wo2c}&410Co13kzt!%p4o1CDS znVp=TriqJ_Vq|Q;!O32r!AN_RRgte~u+((P>Se3WRgthobdF4coz>OV!NI{cHa3=) zmU6}BOoN#qI6+&UxEn7wKW>0DT4{WId~k4Zl$4b9_4Rko@LQd?X|~j0rNL{s*8>Iz zc+c@5H$ZN^-G5`N$bNo)+S=OS;Ns5C(5|q!jgOZwQeaJnoZQ^pb;;|swY8|KvZSW2 zvb4OZtFx}Iu7!n#ii(PFZ*K?>5ul);ZEbCF!{frj!l$RFwzjsAkdR17NYBsD`1tty z`}-~~F3`}>CnqO(czEyc??XdFjEszOa&oGws_g9S?SJj<_V)HtQc{|ln(FH6larI5 zpP$*;*~rMq=;-MB`uf<|*l}@j{{H@`sHoT1*E!v{jsO4wRY^oaRCwC$)>Tu2K^TVN zXFvQT#P05H?C$RF?!dwZq*H&iGaPx~aKRl;>~PTMAgkT7^?QCtb zqGGv4*Tg~uzhTjAeu2p)a$ARvZ#X_>V36gPmbG^_^FDFwP_-PuJIsdr1%#L8W~N5^ z1cVo7rzaVU3g8-MKTw#$6s9nRDa<`gUur@KiGNgTyY<}&{yY{NJZa$1aRVR~{GA>_ zjFV%C;WU^4mmmB+KHD8m8aE*%l6(e*(D@3AF6J+ZU0d~-%-~PjSbur7iU{xT!RHkb zK0FW?(Xg=D1Rs-A|l({+tbzEj*ysj zcYk_(gw)pFK|w*Msj=$o?;s!`nw+G`%hOj`VYIZg$H&Kui;JM5s_X3VzQD-0xxvK7 z&aSYyaBy&Ze0<8x)KXMjqou8ZgNwt(%|1Rp)YjcLHa15`M^#l-Z*OnV(9p@r$%%=H zYHDiH(a|+EHGj3YznPn(l$M^ewY`>>mfGCp?d|R5<>kuD(Pd?2Y;0^?US`qL+uYvf zhKP_~VQHeItk>D$c6flx%+jo`wu6O@!oE=EB0l%gf7YX=zAENE#X%S65e}qN2LGy1>A|wY9Z}hlhiMgL89pnwpx<&d$`- z)QpUbprD{lO-))_TAiJpc6N3qCMM_S=VoSRfq{W*Yip#Wq+wxUySuwMI5?Y|o0XN7 z`T6;)tADG2fPl!z$lcxD;Nak%o}Tpd^uE5nZEbCtnVF7`j-Q{Ot*x!=>+7bbrjwJC z+S=Oc>gxFT`0VWLoSdBFt$*kArEhuRWbR4w|DAomXLje#%svc+ z5F*IEbb;de3nwThq^tzPP(jh^5z2t`3wK!<;$*ng8|b#IOrxO|CFCsUVm@XG%Y%gUuJSoSR;gb;jx{6gS( z{Xz(f)HK@vA}P3h0V%j*A=1+*!PUt4%6|x+KM(lEnOwx+=_#Dx)VzF3Fm2_M9W$^@ z**|)~P#5Ru1BEd;78G;hIKeRm7Bd)JnVGRtu@Kka4+_V#W)B$LPaEB!Xt{M89G!h& zc=WKV9~2!@Y#~H&7)K&+UI-zC5JCtcgb*UcjrxVa@%bemCN#u@!?y+;O>05%wv&Ft|Qud!%4WjgO2Bx7$Cb&qRBoqdaaXmU!xH!{&9q zG#3=pPM*M~n)RtrZEp2;VB-eTgEm3H_ml{65Ra@7$F4ve)gG@W)XsEJ*FM@Ak W7Sbbe55#}~0000gwwC_4V}h^!)t% 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 diff --git a/tests/ref/footnote-break-across-pages-nested.png b/tests/ref/footnote-break-across-pages-nested.png index f87658ce8697ddd422869029b0f687191bc62108..79b09cfb9ff5de59e4081de721311ebe15255b0a 100644 GIT binary patch delta 1315 zcmV+;1>E|h3cd=EEPvhK=kD+Gu&}Vt&(HVw`RnWJC@3iX{r#n-rSkP@z`nk|ySuxrtgP|z@r#R#&CSi$*49u^P>qd^aBy&wlas%{zqYov zX=!OVI5_6#?PzFd$;ruETU&g5e66jml$4bB_xGfxuCB1S`+xiVU0q!uARzPe_GV^g z>+JBZuC7K#M&{<`A|fK<;^MTlw49urx3{-TOG}cHlFZD^NJvP(!O6qJ!-|TEhlhtu zOibV3-`UyOgoK1XK0eIO*2Bfk%gxo()6>ez%F@)_czS}6kdTj$kA;Virl_!~tFx-A zs&jL5N=i!U>3`|5v%7qLhTGlcLPSi6h=|3<&+P5-9v&WFUtgG*nAX_f*xKS%S6{ii z!^q0gy1c~X<>}el-i(fxjgOa?nW4_m*xujcUS3|s#l<8fBrq^A#>U3g)z!hl!8SHF z+S=NFetxH^v2%5Oy1Kf1dwZRooz>Uhyu7@qsHoD?(tps<&^b9dE-o(k`1l$c8dq0W zmX?<7?d?1~JjBGrVPRo`fq~J{(QIsNSy@?tfPk5qnQw1z`T6-}Wo7Q}?oLild3kxB zo}Q+rrYR{YprD|utE;uOwcXv_udlDTxVSMfG1S!5adC0j*w{coKw4T_TwGk-+}wqQ zg`%ROgMWjAFE20l_V$d7jFpv@pP!%J-rnTovxmg1c(UlYr5wQ`AAiEc1D`uzcp2*X#R2#3+Mrxk zh5>=5&)hcA@cAaUn)@JO68akfMSSSY+E7fsC);kE1VtV$xB zlM7VKTwH?T>A^DLfXE;Z+Ob}z;~mqr20H5oAfazCb1G!K?}v!%rFKZTdhLc45~97l zCURor5}}Dn{*Vw7Jk|mc*7SDBSl z#gQBi5$zR~92gOi6F-IUlP4k~A|fIpA|fIpA|e{5A*bLv4yX9V&@kBM1Q{S@bu|Oijb_qeq}z zTw8}js)-PIxzj)?5E2!w@qoPUxs24eI&ou|F(owM0e`4#Hz+_5G5U9rT8SLWc0 z#EHk{&Xx)5G)*pi>a5WVARsyhqtYN?@CUy4Fswp()uxR`2q+B* z3FzVDqgxvv85Mi8ByT=cYkPsBTZVZ;JMRAGN#Q3#VMoR7J7(*rUZ`dVU>vt|-9+@y Z`vp*jBF^;uHhcg8002ovPDHLkV1imZ)cODb delta 1288 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 literal 5473 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 Y%`*Qoxv$Z{t5c$f8hYv#_n(IT7n4q*5dZ)H diff --git a/tests/ref/footnote-duplicate.png b/tests/ref/footnote-duplicate.png index b5a73f74b8dd39cce61113ea61ca3f22144a1ea0..e95228e48cea321b96de52c872c2d2eeaa010440 100644 GIT binary patch delta 7121 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 diff --git a/tests/ref/footnote-entry.png b/tests/ref/footnote-entry.png index dd09acb92e86b61c689976c95e8263beb4f778cc..9e0108f883cda4e11ab6a4448556f51eec265919 100644 GIT binary patch delta 1816 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 U@jJ2cy#N3J07*qoM6N<$f{#`dI{*Lx diff --git a/tests/ref/footnote-in-caption.png b/tests/ref/footnote-in-caption.png index 79b2b5d0f955479b46cdab66ffcfb4f936beb893..cd4f837bb49d21aa9708ec6661488829ef04070f 100644 GIT binary patch literal 6044 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 diff --git a/tests/ref/footnote-in-columns.png b/tests/ref/footnote-in-columns.png index 281ec8836b441f0e3ee4dd266dd49e70f773cfae..8b5f1201dde28b2c9bce100837d1d18084fef55e 100644 GIT binary patch delta 1263 zcmVMc6N6A`~36s_F7t6(9qDz%F4XFysoaUjg5^nGc(uM*X!%+R8&+$ zLqo~Q$?xy)kdTmqf`UXuL|0c=j*gDy<>h>QeEa+RY;0^(Q-4#ImX=^(U^h26r>Cce zhK6%l$4aRw7j38 zslvj-ot>S?$jHph%%rBSkCB;_mY#2KZ)9X-y}!rL(b>4V!d+cm)79P3(%NBTYqq$+ zsH(DxjFenlTz^49K^+|(8X6jhhlib?r+a&QrlzKbh>)qPv#zkXn3|%Cjg`j8(0Y7? zNl8h8fq`;#e8k4ib$5T7oTQ6@FIW@ct|baW695Mp9t=;-Lh#l^wF!O_vtPk&EOxw*OE;NV6^MuUTctgNiI zwzh6=ZiZ+=$ zd3kw~l7EsYC@A9M;?>pFq@<*roSgCT@%Hxi@bK^g0s^V2srvf*?Ck8QsHm^6ug}lV z{{H@mh=}+1_gq-t?*IS-kV!;ARCwC$*XL6cK^(^MXYW!W5UCLhprBZ23JMmmfCYQ+ zz4zWbsMtG7vCu(E;6eiN{yT?PPUhZtyZXDyuz#QT-+AWd?sjG&Ns>gzu7`nG9RmOm zySp1yy!rXseec@KS7LGJHg;_Tm1J~iHE#k{+E*2F-zkcJ^ z5Z$n=r1Yv*JNm@cO`V#HZaAr=)S=xQ9e&i%)&>%-T0gL)B+Xyn9?Y8;uD+S!bAW(t zv41Zs8%uq;2xK_2e5X&H$xM=`j3h}Cnb%>8#%S#pbUlnAyUx?dgNTaT?S>^3MTyGcw}H?|n5)^ec_Re8 z`=N)Xay3;IMy|8cAiFX_;Y<@ zYD))CBRJev_sP!JD2gIaF7p!H?gW^msw#vrMPVRf{cvmGu7{mGK|Y-4^w*qY$Bt7E zH@5&d%c_o24_`PB0Bq->c+m2;Tl8@i90uT-OFxVwIm-)EE%}}7%hJI)EKe@;RDZ@) zc{h^&7>N)Bnc)Zz<(rWH9a~HaED;E<%!Va~SHT+omIbiJoB~+kL4Tk8!=(Sk32VeO z!4jU_EwIG=!mY`$g2&O|hp1l>lJHl0x@sXxk6^>tp)$tVh~F0}8Y5P@8fL}sFNZL; zcONECGT9tFFxrS=dln%)5vIg%h-hf|uyqT8M3c>WL&N!TC2}?5!@=>rJdOD9gtQ)> zCQAR~ajMGIL?L~UqA`l;>~aw_Mh<((NzoX^O|ua+Mz+l94uZyL=8POCPa`iik|d24 ZzX2_&&P-=D;S&G=002ovPDHLkV1lHeu@L|O delta 1228 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 M07*qoM6N<$f_gK$u>b%7 delta 2468 zcmX|?dpy(o1IFER*4PmtBb7sPkL5BEn@i+Q3|Ue%BFU26zL-%-NXRwgH!MV%FipQK zEw_BV4CNY%?MN4c37;LrCG=8Sa=4Zn{zT_N>< zVNu-s`ufOZ^2*A}Nqjg@UFp%l-c-Y)J}D`wwJ4$S7zk(>#UFyWyDjqW1)tH?C8>NZV=|d0_X(6NkilTs+1XJj6om}6Q>TEasNv&=mP%x^SXh9R_VPWo z^FTY|R1BBBIM&=X!gcq(5Mfd#np)YeWNBLRK7Q+ahMVHK@yAiyC(eFWyqR?4HF&kz z^zdO4jkehc9y@jnLJSyt5ytWe> z^P)S`j6Jk*@48_Z%=X6fRZRnZJM(~nM7PdP#Q)B)2@1vEMqv|~WnzhY5 zn46#P-t<7b?LkqMKus2?>Gia@sHljCG%>m5b-9)vxxP*8@%Pi?Zmlm-ga8%?KRrLQ zU(++iJfn^|`J*v+?gT~->KJAcqT#BkB9o_jCeP*7DX!}Vc2Aj{@Z$Q`UI=7#Yz%!N z+n#*XxwIIts&8w9gSDPYM&XkWyDm|eaUmh)^(@yJSVOr#KU`9L6&ED>rh-Ijveu!U z0V^xBOpZ+pKT*btTtH<)JMLrHSWbg-vjnktAYaGNO(Pc}FE4*PJ-rd5h39RJ|2Si} z_4DVjxsZ3#V~-hTe0)52Rn~HWTiLWQi`@wzeC;TUH&-4?V2SSNBc?XxBb|K0gthB< zR9HFWe6;%=EZ|FzS<5HUu}e?R)+|H&W=M8flcS-*!M_m!Vxi+-VDr@p4Gj$;M9x`e zsafnkX=%=J)e!AtGLTIvm?r685qvFz83GERco{GYTK5XEoo51MEmZia>wqAC7awM^ zeCk?$>yaN&(R95$LvI~0i<4XvbZ%f70q@T^gS!wG8A>=(d|LAd7jnQMwnyFY+^ zIQ^tjARSdPz{b-i+HRtA4pW)4ADgat+1fJSX27TDv3p#my_B%^nUV^Fb8jWnW;@g6 z%#{s4b|MwPM4N<*AXmXa(c7|ZVvrn|JOBLp*Cm!R`kA&zGqv61cD?5Gd(rigW7|a!Df1KQ^g~@bOV- zGWF2dk9c-pY}ZKOa+n+aXM&fPk;q3>r1ZBj9ys?|)-ub4I%Iea;nx9#UAe;O^Y#=y zgB~Q@pO;m5Y#tvI7k5r4ADwd{kR8>Q^Wed!4Q>17wS^H=MM-3=1>hcys~@o)#)BU? z_$NbuPEKNR0ZI}${5oHCn z6%w$62aTg2z3neGt0-z*7_JjWFp^c}p$AV!XC+9bcP0k;CNF(>Mo7p${m1Tqx>b31 zw>?c>BiB9=0tJ-X8BeE{($t0r{-Fo|;49_nkg~(2dvV1`fnCcJ#RPY~-^_$5LB*8K z<8H&Jh0rhwi2-*p)0^M4bakg_H#FnJ49$;uUBPt*Q<6=Qt*hL(<>Tv{X4~Sx z5$&RUjgaJ3HK^m7$4auYT>SiYyxE^WUkb?!3Je^WEQs65aAzJNW!PN2bmK!|d$iNM!fYdz)c=^hpCf>(~6b`c*cC z!r^49cm%Slzv5 z4-Ytx(Yig(s{5v-VB4a!nGwoH6~J$_)1*Id8Hlh&(SrtX4u^v*(cuPHR#oL|`+i?B zc@t{54!idu=BxS}IpK-%aR*0ob#=81CQrxDetf&7vNDK~DuHIqvZJ;3@h4PHMuytl zoYwK6q@*M-?`>uqjmDP3Qu8o%NL<(9?9*%mUEN(=)~WupRyk(khk;*9{5R_PtmN~B zhXLygM8U^a46Q^P0STN4_$m^YdD*5kvU7XTUHmz*6n_gJVGHWjW_gO)j6t2(ofsI#raw@pn=o;6L4jnai3=uBkG zc8441fJt3NbLdA@?qPNBK3oMSIF_mTYKk|N@xjK%qU(kJetzb6s43 O|JhkPSv^JhC;tzWKCl%4 diff --git a/tests/ref/footnote-in-place.png b/tests/ref/footnote-in-place.png index b500ac80f47b0d90ad779dd813601a5f5ad2cf3b..2f9681e231e740189037a146baf5275a4c3fac8e 100644 GIT binary patch delta 1091 zcmV-J1ibs!2U38v$IG@NREzif*x1-lpQXlTK~!9qep)z#HsUtitb-8MEhK0ZElbaa`Sne6QB(b3Ty930Bb)HpaeJv}{P zVPXFM{$yli#l^*Ydwb^Q=IQC_!^6Yg-rlaRuD!jzfPjF0fQXQinyIU^jE)R;pJ*+9QJ zBd`Df0)Hb(L_t(|+U?e5SKCk=$MGK&sgpgT~Bne!|vTU~Shm7GeCm8B{mnmGltP2duZJENJ zmaSlD_pRGVOI_c(#n0n#c}g~h2RVc-eL(yD&ws{;v%jl&^B_S3G7rL{DAJdb5JH&A zozNf``pHr?$kHfdwWqE=l9yL6K#} zWZ}z~^bXtq#LLSZVF%L7p9){OItHEfC-BjgL|TPK1ykY9hYEByuxQPvR6K>DXk9~n zBY!GNQ-6nj82aX(To|Cl2|Rt&_E`6DQ{w=H(8wV6!nGL7JrG{MdhhGr1prt8tjq(H zdv_T;F_MUd{44DH;H*M@IbtD1!hxNWVRomwW;+03X?1h)emH)it_DI7i3oN8fUN*H zdW1>XUb{_2p0~MWN&LV;bp4F3&i8td{C`qW2>{X9FpeK%dXJ0zXg|bbKvsE2-yWvP zBxJ72o)*|qr-gY;QWWsoJx<{}9RL<`2w%Ga*!CD-R<7R!N>7|LKHTvV=)ZV@%i-$o zdDi!wLwIddAx2)6+Ti9*Fg)6m zb7#@}!`I5?9rp$*H*g6@f~czI5Vi`xM-$wx6OSk~p|;6JP>GvE5tb?g8D002ov JPDHLkV1i!{EQnwpv{EG#@c zJa2DrmX?;^-{05Q*U-?=+S=NKgM-P*$+@|?tgNhletvj(c)Gf}Y;0_CadFbp(sOfj zVq#)WPEO+D;+UA2@$vCXOG}=fp4iyfaBy&?rKM9-Q+#}Uhku8MwY9bT`}@Jc!9qep z)z#Jb`1q!#rZzSFMdi!^6kP(Za;cnw+GQm7Unw+ODv;qou8^tghMF z*}c8JK|w))fPa9Nmz{!xiLkP|sjIV$j+SwAdwY9(#l^*BWMu2?@JLEh+1umV+~ofL z{$XKZJv}|`?(*#I@myYJh>DUxLQ3G_mVN`awfW{r&xgg@voDt9N&Iq@<*Fc6Po`T6;$ zr>D=)&tqd_jg5`m+}z{i+7KK zxyk?l0)GujL_t(|+U?hMPg`LWfbj<@QcBCJSdk6c#*~e@ySux)Fn49baQBwBl=fcw zC6^HXSyD)v+6Q;k`n9ecv0=ky1Zh*k8Q{9X)yU znz&+bYFB&EkD@5V0bhFxZKqpGT`6d{BVv)}kc0>;r! zN-O~>T82r0ge5Ft2}@YQ5|*%pB`jeH|EJ;fi3FshowQg2(&VfcdU$3+V_DyF`v*df z5b=#LNqzd@J{s#Z^D>Z4TtoSJQ{je(DjMth_2E@GH%yVOy|t}{?w;_xLN7&&asWt& z-hUw6yHWpG^Kes37ZzthF?R|4;ENy2s?D3c2Xe1IvAAk8C>aLIk%Y4JT_sXIa;%}8YBlHuSk$lX* zWbEWIAOpjxCyqnr9^=dE(Ie1v`mFKc!-t^r)EVQ$ib8qwmYQ`*2)}C@qF`+%#u^(O ze47)%onw*-7aDtU?!0Mkb88r*sTVF%>e{{oqg}sziLwRW+N5l*GIz;}q=fA*Dkds1 nSTqtbS(Xl)Wz2*S;!nQ;ry>9lrafK*00000NkvXXu0mjfw?0#P diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png index e110eac6d4cbe9fb78c999cecc4b801cbe2e58ee..2c7e423095c7426aaf7cb81f145adf28f3ffb2de 100644 GIT binary patch literal 12817 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 literal 12727 zcma*OWm_G;_x6pudvPf4P@p&)XXEbf?(XhV++jnpqQ#4C9E!U`ad&sP`TqXb3%KV= zR@Nj(k|Qf;W+tDBR#K2cMIuCkfPg>+N{g#}rd|*bkdFvZpB7pV_y_`mJpd>!qUN=7 zy3lpP>r4!PnW)M^j~p0GDNMsp0i+LoR1l@0GzXdW^#=8jM@RP?B~4h=la5s71`~jt z;nw`SYQdTr7kS$LT?G%eKKG}45mJjMes7{TNj=~xKQj!@koQSA{mDs`5XmV7Le!9V z%fi6-te3Y5uE~RxqVk(3CcS@l+-Wk@gz?)v?<=esqb{w)XpsjuydeR?EE%%3i$f;t zO*+h2(Zwp({5Tik0o$A3I_KQkPbf<*3a6UL%U&FyljWnid^HD8YpXY55j>v=L+Q8NxJLM zG8!836VkiWKrG;GQ5pkA4-2J_A#LO#oHTI=u<6m>3a3ypZ57rnsz`(Vw@D{HRG~x# zDSX?FPh`T3W2(WDs$f#={VGS$)zg|Prq?Sb3WPxE5yJ|@D1t+V@DATxL{}6Zj}Vea zF2rCM9zI}0(d*!bDKa3ghpdc&d$mkw^Eb3uS`^A#4Y+0zcxv2-v5u=R3GbrFp1aoi z-F3g+aw7K$oPc(G-PZ@LZtyG}qsU2Evfa|uWKK8b>+4%g0XR4~m>e6EW}I1Gw&u=x zd44X3$Yo$<73Sy9$L-BP*vS=8oZS8&;_c(( z2)#Ac}5pAQZ zq@*Odtnt240(qR=^XkOJ#K;Izk5Of1B}P0lGIC#E-wLlm?StcJp&niRAXFO|Oxjjc zSO`fL;}8%a#LAk8U*ngviv%)-L5j{}ne420J1hi_j8x3`bZw=_3z5>j+Dx;i*G zfZ7c70s;fkGi;ea$Vf;rF)>pjmp%Ahjg4vw>;L|VPH&l+n>)|(#Tn}90WPv`*;BvW zH`UkI7ZpJX;VtCI_Vq|oVqTgPerQaEm5UJZ_OQ2Os*?_F&472vhMC& zEp31PSUuByZGwk~UtL}Go5rnmR40&wE+Eg zYpZZ{kCM=j7)GOVK89FAKPM;Wt9Q@s=H}-0HCFZR`8iqvDtWY*h`pWN4->wgkx)e` zH$OkQmaoo5k|FCyNA$g!Lpu~|A}8AqdEJ<(12G z#NNn}vj%%%!@z_5LCbK5_ztHEV5||#+UCQVLONWGs|m2Sj!rH%l#A|9nZMsTX}RFZ zLcTpcW$I9IbiC*)tNTK1$o1@yGoH>(peXQWvXP)Zj}*cN@%_lrJ_+ZFUYrQoc6tT= zwmGD?ieN8T?AV5=vE>HK$>2 zP+0e@NZrs*UAYMNFL5`&9B!Xt-RPNZLd42!$ffi1^NFzNyxOEVo7cC1jRWyZx5|0S z1ql3Jj41do*84CaoN`Ib1p2>Y>Y(&{;{bzSA2E$ZgYaVq-1x%zxe4Rn0)(^VAk+yY zQ~KP$iZM*tabwo!r#LWr!H$X`!i5IQR_zi5<<1D0&`@1{wQ0%@#NxMz5W%Z6a zxb$zA$L7FR@SY2zug|$Od7~dr>_V4VNtnr%kz_*pj0^~?{pijoH^_%63+^>h^5?uw z8Y?ut%cimK8kA99@O3chuH2@%hTYE0lI2%f%obPu#h!0oS;~75qaN+3-Koitk^7@@ z3>!RNX2(IBv+LWa+E&5rSpr2Ohcbc}AGfzRPLErWJOzvf1kgy#6SC_wIYKG3_SVXd z;Mj*!dF9`Q9Go9`hQXGcFH;SqbD5$S40Q=uY7DUYNYBDQJ@3;xmR5-vJx*GN(_lY( z^&{7lxgN&jahcX>G`YW-Q~G2@86dSWbuG2$q|^OmL))?Kit=5wM?%6L85f_b2(SG2 zSAtnbyd*ZHKJE|}KLi%jyj0mHW>c0vOuPr@4%cS7h;<4bT_CgcK7=jw!cPDr2<=cD0PhB%h2jHYLy}BE<{=ygDZsdW`Y1ure3(3I?)0tKa1M2%@UVaZKW?&qYiN1B za@`_^s8ifyDOp+MZ$Ik#43a}_^eZE1dXn=%si_lKy7c+P*!Ek}L=g!7Q4tdH6*%H^ zbV>ng+97~h5-VXVnU&qlR8j2+Y9n@1JdKcBK#6hokIu$B_Ie>EqLUyw%_{7fm=b`f zb$0!rh8z1_W4NVCiScZO7a3ApdB8!vMrE29y&cCq15Fp3ll$x!{QgS+2*#tin0ds1 zuVmjVzV0UPilrD6Ti+}`KR`!egWpR)iNU5;Xvoz}gz{H1}uIiXsl zfmd)VWH6=iyu0^rNVPZ#=*{11;0o(VY<@$^A}X^Pu93*s$44(2EB-X9oFnQ^H`oG^ zV=ziAfov=C7rPddRJbLQ)n3<9C}9!3h;AY`Qb(q_L7Bb1{oGt|aPY*0!l1UTEu%_$ z4y=IytughsgKjrM)$#Z&b9WtcY?pa28}mLLnY;v*W5>!p261bq2d(Sc{Jcb>x~UTU zMeft2<_iU&6$}|03wWX!&0RMb9UV1oEab=}P_26-xfUF!~HFBB{W@t`AB>yWf{T z2yc{o?h--kr-q~WgiIc>5q@eT{AJ0e6UM{8aH9vIm^YNb3xQ~q*3x)pyuRZ5NfEm& zPp(dt0#6L$qIk=ja0nD1G%c^sJ`fxY`3|wmK@NQ?bqse-Q2A{ z2)DlV+!nrIHx<_T_hG|33jK3`Ir`_Tm>@tX_)Q?dX8$g_WY09VfE87q49pKcn169? zAe&U2Pn-p~>Oq$8p^MXka(#p%#5gTjKy!>)tEya3&!p5rVH3k(G?e}f&556BC1_>(3;SdzMz)7p!8>hR)>W>VW zgM7XR;f=tFr+}BWp^Z+bJI{%Y*Z9E42imTO(3HSu&xQ^EpN~U<-p`8f`_UV34RvB_ zoW+AsT@Z)(bdbShP=CaN$1VfiM&O@gAnDKEkl6;&P*Hm}>xM{)ZjTDGvb43djI{3q z(lsBNQMA=vv}H0PV@(@WZ^D%||1xWzmQ*jy=XJ^=uH-W8Sw>jGlrA()=uXsIGy4X} z7NxQADVbRYRB$R2$k2<;YmDCVL(=PYwfmXg>>IRq8oUiF>{$?(raOAOaTWrXkA{cy zNf6YoFmV0vPnShL-h+{h6ur?j749Mpy(Hd=DFxNC#1VBBPt|6i8q%*SXR2U)$cj>?h#}g)o^0mu+zLml_5VFeRQOE zPOrgcMLpzf`lax3ytaVXXD$lsb$Q-GhtTI$?WpXH111nj1H}Ngz8^Up3Ybh_3StY| zM%*k3=YOyW2@J~mA$8n#p5q9nM+jjW{o(Ku$UrzF70j6a2c4IjomvGfJvB6AsgR*y zL6zS_T~^?jqcD~_%eVKa@-nf6@H7%1`#KZpb@#S16WOxyy!8@zci)vSy!i1F*m1p( z=n&}Bq%Q2l$iE+7G6+vrU9)bm{tsN&q0)YD{jq)UR^ZoMuf5#qQr*#NY_hWUtMUD2 zBvd*7TYm=LCK{~&QJWAOfX!oyi)|kY7Pfd8s_W2*a7riW4E+^akD~hHZqZN^XRjo~ zfD)VwQ@E`qbPU^QRK|4_r2>+!G1gHYQ!Fc$jUCP!N{jz+Vtsu)LkVu~&QQ{$&&Vg$ zm{*H1ikPh4xU!PMdnJzv!N4}t#aPmd0FD~Bn`_!yy^8{tJJ zUY-T@y8TY~0vs8AOU4?*a4?I2A;Xj;bA|jBWdh0G5#QNJA@tJwn+jC@M#wmm;mJR5 z(5dVw$UpMFP5Cjdnb3KuW@Div%v<1NCXz*5#>H&YyuO}o9jPTN8^bh!qgnIFNW?wh z6hhY`ACvLa*K|07FFMQV1?OcbQo-x=J9H8jHH0xp?Phvfd8r6enCr4aI{9|Rws!q% zA*2q|Qp(u)xX3XN8>L8q@E?TowxDE$@;#r6&WG;S1D95X_N<9rqi8y(e*Y-IFnf}; zU%Ht~HMrp0wS{A>aWz00mmPc5Z5O|$VD(RH)5lnBSH0VjSo|5mj$sbJknh#R`hkM5 zd+*XsQZa)nffN;RiIhfk?aVEDX&}>`cwP! zE8+xvW!ilYoUeU3_Z&H8tQ{=PRv(QK*M+0p6J zb8+Ukv-l3G^Q0{|c)zdQa4t>uO#hc;CV{imn3?3AzV*BGFC~vi%%({Mby^tt$s1DQ z5-A5!b^Q)i#n{Jw<%h>dn@Lnb;4tn}RRK~toLECz=b=X}CZ+K;f*3yZDr(pmo%NO0 zL`;C%x<2waNpM+BZPl;)!K9>NLUn;WK9!B<0|HJNOyijmBFahe3QAijsX7W;C5|+_ zFZo5H%3MDpafDLjk&C(oKqrq+8DFs}#}UDX03M!t(03g*9L&~KEGaGdBN>h_&C8|B zfye#5`{Sb$p=2z0Ir};RboO$>s+H#`Z`X?j6lJ+Ou*!DxQc-Ax<&)-bW94$5!vYd( z>>BnPVqhUXwrzqWag%U!F)Xn2K1ClNi=kl`*lpUQ&GuxqJAFXq%1QF32%vHly3Z1p z!5LZ*_zcYcel3$MTZRhE=ju?M1QVzDv6LvIR3`@$u8IjXfvmBMISO!Qe6v02O%~fv z!Q3-G;%49COyc*DzlIx!l&U#5F7J}^#e$owaAybNn*s<_v(-?Dc`10qmNAy|CtZh+ zO<1s(=+HX+oT+-r9 zFtvgg@pxC7uzoeVH9X#OV7cX=o?-; zsO313qT<_5RMaUE6Q%K0HN{w``I%o;Ip&+NB1*pLSeq{b9d+X?sGU-+frCraQVHXl z-xppbz2!nI0S}DsX=UG3^qPN=7^NpvjvF{!^SJO6@#6-|plB~pHP?rAU^?OFE}MIr zt+};F5`+jpthwUc8OfSygW+*WlIQMyJx9j(xDICGLuCs*H61T5>=uDog;c+P{!<$K z8KQ*!lrI%$QG$cb$>_=dax&Xi+urgr@1E65OzXlF>1_2f-8+^PCU<-Xy_D>@-l{HK zE=Jd-TAf(=%T4%DBJn(=HPC-ix4XUI!2nvnznw3>y(pr7(9t>|JvADy(F!e0y^6ql7AoKAv zG22s9Fam_2Ohzy7=kl|oN!|NktzB#&x+b)oYX61b;O+)Pm3x1UJWlD)#$AEmGpumo zYW15Pm^_AQvY$z|iC8M}x¥54Pr}4mfo(jDWy`RD+5C0#b#T!-7_TA+ImBR)Rxm zy2NKSfEm<-xFpvTa6iVe+YxzrNI=+30+6FQz&KHp3z=uu?yC#z#^5eVi zWIwmQ)l{ zP)JDStAgZ+dTe$K-Jnh4paX9}7UbVZM1mbiBUElQY|wc?kW60}BsbgVTk0c&SyaM~ zgX-i=;%DRjpWD9C3t8Oz=X2954MXfvla66(xzM)AtXW8UL#F*ndSZ92v`z z6-k69A%#G6W8^}NO-_bZk`NI=>%z^#5$YdmLwF#OAQ|{wNszz6(xSmrLrlYpK)g0M z@jx`Aj3XPN$fE{f5#IatYf?-KvvRFouanRM*VqbwMn#*-B^MV(YyaFVRWRblSB_SH zHwz88?a$0#U%bdX`#E;$vscipN7dyhoszuH^iWLAE5%G2qD zgM4C63Y?(o$Yj^0Zb6M+XWKjX*OI&-*P?`CthVkI?&~DEt;{IO?|}4py8K<(2+OB1 z@-KWq6Q;je7G1c{OH#m>(CSEJwUP1?Ey>(NxfrBzmopabmzpB%Gw9(T*HJFb=bY7l zxSy5~nfSi@w(ngRNTH-Kq?5nRm-SNlAz6=zV9%D%$ zLt=VV(I~nk$HWkuuU2HL<0QEGub($UvvT`#4<{Gq?we!&1no4E;PX1noq4EJAYw=) zvtOrlyOHrpG?;J*(1aI`UdD|ubD(S-UnH4j_rk%AC*?1<{G7UYSOssGSx;MkoB4sa zQa2X+Wc`bcepN&BJ5~~X>SA7=eUc`p#-8=54w~3Z#h5bDs=fY*js`%5eaKQf0`YV_ zQeoPlow6cZW^~JC*OGMD^xJIx4TFlglNO({M6 zEOb1ec6uFOD~U^G#^hYOJyfh0F_&Dh3WkEX8R2Hf=cnbgokItmOQU`3tz^LKR)ZDy ziBGg$)H05FWGQ|#FW5@gU!?f!snLQ$36}m$9|5+TN9}^coty%mng$_riPIrY{8oq7 z)Sg=BEdODn6#h&r1)%InX*yEUE$4(H&nzXN-T9njNFm}gU-jpj)P5!^N#Q9 ziLEn$I{+zQVuy$oYp4c|1tUz#LiwkXXuVZUNl7_OXFx)OWk8}GgDI6ohF+)bN3-+~ zjlaF|bm>Y~?Z1?53GFcLj~F=9LoldI&(P6OtPP~EB@H=*CMi%J*4Lh%H~2lzd2fUd z1bmy_tcQx4qss(8Y(7>_uw(I=pDc?y{heQQo7fF${0E2&2y{=oLB8`r=jXkKR$!(A z!{o1|!3fdY%nY-)VLw&>cwH3%;*p?eRKOCzn-XGZxDQNi`d`JU(D9hvtjOBqKGG?r*sw7iHyz zE9CA`T0kkqR3g&pobQen?Mgv3_i<4C#L!0xB>btiO@u_)V(_t2OUakc*xMX{pCr{< zbs=?R)TfVEJ2pyQ@y@sHCYq; zhA&tFq>4vub!17vlCCMY+wf~&z3wKp(S0*qrutfQAPnD)t9FDtRMR~5Hw+*3f2FZL zYX>W2DS|c}v#F*&QtC>9P5(pSPne6=P7fS2M|6XI2osTkRG;}o!hN7mk{W|dH z9vT{YBodtrZwmG6s6Y_X4$fC29gu%^tMks_)6K!#T7zXGGfmUW0U}5)EH6s# z*5!3|M1C?H5a;(*76XQTZQw-0^MN%bnJ)bNb*fM-010Ho*b$=Sz5VX;ir!DcHnS0V z`mb{p&m% zl>tD$@P4&3M@tA>=wANed>BdfV!Ybm+d^9WL6Oj)@jVQ2oEq2n%gVVq_~uJOqA+&qTmeULRL=J* zqv}nX;7X&m-&_jv2u`dXo(0EG&~(1xlEFM7#zf|w=0pi>K?ysR!R=dktYcb@nX`fb zG?h5LczGOl%wLGpC&%ug_NP|1hT(ruRz)SHq)5sFYW1YbdXtj@nrK$FP+O%PVpz2n zkn%eio-!g@Y_;qCmD&JRzA8)R^)UU0d_ni)RC6V!F#3&jn34X9gmh}-sO!6vMQccp zIy;O$?(6&7bFe^oA zL{(5ASYgk;MjB)$#y|$>J%(&$kNGemQRWWso6`G{Tz=+9o^V|$-5V&J1PrNs>=?HD zd1JS4u(GBMak%XMIz*L~DYS|@0f^qXJ{Wu9aMRC#96q^OhxhBfQDyBY^Zofs%^n;3 zO4=#|>H+%3U$bS3y2uT@Y|aFCk}V^mRt^!rd;fL%R;T_Vm^=CR@1N3Cyg$h)HN}9! z-856}ml!+*a@8=3qX-cYIUA^F00W!!p2jg0GauN874 znsCeAW}=}CQkgI3o{+$9DB-x=(=;_G!oH?|AU;e^A9uTOD61|o+2xcF5022VE5n^_ z{7@jBw!^21r*F?m5a27Nj{xi`!Mc*@8xS0rl~-M4{>?D8$T*?IP5*zo_McMyCs~F- zV8()XGVwI8JJl3Dq8n5mfEMPzBpJ|^3NVd$`Fm?H&`+&?Q_-!CWyOE7)_SuZ6D?b@ zuTC*M31KmqIPp0B<$)5w#M4YN8QXSgcGKIF~{;dwjX=rWXas^aOl^wVop zw9dgXQ$9aFzHYFp)mBtQUj(vLVr1A#L>FTBg8}x=km`RviT85J~z#F}rSJIbAmI zLPk$l!g>6iR{4dGI)-(}U$G1Rf?8~#`GW>NPcxXj%B6j+&HW@An|7-5v(2zeX&49o zR`PWW6<04ChH`XEQhiuQNmMGnV8-}?B#s}`!UohS@;_Yd#D2xnV^ye`) zTA$rGj<5wUWc#aTE)*YUhQl){u5v1yMiQ6p!tbRcT;ttFy(SxVD8Y6J1qB;j3jv{R zM$MXCsEH&VCJ#2;6#xX{pnxzehgORN$yc07M@c%Cr_~lm{C&pY{FE@}XxC=F&24Lh z;{ltB2(}c$zdoYUh{9PMfBrQL4?FdfniPMt%CyX^Ue3ol)Q0IV)t^R!0b))B=?_Q2 zwbj`L^-U8qi~K>uC{XSICPDX%tC@khOQPcGRC4rHU<-iNVUA&3xf=Z}(IMIcEtzUP zqm`SK;T*w8rGz=e1VJ}twS%uGt^%#*wR1LfK~5t zrZX6s0d3{%)GC9LfkjGS;*Kud0#bM>RW*^qXVtnG#VjYv>1xay`?;xVeAYT(p1PVO zCMqQ*rE6X+DC|!)1w^#9y;*irId183JrE)xB+U25-(UHQrS!*M(3_HZrzJGBg-DPe zEfxKEu|_{-qi6EvflTkuI_Xt?UW#PX5t`&30eVsp&cY?7Z>t+`9QCb!kAib#)b_WSa$zz<31Z zq^Y$8y!nK&`(^+9d}rs4g9$A+k@OL>%-H#xY%!Y7+~H8t=j?Hd70u}+p@V8=P*ZR1 zQgc1Ns(2o<4-?zk>gT*D$RS-c{TNL#_zOA7?z~L$ZkAmo9Or~G*4!J}AGp0gufi(! zJDcf5QA--Cx4HG)CsWqqUH&t!ARS1fPvFjqPqmor3;5=BumGbJE7 zYXxeC!VpPehay;Z9qGC@U|V@jh*PA9ZR*jWGY6cA|7Cc2j{SjPp4HGr1_k`qmsQIP z?~R8`p~;6UmgYf$YiH=Ud}M-NefW~BgdH~1Oy~Z#NmJd}x&g6i!>BN+b3{TCPpxPc z%i_0b<5)4D0Ba<7u|J9X|7dx$8AR~>X9%p*#)FQJ`Icpv+W92eVTY%!QX5qoI!`WH zD%`GQjCiT}bHp`}+`S8a>RL*xv{^IlOOnMxWgS!MFi z(J(2Y{P3G?d5p57WQOu4EIR!uxNpTk<>TuMU)_&wq^LLva#WTu!e3fklq&EcF+ZY{ z#j*-chtUDS>ilQChWta9gL;chhe`p+LNQMz16p_wLqk{c=H7H$R4ECV4m+>kQ^szP zXK((?)bj87qR(h*)MWjn5m=GF#2D|oXdcZv>h6XBvJDc)xqfZaphM)-8kL*T{wugk zrO62m6F)0#ofs|}?8e$gfnXJscRt+C7RZHZOU66}#f;eazh*xTH-4GK!4GM|>1;lj z|GI`}MS<~OPurM9W@-HW{QMSVu(7c-$H$)W?Dxy1S`dm$HhOzSeS;!6*4Hem!saof z$-aScqs4z$)FTiR5)zV-q_qdzQGE_o>|}9kIH5+3!8^Y9FGI%Y(-`&W!A-(W7&A3W zWX-4O`TU)c$WqAtqjfcj-&#HJSfTs&k0%}cYYDL)J8EbrB_)MN!GDcK$p7BuAm?P5 zHHG?<*ka0(GiiPJWJ8wLW=4+VT}lYKZy1cSkjD-pA|pRpJ?)ua?dUtBC-P-z*tQ)0 zwoT8?s%-*YudR(I*2mIRSY|aYJ%E7DyZhU$qlNWUl* zflM>zVbtQfM3c^5Zzh)Me$-FfldYPQYc##h;5cWC$h3+`j^^Y-dGRttuJazPvu$YE zuTDDtH};C+{Nt5JHKt}}VM!CEumspO#su(}p#;&}0}A=C zZ~+kLr2Uhd)2B9=CB8;hh zF2lpo^zHKPJ>Nbsy>SQo5=AzKybru1i>s`&Gv#OMw#A4uSPRW*knq$Zg5WqoUqF3Z zznAo^;{WKeMD{uF6^IEppO757@mZq>->`T7+}^(VP$-+#egQ}*|Gr0CM8O0=6GC!= zAmbsPp~T4QVW$Gc&5=R>dAGvz70O}4-&J98}YJRx05rlokzdd|W)N%B@ zTX83x1(#Z34thzFfqmNDZFTjclySm6Ab!?j2I-(Prz1pv%g(0;7MZg{m`8rL`xZb3 z!V|O279y5E64}vXp&jL577wA>pZ@GS)S-NckdVQG&CA(6KSI6Y#rZ|*eZEjAIsfeq zeyyX0K7oK3X+_lYqZXG(_{ive+c_LT)iv5>_ofJ;T(IkLo^Zi z@MNz^mkx0IQVyHdFP=coJ$pCaf>df2tX|F10OSx>gCPTKaav507gSh?fN$KjMTwu_ zDCc7nlFriyGoD`V_`%KRV$UwdiIvE0&6I*BoI(J7htpY%R{;v~C{g@HezSkv8J{jA z0Y+?Et3~9UYhES$R5EACTGit)1$1pVw9JyH3_tpTtvF+f!pxyP5{JI}z=8F``JTm6 zV0HS>JWkpa-iSL5SUIaBODCdf*NLcs6&jV?IjGr(zrsRb|7#C%g>8)AvxQ8EVb+U{ z;6Lz(0aiYJPtJJ4kg3wU!^NrS3SS=&Y02Cq1qZIgIzSn-2_dFPLKzbl zwkMH@6-VS&R9pgwSZoD37JuSMNahPsl6{^=!vJNJp_gf9J4aNJdWypOlo?PCwe%aZ z>OE~FY#4PcEpRpLqd>2N_Y&hVFXcht!gt~(umftv$pXsCtpdR8rL3GJNAp#two5*S zi^8@Cam5fo6fb$oQfGIMTH@(79YmKUh0Q6V3L`&-Bl17v?B}+as!;CUoCqf8rw0?` z|L($^Y3=OnFf%iwOZ=*?MpYalYpb4_QQ;)O!(3Wf;S&%zB?5`G78maZ)2XYg@9pgg zI_Z9b9HF71lu(Qe44*2wRFi#HftU^QCv*e#+iO=ZjE~DtZ{fx6?e4-rhxUM`t@O~v z&3CS@>V89ddwCr?4u}R(x+xxBTwEL<*K1X=Y0i8ZODvG%LC-BHPy_;l8{h}|`T3=3 z`S|#hf@24HHw53D4e$7gv!|9M$^xmKnQ{?1hr9L92lAhZj&_$U4c~l<^Zf2#r!O-l zZEwk@%H3=o9fxwK=H@s#I6_vw>uC%O3^1h0l+E%cR@c-FmP9t9@5IH$&CSiVwY5!6 zDJ{irA^fvQtOr)k%MdfNu%HP?+O#h%E!Ed^Y#E7&h$t!jFw~#^ge^a@&8@Ai-o?T~ zim}4-Q<(F?!NCtysS;*|dD|Qa{qjs*UES67b$J;Xjx4#~?y_=nsXVwvmp?^8pE$E( zUz<;0Aerh^JIZsyF1Kj7orOgy+rOnHz(&jNZt8AoTv!+ke!>3VJs0D+bDrMrfCvZO zT|EnIIB#xP_|H=iMm|PHy7+ zHm6T6Wo;A|HIt2~sA$m|9*3xNu(7dmcz8It?iVK)*WI@L=Ugbdk@Ia`uk#CiD@Mo$ z(TC7jg_QpPH9P^89W)`}!i!Ft|5xeWd8(`|FwXD#DI>9qM&u%7foOsjocHc^i8<&| z$CY(Z9blA=NkXEpsv3HAd&_xT@X0+HF^TNB4oN(iq-w%aXCnu+=PZSuG5C7(-@@{8=Rtl13j2Zq_u&KSWy4v2^iK;4La#y^{^2)lU zsA_14UMoXTwu6w%nxxui6bWpg)r@FuZM}4Xb;z_@!T(&9drlD&6H@gPBC+^Pgs?;! z1g$1=_4$zT1K)`W&I%rS#WrQ*Y}c$fH8eDOXQIrGe)Yp5f2zaT1&EdPSGBg`fBU$}P&%q;3)x{QUf-rKR`x`SS0}jn&oFqN1X3aB%hY^^}y9e0+S4j*hRduO1#AySuyQ=H~eL_?DKIXlQ7y zt*tIDE`EM~+S=Nop`n?XnO9d=m6eqsARsn2HagwuTTwI@@pITa4sHmvEzP|bS`LeRIzrVlY;^M=@!^p_U%gf8==k4C! z-jkD)ZEbC!prHHv`~Lp^*Voslr>AaL2?YQE0y0TNK~#9!?c3E_8&LoR;OS||22BVF zR@x%l&*8|yia9*Lat2?Z6ixkDRtA97Mqj28t%U6I9oY&z8@C(k%wt*wC z8659>tAUse=M4{KCgLi$ND&maZ6Az*z}<*^#z*?E#+lQk;J_(Ta6=oMw-cz}SOv#< zcC>morPXecBAB{epBXn$#0i7$CDY^n21fn!|~6g5Fmq}SRBFhqSn zrd2U1iRPw^4Tf-jYcnWvoh{(#Xa_^w+P5DJ{q1|zFe#NRIHI3Gk$xQlMP_uQ2^?SD zpr{VksbCA94TgB=`8XI7;TND7|2UZjhsO(smyd7X0)zd;Qm+@hDmN&iKW#Ade=oK` w@uvG=3=Bhu5A}gz>_~1H6cJm+7LobjAJ{*CW}ABX+W-In07*qoM6N<$f}D>zC;$Ke delta 1059 zcmV+;1l;?}2)GE4B!AIROjJex|Nj600RH~|C@3i1-{U3g)z!np!{XxNzrVlv`T1H}TE4!%udlD4pP%#d_8uM{>gwv|=k49y-CSH;o12?B zI5m6calSAc+k+<)BM+S=NFets@4F0HMtmX?XlSUYsE&?~va+&#e0-FYl=b!XaBy&+9?L`}_U<{nyvmr>Cd4PftPs00JUOL_t(|+U?utQW{|x#qpEng~bYj8buR( zdhfmW-hX@VJ(?a%5^Dqzlp@I0<_`Q4c+be3-wiwiJO7>iav&nI$Sqq6iqvu+C}zHW z2gBUouRnf)V5X{i5pU{wzoJo#`N7=EVD zoehG(103vtea>4Tvjz4Q2KxI#id&>8Zr-_@X@7xzM{nH*BCxMJ4qzqh%M61f`WYPS zdMbf{1N(*sef719TcijIJGQq6K#*@l&X*v4SL5k3(&6ZH(&4FL*mn<@epLbM()UM_ z@BFG;qzEPt9cb?WLGCczC>T6A;ew&@)X56cFRpaK@B&Cpz`o?mc>kxM>J||Z5fKp) z5q}X85s^|@LZW|OM2e}GG?+Yc-f#w!=7Ql2Cih;v#2HK)V>p9JV+?07X^i0vCcR#- zUMV6XA|fIpBG%0>2&Ge4kRD8V$%CTd@)ckc?DJl`iYgD|@Qv$09pkVu#KR9@Uqe$9 z<{v&%+)_Ml&DtCoHdm)7!Lcy~imEUu{C}&R8Zg8XUuKnvfWp_(Iu4FCc~De_s})$;bHGp=+1(6=n$Rv# zG_SAogCm^;!_MQU&w#->=Bn$mSCI!rY=r}c&etv|KJ+~ffMKxrNgEg@K4ynN5gc?B dwumeS{{XfEe-zC(A87yp002ovPDHLkV1l`(C9MDe diff --git a/tests/ref/footnote-multiple-in-one-line.png b/tests/ref/footnote-multiple-in-one-line.png index 12def79ba83c57057c790866268f258f7196b23c..41faa1fb13bdc257ac3fa3b5fa8e1cb0116275b8 100644 GIT binary patch delta 695 zcmV;o0!aP41>*&fIDh8n?fd)u>FVyjz{vFU^uNK$m6er~larE?l5K5mprD{#U0sKV zhdw?&?CtUE?C|R9>PSdPkCB<;;^L8$n}mjrVq|QIi<6w5rlX^y>FMc-iHV4ah_SP~ zczAfVx4%tKS7T*uX=!Q8%ge~f$jr{xuCTa;hmWkTwx6M?U4LI^mzkkLL`-UIbS^F~ z`1tsKetwpgmNqsv+S=OVQ1uRaaM6zP`Sxsi}Q^edy`!b#--NVPQ>8O-oBll$4b9_4ROY zaMRP%*4EZ&Xn$ypjg7>_#KFPA@$vB_BqV%%e1Cs`r>Cd-`ueJ>s#H`|{r&yx>+9y` z=KK5m^YiwLi;MH~^I)lQUH||AT1iAfRCwC$+Qm`>K@>*epXr$(3GVLh?(XjH5F+sY zdkP4}F2yF(P&wZVoT|A!-J2@_0000000000ASGPaRewUjbp!H^F-8fFF-|&!vwp%^ zIX;EUN~Mi;BSpgnb$hp&ef|D~JK95%88VV`LppeLS_b?HU)>f-U4u=Mf`u1{k^R=T zO!**kc{4tdI9$QJ$MCaXhwEx{b!_xI98&h3PvO%uX=FI|6>ezMTT)7$*Q6Ls3dB86Ryf933iq^F3iozf3RjhD zXZPIBjIS>)?Sxm(x2=S;mlv1nZG;t5ERFu-fa`L71OG~fC46}R000000000000000 zEbp_t&+@*trQhPp@`__w;N;XQEDK7iYcxMMV=!mN3tGFj65dkq#!C3;`QgP%_~l6~ d?*sU&-T}FpQgx0KH}C)e002ovPDHLkV1f*Tbz}el delta 655 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 diff --git a/tests/ref/footnote-ref-call.png b/tests/ref/footnote-ref-call.png index afc103211207345dd73cd3bc982520aad1cb698f..635865e41bd5e3af9e649263f12b7d2f20e62464 100644 GIT binary patch delta 523 zcmV+m0`&cZ1fv9yBYyz}P)t-s|NsB>_4W7n`Q6{=-{9xw=k53R_~z&A`T6?O)!oj} z*vZS&*xKUU-sacX-*Itq*x1;NjEqiBPOPk~s;soY!phOp+r-Aset?K%W^SIKs9<7h z&(Yb?(%MT+Reynr%FNV4L`-mUdPquA+}-85yTesiUyqTQsDG-m)YjfmQd*gtql}K0 z+S}t=Tx587c$}P^y1KfSmX^}e(&*^u?Ck8mz{ru4o3609sjIVyijtF+og^eA%F4>I zv9ZO)#a&%pu&}U|m6a|oE}EK}{{H^Cxw+QX*6!}^`1trjLPDjbrSkIf)6>)G>FL+k z*WuyeNJvQc_kZ{M`~3a={qFDcii(Ph3rrCJ007xZL_t(|+U?cHZUQk7fZ>Vl6xdBj zPw2h(-g~c0Ti*XskSxwz5QBjEH1}VY#u^~xmNDr`&K3}HNyuUM5&n7Qf2vRn!e~Sd zyWFbXp}YnBKI=E`i#;lLDkA6?7TmDS8wy9F@kC6R|9=^6HH9gBibv^L&I{d4LQb(e z3eM-@D7axv*Wn^`&My=$78dOR)EmRm7&lxlL8}StZeLsAhAUMty#uVEH^dG98o=~1 zolc-Lz+SGyi&JUr@cQ!$mbH8`gB_mDHCWDbS?xal@rE(Fn1o!DJr!9YNSsOdIsgCw N07*qoL{Q3F$`1ttu_xbnt_x1Jl^Yioa^78NR z^X~8S?(XjG?d|OB?CI(0=;-L;;^N`q;osor-QVZj-R0fg-P+sZ*xKUP*x%UL*w@$B z)YjhC*4ES2-O(?(%RC}($CS^%+1lt%+$%t)5^-q#ec=cxx2%WHj zNJ2zRLPA0=E`Kg0BqSZS1abfX0K!Q`K~#9!?bOAt0#Oh}(F5-_?%inP?(XjH{Qtj@ zbQm*&3Xoc<`A+5L78qmf6Yyzdk*%LV=z1xg)RT4l*ntP;4QtW5MI&C<1 ztonB?_huk2r?eX@L}fVqRz`(E88sgl#@JUFKZWgp&cD4m_1?(Xj2-`~Z> z#i*#LNJvO|d3hfnAM^9}=I8C@<>k=O(9+V<)6>(;%*@zbnf`WqY?|<(&I5?%JuS7;p<>u=4_VywoA~`uZ`1tsCc6P3?xTB@5la-yCoTQtb zrI?tQgM)*YmzSofus%RYqNJ>3W^UZx<}xxe^z`)L;NZc-%g4#lB_$=>+~7q;MeOYC z{r&xAXK$XMsLRdOT3T8xEG&kGhH7kdgoK1qQBh1xOn<_}%-h}NOG`@~9UXmredgxo zoSdA1fPm@g>Fez9?d|QAm6e&9nU9Z;xw*OA+}x?DslUI!-QC^I&CS%*)I&op<_xJhT-{QKtUsny_*4DXfh_|C5zAA^u@)CK zfPe8F!(Ro5hljp9Ve2+0>kIR%yR(yWa>@KqBlded){06EAT~VOB2xtVb#g^TM@LDw zhH&t(ancL?%%`wSe(Ca+5ddiK={-^a0O`}5fZ∈HOV~3bP0-%Txg9$2rWePQxka z7V_2z0xHVczuWSqSsk40Fzf3ES|`u!(SM!{4()LeUR3C^YBDNp?yJ`h_#ED~)3{H! zQZ+P1vd;D1G1Y;76>^BzSv9PfAacj{1ozD<->=wiS(~WU;(-%lkrQg-r3c3IH1UHt z19Ae_`TA75A|Ym}Dm;mA3|fhgbG@Mb4f-+;;&~c3pq!wK(h^_8N<*e1EtMsj&wtJZ z1AQNp5}nVR+FEoSYS*DL$6zALzJL+4X>(cf77r{T1P>EAL_Nm&sD7t`Yli z{a*2>ak1gY1`8ow-JJ@t;YamWVkPSv%wb}~{+e9U7nPLMf7MSS5Zq^u>qY7S0000< KMNUMnLSTYPeU+vF delta 1171 zcmV;E1Z?}u39<>0FMsau^WER)_xJh!{{FYOx7ym;*x1-{adG?m{K&}2#KgqAySu)= zzIl0hSXfxVz`*tO^;cI{A0Hozii*0rx`>E~`uh6k=jYPW($mw^%*@PQUS4HoWss1N zXJ=<-W@e0xjDLTBZ*OnT&dy-QC^I&CR*Fxt5leP=8QRU|?Xdu&{o9exRVB z{r&x)pP!nVn&{~0M@L8F+~B6Brk9tOgM))|a&oS&uHN3>q@<*ai;Lyv z>O@9PrKhjw=zr{IO( z)z#J3*4CJqn39r`fq{WqT3ReDEQExF($dvYQBl{|*MCe*Oq`sY=H}*ARaNuz_U7m9 zb8~Z*m6hq~>Fw?99v&V$J3I37@=s4sQ&UqTBP03w`D$uvy}i9jNlE+r`<|YjkB^Vs z+}x?DsjsiEva+&6LqpWm)RU8w@$vCOLPBF>W22*^+1c4^Yir@*;faZfX=!QD(9q@O z-|p`2ot>R1C@6eE-b z>Y}2eH#aw>rKOaVlw^EENB{r=FiAu~RCwC$)m2YpQ4~Po#Vu_qb&3|JxI5$S?(XjH zu8bQD?q1wWmESg`nasmXhNs?!ko&!zrU9cPx;rV|5!h z$z%dzYH08->(0;Q6U+*}hr(q0u&xs?=HP zdQw8JU$b7G80%CrQdB6{=<6%g!toRPe19H(_{g};{K?)oMXFi8Mmd#?WhrimHEZ-N z3nTRYy*T0MG2h=*wYQm*ZSq?Y%74Uo z9Np8AI>Y+1)Mw@8Z>51Fkx1r*=~*G3fLTcoGoJ*Xzc4;J#Ea2`A$I&pICr+{jRNQ7 zA#1OH!)H#T3W8zkYW*8NbrMyQ$J0tprhmgS+3y>_3io%oz@%k-aG>9XKLrN{E>6=B lotCZ}mHxOyBAJ7J0T9}t2>3RBF$@3z002ovPDHLkV1m=Kf*k+= diff --git a/tests/ref/footnote-ref-in-footnote.png b/tests/ref/footnote-ref-in-footnote.png index 94498598360ade600e1c4f7797ec1b57d7c45edc..73901b479b0d7275941f12b304dded257172dc0e 100644 GIT binary patch delta 2571 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 diff --git a/tests/ref/footnote-ref-multiple.png b/tests/ref/footnote-ref-multiple.png index 899afca16e4d3445d6c4e1592a3353f2b0603720..59e9fecef63f228a9255dd9b70d60d77976c7c83 100644 GIT binary patch literal 4425 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 delta 1448 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 diff --git a/tests/ref/issue-1433-footnote-in-list.png b/tests/ref/issue-1433-footnote-in-list.png index a012e2345fca796e807c0534e93196f8ccb201df..19934a709764a3ba1bba0f7ff8cd7fcacb0ecafe 100644 GIT binary patch delta 533 zcmV+w0_y#Y1g->-B!AUVOjJex|Nj600N2;osHmv(^Y-TF?OIw|xx2&o`1nXjNST?L z?CtUC>h9?1=-%Gmjg5`Y&dy$5UQA3($;;El$IqFYqh)7ri;b0SZ+Eh^ys4|Rq^7Q@ zs(lp{dr_+cY#Z=I86{?C`g@x8vjEIyyQ50Rfej zl}ATM?Ck8nzrXwY{P*|y-QVZ#@AH4#|64ntJpcd!6iGxuRCwC$*;kIjFc?MQI}8ld zd+)thrqdH+_J6-5-gx8zL9Phr+rW_=%aMNz047;C1B3|^CQO(xVZww76DCZUFk!-k z2@@tvm@r`g`IUx{${}_-amvE)6;t@*)3wyD@b}lUvea%hJ)0|O)IDKUYyB_>NWR0x zQmM$V5&!_?(u@f!55`Sg+D0jEmrdcF-9JybvKKw!w}1T~#!ZZy2s3U1000000001B zShE9Zdcyk!cXoa+9@5k5NB-K`^hcp%MrHkUEHFz*Q$786g{BE%@y zGwzffnM)6k(u%SdpPse0zM;(hzr1Qyo2GAke&z`$*B74f@71LzY+wwhA#lk6CP@#Q XDu-@(;|^~}00000NkvXXu0mjf>w*_e delta 498 zcmV)a>hA08@M2_aiHnns zkC&vTuBofDvb4OYsE-$9bH!y5RcgFG=7i&+{uRzJRTNY16aWBP@t(xa z18r6Bj14#PsWs(b{*%p(^+-`HuEp_D zMtEs8PEIqz%PSFQ8R4=_3~z6xeB(xKr(wKlzsZTwh@Omb8Xt?k-t(y3-26iH$3@Zw oa(|Z-Zrt4Fgx{{OOc}s8A3*+2QJ3M15&!@I07*qoM6N<$g4Qz)9{>OV diff --git a/tests/ref/issue-1597-cite-footnote.png b/tests/ref/issue-1597-cite-footnote.png index 6ec017c76814c55908cb99a44cbb0416eaf34389..e7c076b14e1d23dde9bbe9defd74fc430fdc0c80 100644 GIT binary patch delta 488 zcmVJ^p$f&BaNJ>(0a(Y5UOu)j*+S=Nbl$7=L^>A=- ze0+RU3#>gq^HNXyI1%FNWy(bFnbM&Vokl3l0F z%*@OT0k`|#Pk(1Re%`upDKKnIjIduLz&bcG%Wpw2dfsIotcgio{*I3^EGsMYO*P*ReuQ?PQ6ck eDdi^zy50|4lOUp9I;>Fu0000sj*gF$!o$a|u(+(Qw!y){)z#I;#>Qx9X#4y7pnssCHa0eJaBzHleD(G9 zl$4Y{K0cF^la7v#r>CcNb#?0M>YSXMmX?+Amh{)>x?^Yin<>SenC007fTL_t(|+U?X=ZbCr}M&Up94I#bv z-h1yM38X{V|9^gvc!#IU7{vKDa5Q7t0)QU(_N)-^<$HzrsN5++_3Zd^J>bs1cjHD7Zt+Y38!IZ zoD={60OYLRmOAIZEe3-eWcvr%_t&WX*vQOmrgZG+&`JtLpPrg@AGVa?yI2Y&}#6T$ua`-xmaqgz2& zm>$;zJqwzM6!hmWaadW;zIfRS?q6Hq&LuRu6?BE^aZS*(pa}&{K|5yQo&lz2_(_}l zK&OWg5J0zgL5~HZs~P)eeb8xu;qhaD4!uvideqMkJvjp%U6>dF^pj!e>IQExV}A&s z`yoab3+UqF;(v{e4L<0*dKesKlOCO9b(sRZa|F=mCCrA!6&M_W{vm)*>Bml*uYcF-!7%4jr}l$2l&e-D~nXg=c+5oj!Dx7!^Gg>*Wd zTrNK`(9sXjqQ*{*dUnu9tpgJ%EG#@R(DzO7!i|bpK=17A)YjG>>3e&7d3kxsWReUT z4`X>bAN1F6spujZbklY41RxNC2R&TS*d^j|mVXf*Cacwoip%A~?m-02bo|jNKInz{ z)ZrlkH1>3Z3EnPY{#?*_cEjQD{{gMlYGpDRW`MfKyS#N3WWmOw9RJ2fshOubHIw$w8PTJ4A8o^)anWW zw14?AhEA_e0~fU4@3&YiqP#~V=#dHX{a>L>gI-%t1$-HxtE$QSee+D~5E2&jl_s*Q z;c*7EV*0`(p8 zz=E%T(n;vimI0h)2m_0;7fr+qIv)Ry*+6GsypKeJ`?t1Zxr9cyg03(Gx)U2h|6VB!8SqL_t(|+U=KHPZL2D$NfY+^2h@T2`{`DO!R>_jf;^43=$KO z8dSVd&=ji`1*~LkMNMlef?O1^rC8b`PVK2F-CtEfE1{ds@UaUVZpA}J zI)K({wUJ09D=X`7pyhV<+yOuj1=zEx8R{GM0bN>JDvF}l>n$iKpg&p_G;39NiSNIH z9uBf^JQ4!oLK=ZySXdAQp{J)OGcz+33LO?`*8Jn_zYb0854x(Vs;Q|dFE5XB_lN$VeB^ zYin!8#l>-ZV`C#DBO?}zse)#k@zklkpqE!L6i^3!^ENm=;PFA@%S6z0N<^ayBS8?% zW-}RfyPZytRM5(a-`cqs^vHMI*+~V>&h!Q&jDP;1{1ZXb-u3(a#{;^quCBbioH8KW z>2%TsfOhfj+e!`3r9lt95^GkgbnlMT)-HE-4<&ynw!DltuO)5*>FO1Ez=K-p{#`c3 z-p{(zKc!nYqS0Rlg8=|`ZyjZ2Wg3k}6h*7my1l&}_s3$tC0{^#q?4DT9BQ=EvwCS^ ziGOunZmF?>Z6T*$f7c9eG6kn(pEHCO= zO_8cb1GZXI3l>&=IQk9{IugOgr>X@D`#Aig!TpV=z=?{f*q~(aM7LNb`002ovPDHLkV1lwo%0d7D diff --git a/tests/ref/issue-3481-cite-location.png b/tests/ref/issue-3481-cite-location.png index 01139e25f55ff67f2742e2b9a20076b9019059f4..110ee4a2493c67ad5bb2b0b8c9dc00c8c748886e 100644 GIT binary patch delta 482 zcmV<80UiGI1N;M!B!ALSOjJex|Nr;*_lSsyi;IhPc6R>${-vd*H#ax#?(Y2j{B3P* zm6et3?CfJ>V_REW#>U3vAYm*K6&tep`}m!Y23jhEB000000000wF%*QK2hbHy z4TZS5y;lfd2?{SQ-zkK<6VAiTI4J;t)_&Vk@BFpJU&bGL~p`)uxOjJ=*TT@kCl9ZgizsG57bGNy{LPSi($Iq~`x{i>T zNJ>&zT4JWCu#=UYKtf79K1O(Yf|Zw_DJwIHi<37wL9?~JjgOakdxN^X#5+AjD=jtU z<>l4Y)zj0{9e*Du`T6=}W^PGKQzj@dUSMdet+hHlM5U*%P*PenHa^PC)J;!UDl9Zd zNl~q>t?B9MV`F2*#l`#k`@zD<*xBErqM}(@SwTTTPft%nLqpKe(Ek4ZdwY9pYis)Y z`Y|yvmX?-radDKClyh@)sHmv$@bIjxtaWvDR#sNX$bZO~n3$iRpR22@nwpw_e}B)< z&*gwu(gp4vYJgTg;QdC^0sj+f&e0F$%aB_MvGdo2`Pq@0mnw+FR zK}o^G%P=xJq^7P;P*`(yeW=6zQD-N(Ad=0-j9))uCTb~=k53R`T0?5umAuA z8h=SdK~#9!?blUPoM#w^@#pHh%L*(k1Px9g2}FrO++FH!sibXEcXxMpZ{zN+5Q88= zgS)JI&&%u}GZ|*yJ=jh=9e!WnI=laKiva`v=l=4ytAuaxH_v}O{4sKdWYC{;yXarr zI^l!oKQjEZ2^d!mU`m~N3a=1)1&*YTLVrQGf(v*}7{3^%M?q&;Bv9w}to=^Ndhj_x z(`HCmS_eRU9A1tr`l#unokX{=K>!Si2~|spbM~v4rsxRZSV^nr!w-b4HO~rcCqhtO zJ%Q`>L7M2nQr!3;Ej1@yC{-yM^hHxp&(lr^aLrIQTuP1r&bC;4!jc)aVr+PsT7NSO zTZ`kUgh#yfrjYZ*DuGQksH`T>EoBiL5qt9Y1IFrIo@EaUd3Sw@zj>Z{TKM2A z{H^T^;gc);?e~F^=-)ErI408{djkDyTQ7XDV!2^#BoH?qz*L@d3NHn{+#~5TT%ud~ z1-vT4!eMd-w3gZdr^T~Y_+<{ys~w^?%0&{DRgu; zHsDU`^nCr8khT1Afo;dnD4Xnz*3Xu@Mxr1(@4R;;#;A%~nW2wDt*mzgB+fY2Jrao< zf_uWw0MwdWM>#K*Ey1WV{St~tJo<=`^TLY)Tj2&Nz0$2~#~Khln18}1=|HWyoxMM> zyF+tS7fh}_Fck<+nw0Au3Bs+;te!CJuVSf=xt5!VZ#LG?j%^dG*T|2=O9ETN#%M)~ z)pFpnkq9D0P1VhjJtI+2c>Fltu+FI5(Ho8|g82&C4LL*^x9w)?J>r3NLe9&thyW#< z2T^Qu{HaldMAcJeWPhl;b5CgC65YA-<9h>#>b@a63mBG)gQ1pCP#)3k<7_g4fMX)@ z0Wmi&Uc|4?g9)4RX}?$8FJ!&_j!*^R9um5}*_m;j(G=8ZIbY(aZ=q4onBhGf9qrpd}jj&{PX?>nq$vFq(+&000000NkvXXu0mjfZM(q; delta 2296 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 literal 5129 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 Qga7~l07*qoM6N<$g6Tx#a{vGU diff --git a/tests/ref/issue-5496-footnote-in-float-never-fits.png b/tests/ref/issue-5496-footnote-in-float-never-fits.png index 4ae5903d808d8fe616b9e3b7d484e59e508b0ca1..85851f5ad05c51eab1ee30bde1befbdfd46b90ed 100644 GIT binary patch delta 329 zcmV-P0k-~+1DOMmB!6H@L_t(|+GF@n0bm%_OjXU_zyE|Fax~w){~Uq_Li2$`r|#T) zMvmrFXRbK8_}sYlh)kOcimJ%eeE#B153isVt2R*-V56Gpp!wPJw-AI~GbLcO7#h_) zs(DoN5YaqZ0FG)V2*|IW-@X4#rsm@(FOsSG=<)NZ>G|Z_Tq9A{1k_As+q0pm>+zEp z6b0A_(Tr$^eg6Dyv;Z6}Nk%n~c#*p~e~y!UpFe*-gdG?d*w)qt(Y$u;TB4kN@7}%A z($c!RI%{j|f`S6*aKQQV`T6+-G(UX!P*PIz=FOWxc3N5*MDy|E$CZ_pf%X&7ykNls z1qB5lFGdrb#*n6P1xBlUcAuP*LQYy&dA89t*w=ikeD`Y+U3ia8-ao6 b=jR6iA!|pI4=Dx}00000NkvXXu0mjfn%%19 delta 319 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 b=jR6iA!|pI4=Dx}00000NkvXXu0mjfn%%19 delta 319 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 Y8*7pP0XFhPE&u=k07*qoM6N<$f`Gj<_W%F@ diff --git a/tests/ref/issue-5503-cite-in-align.png b/tests/ref/issue-5503-cite-in-align.png index aeb72aa0d169fee63a9e8db9bef41fbd4427cc4a..eabc4dc3d6f0e7d07a2d76aa0f45d2a56e1670a5 100644 GIT binary patch delta 369 zcmV-%0gnEO1B?TZB!8|@OjJex|NrLa?f3Wjz{1M;`TFeb@!HzjU0q!@H8p>Kf7H~} znVFf(&DCLJYfeyDs;spA{r$1AvFYjQ`1trsOH13^+kJg~si~>h*x2^=_M)Pqn3$OI z^73P2W6{yk#l^)oHa6<&>WPb!+}`GUeT6|oOGQUdWoK`Ce1C+zzQ(b$yOWije}Re8 z)ZB`Ul-k_nKS4>Gou$su*o20TOHEbS+TzK})6vu0Pf=Oj-{%Zkquu}j0GUZdK~#9! z?bSyP!T=0JQHKDb_uhN&Nl51Y7eiu`H3-=Y=3U^+8;=mt=hCT!XmXJFlknYl0U)~g z%HiMvgjL{rQ)&*!K&e~-5bKBWj2Tu6?>9E#aJN0^hl4ZTmTTqk@#F$Pv{>neGfsk& zNkrc$=EX+A6B}8t(XaDiT#nSBu?hD|p~* P00000NkvXXu0mjff7;Gb delta 366 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 NgGeh&RY@2H{09z(kemPj literal 9441 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 bZiL3@p4yiWe?Gqcu!x`}r!HG34GR4qADlT0 literal 10391 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 XU`Z&<$A(oL00000NkvXXu0mjf9{dqA literal 12210 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 Ae*gdg literal 6423 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 diff --git a/tests/ref/link-basic.png b/tests/ref/link-basic.png index 0d2bd75330140c7deb06e5ab9a4be67f6a339aea..f53223ffd87375d2f87802dd4c458b8f4636c21e 100644 GIT binary patch literal 5991 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 VtmX^~FIfNp002ovPDHLkV1o1PAzlCg literal 6240 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 KMNUMnLSTZ67n&~s diff --git a/tests/ref/link-bracket-balanced.png b/tests/ref/link-bracket-balanced.png index 8b7e02db23dd0f1a940c3f0c65d86d9158b2e397..01bfa87973c1cd1417cdb078784ce8ba3fddb5b6 100644 GIT binary patch literal 2506 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 omU1LM`}}U5%f2Vk zLTQ_}TH0zCacRYU89*2o*%#TA8FpERonev1A{1p&kww{IV3er}48y*NTA)oSHti2Y zsoGC+6G9q8O*2ku`rO=1&b`mQ=brmL=icWz-+AVdR7_Hyk$)hNfR=!kiUhO-v;?$N zB%mdrrDANL`}_Mf8qLbz#{K*Eo12>_Cnx`@K$VH6|`q3_B5}U3#`^DbRTZ-!L7W$fBZ|Ehxicg$+E4jZX`UhzH8k zC6)GacgF8KU|;z4S?sYre1!ACFa~3Q>3}x3Sg$vXVT6BRsEFv^zEKg;mFF)Z06}Yksc7N=dC}2yt1dJAerc+!vWfe6z3}O3M`@kjWSOk9JN&)DsoUa8^V6DB{Q&M^! zN79tpI)g95nipTk5*BrI_M-X9Yj0y7psao6EtCtOnYih*?cbca@ZJX-&sWt8pj}*j z@eLQWp`?0Ze`FP_NtY7Bdcwx<}SGsz^g-f3%!n4o4Na!JVUg^eFIFBf!dDy{Cc+29Q z5BUX!1Bc{`O<@v#6+jc4L@1h3B3vs*>aH1tOt|261dQLgTeheNzjB5U*e07D9=*db z(|-Zo)Nc&sE5xD|ZemUBV2SE{2b(h=NEn!ZRKs#GrjquW1GkyVG^qal)0pW~0 zxx~c50%Z_?!|Xj?)btY&pb2Cs6xUi~vwxP2a2bmGm%D3peCp+O<~$-+L?m%_FoxJi zmFa-aROfnl`VI|`C#Ph1`}mKJO%|7&J?bAkd26b%sm;yZyH`IDd`xNY;2aelS5a9t zclXz#)1@h?nHiZm$emhUTe~(bJ?qEWpJ~?CHC*kwjzI11F}S*U#U~`Ayzo>pV}Hhafu&noJG{J)rlzS|+d7?`-DqMGJ;NgtX6Em(5UcC^1}ARb@jDu% zP)7VT_e*|35vHP(Rl>rf$0u&z`R@C`ppXzn_~6hOooL2d^f0*c+(l;>ckE0^O!4&# z92gw+@eNQVr6nb&3p*DUep@LCi+@t7tgfzJX29LMcb5o8E9U0rkDgk=)RmPP&d$zS zSy{>D^7;AsM1Zy(@nk4<0-94;Ge*}u zH8o`lp!q1?vTgSt=o`L0#YZtR!V}i)+#;qRkS_9NjbOyecl{Su)+}LMwSOe5Gc!ME zwOZ32J9mcPL_b^-UW8h~_dFFqGk^>mCSd)CpFsc!pn;Nv9#RGWk?0c>bixI~s4(%l z=U-+PE`-q*89$C(N-7x)h7B7wtT8c_)rg6m+R|2(@! zID;66S_6eAB$YT@@PC;C=nQoMjuD!NBB{hl`cqPg_YhGpfaZ@IYisML z3p8hc^Ol|B9C5f1KH0PtqX`7T9U}m=cngWsCuB}KA<5ArI)D;F`41QL2FuSyXmdI6 z3s=XRhfo!I1XA#>G=WvT42WEQAv`$^f>h#8z&G!&-vlm%Eq^FuFtF03pI~oj>O^Kd zM~W#N6f6<=@}3tFE!4syiPhn|*(|uDN&pSPpi-%x@>fT4<{_CwuYM@{M54XJ5kvoI zdPc6hho8Q0xT3P!LGG&Q&;-P6NqS@G_L4Tk&w{)VJVO_eO?3}`==!Bu+n|t=kic8AN%Buo{lzI85ZEPJc*SFdo zc4k<8L!0%^{WMic8Mby#81SaqyK(U;Ufu!iny$g231=5iU%y}$Ww5K8Po_E#nWue* zk%7T+xuaX~u`t$RX&xyf+x+n1L$zAHB1k1mOG}wdW_nU- z)v8q{AeDF%E-WlaDY(RSNJRo#0$KuEDiY8V&{C0r{yWO=_>r^4gNw z`##@!-kIl|Z~iOOpOU_j5J;dU&@xG&CD0ORnIzB>Xqoc5H0y?c2Be z_wUci$(dcCU%q^~XU`rMpa5-RViFu2JgxHm`}gP1pHGw78r{js$A^goJx2R z85yZG;gye%PhenRVqzl2*w|QPPfyS1&!4kf4<9~EWq(LWh`YPHHqgn*$3+C(iQYtzkYpcYAWM0y1&1_zP^4+745kpbL1VT7@< zF_p}o)gmlkzFZ)9{*JeL>uUol$1Ab-mn+|Qh-iKNMJqybno6h8ylPQ z@^W73>gs~5(s1X_ow$3aX!GZAadHanFoSFWT8!SND&V@*v>`G3Y@xvg8b@-c+hwzjrPSA2iaVRUbAZ;Go{ zt$O_UF}oWXn$r+Uy1KejG%zsGK0?sj;NT!fl%-3TQiRB?f>ErjtmtsjqD5l6up#IK zYq4)2(}@!&sK0aPj?zzOXQ!T?9y^0^h0R!zj*bq(k|j%S-MS?>Gz&m85{m~T9Dg50 zrUiEduZ7D4R#0QLf)^xKa`ECtPAok=Jrt{}t0}@+@f|BIErnd*&LZ&`te>}n%Rroz zl!V!^gwp@z%a?JHyrLh7Oifl+7Q{qf98~C;FGp@}uJ%#P%*+f;v9)`9d+F2K+WNtR z2dss9Mns=IM7P4%k+2F*faNfd_J8+=LGgiUX=!i*I>*Pyv%`jlh8i0isbO@?ATKQP z!XknG@IvwW>&fHeZ|4l?fx&xUPyYSUeY$4z}|ZghThZM${tB+wrK zeZey!GQP03q3e{DKTNc=4ZRYbuK-h5Q z1!#M>7&o6J#qHQQM|%0EWq;?_(I?v?J-bHb5Z>N-J1M;)I5IaTsTego*BB>{I0UBQ z9gwE@B6FLN%-mWB_t@Zw9L7555MENz%Bzy{7Eg65Q*CVN6KV;mWtQhdd4&)Wcb(@K z0d#0oo`>I61ihc#qv8u8_`ZKTDL_+GRCaSJqmwf#T)mQno%}*Fi+{^*inW0kNC}|p z8oJe?S=wzga%;`3gAsQ9XxDJFx1vUql2O^&GbBu_^Djr9*Grr9jXlN0G#$SwZD}Za z+pxm|OmnW+W2ElBVO;}PDwz_^g&MenIA0*sbHSKl%lBu7h9As5(9rpkIu+rlu_udH z$OzB|CfLo2&5z*VMKQrXJ3sHoP!523Jem!-BEJb(m+ zWzAS_?Dwa01~fJYL_r+ZEEtUgK@i_DoB-P#QVR|$8GCRbiGNNk249M>3`Vmyn)3-w zN-Kxa2wV4^!))6R*Z`|`b3U;8CojR`y?Ty<$oQpmP7w&(_FF4nXvYudCpBamxDJG^ zIx0bYqhI}SM_YEE(R2)w59m7!HTDex?g&mGv~>(V9DOopK!fi|b9G)(gUy8~EBmmV z!upezYG_4`p?|4Qaz>?%bCjJ+40Jy05Dwf3}=^ zB@{VdO7!$kWnmMGH1jR1*5DscpA7_Y6hv-r8-V@@Sg5I`U)11O0bS)79+y%o)P6Se z<*DQwjotVG49}^N>17wx(aC95P+enpesQC%OAN1IcYoE5PSot($A1OuyJ%T-J=wiHI$jtqyDIJLS!Aa^_Iai9%k%~886cSU*Ird7Rr%&8sIjepO(A)*Mmwz;bB!QkjMZ{m<`GakEOn!Vy zDX|{z6n|w^ZCvV7Gpk%Y6Jas;SFQVY>O$gi|bD3_@3`n3q325>~ z-((^i&=VJ-jT)X8zd>{cpoup+UW`@HEGTLsTz`4^xI4caNT6p*glvu&c?f7=CBK^x zZBX##NmzN0o&$oDM|^Zb5qDyual&P{w%-C`5y2sZjd^w)oM-~;X(fUFAaUgQ=@+kn zuq*+tp@}!aAR%wsWd*^B_ToUW0a3bhjuGe+wieMJbbhrChS9bz(ZrQ-*5D4uF$sZb z33BuYDd0+$k_mf)cvfB=GST|)57`kfMg9XhBCbFJJ(eZ|H8ojo*wzakOBbEF5`ihH-KNZl{*4FFSub-TpeEs_M!Gi~L za&qPu=x5KK?cKYV1t>u4>FEUq2EJGM-o1MZ7cQj9e2s2rXJ>0``$4q_4<7KWtE=0v zVZ$e6^wzCgp?}!Zr%wX{0zL()&CJY7OG}kvNJvOCdCzkhsuJjLkfXk<4xx9RC=cI)xu$Egeo3V(8TcGd(sF)=YRGIGa`9hyL^ z)oMmDGBTpA0yH~}30PWMc6N5c00##L%o7t6qqG_t8e-1n<>fTwRcL6aj*d=mZ!fQs zlapBsNdX$i#bzxjDG_Flii*NPDP2LYjT<+nq@*w|qx<^$YHMrP)YR~*t*woISl86l z)cpA9qkl(_UcGu1Au}`6)zuXN91$Kre!OMN7EyoY$`wW5%gZZ0Jsr4t!U$bmT`HM9 zt3_D3a-~4<#EBCM(3tbmrAri9i6+oVNl7nXzGN`~qyQZk7sq@6XncI!!os4WqJme) zjvWJ8rQz<~yK(oJXYu02G@-e>yE|6FY`k5!ZhxHuGzJDTVQCBl;tLim(6scKGiL;& z7cE+ZjhmX9^z`(oVeKd*J2^Sc571Bst|5SCL_`EaWo6~blP8(C06H%(&&I|^(LZqD zKx1R0kkJ=57#SH6TW{^!wLEc0P(uM)5PY`lpdUX!KlTk2R)A*Q3l}bI+qR8m@7%c) zUVrln9MM!7?%THyGbqC;GlZdVf9cYt0_Z(^_N-d9>fytO6yX6TWS(PVWA6o;(GmLl z``MH@ZW>~2^x5n412kAbG=zeJg2RUoLsgc7h?^bMyQ6@3R)_ z84-QihmDGO-+Rp&^b0XmK`=cJlxRGKnPUXv&uUgv%o<94)UHReE6VxQo|4MPmWrt4M z_YaO2ls2YjRUv5RzJW1)vj9)Ow0G(oTYA4aWL{O*jyy-sFK+njelvtGB!B<<@VCzf zbX(`pky8##qPKsnyKiLnYN4JVkwUVyRD1CN({MqYBVhoT8jO6NExn z&s_`qaChI-?EE_VWLuHlfqORT6%?ATuQmIWiYQ0!eR<}ei1+iN94JB zCnIS6=o}GS1i=sdV5b00O>ueiY(^)hs~p`Egq^&DGD^yu#oE9Nq<;j^b@g37!C9JZ z({pPL%mWd=Jz`zo+@q>dC#6?*bPo&D?*E%D&%&}sZCy7pF?IVLr7aCbZ<}@*foV?R zEsWIFJ96x_Bb7{v=0XkJLA;=(QU7uvX4v+f>F~(?&kr*LP`G&ToBK^)dB7>xr#5a$?9fNc(`1qYRkJu0fzk?|$qOMelT!D!Y-bB@r2)Cw4l zu>HVg%(nB81+Z#1=YZ8de+?Gz)3Oyr#xGsA3q#m(&|L9CyS}$n8Y+y&bs%j2eR{yB zbI}`jv~7>6x_yv*NZVehv2PG?M{ok+cKhIiiK)*9G(O>H6Ti~R7Q7zoELJt^UGj^H zFQ!Jv@Onm0oqvr}w4F=L(B1nuR{xMJ1aj@IPW{XN{-IgOaL~;s+4n}~;LtsaxI$;o zr0ap{J^iC;IW^}Ey>T5WSydO!1JxaaD2UwIJscRGgAf#++uk)KYD}+$M8*|cIz%LA zR0=g+LvNl+ZdDH$Sq6ItW|XK}m|js?Bb{8d2x?Ielz&yW*gD7X>SjY1E(Nu#Zt+ZM z@XjwVnpaG0YHIqk0iBjz{l(_)-r=U!-kgG)^VK0Y26;1lH`Ug6ID00@E4WM&XbH3g zdVUg-raQy;B+&0qW8;$q=XicFXXe!MNjrx^YF71kN34{h1bRl|lk-OduaAkya2McS z{_!Cs34ipA6c$^!`+KXxRTbE!+osB&kf_&`4H;B#vH1S4TmuLmeg5pNP zmB-II^SglrdalIzc0$KhKm#lJ-GmU2f-g_P%6qkJ5bRuI2}BTwBN``MHa}AVv54Rh z!p1zi4qH-$^|X>e&q#c75Zt;EnI}FK&KY=tk&uap5z@1?53jCk$4gu^53q8GMBg(Y z&41LI=uZ(kCRR5>qYG?ZVu&jxrm227=TctT>g177RofE`F{<>lq$G|y}i1;#Jak= zwz$Bxx4*QuzO%Kxu(GCT*uBN7@qkpBXqobpsqN<;vsiC2vo}j3mpQoIjrkkCmnw+GWo1>bVnwOcOnVFfC zmY$K5n~;*4kCB;DVjiIItkiH3-fh=_=VhmVAY zj)sPYg@uKJgp7oQgn@&LfP#vGf`Wg6iGhKEet?L8fPa8}e}{a2hJJp2dwqp^e1v>_ ze0X|-cX@$!cz|_xe|L9xbasAob$xVnbaQiaa&mHSad~cVcyDiSY;JaHY;rClkmU#zaaslp$J|7_Ng~mcK|W3p zX@Bz-nd=K=u2mIT8Q3*Jd`YCgh*vM=KGt0-B+Skbyodo}vEp{1krj=pO(MqYAr>#U z7AY(NTP(R%$3RKMa+?(87Du%+aPHt`5#Wuf^kzu&;{wL<)IM>M%m(WX(xI(D!!tAF z>sm!-*#Vg=4P-ajh!~1jhfEZ4nE(NsBc(ScXYGDW{*2BA1EhpuvCB|t^_fVMXZx=gSfKUeb1c?T$qgCoC7zG0a Y0MK@LJ)eXi0RR9107*qoM6N<$f`tGNd;kCd delta 951 zcmV;o14#V!2f_!C8Gix*001~<+;IQ^1BppQK~#9!?Uw6LQehm&`(v_t(WxJu{ zQmfFUvRYY*7rZbPMI9m_A}A@|1x+RL0D_l=rX!i+@B+hYq=I-UG_x7HIYY&*e(P*! zE|Zrn-1FUUXW!>{p66`OXW!@9IrJ0hpXd-mfIuVANC-3njekHRA8B?{=;u-!2nvhbdA*k1>LEWtNkAI_V#vQU?Ak0LZPVD>ebcNpr9a)Mq{_z;jKs{YH4X9R%x4bH*Zmh z6P=q^1c-I*25K}NI+ap+WK`nrMB`*rQnTJ*&0y|5h`BsQ-hZA{=w04n-lu_ zE>pnKlMw2I09q!KF&GRm9*-v=Ai(W*L$sHdmw&-XrBV)uqf)8htynC^Fl=LEgSew# zP!r(NgxtZl^F+nznLL1a;Sy=m{^?bnl99$Hpuk&Ueu-^j7H$K)|0E&?;!jL~AOl>u zSk9fl)ZWnzoxXt)_%e$vsI6B;$0k8Xtj09iIkTU;p7=U2nh|vpM|o3K`V$H#c-TI=gcC z!jjTTe2CZR*vD8}qOrFhd|A0Pzo1Cn*bJU1OsQd{X3Whm44Q{%arBpkC9`v`%Bq^` zntI3dTuf|&uC3Fx@D)sRi>}AmS6ip>dN6m;e>D1!Mna$wXe0z0fkvQ_5NHG%34#7! Z^a~*ZhhP0-rab@v002ovPDHLkV1h=6&wl^_ diff --git a/tests/ref/link-to-page.png b/tests/ref/link-to-page.png index 2dbf76778ac7a169abf2bef3b407ded9400b77a5..d618f066b91f8d8713610e51ed0d8aad06c439c2 100644 GIT binary patch delta 870 zcmV-s1DX8Q2mA(*BYy#{P)t-s|NsBy=k3hS*2c)tl$M^!%hTN6=8lkMm7TS>zi)AQK|@PnV{6&lfK zW@vC&T4H2oZnL$$z{1LSdV*6`T|h!gb9H?&Gds}I+RM$=#DB)lD=jslqpMO>T%@M1 zUtwvesDVnjg=Z4B#n=kjE8`Sa+<$2cleH-FHt0#7X~ZI_&%eejy!Rr_|5VilG45k*!DJ zf#C1Itb^uoWCq^U{rXw=+V=cKN)3op6(%6xzWVWfL;@UgiVYmM8D2&O!qoEaZAhZn zNMBo$vWk?pu(8BaSj7B-_mx^!9!xeiamx(+uWt})IywRR!cR&(4=UZRe#>28S9!!< we1ChZ&^q64q_i@jf3CB+o;!VMb-Igi2!BPFGKh_5>Hq)$07*qoM6N<$g2X+-g#Z8m delta 960 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 diff --git a/tests/ref/link-transformed.png b/tests/ref/link-transformed.png index 4efa32f3c3fa1d4b170f1d3e95c1fd853ff965ec..c391f080bea18465a3f7915397f091531e5818c5 100644 GIT binary patch delta 1220 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 B)F%J{ diff --git a/tests/ref/measure-citation-deeply-nested.png b/tests/ref/measure-citation-deeply-nested.png index 596c351ebef651b56d9a9d4e673de6476cd0db85..6711fc732eb40ad1c569f1f589486efed7039e5f 100644 GIT binary patch delta 687 zcmV;g0#NbB3w%F=Rld~9xZ?7Y+N!++P7n4o!kgUCOC(qx(Q z*5vTX+tzWRJAL}@^m~1U(`A{wzQ!XZEzQr@+}`Hhin8v)*X_a9&Q_1;uF31U(do0z znw+G^J%8YnxPRrN!pK2^(`TCR$lKIuoWm}4*?g$RID6riy8G|+%Swl4XmHBR)RL5( z?!?&1MTGa^>+|#W`T6?&{r&8{)BpYcet?L!xWM$-<(QhHsjIWbIehcf;{EpddVGYk zw7lKl=lA#dF%dOH0003aNklYH;gmvd)S%p^vqs%ArgsR@~meCW)GYY(ma5rJLhegB!Kv-|HFB3RYs&KMj8~QuBjyZL{J;pJ90Cz8-$5tlB(cEH(r^6iK2Sri59(uTuIs`ls0KQfV z@2$lFf1rlJ3yZ5;ChTN-6f6pN5VLNlVWajB+$h#=Js>?{7Yf*O3F}GxnIh4D=nF1k VQ!KH&%+~+_002ovPDHLkV1i^?n27)Y delta 687 zcmV;g0#N+=8p{`&3n z`t0)Z(&7F1`r(zi>b1`P{Qbo>dE%SB*m$Pbb)@#*>C$7E&Q_27^!Ud;f9>w_-;T8D zvCEpAr1RF~;g-7Hin6-9zrDZ5T3lq)WtsZ!^vzL>$wPzZtbfSrw$JRm)5t%7?ZMXW z!q?=Wz|v%y&{~r4%iV*7h-++cO;1;|wY|K)#v>&ywz$B}&)4SW=74~Jd3kxv%*=v= zhrhtYtE;P-nVHSa&4`GIkdc+KvbOf$>f3;;(O#9)W}5KH+r~J1$2)!0Xq)cC*Vb{N z$wGqZv(4zP$$#s)(f<4V=&#Dc!otD9!O_vtl9ZfgXmE~>j_K*?<)gyjleqWd?ECNZ z%SwmYe5lY{lgL4V(`TCR$lJp%b<}B``}_Rv#MsG2g!kd=_1);sRF3Su)BpYc^w{Ob zIeew3uZM||dVGYkw7lV$y6wQ#_xJgs<&BB}009(9M1Mh4c-rmM#d5-66o%m!{vg5H zQtCq8-QC^Ys8OSW)&h55CJfVDK+;X~EWYhKXLB%_Os2nc8L|Oz8ahKC$?mQsM4!k> zr_;6N*||YgD5J`JUkD+_#)UZlw4jSbU2v1sbA%j4SzmQ4O86L+uOJ`(5~5~wM2HW) z!oZdl-hatkTLJctV)yYcvKdQ*p00e0>G< zdXOIR_ZS(DzQvrf>@0G^;Q>%#1xF`Hk>I0%+6RW^@9VImgS`khLQqkO;3?CHz<#>tR-koOxgfASzIu`smb)e{QxAJ VGDAqH|Kk7v002ovPDHLkV1i#BqpJV_ diff --git a/tests/ref/measure-citation-in-flow.png b/tests/ref/measure-citation-in-flow.png index 18617beda04614c0e03cd01ce9fbfd20d2b0a127..83f92aac4e686fa740b4b7a0ff30760a6b4f5db8 100644 GIT binary patch delta 642 zcmV-|0)7421=aFMd{ z=;-F>?dRv`__ z(a_M)&(F`t$-kC2v$iHU}YkcWqdgociWhK7QKjDdrTgM)*Afr);8etdjQBh1zR!dD)lW+kW ze>potH#avjGBPnSF)Au5CnqN+CMF>#D2)IB0Ub$1K~#9!V;BXaU=)l3Du5USaHXro z2?*d%SBu3EK#;x`rv%D^Y}JYm!)r0Sj~taP&Ij6RWGyBpnFX>qfEh_n*_c5d&QCI> zvc(x<28ZeRxYVR$pNC3-ohFlQPK=S zwq-Q6m?4kXFpZywfq}JrtyI6)CRlXu)4r$#2fy_ASTdWcstOl`IP{!Lr z$q}4Q@9*#J z?d|OB?Cb06>FMj~>FMa`=;r6`=jZ3-SWoSd4PnwOcOnVFfGn3$KB zmzI{6m6es0l$Ml~l#-H?k&%&(j*yLxl8K3lhlhuThK7QKjD&=SgM)*Afr)^CfPa5~ zetv#@e0+I%d3bnuc6N4ead~lZac&xJaBgmHVPRoiU0qUCTvAh4Pf=M>QBh1zR+DW3 z8-F)9H!?CZF)=YJDk>)@CnhE){gx^I0003XNklv!OpUekW{9M%#d(4ZPUQ*=3>t+%jupsa)@mRt z(w#Oz9i9ZUxKu8X!_Az5fu%}ERm3@-Ay-?d*qmF*uuwERfw~s+W%BVU1_!I~@la)Q zF)*n5T5Gz2ldoie5u=w6zqSjbV4x!tgR#4|0RweGDL;f($Y!7~u(am|QL?tWqZQXE d7zINK000%rI%GYlM(qFq002ovPDHLkV1m*LUTpvX diff --git a/tests/ref/par-semantic-align.png b/tests/ref/par-semantic-align.png index eda496411e511b17f950f54a2d827e07af63cc4c..202236efe12c7fbf6612c8b8e879a9f1a62ac7db 100644 GIT binary patch delta 3099 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 delta 3076 zcmV+f4Eyt-7>XE>B!AIKL_t(|+U(iQt6^~*$MO3g+`E|d-P{F=Bum7~!rEUjqbx`^ zSeR)BTS`KNl!ZJj=JQ_t7Qb^AuR5o5 zzP|@T|D*>H02O{G#{Sr*Vj3I>BBk*LvV5K+6`mdRv3pYLvu-EI$u!>iRwB9Uyj zTR;oxcsxp_Qo5y5spj+f-JVP)6NyBKNUzuLb~``|iDg-tO!oPfTCH9#mqb*pR`dCM zG#dRG`f|Ah0)K&IG8v1-0R0Eh&(F`9TVk=8gPu;O27{s1YH2i@Uaxltol2$B=`<0! z-EM9HXui;LxtwlkwOS53pU>NDw%KepnM}^-^BuIsVqqADh)SiB!{Goluh#4J%gYPh zQYw|h;cz@2D-?>;>9pVPIoWtT?)UqNNTbm#7K>~)YkxMIiOB2qRw@-h^D2=@SgqFY zUmZ4^Effk7Q7)Hrxm?9!k!4wr$HNKq`+d9Jp3CKks8A>bgF$YMcQk%j01aqB{~yrx zdj0Y7@fG3`2gNXqKxnQN;vLQmaX1TT1ZY4bKm!`k2+)8=LZN%T-qX_)<`{h9pt{|z z@adhy;eWsogHKMU6Jt`$qX7+QK>x18;ZUp97={^*Mnu$XHtY3zzu%`HnoK4}qj5MK zmdj<6$@J@6eR=)H{Pvya8Iptb(tn8|910oSgx||qoHIuRA3o3UIgb_P z^-UYB)jl{pUS3(ne6A}R8iv>F73QZb%Vx7#FDFWp)ND3KqmkWi|M(^C&db_{^zhUX zBXx4Vy|b$=#nxPdMn%VBF};fftfI`3a5xOmm;kgU(=f&+KyP@q1lC)*etLGX?%vcu zV}FZ4U%6ba)K?OTga8f23kD1<8Nc7J2My;a9*^UY2?m3lb3JH`>zfLe1p2Rv79Rx8 z5k$I3B%%lHa5z*|6-P>;P$1C8K}Vxe*o)$#9yDY-VMgQLn9t`4^i+Z2$hSEPq-o7I?Z4=>mZOmZ4B+JRa*o-#@nZ_Ky}8 zmkPx)f&K`1GBTNr)oP{AaArZ7qu#}RQhy8Q zyx;FDibA0OXA6#Q^^!700LnOMd`H}Fw?vsDIM92&9zDvUXNCltLZAsWg+LQ%0!<;% z1e#(LH2sJ%eT;)nCOEFFZ%np)X{(@C!m)tb${{ll3TOd4H#snPX&jEtJK$<{P~@Z9F*DFIyE)M zPaenW=m=fE7BL#oWTt3oX>NF^`S~dTgBvXrYxXSr4eLEd1Db?@mXqU|I>i=(jDL(ofC2sM zXBseRzjz)W8L6^wuOC`KA3GXYQDF$=Hq@KUne8wd(B!A+!-oQVedYJ;_T9D9=k==u zw194IGF!O73CIO1i;q_y4QOHmT1JK&9?<^&3TIA-ynGR_uP+R#62E>)*|OQIveF31 zj)+jXdL?}23fIg`Z6J5)5`UM@4vW!%Cbre#>@2-{6|~h67^nzrU;u-7LceuZmNu|t z`0_agSWqM;YMeP83{3ApaiGfnKI^Vd%kD1AAK%kP1A4T>G8)jMV;!SlG@wTVdNiO% z19~)|M+15kj0W^*Ko2zdmq{%&8qlKwJ<#CKpFel+L<12pn1J(b_kZq2zkinmWIuZf zWWy)k&=GL7JwIO;E(;tGE-N#5a6e`=pmBoVzkjEtX#q!Tf#J1xk1sbj%cF;}KsIoU zH!e=?^QRPCiPy|b?AzB=I5#Rv_0q+#(SXJW?%a;Dww8c|m$$e4`E#K_#*)R(4khPG77`+CWtmI4D3QiSkXnY zA}Wh44N?bjCN+yP@)nhgHj?N16{~&8nr9(8fhqGwNgp5T6iNx2v<3jZEDv; zUkqUjf93*>yni1DImemVJahQ|GvDPspHBn&(ZfT3gDxuC+}V-NQm$iu^MmVo>1IOa zzOZl;6Wgctw+^tuaT+Y~5+pp$DN~~|K6diSqJIp%%fZVFw0^J%w1P?sDhaeq zpaoi>1zMn05;o5eXo3FkK@(l1PIu-@0TFQmE*;O)1v)ICK^z~y%u+sp){YLN>ma{C zR^mlxI&Bk{xO~uHG^!TRW=q*b<^;Y-^DZsh#=I9jjBB@<9L z(DWY)bbqj*$>^|)&3lm6WYXQ0LDbxGYb;4aR9Ed1=wLy^*FclRtgPJ0q@_8H3GwoT zgyk&TT3tLd1v)6uIOWvT7&1EK=OIf&GL1lGWjm}Ag{ld@29iK&X)#;{A<#NU3$#GX z1UkY&d%a#RgE=E}V1TNts|ye`c2DNmBrqr?M}L8q3A8}V1X`d4S|-o}EejO%?Cfk_ zUY_C@P7F}oe;9c5E+ZpDK@2D6cDofORXkdtrF$0vps6*W84oqshWknU73e@fQ!cY) z$s)ftDZS?xE^M8nh)(mV)p(#{Iyxeu?m%}maQ)Qi{C_`mOp55?0u4SJF4r1UL}!nq zh<{G07&0w$K-2eoxn?(t=oE?Zsz5sh9UHrpW@sCfGy zYIc1yg{Y&W2vAb8jaQNJ9vnCdUletC4}bd__ae}aK8!EM;Zi$lrw%kLu3y{FsiVIy z8wYK-R?vPy*&~YRwO96-x<5iMh>URT)5B0dSOi+J4w*m;v_K2AKnt`?panW&Omb`j zy|C>2Y;jih=8B4agM+7hzGDBFnkw>olefgJ@c9Hf6gE9w>^>6zX`+}hJaV?G`hQS< z;V$OChrJ}wAp-5m`~&*K$1_dMIR@8;CsUxq0GiR)pYO>OXn|fZpnLoBXs3gz=EzmN77HEMMXzAVww0}T{6Er_b5hgW7^xWJHNEr)sSU^*Po|w3Daw3x-0B5R%qIUR@s7rO;SV;F=*7ziODkI>U{+6EoXxF zpkbCLB$pDiynE*$H|7d-u!`tFEG^xRBD&q1z@5%;=Q(LEn|?R8d3pH`DWdBfEzklj z6X?hX-P+n39UZ+^=D+~e($W$jXlzpE*d#D0B}ajl3A8}V1X`d4S|-pDYQF#wuSVm( SSXqSt0000AiPRQN)5^NxsR>+<7xN8h+4u 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 QMF0Q*07*qoM6N<$f=JsB_y7O^ diff --git a/tests/ref/quote-cite-format-label-or-numeric.png b/tests/ref/quote-cite-format-label-or-numeric.png index d1dadf0e298bb70091fcaf2e71012ac55d99d387..22654a0d71815c7a4a75eed58fdc80b187b4a8e7 100644 GIT binary patch delta 2131 zcmV-Z2(0(|5a1AyB!4GKL_t(|+U=R?PgO@0!2M^Mero*Gs7;HMMHB@Q1rbpeaba-* zMMN}M*~FbtD~ehLR44^m6cCVAc94AsaRo(jR|J>#m!4$uA_l_;&3oxg$mF~;bLZaq zoipd2bD8mvZr`NM&`ETH4#;~gCxpFMl_#hH&EKVG(M*=IqoSg~SwczE*ArAwDS zyVS_Y$is&ZpFVx+;NbA}K!=2cR8>_O3+Ue7-km#l;*b6P{cQf;y?bit{rmSF9UUJ& zd{7z>4-bIezkh#U__nvV%jk|BJ03iEfWQz}$>fQN3Awzpv-9Q4m)uCLvv1$N#Kgom zZ{DyV(C^&2Bl^2{@6xNrRegPZWo2bAUc49>7{JmfM@L6xK6e}%8sbN z&mN|oKYt!-bLPxJ#t-)H-CJ5(8W|bc)6?VQr;CYzg^XJlkJJ3FJ{n8MxP z-{0s!BY(KNyVLXNV`F17#JS7O&0W2Ewft0b)~s2BpSrrbBA-5eIxjCzK*K9PKcAmC zyPB_hHeXXygCJK`R8**;z`#JP0(34fEG(2cuV23g`h^P@WTreuQc@Cg5SA`o3Q$lQ z-bKt0VnUqU(TG4-R#vWEyOyJ8V`BrLl`B^&zkdTIrxJYfqQe34Py{C@Cr&Y&?CR>m z={Vr)*RMy0OG!xy?k1OG3{C(mLy#3>Vq&m?4B`6;2?-npZYq}J$P?)6>+8$Q%N6J& zM~<+pL?J6HD;C7%x$mP#kHW*lSsC1%q1m%%V`i2@LtF-~pPwH+GmjrXZY-d2+oGZ( z!ha||k4uk7$WUu*E60;3=J@jy7L0>*doxGg4ZK3Jhs+^@-@0{+Wf@`uE>c`vOtOK; z@Yt+BI5?>D=r1&4$iagLo0^)46}n*gx_^yVG^a#YhPpD;&1gY$NJ)>TbxBlI6r}`j zFy2U0Oe0lTw{D$eGLsuGD-z1ddS1USE-oY+l2DT!t0AhSckkYr9%w#OP=g>$7j#NW z3ONQWr1+kqg@%So!8y52abGVlFDWur60Nckk_rk-G9ps~9UB`P6cmKVxw*NWJ%4+4 z%a$!{gG{cVpn!6YWNXu=O=P1aRlNNvy;O~xnwkn|G$h5y2M!!y4plE{bxuwW)n`4>EaM(ti!aB-bn+9UaY?AiK}Z%;Xlx-B_0nN6-BpU`X8>1))2nZmh^?&vCWn*@BcIZi*QDO-w7=mtVYaAUx#C$CRx&$Sv8$jdN944w2F$@+3@%HW8306D|HI6BP=5@qp8XS$| zNqt2;C1P;Eal>=x&QV-Z8RI+bnJ*G-n@UeEq-=vMP}sRdoAmT_^ejTO!4m;T3_;kn zYnKes_!xBg@@0x+aPt$0+NK*hfVA6e+{~C;QnXjs+#7s^eeUX zZK-KF(^`oZl*;gN@awOrWNF`%{-8-JJZv)R0)^XY-x zb{TFSUO%l?Cc!%E6(4TqeTyQBZ^`l-^fynGR?=GlL;e`gmD_8)&T5uk?p z*7kn-g?3K#Ou&&Z=YJNfOQdgV>A|mWGe& z!^~ODt-aU@G=HV#bs?eQc=s=JZRpL+=V#><60eztj0X8)YF6@v4A!x*TEybaBm^nY z%IdR!vLMQvHt3O&$7jx_NYG^SvbUn*DjbdA(A$5H2%z-%GI|VyOgv?RVE_REqm|W7 zVv=hm)d++IluV{a$;u*E*S2sbu%Zg++=U+KvYM~|HcYY?wJf-iBccwQD(H-iO#Igf zljZ1`*vRN3pQLvjBQhUd8S2VVHw{{Y)}VFMpfzaSH0W>D{soAyv-~P>!!G~;002ov JPDHLkV1hvd3ZDP~ delta 2157 zcmV-z2$J{U5c&|1B!59kL_t(|+U=U@Pu52e$NSGTebw|;Q*ByGIYg--q97v5As&Di zSVTmF6;ZsZlv+`=RltK%ZUu^z8|9Sa;l2+AQ4}8p@oqoqB%3EhY;0^|V#3A6<--9T9v*(<#tm};x~r>e*REaoV^2>Ho4#J9h&`T6tbGQ6#=?eXKsETra%iHS)} zOnma>2@?YP(9n>`hlhu06)~#2ySucsbaHaCx3?EdBYg1Sf#|d1$jAuCf-V;A=;*** z`uh50Ebkc^8F};OP4fbp-C9{$Wo2cVy4dN{r|CU>_6x>UewYHHfJ zaUVjg3ueYO1fVFStk-b6&oD`Ps8)j*gCMQI^BlOei{l zyLazi)Gl7U2x{}^&xgkk_U+qOQc@Bf9o^a484wUKH8thz?2NLtwRL)Wy1TnO5{@aj zZ`!oU?04v2@sxVgD;irHj) zdpl0Y0pGA;13b7C7Z>AhG8|)Y0+<;Dnc>i(L)bvN@co2@1dalWisd-+1p3o&<+j` zf?!=;opj-hgdH^1guDPk4iN94;x)Fx9>g5%jgEX9_=I8)(IJ8l3=A+WT`1rpMMXs< z8+Z(_&HVlS{W_0+2N6S#9zEL7&_Jxv1%JzjB|g!d5?vYU%21Qp0?i>MJ$kPp2M!#d zl;8`-7ikVPQib*F*GncdoA_9fP|lX~`StMdAlZU1m=B# zzH;RXIR;or@jVAcL_|ozIh*EK*U!&SicFP6t89d%g2IxF$dZ7Ljg1Wr4aMWUynnpT zpFh8C+cvgACRb2UKsiUUwPni|vQd&MzW$V6s>V%8NdagiB*pMYjvPUUs+Y7nCntw; z4zDEVBeS-(wN*fqWitvPyb#_>4oArYPu`aFKLIhxHIpAac#tzecAuG<$rAA7$ecgS zaPs6ya&3zNnr#Df)y`Ajblkb^Eu+0 z21ny~QeP2Ii5MJk-0;GM3lvvW#`q3<=0<{TQ|ZY-$~KS%6m~ArCM_*ZF7Svpcp|_N zLtu9A-Ys1amqAyrUZpq&Zhw9PBJ08>_7fYB7mC8n_GHle{gc6Md3zO ztEf{G25k-%=2~9ylBFw! zk2(B{1prOSH)2Bk5o8Xa?H!hE4GzbZW&?d7=7hIz0IoXQ4HrAR(*h+&0qn%J70?-3 zfB*X1@9mv~w)Rd)!oq(7HI716O*01w8@Mj_nhi9MrC9tex0uP%Bm^m-mDT5c zZ9|l|Y@qqSgN%%^`xPpNy$e?$B-Kgt?(TkT`2It z%hTT8SzTS*)6)k(IXTtb+yc|zKgbZ2X^qCm%q)Cg-)6wXgt5H5a(w(DpFAY2LOVD( zEVFIj9y&ceqoy4ix(y#28^=$J1e%LF`L`f*BmUEjOUF0TJ1z^!6m`L(3l>cpXbrRm jT9XD^1FcB|{jumj70?|s%JFIwpiBw5CU0wXzyWCR2xN(RX}D#@G`6?ONiI$K?DEsld#1M@Q6u3NYI?LN0} z_j694drlX8^B=7&O8G`Zpn=vvYefUCf!084MFXvY){6dtet+`hNn2an-1bkOJ~cEn zynXxjYX^M({CP`D%fd~V*)nH+0Ua3`X>4r#?Afzlm3;K*k%@`PXF;2rn@>+q&+W>} z%KGe34<0h&;gx0hG^<^;YE@27j;@Jax^#)&;D6v?)nZ^^kei#kZQC}1)!yFj z?Ci|)1ax9zB4q9D?Uk$9xN##i*Q{BSo}O-NYYP|YV$773ly~pmZQ8U+y_DCXYz7n= z;QIRdB9@et1hthbS5n6g{QUeXDk@H%JlWshzjyE6SFc`e-n<#%)vH%$W@eh1nbE>F zg}aN3i+|pMreb4b!=0(m%*;p^`>vp%z}nhc35$%KogH>+VPQc%CMKq^uuwq5tEi}m zpBTH6S9ca)Utdo}o~o{{R$Ye<9YQNW=kbz~5|MfR`Zds#laobKrjd}4fDD!G+qVN0 zl)7&b^@A7}CvVgv&^0wR4h{}%Jwro70PWhfOMlrNDA|?ZlMfvRh=o!yH8o`yv&i1w zUW|?n?&Rb|9WLeN<(Qj1jxyK*j0{0W2nq^92hxS@M@2=k6?my=jxCR)Z)$3)s;W|; z!^6WFR=m)@uiWdAbaQE-u&mGB&7cc4yXw0^> zw0{&g%AM(Q$0DSwqoafE$&A_l{DcL)Am%O(x3RGiSSWxNgrNya9H{b=v;%_sojZ3R z*xcMKT^J*72T3&`6QGd|#2l2rMmOjKpM$=Uk#7T^Q1l@(c<`~YF@~iJ0ZgQ~h%_f!bX#VBYsZ*p9 ze8Ko4eL;*+!O_uCBAK~`j}-ys+;%>{J9q9R*pPsl=vZ};CC$#xEsD`C8Da zsj0*mu#n{Y3+3q1qmppWEnj%=o;`ac$y7nKibe=3NGyqnmJ;ZQh={|74`XqbmVcJ9 zv9Sja9%LCra>d2Pq;mvYZfU2254Fcim8W$gdjuKOIV$kmq$8> zRTA?NS+8BYR)HqU<|!K01h7_OI8q+!#BHP`6c7_!Gk9QNAbW!7K07;`S5PNLX8)mv z^XJbKYcCnlEE`O+U=Xt~k^*;kcYi|Kef##YFe4))+6kO#L=#ZZ1>M=%Nf1UTjh@gJ zku@@xFJBG_2q2!7bezmnfhGZgrmhuB7U$~f3N_SB{p{JZ1jT^HMW6#zE*2dY7DjIA z>FJ63B?F{!DS_q;fr^!t6?d!#GXyT|L;!-^u|d|R`yfxD41{?^j>-T;1%JenG*cKy zj+{tXs7M`qks`rAJ(;}dYQzE+oG7qjhc$B|!LrHp*^W#VY69p|DErROjTp_&&fk0Q(s}(_)XR|H~(^xbzm4%h8E@>|L<&_{3|- zN5v!=ZP}s3u=?Q8b3kElUCS&m-CF6y5f6zUYtF6M&1dM!WT)EO#FPJyP2 z3#MW<;D!svA||Gkp{T7}zgc%Xi>G#R^MKcn9>=-E5zng}0@OUbeDO%)rXWb=r*qL* zWoCBKnSV!~3=F4^vQZoIyQ~}{FUQ+Ex}g<*QeiYE!+*??;m%ODcKg|yyh!H8VY*&K z9k{c8vWj{~=YugsRmDNqu@rmrytehL|orA!o1)$vP8rlUGdtdx7lF}>C#dyVXML?VtGU_K| zio=B|TIVVGzQI&JZfYoLXJWberED5$#C8UVM|! zUHDn*dYl<9{j0ZcWO!tP%EaWunVEO9vmci4SqBsr!L&!mrn!@dYoImIiwl|$8W94K zvTBjwOyZfj7hws)L@a#z6R55<(A0*)#({OrZIj`I^JJ2)MXJWvDui=fHT zFMmYFhJ{}w#9Xy{gQU(}gvLjX1rTcz*OQ3)`UTTSR(b6BDb7+vX20uUqCmv*uHPic zBZ4NN1UfPV)CdrLQ%^{|w%kGY^^b}%Za1}3CmwP+?R6*DCwwOLKnEn>P(~QvLMA0L z=zvWCXzFMP11CfBQG3xLDpyZSIYlx!^o?aFE}NqQdxFYEGO-7KjnrL@b`00000NkvXXu0mjfjs12F delta 2883 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 diff --git a/tests/ref/quote-inline.png b/tests/ref/quote-inline.png index c09faa3a86db6c3accef91c0b6bd06318a1675b3..9205d68392d88d7715fa6e517d0e6f31c07d7b1e 100644 GIT binary patch delta 1458 zcmV;j1x@;$3&abMB!5g$OjJex|Nr9R;$L51=I8C_=jZV7@JB~S`uh6l=;*`4!}b;82J?(XjP_V)Pr_}ACh(9qEG^77u^-ue0YNl8gcN=nw&*7Ni8($dnm zx3}Tp;b353d3kxLsHl;Vk%)+hqN1X$t*v-?cz=I?oSdA1fPa9Uot>+ztEHu-o12?f zR#wr`(W9fI%*@Pzfq|r?q`|?#zP`STi;HS(bXi$hMMXuHmX@-zvS(*!)YQ~bQBh-K zV~L50etv#^eSJbgLi_vsTwGjEPEJ@@Sgfq9%F4<(I5>QKd`L)0)z{xwS66#`d!eDB zRaI50th9=Zlz-UT;*5@#tF5)HuC_@_Q>Ur1Q&nA?ou#t0yg)!euCA_xgoK8MhR@O2 zRaakmdxLIpc-hZAnmzS4{ii$ZoIYdN6US3{nYimU24T3Vo>pr4Cdy?|<)baByyJZsg?TY;0_pn3%D#vADRn+}zw~XlUBn+T-KnP*70m>FM3w-SP49 znVFf#$H&;%*yiTu>gwv%)z$R$^!)t%<>lr5{r&#_{_O1Ryu7@Ridu3200T2gL_t(| z+U?d=ZyQ+vhT(S%v7JmE+u=CmFeini+@{OC+kY-IGc%WYi)oX#VRRg*A!dg8V{2Jf zs@)5#k}Fr0{77>(pY-b793ArV^76>u$_)emQCvbwd{oc>c&5mxq!>VXhP!ccL0bDk z$sLK2ba=ahls_OP=vs1;QT-gag2j8e8{tUUaYS!(tpJfw45(8?iSLQgq9z&vRVY?f z?SB9on{;(S0oDPkr?-~66HExG10Hw8uPg&as}J7k($%@vK|oiQU?xZgjw8XJZ#x06 z<1(;g*IpWX?aCJQme5TouFyTyc`E>y&E=KJnb&5A(spx~;);3YyHn#A&V?T>ee3O> z@S1G=YM8kZgv=uu`f1HM@@^`X|0Ka3>j<3p+kd= zi^Y}JNf@*Lu3?xyE7!xTsYRoIgf$-|KN-uV6;_F1ei^6{GK7!)6)A|H4ZytHsi(Yx z7ZR(6_UhdR`{`2<+YzbM@2`O^-O2*?m2E$vg|U+cR99oR)t?SWu7{pf7ewlP*MEGg zmtbMhQ-Dl(c1o+d8b59iWeU$pRgzlrgYkk-N12q696X^cmyG%v0Tfb5s@3*GxK~L2 zstV7lVSRu1Ew>b9=>$yQ=#NT)Z6$G^@I6!;Ym+NO^{mS+1vsPO-kSXMGb_1PSd{ef zCq+etpO;U@#wRBfQ!ghLO6nxum1CK0N z;{L@&-O%EA*kT?pe=2m~@B{$zO``bnOQN_b+wjCidd4QdX*5YUQzteUAAbXS;=kD} zG(SI7JG*FEGB=y1`u=7k{7Uv=ZTEx)Q)}ew25`pC&%x%}I^b6ZjPoi@!;o$~xTEKKbp;QX8IS@4DuEqZdp^uL&s5MxX2S?|i(xY2i=2UCq?@^$vI% zNQXc8@S{{Xr{rIKDJH%5ek;zypMN1H9eh)s^D8s0F#mAO%e#;M1-wcAMj#&85dZ)H M07*qoM6N<$f>`GzX#fBK delta 1419 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≪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%0D39lxc;L146rmSK^d?(F81; z=LPg5uQr>_D%|)_cO5)HAypxG0bN!=7tjTCSpi)@mu>7P8m>=cExjdq00000NkvXX Hu0mjfnwp}d diff --git a/tests/ref/ref-form-page-unambiguous.png b/tests/ref/ref-form-page-unambiguous.png index e7baa2f2ca7c99f8b6edfbe87486662b1942eda0..3b37f115b88c380a360f39b7d06417118501ade4 100644 GIT binary patch delta 2923 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 V(Ds{Z-01)S002ovPDHLkV1mQ$p!Wa( delta 2853 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 D37&G( diff --git a/tests/ref/ref-form-page.png b/tests/ref/ref-form-page.png index 52fde86d742c523fcf60dc5b6f6932236dac92b6..0cc29a4f1ebf1f6266cc0212249a0c8597dad4a8 100644 GIT binary patch delta 3559 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 delta 3590 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 M07*qoM6N<$f;ONju>b%7 diff --git a/tests/ref/ref-supplements.png b/tests/ref/ref-supplements.png index fd715339f7467d29b3e6275cf2c63ec74f9eec06..e400c1feb523adfd89e1f09f9e7b35712f631658 100644 GIT binary patch literal 8167 zcmVJ@ZNK~Z!oyuGn_tg`hDyCKF;@G{GxaWm<)=CfErDWelpPXd<>}3)M#on1vUD6 z^yusLiYH?D`3_n%{-@p@kHWqbvQ8o%F< ze8}Z;>2$i)YN=GJcDoJxg25mJoleK?cKdw3SS(g77L7)uSS-dtl}bgc)#59djua}F z%aKUL>2%V+;C}~VM5$C_i*ZRdn@xwq(d~A7y&jLpi$)oMkUxPxj4M$-VV*K06~#Ul3}k>O$l-(a;`@fwiMmh1z&a8VS({UfMor8)|EL&@?87kTiV|a zG?)qrZjp6aQC3&0L~AXAXrM({g4{$vpdeB}0vj%+P;MenE-eH@NHL^M%WeBhCpp`+waR}^PO|%%ON5=4G9e*ByN@m{EhWOL&7h#5j*brQ=kPag z-rU&ONManS@YbzcxdWl2;}Efeo={~)+3ef5kI+G|Cg#vv{x2TEsf^qzaPd?+C@cEo zkt#r0$M(epB1FVN%B z?|q$+cp}0aW{l7qBJ{^1aKcZTW3R>M7ZlYuw4FYERzS}eG^5h?L*B!~!@QconV6XT zWn}cHpC2_gJrvLj4BFlVw15`S0$M-|XaOyt1+;(`&;nXO3upl?pdC&|!r9sRiAy?| ziR$F+Booo+WuBg%yd>=F>zggXz(D`K;=*0tJ4M}KzSiB{os*Lj7Z;b2k&&C5d+#s9 z;T7%GtFc*GSv@^H^3HI@Y&rW#@E+(J_w15^9&;nXaKnrLA zE%xsMjYQ3vGiNSdyvU;B3l}c12hE=+o~6d;X0!PCc-ucQ7E~Jy2AsOOI+mW#mW+pZ z4-XFquF*a|KE=hwuseSIIPwH2q0Et(Np)UcUPwgDmVia%8#itQneC~Kkr0GeaClN@ zDfJvFS=#Qvpiz#gsHnhU88>POjJ>H5iKeC|hJl#xoSmI*KabyNG!_*Vsm+;OkY}Pz zAf_;Lc{=$pM;#@F^z?Kbb~3uX&dA6}=07UXG$wCvZ^{+K>({SGd4VDi4h|-n zUaxO&Z*OU7LFz`G#i9+4n$2d$tz2DQ6B82=CUTe>&BgG&Wy=<&FmdI|73#013^o(= zOk-%&WHQCX#Gsy4SXij-XXp+cA#D?s+qP|kY*JDZn+i0}(W6KCC)$6Y)X7H`8=KmG zcX#)cloS*!Xd-IUF+zvGzrX#W5>;!^1fw1ylf>BOwDbs5X)&5OV1FnyG*mf=G5PxX zYWusox(M9L2b`+l)a#Lv5&V*p64eb+OcDYpPye;R1|R8gAY4ovG6l9Gp$xDk%4%H4{M5H8p^MhK?CMGaV^E%5U+9i9C+?hJ~vll2D!LpoobJ^(w%AO%XlV)1ON_u+M?b z|HuPNU|=8-kb3}RH6gyRnNw8>^zvkA!&rwxAH}^F&Vpx-+9TNtXfXjTpary;fELhV z0$M-|XfcNYeYfs}rEvH4%=fbjK8VhEFZ$N@FH*NBWPOr)J0!bES5~F7)&*G~oJ#)o zqsv+ErdA%PXgycb99r6{t7toEeR#g4DY&fZd|5-(S9kp5a(2eu^2#prscQQyKJUYf zySsjPp!=>xS8(62vi69zAuy-X^IGOx*KfOreD3P|={j?ahbj8)*qgg;&)t?RdhYgU zuz%GnE0#OkoSas#S^Cn-73;>QMkgi)#>XBH{xLc@II(JKY{P5syuM-G<29>nFQUZz z{qS!;E?csE(WKMrm)~5nc+u};{VP|z`i7g!eZVfGCpJIQjtO zzG`$1-ukEzL~XNG#qCb}o9~{RivGvn`cD8IMm{MZt#e)H`D_tE%jFnvr$ukBi+;R* zbizJ*N}?arN>&6R*|)QmW^cqT82yIN4ZyE-5G zo=C0Llr_tLKga%H%1$j@Or#(E)89N9zI^iX?I-QlfE=GjmV6E>K^I#xr6N5LV7M0?lW_C^&{gx>g3;)H-IgYXCM{bnXs*}&|tpKz{o3=j+ zf`XRCvZ+5>+mCR}G16pdr#ZjhRB!%NJD<(whx9kU_~nf)eSVmYj?Z!+Hk$?m5?)!? z_^qFR{Z%Z-rV=4G#h8SVl@)!%K+Og!i<7Hi)a^PcxkMDGmiWEv)zkOkbQ$d3800DX z%@gZqf42hAv&gdvA$jOd`gSNuLM_fxmV-2+C8h!intO|8S=#!9J{eh0pP#zka+Ji2 z2t5p>jmG*H9ghf?Z$8>On@__yO9N7XETss|ro(wQngNkc^E^%S3=)Xb?5yh_pC&2J zM3p`76(Pm~m#su~upCW`2@lH}tU#8QM6-jJ3K~K|H0T*Ww)>)9HXF6x z`PtO-MYEHO9FpsuFCPBY=vN;*E?ev=A3gtJqgn1e_{ZG7{Oaq+KP@$smNaD*MU*H=3#`=Iyap+XhXAKOLkg0^B+5dZ z2Vt}bqad&$VB;dP7gvxHcG~PpKp*#J-#;4)N&pe>+JIv)qi6&$ie8lE=zZ^5McihH z;T1y3FuH?r4HsD!)mvMGX)ro?zOIW4Z0=P)|L<@AHKgXcCNO~0IhkV)Tf&i|3_g9RL_< zqN-?WYOz=Is4kc3!X8~rwoS(OmM5pLH~Gx$+`Vv<2TFU3*?-eLA`9x$Uyfv7m)QaY zM#a;4nC3ZyQi?fZAiB3LytBo}jwsTietB@S_#PbN_k61@E4!xex&I2xZ0&u?sRTk~jm zcv-3#&8m??-c9YYQCyx(4Tedg{-aMHI)0>Zv``XQUK6OWwqD)6_r;sTqwCkVL5?X3 zLx>ZAHIb8`ARyMvsrfw1roKRXzS>$7w}B`&Kb-Oe-R|5R+C$5>A=QjAT!}iNK$3lYvBF+yw?MWskiKi}LS%OuL6sg?>3x(Q4Xv(@Yz9K6=cdgod@ z3~}yFk}Qq=LX(Qafs;dsaYi#}VNI%gdyHOx+wZNHtDz7}oxFSXgI224^^AoDSD=-E z=0t)?o`FnNOInHVj_hTeumm_hDXW5?B$k(+q4)QVCLnl<#m&}ww?E#y{-BRDqgJXK z3I$NH9I?&`h0>WD5So)Dp*LP2R+?D0D%Yjv?cr!hD0sDSo7&pd=!eK(+RtBpuv1y9 zUJoN`C7>l1wd#zatA?yl6xD7LPKRWzU6BJ?X;4-L=$<@V~ZYoO$CmYw=d*=4; z2d|&~3Xt~=y0p1TaQ}j(va*Ci04^=B-e^tUyfbP$l6gJD&imd#pn=Ts6?*@xyaMqu_JqcdCnLl|wd-`V9?G8udg({;si!gB4n>e<;&6;+0 z4$c1al^x)zRcKW+oWJE{S#eqlzj$uVniE+fTOq67w+`W0;J9k`XVu$aKUiW+b zg%=)Qj2W2iqV>XI6Ij8Se0n~y7Wryi@7%x|Mgm_jY?iUCu2s+w_AYEFd4|FTZlLJQ zE>f+CeEi|X?pxRQ|M@3hTK2i27iHjj=ik#1Ix)mK$v`@oSa)vS`{c9w zv$rRCFuS?iW+5f>0Anfzc^FYGzVT{0;5pAvskT{NiA3v)DkFwVAWdRAPIK28O>su_ z=>T;5Y!uU11K25-Se7ri%8x(2_u})iIdPHJ26W2 zs#*QtZ(ojnsr2TMVv1m{6k~=^RFI%XP9e_KeYqAztuiG z?fv`9!_Rm1zy0{(@gzAq9H%8QhTwmCgPq#upX>A3`gN;5nr8i1o|MBvZZ})^za#_= zC*2XX`1G2}*(>Ei`l3Fy|ftP=wKj+%`v?w>P!3%Y{@C950!U9Yz+zC^@|A&C`>gjQ*D(;lU@vjf|_V1hb{{CQ4{QSEgM6M2Knj$6Nk5YP(#05a2(k`3V zsyIm_KXv_-Wf@)&DMW=qXlSJ<3d6`zbR$?yQ_3w+vJ%jc&_RHv?vN|z6c=b*2Kj`L zpea!84b?O>I}EKE@e{edwN-B^(^2;roxMHzH?vVu^{uNRwe;q)&P%)=d5PuOy2gVX zEbSS^$$CjqYgC3Qp65KzEm$xMmVx7PFfJ&Y1;~kG5JxKkO)(&+fFV^OLtNLT3s2)~ zoTN{!1(OwaGMix_OX}W!i+~h+vA|0cE97Nv90x2ttr(1=R)n%43ULytoe#+P97Iu^ zr9={QHz@>~B`EPCE6yk?%e}eBAwpA3N~9voK~9ozH4+V|*r-q$4if0!Xmlj$<0zWV z-OK$O>r}23X;uzGYq4;nkQIpSTv5m=@n#fwt+l#irJ^WFRaD#H!ziaH$26<{wWN2J zOs7CJOjb^**mpdkfF(@?qnR&CYL;=!g`-M5lBfK28!a(QX0y3uDpEsXdNcRI-Ft>&gFJFWT4ZZ00ZlVnn#xg{S1PK5 zGXO?T;;Y=|!d{w28AmbT=8{YlPNh)B}Ausb4PHfamMe1*u8&$Dd)_%v{ z`KzcAMR9mW$I)GaNes&tfh6FPLVW;B(MC`U3kw^uu+YNB-bN6?7Z8PrjkSdjBB_LI zD+>w>>+Z5T>deij1B20QNUB+~Zk3$Y(#kfBxiE<-5D5`>V@~k3R2z2xB3H^lxo?d%e&eolaRn z$nDV$$)PI_&w8_QDK1HDi$dCFRY`Ul0$=8}?n0J4Zih=zCE<{SzSbo}H|TE|Vxei^ zNzcz!UEi!$GUueI8<)dn=n8j*hAuxW9zAZiLt1W(mEu(0F7L%Vzm3ea|4nL}EwnAP zZMM*-Cp1Wg>2wO01R4YmMe!0gpU-Wfe=)c(5NN<(M59qaFVL!>#rzp20rCon2Vj`z zIzR;phr`G97NQ4683d14B(%l+LF;3=XImz)8?Nn$0HjaR4qM0szcFg&7P62a@%AJs2wx zYxe8GQ~;-8Mw!b{Ct;kNNyrNlXrqJzn8=@EF#{*Cgs}-&cD4t^y9+iB>^h>1@$fVy2ol2z; znvIt&0mL19g9&*gn5@pwl=%9oy@%yyXQodU<{|_KEjW3=V!>e0>d6vD7gbfsC(wc5 zLb5QeRx$%>ntn2w1Ue<{8L=`NjaaR>J0lcwxm+w3gQ!O<+j6JV;n2j8`7>s-84ihv zC*!B^tb_dUZD-^0m?HeOZnq0GjL&MdTB%gh>9n=)p$_#3N`JB{axg>Il_E{7P_xLD z2}P8(4g|T0MG6>?dNL2t!eX&VD$nQhgp>(uBV|+1n2b+1=$^yjkQqAV8y4c)%E*Ay z#7i5w1!SfPA#Al;;wO7&ud+rEM&T_K4E}UcZ^6pKD=>W;5v(NO1!y5+BQ}<)yo6wD z;}uxg1`A8!6LJIj0)}i7H*6jpvh0t3Q8ObBc@K-MJK)Y9XJ>cLnRnj*c4SB^V<}Bu z16Z)tHvn!LI&>O7ZI=E-hTt*;7YNZpv_Oa!qJ?OI5G_OtEF&5%$-f?+=lKSJLO^oR z*eTFn@O11ZnhPL-gntc?<t$V#FS@A33V<&n?l(UHSNwDH$bWV2bv zJcwC;xzs+Qxk<@D^X3TdR(z0H+j$iQcdio!(>YAEX!_#dt9L~3)pcFVrtM-%jYcEp z1*p_nJY=v9)${!l;N;@=;*DVp>b0h@bg5hL)`17l#1gzWWfqUeV;Qh$dkf+(JeL;{ z2U&>L>54I6Nlq)oq?aMM48etHfeo6VZdrf&dPS<2I5dNmH<}4v9xFkfE@ENnR2;zJfh4M^IBb7>p ztrFTYAvX9sL8*NUqpa8KK;iB8`zpdG06`cZqKGwlP^;B^-Jo*_CO)X!XuI8FwWuTQ zcH6S91G|VzBEnEvS>0}TI2>MUWwlx@?N%1?TSR)Xv)OFk{s94As&p_I$bd!LV*{>F z7V$O$`$DuftX3M;oxbh6`P+xvU;5E-V38VfBAabM!sSv1o!(-4&JfgZZ+gMxy<_>kc zE76!kXf(<$L#biNzgR4s^C08|3DHS_&?RI!uBXvxpnUQb-ig5B_h0yrkr5DyVsp^} zN6B_nk-IRix z;F`j*;ivQA-f1(b3ViwlzkaMjE)fm91vyp#465y zkByD_&3RT14-dDrvF}mn@kW>TJE3epy$B@$qpi$TwoNLPF^B;K}4?n=8yT z_zQf44~c)29Wz1c6efmUU0sDj>a9e}({gZoF)-wjWz}PTU^+WHCGundATkm{v-uml z>+kPhTwE-b(U>waGD4{RY-8?58eguiu8MJnt%SO=VXk=ny0_(Fu z*@9S6$J5i(`RdRO>>C&ukTz7JCW%!zH#cWrd92reMxXk(%pp$d9*LGqLj*LhZZm>qL2=b;AT@(6Wk1zL$Q*D>{da%Vzq*rv$d$& zNP)49RuI_P*;!Iapb!UO8;qr;rC68-r&u`_l5~G<5M!1e7Zw(16%BH*HPvW&W#TSv zaBz^LTy}ux=jX%y9D*S`02Q+=%R(qiP*Cmd?VO@@7uHV$UtT1~$GiLa&;18aC%%~q z>7Rf7W9QDJe3|F_uRs#SHvZG>%fip8pME>HdQsxqjk~`u{8?0n;O`l;+Hd7^>-NL1 z2EUf5@v+M^##LCK9F&IeY%FGHek?d8>c^J^S7FKU}(er3Rzl6w#C2XW24)d;7_#RFY9CnMsp9I#FVD zVsv73qLLY1W5?z(n_IPrA8~W0k;Lf4=)~v}sAO<98f6a43YW^9pkfHUpD@FNSVFGO zD6|M8;1(+yOJ-HjG{iCoD`0XfAi1A2CF@->lO{$dN{mj7PK-`0NR0lZ=q(vn@tFhB?nM9q N002ovPDHLkV1fo(kr@C0 literal 8266 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 diff --git a/tests/ref/show-text-in-citation.png b/tests/ref/show-text-in-citation.png index 392487bc8a53191a74ffe07174bfa3f61f27e2ae..6533a4f7c914d47cdbb5fa20e70400260418ba3c 100644 GIT binary patch delta 788 zcmV+v1MB>o2CD{;B!8PwOjJex|Nq_J=ilJx-Neh^+~YYrL)zTr+S}v4z{n;jFz@g4 z-rwcj)7#zD+uhF9-NMMh!^?Vngr%pi-G+&ckC)%xZ-Kws&w|~F8y~W+Iw%x$Sz{1LA zXmCG4Ny*F8tgg0NTx4!=c&@OxPf=NEYjeoT(w?BGxx2%Ff{NW`X5H1@-K($N(AaEj zY#|{b@$vDHkdW@-FMc-iHVMmj!sTa ze0+Q&A|j@yrhk!Yv$)-Se%;#Q z-Dhdtw!Gb@tKF8Fv$M0^pQEv{vE6}$-Obb~DJeNQIVUG4=H})kBqa0m^B^D~_xJZ$ zSXhmXjg*v>#l^+?`ue1#q}`5^r>Cd5xVYW2xZT6a-G9u}+}`H*_xSt!`}_O+-FJE2 zsjl7A+AS?Dxw*Ob_xbMc^BM<^K>z>&OG!jQRCwC$*Hv~xQ4mDYs!QD6-QC^Y-QC?? zNFW4waEF}_m_j-$$=ee+f7PpDGW}&Bp$90A>*i;?Hceb zF3itChJU?*L9wA!Ev*{Bnd4~|GD;dr22yck7=UqkJOddyss8Zj5CFq^KRp2vStgfvHJ|NUbA-LaA7#0d)6Z?HM96YsrM9WK~hA0Z&)&>my{arkW&GO1Aibl#*m{ z2!+GSCtRs8^GJMmIEo|#+~VPP3vC^ps*D=&oPR$&Sf#>FS`OOVRC%EWaN)SUfecGL zBO{ob$lQDY#?#{xWE2#Ubmb`y3<5BoSC>~HBEFd1+oxiGpYKR)u0zDp3vDmVNawBw zaCME%4T#v;=9|-*rcO^Z;JLehJ%kKyMUbR&xy1)0nj`}(KRB7@TV*o+CSO93Kupr0 StqrXJ0000DJCW+larH? zk&&jRrkg(_5>Fp~mH6bA(Y;0`t@$rz5 zklx?p$;!?X$!5)ljOr?qUo_a<1V9hS$bpQSO8?=mP5^pfXm1Zh)YkbAcXt5L zZRqQVh{}4>?V`bX4M0m9VuB%}u|d}--;T{k!}4L}c$%*}&{lMV7{jRq&Dex5{DAw+DS({awx5zcA=!b6Z%1Q9Dk!^amF z<6|>(uYe^YG$<271_xle_CVUR8Q+UqQbkdI+2L06!!M^?CtTuz`#*aQD9(TKtMo{kdPuG zBBG+A+uPgL*4EhA*s-y(%F4=ld3nUd#QXdEs;a84uC6XFE-fuBVq#*6iHVh!l|@BG zw6wG|G&FmAdv$en3JMCOq@?%v_gGk1($dn`*Vh*p7v|>Xlc20uSP zY;0`E$jB%tD6+D$b8~ZQYHB+>J5*FudVGXILrdJ==7))qwz$B|&elXmPPe(iZg6B%GaDOI;4`~Te_*> z(9$Zt6$=viioHvuuGT25v3!+;@3Ws8`MAC^4nI7r9)BOH-n|)zbE>B&ud14}59?4W;8^&TV!@7K)5rNkj43=eYm4NAq#VDyj&F- z-roIj7*OmJ-?nzX9R~XOD**tYa@7RF$pc{2WYlETWYlETWYlETWYlET+9 zsi_JI3UzgLdwY9DMMbf(vDnzy*4Ea<#Kd`ddE49DqN1WAA|hg9VvvxKKtMoXU|>;E zQI(aIz`(%v_J8)>-QDr=@mE(@sHmt!L`1v0y8r+HUH_Wr0003NNkl!aet9j(@zDpDE0W>KnUclz zmAE!FT}rgLyx?02007Zd4TOIl0HY@F$Bdecnv9x^nv9y Date: Tue, 20 May 2025 14:57:19 +0200 Subject: [PATCH 035/162] Underline file path of failed test (#6281) --- tests/src/collect.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/src/collect.rs b/tests/src/collect.rs index 33f4f7366..84af04d2d 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -30,7 +30,8 @@ pub struct Test { impl Display for Test { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{} ({})", self.name, self.pos) + // underline path + write!(f, "{} (\x1B[4m{}\x1B[0m)", self.name, self.pos) } } From 300a782451082e5d7bdf894f0cc756261076008b Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Tue, 20 May 2025 15:54:49 +0200 Subject: [PATCH 036/162] Always run tests from workspace directory (#6307) --- tests/src/tests.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/src/tests.rs b/tests/src/tests.rs index 26eb63beb..0ed2fa469 100644 --- a/tests/src/tests.rs +++ b/tests/src/tests.rs @@ -59,7 +59,9 @@ fn main() { fn setup() { // Make all paths relative to the workspace. That's nicer for IDEs when // clicking on paths printed to the terminal. - std::env::set_current_dir("..").unwrap(); + let workspace_dir = + Path::new(env!("CARGO_MANIFEST_DIR")).join(std::path::Component::ParentDir); + std::env::set_current_dir(workspace_dir).unwrap(); // Create the storage. for ext in ["render", "html", "pdf", "svg"] { From e90c2f74ef63d92fc160ba5ba04b780c1a64fe75 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 20 May 2025 16:20:40 +0000 Subject: [PATCH 037/162] Fix text overhang example in docs (#6223) --- crates/typst-library/src/text/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 462d16060..23edc9e98 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -348,15 +348,17 @@ pub struct TextElem { /// This can make justification visually more pleasing. /// /// ```example + /// #set page(width: 220pt) + /// /// #set par(justify: true) /// This justified text has a hyphen in - /// the paragraph's first line. Hanging + /// the paragraph's second line. Hanging /// the hyphen slightly into the margin /// results in a clearer paragraph edge. /// /// #set text(overhang: false) /// This justified text has a hyphen in - /// the paragraph's first line. Hanging + /// the paragraph's second line. Hanging /// the hyphen slightly into the margin /// results in a clearer paragraph edge. /// ``` From d42d2ed200c8f3f167ee09be69fcf86f4b645971 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Tue, 20 May 2025 18:24:46 +0200 Subject: [PATCH 038/162] Error if an unexpected named argument was received (#6192) --- crates/typst-eval/src/call.rs | 14 ++++++++------ tests/ref/math-call-symbol.png | Bin 0 -> 703 bytes tests/suite/math/call.typ | 9 +++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 tests/ref/math-call-symbol.png diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 1ca7b4b8f..6a57c85e8 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -404,12 +404,14 @@ fn wrap_args_in_math( if trailing_comma { body += SymbolElem::packed(','); } - Ok(Value::Content( - callee.display().spanned(callee_span) - + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) - .pack() - .spanned(args.span), - )) + + let formatted = callee.display().spanned(callee_span) + + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) + .pack() + .spanned(args.span); + + args.finish()?; + Ok(Value::Content(formatted)) } /// Provide a hint if the callee is a shadowed standard library function. diff --git a/tests/ref/math-call-symbol.png b/tests/ref/math-call-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..8308bece1fba2d652a82e225c94b1212a90f1025 GIT binary patch literal 703 zcmV;w0zmzVP)71w8wCUzNE$5E!E}jP(cDv_^;`hG!e(qlIT>PFB{H0C^Sik}nFzUlUjndufRT-{{ zA@$j%D!l&!xp<*Ua5t-|8Vp7lZh2Crg&XR?W1HxL=5_5nj7H8JIpMB4a4V}zG@zXW zMhl38Mjd#*SnIADprajEb3xmrL$yzh<5bB41j;#Rnr<9~Xt7H0N)Z5WqeykGs|3$} z10dK109%9NtroC=1uS3z|1RFhvFu+}fm?hWuQsR#{FFahey1pGWH0TS*I5d)jgM>Y z17OPfvHcQmDhcmcsNdT(^|iqSXG2Z^#;^MgI?yNy4|&2P^sNsz1G_0i00>>=fgSEB z35T*Orrx*VdQP~krC_5Uy$`(Eq0V~SkIeGm}^lKj${B} zT0?5;5VBVvZh$_jD7@1vT?0}cVDVD`>~rF+7O;Q?EZ~0yjw=K=g~5wd&>8^b7JSmR z+7yA!P8^k2q`hBZIX8n`?!u!QY3X80!1=(M41dX>CI`7^hjqEq8tG0dY#@|=)gHK) z0CPaT9^{5H-O||2P9_{P(dv&iIJAi{&h$9Q?c_BgrAa2dG(kU8vy0`4u)i#}j8veK z$&*OkP9{vwYh!I4Aq`-=5NEOjj#4YN)s%+)TJT+>DFwX?@Z=>G4qq;_0!6S=|DhGP z4!-O4`gwHjqY}XO@l}9fGte`hRG2gXX2t*@B44n81v#4^s3xg!v|GC7FaUcI Date: Tue, 20 May 2025 18:25:26 +0200 Subject: [PATCH 039/162] Removing unused warnings in nightly (#6169) --- crates/typst-library/src/foundations/content.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index daf6c2dd9..1855bb70b 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -414,7 +414,7 @@ impl Content { /// Elements produced in `show` rules will not be included in the results. pub fn query(&self, selector: Selector) -> Vec { let mut results = Vec::new(); - self.traverse(&mut |element| -> ControlFlow<()> { + let _ = self.traverse(&mut |element| -> ControlFlow<()> { if selector.matches(&element, None) { results.push(element); } @@ -441,7 +441,7 @@ impl Content { /// Extracts the plain text of this content. pub fn plain_text(&self) -> EcoString { let mut text = EcoString::new(); - self.traverse(&mut |element| -> ControlFlow<()> { + let _ = self.traverse(&mut |element| -> ControlFlow<()> { if let Some(textable) = element.with::() { textable.plain_text(&mut text); } From df89a0e85b80844ef56a6fa98af01eaaf7553da8 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Tue, 20 May 2025 18:27:14 +0200 Subject: [PATCH 040/162] Use the right multiplication symbol in expression tooltip (#6163) --- crates/typst-ide/src/tooltip.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index cbfffe530..2638ce51b 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -86,7 +86,7 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { *count += 1; continue; } else if *count > 1 { - write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); + write!(pieces.last_mut().unwrap(), " (×{count})").unwrap(); } } pieces.push(value.repr()); @@ -95,7 +95,7 @@ fn expr_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { if let Some((_, count)) = last { if count > 1 { - write!(pieces.last_mut().unwrap(), " (x{count})").unwrap(); + write!(pieces.last_mut().unwrap(), " (×{count})").unwrap(); } } From 2a258a0c3849073c56a0559dbd67ee2effcd1031 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Fri, 23 May 2025 09:31:26 +0200 Subject: [PATCH 041/162] Remove unused Marginal type (#6321) --- crates/typst-library/src/layout/page.rs | 43 ++----------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 62e25278a..98afbd06f 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -1,16 +1,14 @@ -use std::borrow::Cow; use std::num::NonZeroUsize; use std::ops::RangeInclusive; use std::str::FromStr; -use comemo::Track; use typst_utils::{singleton, NonZeroExt, Scalar}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, - NativeElement, Set, Smart, StyleChain, Value, + cast, elem, Args, AutoValue, Cast, Construct, Content, Dict, Fold, NativeElement, + Set, Smart, Value, }; use crate::introspection::Introspector; use crate::layout::{ @@ -649,43 +647,6 @@ cast! { }, } -/// A header, footer, foreground or background definition. -#[derive(Debug, Clone, Hash)] -pub enum Marginal { - /// Bare content. - Content(Content), - /// A closure mapping from a page number to content. - Func(Func), -} - -impl Marginal { - /// Resolve the marginal based on the page number. - pub fn resolve( - &self, - engine: &mut Engine, - styles: StyleChain, - page: usize, - ) -> SourceResult> { - Ok(match self { - Self::Content(content) => Cow::Borrowed(content), - Self::Func(func) => Cow::Owned( - func.call(engine, Context::new(None, Some(styles)).track(), [page])? - .display(), - ), - }) - } -} - -cast! { - Marginal, - self => match self { - Self::Content(v) => v.into_value(), - Self::Func(v) => v.into_value(), - }, - v: Content => Self::Content(v), - v: Func => Self::Func(v), -} - /// A list of page ranges to be exported. #[derive(Debug, Clone)] pub struct PageRanges(Vec); From 6e0f48e192ddbd934d3aadd056810c86bcc3defd Mon Sep 17 00:00:00 2001 From: Igor Khanin Date: Wed, 28 May 2025 16:05:10 +0300 Subject: [PATCH 042/162] More precise math font autocomplete suggestions (#6316) --- crates/typst-ide/src/complete.rs | 45 ++++++++++++++++++++-- crates/typst-ide/src/tooltip.rs | 2 +- crates/typst-ide/src/utils.rs | 11 ++---- crates/typst-library/src/text/font/book.rs | 3 ++ 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 91fa53f9a..15b4296eb 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -15,7 +15,7 @@ use typst::syntax::{ ast, is_id_continue, is_id_start, is_ident, FileId, LinkedNode, Side, Source, SyntaxKind, }; -use typst::text::RawElem; +use typst::text::{FontFlags, RawElem}; use typst::visualize::Color; use unscanny::Scanner; @@ -1081,6 +1081,24 @@ fn code_completions(ctx: &mut CompletionContext, hash: bool) { } } +/// See if the AST node is somewhere within a show rule applying to equations +fn is_in_equation_show_rule(leaf: &LinkedNode<'_>) -> bool { + let mut node = leaf; + while let Some(parent) = node.parent() { + if_chain! { + if let Some(expr) = parent.get().cast::(); + if let ast::Expr::ShowRule(show) = expr; + if let Some(ast::Expr::FieldAccess(field)) = show.selector(); + if field.field().as_str() == "equation"; + then { + return true; + } + } + node = parent; + } + false +} + /// Context for autocompletion. struct CompletionContext<'a> { world: &'a (dyn IdeWorld + 'a), @@ -1152,10 +1170,12 @@ impl<'a> CompletionContext<'a> { /// Add completions for all font families. fn font_completions(&mut self) { - let equation = self.before_window(25).contains("equation"); + let equation = is_in_equation_show_rule(self.leaf); for (family, iter) in self.world.book().families() { - let detail = summarize_font_family(iter); - if !equation || family.contains("Math") { + let variants: Vec<_> = iter.collect(); + let is_math = variants.iter().any(|f| f.flags.contains(FontFlags::MATH)); + let detail = summarize_font_family(variants); + if !equation || is_math { self.str_completion( family, Some(CompletionKind::Font), @@ -1790,4 +1810,21 @@ mod tests { .must_include(["r", "dashed"]) .must_exclude(["cases"]); } + + #[test] + fn test_autocomplete_fonts() { + test("#text(font:)", -1) + .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); + + test("#show link: set text(font: )", -1) + .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); + + test("#show math.equation: set text(font: )", -1) + .must_include(["\"New Computer Modern Math\""]) + .must_exclude(["\"Libertinus Serif\""]); + + test("#show math.equation: it => { set text(font: )\nit }", -6) + .must_include(["\"New Computer Modern Math\""]) + .must_exclude(["\"Libertinus Serif\""]); + } } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 2638ce51b..e5e4cc19a 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -269,7 +269,7 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str()); then { - let detail = summarize_font_family(iter); + let detail = summarize_font_family(iter.collect()); return Some(Tooltip::Text(detail)); } }; diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs index d5d584e2b..887e851f9 100644 --- a/crates/typst-ide/src/utils.rs +++ b/crates/typst-ide/src/utils.rs @@ -77,23 +77,20 @@ pub fn plain_docs_sentence(docs: &str) -> EcoString { } /// Create a short description of a font family. -pub fn summarize_font_family<'a>( - variants: impl Iterator, -) -> EcoString { - let mut infos: Vec<_> = variants.collect(); - infos.sort_by_key(|info| info.variant); +pub fn summarize_font_family(mut variants: Vec<&FontInfo>) -> EcoString { + variants.sort_by_key(|info| info.variant); let mut has_italic = false; let mut min_weight = u16::MAX; let mut max_weight = 0; - for info in &infos { + for info in &variants { let weight = info.variant.weight.to_number(); has_italic |= info.variant.style == FontStyle::Italic; min_weight = min_weight.min(weight); max_weight = min_weight.max(weight); } - let count = infos.len(); + let count = variants.len(); let mut detail = eco_format!("{count} variant{}.", if count == 1 { "" } else { "s" }); if min_weight == max_weight { diff --git a/crates/typst-library/src/text/font/book.rs b/crates/typst-library/src/text/font/book.rs index 9f8acce87..cd90a08fe 100644 --- a/crates/typst-library/src/text/font/book.rs +++ b/crates/typst-library/src/text/font/book.rs @@ -194,6 +194,8 @@ bitflags::bitflags! { const MONOSPACE = 1 << 0; /// Glyphs have short strokes at their stems. const SERIF = 1 << 1; + /// Font face has a MATH table + const MATH = 1 << 2; } } @@ -272,6 +274,7 @@ impl FontInfo { let mut flags = FontFlags::empty(); flags.set(FontFlags::MONOSPACE, ttf.is_monospaced()); + flags.set(FontFlags::MATH, ttf.tables().math.is_some()); // Determine whether this is a serif or sans-serif font. if let Some(panose) = ttf From 9bbfa5ae0593333b1f0afffd71fec198d61742a6 Mon Sep 17 00:00:00 2001 From: Shunsuke KIMURA Date: Wed, 28 May 2025 22:29:45 +0900 Subject: [PATCH 043/162] Clarify localization of reference labels based on lang setting (#6249) Signed-off-by: Shunsuke Kimura --- crates/typst-library/src/model/reference.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 316617688..7d44cccc0 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -21,9 +21,10 @@ use crate::text::TextElem; /// /// The default, a `{"normal"}` reference, produces a textual reference to a /// label. For example, a reference to a heading will yield an appropriate -/// string such as "Section 1" for a reference to the first heading. The -/// references are also links to the respective element. Reference syntax can -/// also be used to [cite] from a bibliography. +/// string such as "Section 1" for a reference to the first heading. The word +/// "Section" depends on the [`lang`]($text.lang) setting and is localized +/// accordingly. The references are also links to the respective element. +/// Reference syntax can also be used to [cite] from a bibliography. /// /// As the default form requires a supplement and numbering, the label must be /// attached to a _referenceable element_. Referenceable elements include From 9ac21b8524632c70ab9e090488a70085eabe4189 Mon Sep 17 00:00:00 2001 From: Igor Khanin Date: Wed, 28 May 2025 16:41:35 +0300 Subject: [PATCH 044/162] Fix tracing of most field call expressions (#6234) --- crates/typst-eval/src/call.rs | 7 ++++++- crates/typst-ide/src/tooltip.rs | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 6a57c85e8..fa9683416 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -37,7 +37,12 @@ impl Eval for ast::FuncCall<'_> { let target = access.target(); let field = access.field(); match eval_field_call(target, field, args, span, vm)? { - FieldCall::Normal(callee, args) => (callee, args), + FieldCall::Normal(callee, args) => { + if vm.inspected == Some(callee_span) { + vm.trace(callee.clone()); + } + (callee, args) + } FieldCall::Resolved(value) => return Ok(value), } } else { diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index e5e4cc19a..528f679cf 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -371,4 +371,11 @@ mod tests { test(&world, -2, Side::Before).must_be_none(); test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`"); } + + #[test] + fn test_tooltip_field_call() { + let world = TestWorld::new("#import \"other.typ\"\n#other.f()") + .with_source("other.typ", "#let f = (x) => 1"); + test(&world, -4, Side::After).must_be_code("(..) => .."); + } } From 9a95966302bae4d795cd2fba4b3beb6f41629221 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 28 May 2025 09:44:44 -0400 Subject: [PATCH 045/162] Remove line break opportunity when math operator precededes a closing paren (#6216) --- crates/typst-layout/src/math/run.rs | 31 ++++++++++-------- ...ebreaking-after-relation-without-space.png | Bin 439 -> 2630 bytes tests/suite/math/multiline.typ | 3 ++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/crates/typst-layout/src/math/run.rs b/crates/typst-layout/src/math/run.rs index ae64368d6..4ec76c253 100644 --- a/crates/typst-layout/src/math/run.rs +++ b/crates/typst-layout/src/math/run.rs @@ -278,6 +278,9 @@ impl MathRun { frame } + /// Convert this run of math fragments into a vector of inline items for + /// paragraph layout. Creates multiple fragments when relation or binary + /// operators are present to allow for line-breaking opportunities later. pub fn into_par_items(self) -> Vec { let mut items = vec![]; @@ -295,21 +298,24 @@ impl MathRun { let mut space_is_visible = false; - let is_relation = |f: &MathFragment| matches!(f.class(), MathClass::Relation); let is_space = |f: &MathFragment| { matches!(f, MathFragment::Space(_) | MathFragment::Spacing(_, _)) }; + let is_line_break_opportunity = |class, next_fragment| match class { + // Don't split when two relations are in a row or when preceding a + // closing parenthesis. + MathClass::Binary => next_fragment != Some(MathClass::Closing), + MathClass::Relation => { + !matches!(next_fragment, Some(MathClass::Relation | MathClass::Closing)) + } + _ => false, + }; let mut iter = self.0.into_iter().peekable(); while let Some(fragment) = iter.next() { - if space_is_visible { - match fragment { - MathFragment::Space(width) | MathFragment::Spacing(width, _) => { - items.push(InlineItem::Space(width, true)); - continue; - } - _ => {} - } + if space_is_visible && is_space(&fragment) { + items.push(InlineItem::Space(fragment.width(), true)); + continue; } let class = fragment.class(); @@ -323,10 +329,9 @@ impl MathRun { frame.push_frame(pos, fragment.into_frame()); empty = false; - if class == MathClass::Binary - || (class == MathClass::Relation - && !iter.peek().map(is_relation).unwrap_or_default()) - { + // Split our current frame when we encounter a binary operator or + // relation so that there is a line-breaking opportunity. + if is_line_break_opportunity(class, iter.peek().map(|f| f.class())) { let mut frame_prev = std::mem::replace(&mut frame, Frame::soft(Size::zero())); diff --git a/tests/ref/math-linebreaking-after-relation-without-space.png b/tests/ref/math-linebreaking-after-relation-without-space.png index 7c569ad1fd2166800e30b88c2f5ca689be659341..fb14137680a55a42a8717f11fe555378489031c0 100644 GIT binary patch literal 2630 zcmb7G4^UJ09S>FfgJq7Q(iS0|=-7reb#CN3kVplsaT0hc{*c1LB%sLZ0GU zN)f2VmKN?{U5|2=K}xM4%bzEI0|i6`it8FYoR5OK^AAdfV;@xqI*X ze))bs|Gux}jpW0D0nZ087>vLpuO_?+zD3~k(S~*4f4Tf8(F{gF;E{y*-;4Z=H`n}q z=N~@l#c83btZ%U4Oj1U3j;3|>@6@H*PhOMN{(9dIlk=Unx6gf99FsMk(mIq%bAP6p zze+cLn3F$Fr}nkBFVep{sY$mYTffO^cA)|HqPZUayV*63liouQ2?w1QJJ25K{>$!V(8~qym0ptp}j2hGksV=yEF}z!@)W}Ed z`{_g8o0m#MSeDb6?X+Y*Q~Glm`JHSK4Tili4F;*n;Hcy{gL#I(@`@+K$_c%0!@X!$ zkx*1DynmIJaBLZz-ZdVZfR09OS!i#RY9`8ZRA4P<#LH#h_DCdK7#eL zy`h3xDPrX!Jp!^41Zy%+9j4I7s3Ix~mouE;nLnxwVpTEbD96Cq zdq2z}1Y>FX38`^Hq-RSr7}Dtw=>@j<8e8wX%RDX+RH!)=uDBDWdeL!)v zBO^hfp`l*wT%}u1%Z<@0yBMtsj!ZV6VA@Ws-pC859cm(iV~Lm)>B|(Lt;lQlE+K26 zrZ~MO)1Er~xw?)<)ncQi9Q1qTO^5IRbkY~(Sc+#0;;cgYLlqsN=4zRr0~D-fdG4^lK~BmgqpJCJb}&glrn`kX zfIjo65FeYCkeMeC&qO-nv8IcS*Fm%4kXN_cO_IZOETA6iOr@h>Ln~YP_*O|@v19|l z18DpyScrMF*pLvh3jv!xQQurIVd!i*y55JPj%TjKO9t8`n^r|VB37uyX2_?{D7=I^ z%P;FXLkdSid}1#M+K-o{%(iMy>jSm-18*!M{wFnL6uCT1?UqrCvRnO?ksv@~;l;D1hJc@@-FsA-$KN=Ns# zI#bLrH55Pq?CVFHU4o0iEEmV$?y-Y$@}@ie;~rGTm8?%|Ix3mtN_W+{zN!NnZUVxT zeT|~S!omq*3;2crCb>XBMKd@x2X3-XwVKmM- zdSej;ZU_SFmlNo)aAt#^P2H~p(wzl(3#1r+f# zxE8bB8_XMTLI{b^nXbnH@ zqgWHvK@h-v9TI|#?DPF>$jC4`IBYf`ltE!yQtgouzms+4XEou|Oln#($}S9EZLePE zYSPWHrFypLU9QMqyV#`tz`Y+vdM0AabatnZ31I*E0iX|ovyfZRdiY)@v6n&K-t76Q zm-4P~+e@oOe%S=nss#Gc7j-lPSvib!Cn?90T3TAb(t+Xn*w$BU(9XQ5Eo!91^8FiS@oF=oT`IZ3{26RJ} zSBXlh824*b4{Agp4ETS(_S{3F_|zPK-Y&)LqOu85pw>~Qd-lAJ_UH_?V$(F73Xr|_ zq(AFb5e37tUa#^7}^o0NhHpOeC7`8Rvs&^m3Jp&m?N`Fx_PqHN%>pa6Bz-*L?2?dcZqPE)DS$d#f0VGHijBy=Syq68$IcU}B1?`b`pr`|&TRJ@Yy=mPJD Oj3bH33C*t@FZws;1~!fW delta 426 zcmV;b0agCS6t@GA7k_F900000QW46Y0004gNkl2#n5REgAXKhDI)yq{y_C3#65@6e+Qmv7MUKF1EJWOj|y3?vLF3zMt6l{RCch zKIeRo2c$d-E3B}>3cL0ip}^16;Ti$Hv$j4MkGj-%$=W#KJAZYD2aE*(=M#r+@Sd^Y z^rQzwVTH}ZZzQN{Y`U?+9S67K6u7bG&!C=_ zhRch=0)U&4FkI?MlQyod1oF<#@*6idta~I!=ZZ<4Q^|m%a^qPJRMh5r=%1qx^C UMq-$2r~m)}07*qoM6N<$f)`iH{Qv*} diff --git a/tests/suite/math/multiline.typ b/tests/suite/math/multiline.typ index 34e66b99c..70838dd8c 100644 --- a/tests/suite/math/multiline.typ +++ b/tests/suite/math/multiline.typ @@ -99,6 +99,9 @@ Multiple trailing line breaks. #let hrule(x) = box(line(length: x)) #hrule(90pt)$<;$\ #hrule(95pt)$<;$\ +// We don't linebreak before a closing paren, but do before an opening paren. +#hrule(90pt)$<($\ +#hrule(95pt)$<($ #hrule(90pt)$<)$\ #hrule(95pt)$<)$ From 82e869023c7a7f31d716e7706a9a176b3d909279 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Wed, 28 May 2025 17:14:29 +0300 Subject: [PATCH 046/162] Add remaining height example for layout (#6266) Co-authored-by: Laurenz --- crates/typst-library/src/layout/layout.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 88252e5e3..04aaee944 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -41,8 +41,23 @@ use crate::layout::{BlockElem, Size}; /// receives the page's dimensions minus its margins. This is mostly useful in /// combination with [measurement]($measure). /// -/// You can also use this function to resolve [`ratio`] to fixed lengths. This -/// might come in handy if you're building your own layout abstractions. +/// To retrieve the _remaining_ size of the page rather than its full size, you +/// you can wrap your `layout` call in a `{block(height: 1fr)}`. This works +/// because the block automatically grows to fill the remaining space (see the +/// [fraction] documentation for more details). +/// +/// ```example +/// #set page(height: 150pt) +/// +/// #lorem(20) +/// +/// #block(height: 1fr, layout(size => [ +/// Remaining height: #size.height +/// ])) +/// ``` +/// +/// You can also use this function to resolve a [`ratio`] to a fixed length. +/// This might come in handy if you're building your own layout abstractions. /// /// ```example /// #layout(size => { From 3e7a39e968644ee925598f792fdc597b55a2529f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj?= <171210953+mgazeel@users.noreply.github.com> Date: Wed, 28 May 2025 19:29:40 +0200 Subject: [PATCH 047/162] Fix stroking of glyphs in math mode (#6243) --- crates/typst-layout/src/math/fragment.rs | 6 ++++-- tests/ref/issue-6170-equation-stroke.png | Bin 0 -> 1381 bytes tests/suite/math/equation.typ | 7 +++++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-6170-equation-stroke.png diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 1b508a349..59858a9cb 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -11,7 +11,7 @@ use typst_library::layout::{ }; use typst_library::math::{EquationElem, MathSize}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; -use typst_library::visualize::Paint; +use typst_library::visualize::{FixedStroke, Paint}; use typst_syntax::Span; use typst_utils::default_math_class; use unicode_math_class::MathClass; @@ -235,6 +235,7 @@ pub struct GlyphFragment { pub lang: Lang, pub region: Option, pub fill: Paint, + pub stroke: Option, pub shift: Abs, pub width: Abs, pub ascent: Abs, @@ -286,6 +287,7 @@ impl GlyphFragment { lang: TextElem::lang_in(styles), region: TextElem::region_in(styles), fill: TextElem::fill_in(styles).as_decoration(), + stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()), shift: TextElem::baseline_in(styles), font_size: TextElem::size_in(styles), math_size: EquationElem::size_in(styles), @@ -368,10 +370,10 @@ impl GlyphFragment { font: self.font.clone(), size: self.font_size, fill: self.fill, + stroke: self.stroke, lang: self.lang, region: self.region, text: self.c.into(), - stroke: None, glyphs: vec![Glyph { id: self.id.0, x_advance: Em::from_length(self.width, self.font_size), diff --git a/tests/ref/issue-6170-equation-stroke.png b/tests/ref/issue-6170-equation-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..a375931b50514adcfc0800fc1d4ac51b08e1bf06 GIT binary patch literal 1381 zcmb7^ZA?>F7{`muxiJ}#Wunqiw>dj?SQRF?%B_l(p_A%-Y0R9}RmLWkRvFsDrPqnC zj2AZ}5J;)cOcsfB11&P}URcGVQtE=0nL;n6wFPV6q4e$ctYrJ(2fr+N&hvaZ=j6%% z|I7c}*6hsasQFPO5-B?C(+!`8r!ah0y!L9i$0hTQl1MW@%G$7QhcLq06?tjl?&`Zv zaLiWG+t+wxGq;(imH$`YmN!&yk~OSMe$VDR+IFz$>KS^Th9y_+1nhUUrfZ<-Jg-0l zcJ|8KpMfO@w0kCzw_AA4ek|(ADmKA6b*R26BwH_6A(z#GQkq#_O81_u2?!EJe*Ka7;dy=-RP1I+4-}ej)E()1+m_C{ljP~g$07A_Qv?3vQ zut0;%KcEdRC7a^x0R!c(8SBY)o$bjG=4ZK*kjXD$r3|K0o6{Bjo>j$}fuXKKc)UZr ztg6^=xOF4Vn1EQdz9&=u{iYR$Yapdp?qho!sG(TgtHk@8Bt;*fE){m(Y+n4J(Ot=z z$cNojI8t_tSQ&iUjk~Ve!JMG04a^wO_p*bYLDyxO@B2*txjl|eaxQ4OrT<|d7G1Kl|O_pilkQ#nHX0>MlG^>HWdror{H;H-8epczB`uM*%^nuL9l`at;i-B!`50@;naLu!>$@|u8MSR9V|`e>uu+|7p^11#bwOtQ z+>cvO!JUkJ;auGDHK#u^so-7*Y!A5RR|yLtn+7UhC{k0rYvq9?bo?E(t|_!RY&_F3 zq2a+^RtXvR9OWpZiNc^uf^|2FwYiP~X^=a#68kYg5PE}1Mgt_&#zo6&v_*@>z{W5s zu#qd?+8FPLkRwMI=41Ll&ToSr-xwpfW7!%5nd11wu1dhYe)vA8BoUb`K)RD;L@esj zV702hM^HUk>Q_pY!w5nh#GY4tvpknie^9S;orXv9&=@vRlOb3AF!FfPdgIX0@{USo zUps$|avS~UUj^>x8%d|hw|FJg%OsLw*RZk9mcl05*8XpvU%=lDx}><9-Pw1OmbFa} z;=*83Qj3hOi2DuEUA+|sUx?|TDvSj<$MRRGjhcK>j&3xLeLNNh7=0A*D)Z*i8 z@$YP74qr4+JYpB0bOQkm_r&47nS_z*d|V|e(AjfzrQphWo*Q|@f_!Fg8HHE^nM?P8<|FwxE*ii~qiPC{d($4w%uU{9qmb&qhp76<}0y z`YF74l(IGcBAf0jTjQ$BbRcu5*YYcFGYm<%<+`uOPORR0IFQqhucLpJn0ze?#7v8D ziGx~Fy^mzXZ1HF-QhTP4AF)poDfKi;l~nAxQt$`k9*CS4WNh4`6?V{y;vqM|sAxmS zx9m2RsewhdkDG27*!$GrLuQ2YBa_f?2WM$uSjM%O4Bxfh-+bqaZ;Tz-8ltR|tHA!L y*;9wbon7Z_X{i%~PB8mr>GXn|FAcLY@5Db-_9~SC literal 0 HcmV?d00001 diff --git a/tests/suite/math/equation.typ b/tests/suite/math/equation.typ index 148a49d02..189f6e6db 100644 --- a/tests/suite/math/equation.typ +++ b/tests/suite/math/equation.typ @@ -297,3 +297,10 @@ Looks at the @quadratic formula. #set page(width: 150pt) #set text(lang: "he") תהא סדרה $a_n$: $[a_n: 1, 1/2, 1/3, dots]$ + +--- issue-6170-equation-stroke --- +// In this bug stroke settings did not apply to math content. +// We expect all of these to have a green stroke. +#set text(stroke: green + 0.5pt) + +A $B^2$ $ grave(C)' $ From 61dee554ba9f8d30b983776ecdfefa4b12a985ea Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:02:01 +0100 Subject: [PATCH 048/162] Add an example of show-set `place.clearance` for figures in the doc (#6208) --- crates/typst-library/src/model/figure.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 5a137edbd..bec667d6e 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -125,6 +125,9 @@ pub struct FigureElem { /// /// ```example /// #set page(height: 200pt) + /// #show figure: set place( + /// clearance: 1em, + /// ) /// /// = Introduction /// #figure( From 83e249dd334442b09bbeebcc70cae83950c31311 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:03:03 +0300 Subject: [PATCH 049/162] Fix Greek numbering docs (#6360) --- crates/typst-library/src/model/numbering.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index d82c3e4cd..320ed7d17 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -261,9 +261,9 @@ pub enum NumberingKind { LowerRoman, /// Uppercase Roman numerals (I, II, III, etc.). UpperRoman, - /// Lowercase Greek numerals (Α, Β, Γ, etc.). + /// Lowercase Greek letters (α, β, γ, etc.). LowerGreek, - /// Uppercase Greek numerals (α, β, γ, etc.). + /// Uppercase Greek letters (Α, Β, Γ, etc.). UpperGreek, /// Paragraph/note-like symbols: *, †, ‡, §, ¶, and ‖. Further items use /// repeated symbols. From 4329a15a1cb44a849c9b6a8cd932867b4aa53ed0 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:04:49 +0100 Subject: [PATCH 050/162] Improve `calc.round` documentation (#6345) --- crates/typst-library/src/foundations/calc.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index a8e0eaeb3..7f481a23b 100644 --- a/crates/typst-library/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -708,12 +708,13 @@ pub fn fract( } } -/// Rounds a number to the nearest integer away from zero. +/// Rounds a number to the nearest integer. /// -/// Optionally, a number of decimal places can be specified. +/// Half-integers are rounded away from zero. /// -/// If the number of digits is negative, its absolute value will indicate the -/// amount of significant integer digits to remove before the decimal point. +/// Optionally, a number of decimal places can be specified. If negative, its +/// absolute value will indicate the amount of significant integer digits to +/// remove before the decimal point. /// /// Note that this function will return the same type as the operand. That is, /// applying `round` to a [`float`] will return a `float`, and to a [`decimal`], From fd08c4bb3f55400e0fb9f461f463da19169a04a0 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:12:42 +0300 Subject: [PATCH 051/162] Fix typo in layout docs, change "size" to "height" (#6344) --- crates/typst-library/src/layout/layout.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 04aaee944..46271ff22 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -41,7 +41,7 @@ use crate::layout::{BlockElem, Size}; /// receives the page's dimensions minus its margins. This is mostly useful in /// combination with [measurement]($measure). /// -/// To retrieve the _remaining_ size of the page rather than its full size, you +/// To retrieve the _remaining_ height of the page rather than its full size, /// you can wrap your `layout` call in a `{block(height: 1fr)}`. This works /// because the block automatically grows to fill the remaining space (see the /// [fraction] documentation for more details). From 6164ade9cecf1f7bf475d24e0123c3664b8490a8 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Mon, 2 Jun 2025 16:15:04 +0200 Subject: [PATCH 052/162] Add `typst-html` to architecture crates list (#6364) --- docs/dev/architecture.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index bbae06792..3620d4fda 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -12,6 +12,7 @@ Let's start with a broad overview of the directories in this repository: - `crates/typst-cli`: Typst's command line interface. This is a relatively small layer on top of the compiler and the exporters. - `crates/typst-eval`: The interpreter for the Typst language. +- `crates/typst-html`: The HTML exporter. - `crates/typst-ide`: Exposes IDE functionality. - `crates/typst-kit`: Contains various default implementation of functionality used in `typst-cli`. From e023db5f1dea8b0273eec0f528d6ae0fed118a65 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 2 Jun 2025 18:44:43 +0200 Subject: [PATCH 053/162] Bump Rust to 1.87 in CI (#6367) --- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- crates/typst-kit/src/download.rs | 3 +-- crates/typst-library/src/text/deco.rs | 1 + crates/typst-macros/src/cast.rs | 1 + crates/typst/src/lib.rs | 1 - flake.lock | 6 +++--- flake.nix | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f17d137..c5c81537b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: sudo dpkg --add-architecture i386 sudo apt update sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.87.0 with: targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }} - uses: Swatinem/rust-cache@v2 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.87.0 with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d235aec5..ca317abd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.85.0 + - uses: dtolnay/rust-toolchain@1.87.0 with: target: ${{ matrix.target }} diff --git a/crates/typst-kit/src/download.rs b/crates/typst-kit/src/download.rs index 40084e51b..a4d49b4f3 100644 --- a/crates/typst-kit/src/download.rs +++ b/crates/typst-kit/src/download.rs @@ -128,8 +128,7 @@ impl Downloader { } // Configure native TLS. - let connector = - tls.build().map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + let connector = tls.build().map_err(io::Error::other)?; builder = builder.tls_connector(Arc::new(connector)); builder.build().get(url).call() diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 485d0edcf..7aa06e815 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -373,6 +373,7 @@ pub struct Decoration { /// A kind of decorative line. #[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[allow(clippy::large_enum_variant)] pub enum DecoLine { Underline { stroke: Stroke, diff --git a/crates/typst-macros/src/cast.rs b/crates/typst-macros/src/cast.rs index b90b78886..6f4b2b95c 100644 --- a/crates/typst-macros/src/cast.rs +++ b/crates/typst-macros/src/cast.rs @@ -185,6 +185,7 @@ struct Cast { } /// A pattern in a cast, e.g.`"ascender"` or `v: i64`. +#[allow(clippy::large_enum_variant)] enum Pattern { Str(syn::LitStr), Ty(syn::Pat, syn::Type), diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 580ba9e80..a6bb4fe38 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -27,7 +27,6 @@ //! [module]: crate::foundations::Module //! [content]: crate::foundations::Content //! [laid out]: typst_layout::layout_document -//! [document]: crate::model::Document //! [frame]: crate::layout::Frame pub extern crate comemo; diff --git a/flake.lock b/flake.lock index ad47d29cd..dedfbb4e0 100644 --- a/flake.lock +++ b/flake.lock @@ -112,13 +112,13 @@ "rust-manifest": { "flake": false, "locked": { - "narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=", + "narHash": "sha256-BwfxWd/E8gpnXoKsucFXhMbevMlVgw3l0becLkIcWCU=", "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml" }, "original": { "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml" } }, "systems": { diff --git a/flake.nix b/flake.nix index 6938f6e57..1b2b3abc8 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; rust-manifest = { - url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"; + url = "https://static.rust-lang.org/dist/channel-rust-1.87.0.toml"; flake = false; }; }; From 664d33a68178239a9b9799d5c1b9e08958dd8d5c Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 2 Jun 2025 18:53:35 +0200 Subject: [PATCH 054/162] Be a bit lazier in function call evaluation (#6368) --- crates/typst-eval/src/call.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index fa9683416..eaeabbab3 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -25,15 +25,13 @@ impl Eval for ast::FuncCall<'_> { fn eval(self, vm: &mut Vm) -> SourceResult { let span = self.span(); let callee = self.callee(); - let in_math = in_math(callee); let callee_span = callee.span(); let args = self.args(); - let trailing_comma = args.trailing_comma(); vm.engine.route.check_call_depth().at(span)?; // Try to evaluate as a call to an associated function or field. - let (callee, args) = if let ast::Expr::FieldAccess(access) = callee { + let (callee_value, args_value) = if let ast::Expr::FieldAccess(access) = callee { let target = access.target(); let field = access.field(); match eval_field_call(target, field, args, span, vm)? { @@ -50,9 +48,15 @@ impl Eval for ast::FuncCall<'_> { (callee.eval(vm)?, args.eval(vm)?.spanned(span)) }; - let func_result = callee.clone().cast::(); - if in_math && func_result.is_err() { - return wrap_args_in_math(callee, callee_span, args, trailing_comma); + let func_result = callee_value.clone().cast::(); + + if func_result.is_err() && in_math(callee) { + return wrap_args_in_math( + callee_value, + callee_span, + args_value, + args.trailing_comma(), + ); } let func = func_result @@ -61,8 +65,11 @@ impl Eval for ast::FuncCall<'_> { let point = || Tracepoint::Call(func.name().map(Into::into)); let f = || { - func.call(&mut vm.engine, vm.context, args) - .trace(vm.world(), point, span) + func.call(&mut vm.engine, vm.context, args_value).trace( + vm.world(), + point, + span, + ) }; // Stacker is broken on WASM. From ff0dc5ab6608504c802d6965587151caf2c757f6 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Tue, 3 Jun 2025 15:38:21 +0300 Subject: [PATCH 055/162] Add Latvian translations (#6348) --- crates/typst-library/src/text/lang.rs | 4 +++- crates/typst-library/translations/lv.txt | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 crates/typst-library/translations/lv.txt diff --git a/crates/typst-library/src/text/lang.rs b/crates/typst-library/src/text/lang.rs index 2cc66a261..f9f13c783 100644 --- a/crates/typst-library/src/text/lang.rs +++ b/crates/typst-library/src/text/lang.rs @@ -14,7 +14,7 @@ macro_rules! translation { }; } -const TRANSLATIONS: [(&str, &str); 39] = [ +const TRANSLATIONS: [(&str, &str); 40] = [ translation!("ar"), translation!("bg"), translation!("ca"), @@ -36,6 +36,7 @@ const TRANSLATIONS: [(&str, &str); 39] = [ translation!("it"), translation!("ja"), translation!("la"), + translation!("lv"), translation!("nb"), translation!("nl"), translation!("nn"), @@ -87,6 +88,7 @@ impl Lang { pub const ITALIAN: Self = Self(*b"it ", 2); pub const JAPANESE: Self = Self(*b"ja ", 2); pub const LATIN: Self = Self(*b"la ", 2); + pub const LATVIAN: Self = Self(*b"lv ", 2); pub const LOWER_SORBIAN: Self = Self(*b"dsb", 3); pub const NYNORSK: Self = Self(*b"nn ", 2); pub const POLISH: Self = Self(*b"pl ", 2); diff --git a/crates/typst-library/translations/lv.txt b/crates/typst-library/translations/lv.txt new file mode 100644 index 000000000..4c6b86841 --- /dev/null +++ b/crates/typst-library/translations/lv.txt @@ -0,0 +1,8 @@ +figure = Attēls +table = Tabula +equation = Vienādojums +bibliography = Literatūra +heading = Sadaļa +outline = Saturs +raw = Saraksts +page = lpp. From 1b399646c270d518af250db3afb7ba35992e8751 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 14:53:13 +0200 Subject: [PATCH 056/162] Bump crossbeam-channel from 0.5.14 to 0.5.15 (#6369) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b70e06bc..30a4db7a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,9 +508,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] From dd95f7d59474800a83a4d397dd13e34de35d56be Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 3 Jun 2025 14:08:18 +0000 Subject: [PATCH 057/162] Fix bottom accent positioning in math (#6187) --- crates/typst-layout/src/math/accent.rs | 59 +++++++++++++-------- crates/typst-layout/src/math/attach.rs | 6 ++- crates/typst-layout/src/math/fragment.rs | 33 ++++++++---- crates/typst-layout/src/math/stretch.rs | 2 +- crates/typst-library/src/math/accent.rs | 13 +++++ tests/ref/math-accent-bottom-high-base.png | Bin 0 -> 572 bytes tests/ref/math-accent-bottom-sized.png | Bin 0 -> 382 bytes tests/ref/math-accent-bottom-subscript.png | Bin 0 -> 417 bytes tests/ref/math-accent-bottom-wide-base.png | Bin 0 -> 359 bytes tests/ref/math-accent-bottom.png | Bin 0 -> 622 bytes tests/ref/math-accent-nested.png | Bin 0 -> 537 bytes tests/suite/math/accent.typ | 28 ++++++++++ 12 files changed, 108 insertions(+), 33 deletions(-) create mode 100644 tests/ref/math-accent-bottom-high-base.png create mode 100644 tests/ref/math-accent-bottom-sized.png create mode 100644 tests/ref/math-accent-bottom-subscript.png create mode 100644 tests/ref/math-accent-bottom-wide-base.png create mode 100644 tests/ref/math-accent-bottom.png create mode 100644 tests/ref/math-accent-nested.png diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 73d821019..53dfdf055 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -1,7 +1,7 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Em, Frame, Point, Size}; -use typst_library::math::{Accent, AccentElem}; +use typst_library::math::AccentElem; use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; @@ -18,8 +18,11 @@ pub fn layout_accent( let cramped = style_cramped(); let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; - // Try to replace a glyph with its dotless variant. - if elem.dotless(styles) { + let accent = elem.accent; + let top_accent = !accent.is_bottom(); + + // Try to replace base glyph with its dotless variant. + if top_accent && elem.dotless(styles) { if let MathFragment::Glyph(glyph) = &mut base { glyph.make_dotless_form(ctx); } @@ -29,41 +32,54 @@ pub fn layout_accent( let base_class = base.class(); let base_attach = base.accent_attach(); - let width = elem.size(styles).relative_to(base.width()); + let mut glyph = GlyphFragment::new(ctx, styles, accent.0, elem.span()); - let Accent(c) = elem.accent; - let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); - - // Try to replace accent glyph with flattened variant. - let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.ascent() > flattened_base_height { - glyph.make_flattened_accent_form(ctx); + // Try to replace accent glyph with its flattened variant. + if top_accent { + let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); + if base.ascent() > flattened_base_height { + glyph.make_flattened_accent_form(ctx); + } } // Forcing the accent to be at least as large as the base makes it too // wide in many case. + let width = elem.size(styles).relative_to(base.width()); let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); let variant = glyph.stretch_horizontal(ctx, width, short_fall); let accent = variant.frame; - let accent_attach = variant.accent_attach; + let accent_attach = variant.accent_attach.0; + + let (gap, accent_pos, base_pos) = if top_accent { + // Descent is negative because the accent's ink bottom is above the + // baseline. Therefore, the default gap is the accent's negated descent + // minus the accent base height. Only if the base is very small, we + // need a larger gap so that the accent doesn't move too low. + let accent_base_height = scaled!(ctx, styles, accent_base_height); + let gap = -accent.descent() - base.ascent().min(accent_base_height); + let accent_pos = Point::with_x(base_attach.0 - accent_attach); + let base_pos = Point::with_y(accent.height() + gap); + (gap, accent_pos, base_pos) + } else { + let gap = -accent.ascent(); + let accent_pos = Point::new(base_attach.1 - accent_attach, base.height() + gap); + let base_pos = Point::zero(); + (gap, accent_pos, base_pos) + }; - // Descent is negative because the accent's ink bottom is above the - // baseline. Therefore, the default gap is the accent's negated descent - // minus the accent base height. Only if the base is very small, we need - // a larger gap so that the accent doesn't move too low. - let accent_base_height = scaled!(ctx, styles, accent_base_height); - let gap = -accent.descent() - base.ascent().min(accent_base_height); let size = Size::new(base.width(), accent.height() + gap + base.height()); - let accent_pos = Point::with_x(base_attach - accent_attach); - let base_pos = Point::with_y(accent.height() + gap); let baseline = base_pos.y + base.ascent(); + let base_italics_correction = base.italics_correction(); let base_text_like = base.is_text_like(); - let base_ascent = match &base { MathFragment::Frame(frame) => frame.base_ascent, _ => base.ascent(), }; + let base_descent = match &base { + MathFragment::Frame(frame) => frame.base_descent, + _ => base.descent(), + }; let mut frame = Frame::soft(size); frame.set_baseline(baseline); @@ -73,6 +89,7 @@ pub fn layout_accent( FrameFragment::new(styles, frame) .with_class(base_class) .with_base_ascent(base_ascent) + .with_base_descent(base_descent) .with_italics_correction(base_italics_correction) .with_accent_attach(base_attach) .with_text_like(base_text_like), diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index e1d7d7c9d..90aad941e 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -434,9 +434,13 @@ fn compute_script_shifts( } if bl.is_some() || br.is_some() { + let descent = match &base { + MathFragment::Frame(frame) => frame.base_descent, + _ => base.descent(), + }; shift_down = shift_down .max(sub_shift_down) - .max(if is_text_like { Abs::zero() } else { base.descent() + sub_drop_min }) + .max(if is_text_like { Abs::zero() } else { descent + sub_drop_min }) .max(measure!(bl, ascent) - sub_top_max) .max(measure!(br, ascent) - sub_top_max); } diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 59858a9cb..85101c486 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -164,12 +164,12 @@ impl MathFragment { } } - pub fn accent_attach(&self) -> Abs { + pub fn accent_attach(&self) -> (Abs, Abs) { match self { Self::Glyph(glyph) => glyph.accent_attach, Self::Variant(variant) => variant.accent_attach, Self::Frame(fragment) => fragment.accent_attach, - _ => self.width() / 2.0, + _ => (self.width() / 2.0, self.width() / 2.0), } } @@ -241,7 +241,7 @@ pub struct GlyphFragment { pub ascent: Abs, pub descent: Abs, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub font_size: Abs, pub class: MathClass, pub math_size: MathSize, @@ -296,7 +296,7 @@ impl GlyphFragment { descent: Abs::zero(), limits: Limits::for_char(c), italics_correction: Abs::zero(), - accent_attach: Abs::zero(), + accent_attach: (Abs::zero(), Abs::zero()), class, span, modifiers: FrameModifiers::get_in(styles), @@ -328,8 +328,14 @@ impl GlyphFragment { }); let mut width = advance.scaled(ctx, self.font_size); - let accent_attach = + + // The fallback for accents is half the width plus or minus the italics + // correction. This is similar to how top and bottom attachments are + // shifted. For bottom accents we do not use the accent attach of the + // base as it is meant for top acccents. + let top_accent_attach = accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); + let bottom_accent_attach = (width - italics) / 2.0; let extended_shape = is_extended_shape(ctx, id); if !extended_shape { @@ -341,7 +347,7 @@ impl GlyphFragment { self.ascent = bbox.y_max.scaled(ctx, self.font_size); self.descent = -bbox.y_min.scaled(ctx, self.font_size); self.italics_correction = italics; - self.accent_attach = accent_attach; + self.accent_attach = (top_accent_attach, bottom_accent_attach); self.extended_shape = extended_shape; } @@ -459,7 +465,7 @@ impl Debug for GlyphFragment { pub struct VariantFragment { pub c: char, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub frame: Frame, pub font_size: Abs, pub class: MathClass, @@ -501,8 +507,9 @@ pub struct FrameFragment { pub limits: Limits, pub spaced: bool, pub base_ascent: Abs, + pub base_descent: Abs, pub italics_correction: Abs, - pub accent_attach: Abs, + pub accent_attach: (Abs, Abs), pub text_like: bool, pub ignorant: bool, } @@ -510,6 +517,7 @@ pub struct FrameFragment { impl FrameFragment { pub fn new(styles: StyleChain, frame: Frame) -> Self { let base_ascent = frame.ascent(); + let base_descent = frame.descent(); let accent_attach = frame.width() / 2.0; Self { frame: frame.modified(&FrameModifiers::get_in(styles)), @@ -519,8 +527,9 @@ impl FrameFragment { limits: Limits::Never, spaced: false, base_ascent, + base_descent, italics_correction: Abs::zero(), - accent_attach, + accent_attach: (accent_attach, accent_attach), text_like: false, ignorant: false, } @@ -542,11 +551,15 @@ impl FrameFragment { Self { base_ascent, ..self } } + pub fn with_base_descent(self, base_descent: Abs) -> Self { + Self { base_descent, ..self } + } + pub fn with_italics_correction(self, italics_correction: Abs) -> Self { Self { italics_correction, ..self } } - pub fn with_accent_attach(self, accent_attach: Abs) -> Self { + pub fn with_accent_attach(self, accent_attach: (Abs, Abs)) -> Self { Self { accent_attach, ..self } } diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index f45035e27..6157d0c50 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -278,7 +278,7 @@ fn assemble( } let accent_attach = match axis { - Axis::X => frame.width() / 2.0, + Axis::X => (frame.width() / 2.0, frame.width() / 2.0), Axis::Y => base.accent_attach, }; diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index e62b63872..f2c9168c2 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -80,6 +80,19 @@ impl Accent { pub fn new(c: char) -> Self { Self(Self::combine(c).unwrap_or(c)) } + + /// List of bottom accents. Currently just a list of ones included in the + /// Unicode math class document. + const BOTTOM: &[char] = &[ + '\u{0323}', '\u{032C}', '\u{032D}', '\u{032E}', '\u{032F}', '\u{0330}', + '\u{0331}', '\u{0332}', '\u{0333}', '\u{033A}', '\u{20E8}', '\u{20EC}', + '\u{20ED}', '\u{20EE}', '\u{20EF}', + ]; + + /// Whether this accent is a bottom accent or not. + pub fn is_bottom(&self) -> bool { + Self::BOTTOM.contains(&self.0) + } } /// This macro generates accent-related functions. diff --git a/tests/ref/math-accent-bottom-high-base.png b/tests/ref/math-accent-bottom-high-base.png new file mode 100644 index 0000000000000000000000000000000000000000..23b14467280d93fba150194ded116d6b15fea4da GIT binary patch literal 572 zcmV-C0>k}@P)v*+FZC^6au}I+YU#J7le%9ciTqo1 z?0@j>I;vZ|cvTD%?|V$|$+DltUy+2SZAku9`=WvB7TbSE=B?=e|9|(g#mGWdz)+sq z^orURzqUr<#-2o1aNnIasd`-@3Rm?Px`G9Dw6VBw5i;+A5r%@~^|Z0r_yS5wOTi-UTRc?ab% z6u8W$jm1iPk$Dq+F%*cNqKU;HnBRb?KTvLBErdo0Jz@P$6N?Y3fT;>-8ELcyp4zn_ z^cEx9)W;K{Y3nqY{LKOKKeEXGmTcNsoID*&o&5r)E~z7A4nU~TCA6{F45i#!idk-H z+@*=d|D;eF78P^Q6?~VaO`Geg9}3s)9=d`PF|@IG-eeU28)GKE1+=j^@(D8USQ&-_ z|97;oIPh@^a>`m!{1Vt8*>M~p6eS`jE?aOks3{>``;4F15zPWt)@};Hll(iDbEx)$y`0Dql%5ELnaGLr){`eY& zdwXzn+U{;yvvTF~m21~5U$Js&+nE9CEnXV!ACFo*YVoMWv;_b(vVqN&POabo0000< KMNUMnLSTaLKoLv; literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-sized.png b/tests/ref/math-accent-bottom-sized.png new file mode 100644 index 0000000000000000000000000000000000000000..5455b2f5b260a860704717196db8b988e54b12f6 GIT binary patch literal 382 zcmV-^0fGLBP)^*)K^trgXzr>?sZh||E-Dt|M$1iB#vkLpvcYkLgAJ!`u{)R7;O?)@B|cY zXdg1qw14hwq2IKz_^lNR_njpY=cCI1|L5Ill%4u-K|Nmz}sk#rsE&2cd3?zfmC?CJ~ z+PZ!F)`j&_)BfN1@c;h@2)99M>i?S`{@+?ulo%VG-n{!G^(}t71`0l+a)FN6I&J-} zzu**heD3TAG)P>J9)f}xS>)k&dQ=h%dJ3bb7e0aWccrDI(WQ0*)($_9BYE-OXnj0t c@ldoF0CeBVg-HF}4gdfE07*qoM6N<$f{N|D(*OVf literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-subscript.png b/tests/ref/math-accent-bottom-subscript.png new file mode 100644 index 0000000000000000000000000000000000000000..818544445587a485c7152e543f2ab8a8536a89eb GIT binary patch literal 417 zcmV;S0bc%zP)_^QKU;L<91?f&9SD7A;fbqA z!u=m1^o4~dA5h2QFTuYnw?ocO(txH$&){ga6&A6x7v+e!2Ze_}Hx(;^TySZ-W0LeC*N+ z@v-~&%Q;lC_}ZM#lK)3mY~2Cj)EFQAPIJ$!K0Gkh;;a*+4S`XM2dBjVM)yn!G36@500000 LNkvXXu0mjfK$6xw literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom-wide-base.png b/tests/ref/math-accent-bottom-wide-base.png new file mode 100644 index 0000000000000000000000000000000000000000..0475b4856bd49b9a4f7663564747ca9635654028 GIT binary patch literal 359 zcmV-t0hs=YP)YZomn-m~=V zl5b$v%y(ew?VOkY4?^UAnQW(79ACM#xNh-NWQ&*1U;8l#DtlA;1+6Up_rLfoh`!X; z7Es&P0gTkYq3^f$!=$_B(#qnSKdt{B`;YA7|33dq&i#k@^nE4Gg1U2N#fqb-7I$t_ z{rBJc4UqM>E7q-Fxq8i-)hk!7Uo+d~H&puT z)UEU^tN(0-&~K-;pWV{3{2$$GCrE1hHd-H#T08_S1^~|x+^P)&N!S1Y002ovPDHLk FV1n_Aw{-vj literal 0 HcmV?d00001 diff --git a/tests/ref/math-accent-bottom.png b/tests/ref/math-accent-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..bd1b921460ca17d139552bd63f73144da916bc91 GIT binary patch literal 622 zcmV-!0+IcRP)zM(@7Ur)P)swAt_M<6Ut1fP)c+<8ciD}Qd5Z3L@6vA ziem*X(`{^sdt+OBld~7U<#W!t5GI`3MM3*;E`IQE_})EF8GlJxDO?Jd!v8}!59;!8 zu*7=tVPmV|(BlV9O08oS#pSwfgwA8b1N9^9Goe$L{Y-uM&%#4QkAPY1RIP0VS+G$; z_N)zj)9f?fjH>B9zZKrL%y{Jjlb0YQwy6t6x~*;#+4(n;HIo9!kJ$OJ3-Cpc>aYYr zwBIqlt|6Q_mC?UO)?!AnR^1Xrc7F~<`P15u2-c;WREy>BMX^zZFgARLdJW;eJ{ls( zD!eEjrBLa@$UK#N&LZUREd%b`B^5r~<+{ zz;pwEogY9=er!;jr#(a^a5r+=0r50&lBwGo!pC!Vs8xn$ehJ{x>MkEpy$wUxEP;`N| zRA|MsNYUxc-?Z8?t?g+&=43MSpZmYb@1B#Jdu{^a6bTAg0RwiHWgq`l#@1!t@JtK7 zS6jZD!y^2MdpL$w3mpmcV!;o<8BYVc-uSUwGy!djb64Q^#Z$_@ADa^_O0 z!KzS}wz{GPO;C$HLJ~1pNEU!Azp~7>L*@Dy?Q8B&^wNU2bAYwIfUMf%Q^bI}n*`Dv zq~K>wwQWX)gGNcGTRRNUZ3B*i8$4u8lY;F_QrO@b1nvrrhrr1qVCGyn;yB{X^@1xW zd>qhG;s4#e^}l(Z7l}95R#|b$E4%`*yYsOQDa>kv!9)zc_@XP%LrRQocXEXV!!R51 zXp1hrUT=y^KNdw%x$G(6Uki4)aeFDZ_W?6KSk!6SBJ2Zg&D!5B^<~!yyC`?L`IS*8 zIe5H5hdk+PWdiX8c;?g2)|<(}LjI#SOYT~W(LF{F&cdozmNAA#pr$XiqXkVyx!ZJs z%$OV?xt>V>xI8A{$|!%%NDmgGT|s*2!RFS9`iN5Z4EIjPpw#XSU!-k0RobEgR=^7Q b$ARAgn*4* Date: Tue, 3 Jun 2025 17:42:22 +0300 Subject: [PATCH 058/162] Change Russian secondary smart quotes & remove alternatives (#6331) --- crates/typst-library/src/text/smartquote.rs | 5 +++-- tests/ref/smartquote-ru.png | Bin 1877 -> 1886 bytes 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 4dda689df..270d8f0f3 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -237,7 +237,7 @@ impl<'s> SmartQuotes<'s> { "cs" | "da" | "de" | "sk" | "sl" if alternative => ("›", "‹", "»", "«"), "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, "da" => ("‘", "’", "“", "”"), - "fr" | "ru" if alternative => default, + "fr" if alternative => default, "fr" => ("“", "”", "«\u{202F}", "\u{202F}»"), "fi" | "sv" if alternative => ("’", "’", "»", "»"), "bs" | "fi" | "sv" => ("’", "’", "”", "”"), @@ -247,7 +247,8 @@ impl<'s> SmartQuotes<'s> { "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), "hu" | "pl" | "ro" => ("’", "’", "„", "”"), "no" | "nb" | "nn" if alternative => low_high, - "ru" | "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"), + "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"), + "ru" => ("„", "“", "«", "»"), "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), diff --git a/tests/ref/smartquote-ru.png b/tests/ref/smartquote-ru.png index 05c79263f6797e3072ee9cb867431b4fb334c28b..867121d1e32d470b9b4c7f4daf8bb37b9a521a4f 100644 GIT binary patch delta 1871 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 A{{R30 From 4a8367e90add92a0a316bcc258e39d522b484c25 Mon Sep 17 00:00:00 2001 From: Nazar Serhiichuk <43041209+G1gg1L3s@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:13:39 +0300 Subject: [PATCH 059/162] Fix Ukrainian secondary smart quotes (#6372) --- crates/typst-library/src/text/smartquote.rs | 3 ++- tests/ref/smartquote-uk.png | Bin 0 -> 1971 bytes tests/suite/text/smartquote.typ | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/ref/smartquote-uk.png diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 270d8f0f3..09cefd013 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -247,8 +247,9 @@ impl<'s> SmartQuotes<'s> { "es" if matches!(region, Some("ES") | None) => ("“", "”", "«", "»"), "hu" | "pl" | "ro" => ("’", "’", "„", "”"), "no" | "nb" | "nn" if alternative => low_high, - "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"), + "no" | "nb" | "nn" => ("’", "’", "«", "»"), "ru" => ("„", "“", "«", "»"), + "uk" => ("“", "”", "«", "»"), "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), diff --git a/tests/ref/smartquote-uk.png b/tests/ref/smartquote-uk.png new file mode 100644 index 0000000000000000000000000000000000000000..7ac1c032e583b0c3a691097b9b6fadaeaaaa7e68 GIT binary patch literal 1971 zcmV;k2Tb^hP)4J*jj{4nWRBDBRDmr&odJm6K)-=ECtQfMYTp8?isUFAc89S>MeP4f6ru+b))l-!sjh*$XV? zOzmJ!W`oJ|CbuX)9`4*!bQbL6J7=qLl4B&5%>jeB!)?H(2-$3$z1vy=1Q_%K4JP1y z&dcwkVM=1d9&$bmp$9eztnOsdC=0k639M7HSe?HWr3RRbX&}#dyc3*by9FN)(?Lh$ zMTcIsTd|FEt);B`OlryL$beC(vrKqwk&M4xHjs7t{(zlx7s0}&0b7yyv3@}GupXWS z_R{hGs+F132CJE*4xf~%hQlPcDKd3GS#L7tE_L@*mzx!4`;r(W(N-S`^HZlCD$YMz zBRNtV^K%Q38SYDQqCaXI(P%n&VHbrL%0f`%s6|vzNDO&SNVx#6lk_cG$ zY8}v#^=o6l2aYz!C!Bu(hUyG!!o@5YUD#H#O_$=p@-d$v7KJu$q-||<)+wNPou>WG zHulu1?r@~+p(O@7Vy{7TzDwhW-dN%ls4Frq;L(A^jBf))I>o-i;!7mD`l_o;#BM#Q zyIX<})dcSwxptxj2)0b&LZqvoO>)mk_o36V7gM#imVGqonm^_p-*xy*17fzORX>?} zSP%dIaG6l-XH~2VOX*@|0iBxFYmGXU_n?eVVH1WBrCw(%8al?c=RExb1w%sk#MB`WyY5@tMZUB-1W zd#ac}s<3YVyA}%84_Az65|rD~|W`RWDWv$Pj?w>3som`{4J(3sDQW9Eb`-v|r;J z&Y`C)(@M`V0w~PMwtcV}&FFA)?*d|eW(4mD>{2iJ~9F1SpQm_hNFF{k57D z48TA#ws_JT=Y6M;Z>dz8xaIL>@cHUd^RuD*or1T>RmU9e0E*=yRwGyor5Y$RlB-W# zkqwhUzEY|qa7YW@jZxddfm|W z#;1_xR+Hpbk36;;h!chSc=V1%M08FD>t-@Ck0fcwifp5S$TMT}hgAXehrKZ3{vH6j zSFw7maQEO=M-o@QXj-2-zExFx0Q3A*C(6@30y#M76h5$+S8EALum1t4+fIfBhoASY zP)`CgUnUc<`$kMk{z(QFaMM3_nC61lJE-|CG8h|}V0y_VRX*wF9X$M)C zSlY^lH#t#7Gp)@`U zERmfPRdsB)am=~qr7=DBHe_$m%h+#fBG%Q{ZuQIu+`n(P+u2cjOnm!;1|%5b??H9Q zr(Ld5vnMbL@b=TfsPvz=(*sS=K^13FlcTlP!mp-62dh%uOlxb^@efh+_cs8~p+&1c^L}@Wc=dA>8;r z0J-bhspo|002ovPDHLk FV1f-SyG;N9 literal 0 HcmV?d00001 diff --git a/tests/suite/text/smartquote.typ b/tests/suite/text/smartquote.typ index f2af93ceb..6eab35076 100644 --- a/tests/suite/text/smartquote.typ +++ b/tests/suite/text/smartquote.typ @@ -46,6 +46,10 @@ #set text(lang: "ru") "Лошадь не ест салат из огурцов" - это была первая фраза, сказанная по 'телефону'. +--- smartquote-uk --- +#set text(lang: "uk") +"Кінь не їсть огірковий салат" — перше речення, коли-небудь вимовлене по 'телефону'. + --- smartquote-it --- #set text(lang: "it") "Il cavallo non mangia insalata di cetrioli" è stata la prima frase pronunciata al 'telefono'. From 128c40d839398374c69725a5b19c24e07fb23c3d Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Jun 2025 08:20:54 +0000 Subject: [PATCH 060/162] Apply script-style to numbers consistently in math (#6320) --- crates/typst-layout/src/math/text.rs | 13 ++++--------- .../ref/issue-4828-math-number-multi-char.png | Bin 465 -> 461 bytes .../ref/issue-5489-matrix-stray-linebreak.png | Bin 644 -> 716 bytes tests/ref/math-attach-kerning-mixed.png | Bin 2418 -> 2419 bytes tests/ref/math-attach-limit-long.png | Bin 1941 -> 2060 bytes tests/ref/math-attach-prescripts.png | Bin 670 -> 687 bytes tests/ref/math-frac-precedence.png | Bin 3586 -> 3592 bytes tests/ref/math-root-frame-size-index.png | Bin 902 -> 897 bytes tests/ref/math-root-large-index.png | Bin 648 -> 640 bytes 9 files changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 59ac5b089..7ecbcfbaf 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -65,18 +65,13 @@ fn layout_inline_text( // Small optimization for numbers. Note that this lays out slightly // differently to normal text and is worth re-evaluating in the future. let mut fragments = vec![]; - let is_single = text.chars().count() == 1; for unstyled_c in text.chars() { let c = styled_char(styles, unstyled_c, false); let mut glyph = GlyphFragment::new(ctx, styles, c, span); - if is_single { - // Duplicate what `layout_glyph` does exactly even if it's - // probably incorrect here. - match EquationElem::size_in(styles) { - MathSize::Script => glyph.make_script_size(ctx), - MathSize::ScriptScript => glyph.make_script_script_size(ctx), - _ => {} - } + match EquationElem::size_in(styles) { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} } fragments.push(glyph.into()); } diff --git a/tests/ref/issue-4828-math-number-multi-char.png b/tests/ref/issue-4828-math-number-multi-char.png index ff0a9bab97de1957f3ea99de261346c14f8ccd9d..b365645d33edd081c77563ba8b3cf23809a3d923 100644 GIT binary patch delta 435 zcmV;k0ZjhU1I+`FB!84iL_t(|+U?a(OOpWr$8ldyFQP-Iplf#MB%}#NflydP6hRd8 z;$gLD3!R(UA8>~P4-zh*(bdd^&g;H1zNcyzG1Z9X`iji({&# z2{+*;Oyl8=^thv&e$a4OZV>Y8z?AxRQo9%P4T394z}m>6@PEh}V4%_%I9>*b1c$&u z1fU!b3vWekuG4|HuP0xS8ppHVAd{|Uo~{B+w~j(C#pWKDz*RiU)0rO*7;~K23XAIFU&7bBVyYUs&mZGlf2dQ@3})r^G=Y^(+%C zb;9Mau&Ft;F@Ika=@R_j=7bn+*%gN2^Hf z0lCHc@b4?S%Pi)Ay_B%<%7xSr_BVl&I#teZ>vFed-z?k@4SyRQ{ppAnvhVGTm72HM dgq!gH{R0B4Q+)?#OgsPp002ovPDHLkV1jDm(rf?# delta 439 zcmV;o0Z9JM1JMJJB!8GmL_t(|+U?a%OVa@W$8q0JZ=pk{plfstp)9aNN(FU@APB?= z1gEA>=GttT;DQ1XJ1D4#k|-_F9x#V#)0sucYf`lp^vQWqM|=kr@!WGr_t z9x0mZz~X6-bI-fy7r^iQUO)9tl5%1-#^oR^9F0bMsV8vm3s&yKnOscn)a^gub#$+3 zkwt>F^Kh*g7JoLAK`n1eCrvRHhxFNuso=(7sSLYI%&jsor1Un?Sy-S}SUCN;t&c`- z#_pstM3!Oa5*_V#3l9fju2C}E`OEqPpBlQFVr3db!&^bJpRsv>78Sbgs?@CgacMX- zEE_2!u>(xJs|;W9XoioYKshcnymBFt!tOUPZDt)$8rC^)}K(W^&8G|wvij;GHxoR*=|j& zkr}%Am6P%dDTJgcXe6;CG<6O)tW(@6>Rjf4&C% zCk=X759{Ge6plD%G~i(C4m;ZA*AsMoHF{EB+lT88&&&Ws)_+Lc`JJ=!A zdf(P|x-^HPOna{U6%}DB2>=qi=#HQjUJ9U4Df1piMe@9 z!qsR|4#UK05H{|kC~r2ijL8nJ=Xj?#fm*MDH9Af9eWM29D`doGm2w~?7sD_+iCLv2 zd89VX8pzE0#(z@VuCOo?6T%-75|r{ET>@u26IPc~K{`|bU|>~;2W@0ZLEqdO?jYbn zA9_(qq9D=fY=7Lju$TdWfkD+_ixb4zvlF~o#<~ahK%oi+(CNbg&K|w60D#k?I^66d zGv8!ho4EA^FKyC%U}I(_kEjkGG@>NM^E~&3k2{xsEh&JIcs@9_#Rr+Ay#t>AL5__Fs>8Sn;48y0E(AOZ z>j07SD1YtBk5q@dLvrHb9zstMS@FR*B%^JYxx!dD0JdJ zhTM6`C}4Z4cs`o&lS}Kl24M`uD#KQ(=Yn-_REN9J6U|se0Ej(@7GZR8WEPzBU>(P& z!7#iFkR=@vEKKHhH{$2+bpYV3P!YBpY$g4att~E8zq}e=)E(vmVGRU1{ZB#<>tQ|o aul)p!nYUG3woT>$0000qHPo<(8zW_vLsD&q5(v17OUl+h!`3+F>=D@UUhHwf1S`*a5 z4t8)8e~1;o4s)E{ImWEi2U=^!qEi)ZkA=^HIL#WWNnY) z10Y(6QVZiD6#KzzJwmSZY8i?Q+twlM78Y~LF}<*fhwS1#7#MbAu{@w)#MQyECkqj8 zrWo!$>SU7PZS$AjOLchL7jCWtK=&~yhH?2Ek`&)ytNCp8paR7YTgOtoJZ%Op0ARUz z&+zN%4bsy3#+|G$sh-nY1BXkY1O>+>?I)pywXhZ*w%?lP&H~-n|Lp((002ovPDHLk FV1nTEEmQyi diff --git a/tests/ref/math-attach-kerning-mixed.png b/tests/ref/math-attach-kerning-mixed.png index 9d0bea27af9c7a50b79a0d8aed64dc01e3d45ae5..64a5468696799f0c560878e62a10d767948120cb 100644 GIT binary patch delta 2408 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 delta 1926 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 delta 645 zcmV;00($+g1)c?vB!6W|L_t(|+U?cPOOtT`$8j&6`bWCerGk>gOF`Hr42yVFeQN61czio(%9vkF2gw3aav6F}?Bw1#>~fOuNS(9t+c`j=O|ae*a5)?fKcX3< z>#4VMID8kB3V+CX!+DUD6Xni!?`PspiNbS5M%1m|DjBeJG@`hgEwD?TU%lM%mQ|$~ z_MMvIWP3wD5iNcZ+84x@okdQJ<^=Sjn!kGa0%x0O{Y$e)7 zw~z&li_Tvmp9DJThGqxlU~%04IdC^o{*j9jvrc z+)WSBQ$y!cKm1J?{&~D0OhoyvW&#AVHxXH1A`&tmM1-}1AI~ez0R10c%(FjCt(9=I zYI4u?yMDn4rFvzeaJ{Qg1Cl12fN0Bt$Y@gxP#+PweaB^Lee00000NkvXXu0mjfCBiTg diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png index fd16f2e6bf3e043a81e4e6e3146a6b15c05fc559..bddcb43c33dbe87cd906241291093c7edb68abb7 100644 GIT binary patch delta 3590 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 A2LJ#7 delta 879 zcmV-#1CacI2ZjfbB!5jwL_t(|+U?fQPm^~Xz;XWpcbV5EJ8ZjC(|!pp7-thxxA8Zo+qF07G6+WG+2W*Sc5keK7Vh`HAX?Hw)`k07?zI* zaz!bqeL6o%3WghEY$$bPGp<@b%DAi90BSJ|^K?6ZniC95GF9%-YoO+Ta*)gpemg1z zzH#>4HSYGnS{;D#1a#Z13Z08!n*r88zas*c%d^rT<0UE1JOsQ4dode$m!(q&;Aa3^ z<$?&f*+4}cYJbhCdf<8{y)CwVZTSmO-i$jZAbE?d0nb=1)6Ci7kpyJh^kB1@(!^VULjcoX zApnk)Be|F?Numz{zQcBmCfbyPQq`1KWO2bPXbZr$B&>DO1{#&|nRIZrEq9RlHyE|P0i zJ=%O1FI{vuLv?;E3xw-eDQwEeh3g?bSN;)v)qmmRc$~Tus7*}XKYL?T;y`w#qi1hI z9Qy({-ZS8ZI`&n|DVVXxIZ#cU_6HUNsg4Xn1-Gyn++s8)cR6%-i` z??Jl2bjdn@gn^kO=VV3^sPiheeB%& zfN1(Bun^!|OOfFwPqb_ZmP-wOU}qrtl3b4QjQZx-GhhH}EPQ4vGA!F&E#pAtj{xJ2 zDkmQ2;7N5mdtOZdQTyrIjG(VN8048vYD+?cHCTf;t*3U+FG98!$i@Hw002ovPDHLk FV1fz?qpkn| diff --git a/tests/ref/math-root-large-index.png b/tests/ref/math-root-large-index.png index 85689823da386744b879c83bb6d72ccd8706d8db..29dd478fe568b02e112791b7bf41689967e0c209 100644 GIT binary patch delta 615 zcmV-t0+{`X1%L&RB!5RqL_t(|+U?feOH%S!K@7bKMTw*- zg-*!A9He4k=1ZeYA#FNSN2Qe8NHNPWmrae$y|HX=*><*bzUi)Zww(=jAiX%B+uy|( zo~x&beNv=_wXhb}!Us0|>smrxIBM(=DMWKzT^L6t`x4&twSTTtfNy=HVKrgFGh_@V z3JA~j42IN%BTK!;r6}1Y#{Zz?qg0KWFs_vrFACyat0;OqsxFMLT|mizVmNFnt?>eW z_bwq{Sc>gQ$h#M-+(05ZNyx!^D=?IfM<$CBh1mpHDK0iA0y4msd^wWvNkeL2JeyEA~5POTS+6GhyaWY4LWm$ z`>vp*deDVhAN$|&KRp$IRyX!002ovPDHLkV1gV2 BCT{=$ delta 623 zcmV-#0+9WH1&9TZB!5pyL_t(|+U?fOOB7)oz;XW|>C~Y^5FG;R5*{R~i-#hEZp!*@ zWW|D7SxYIBs2Sz1u5UzQCff!Pt0uT>3%=z`y0*2>y13)+G&}Q6y*=!XgAP=O`5d1g z4`2B49G*P>kvuzWhwZQ({$Im#S;T5sa<_5}FTPt1>$Bw?Lx0zr)$l^HdWR!h>Gme9 zg_Y(0zAc@j3U|JgtcABj?Y*-Za>|Uz7zn1ybZcRPziJe6Q=czlWTMY%n3Xmi18$?R z>@O~f=n&i3lQIu$%05jv`*yiR0aRrl0_C2^;8r5s?OD)SzT3HH-8}rTabHMWbs(?` z$-0A4N_g*qJ%3x!a4?z^Zv9wv0CkG_s#{$l<~qo>QyEm@@g`Ze>(c(-3|~BCORZO_ zthgYSG6|=9CaC;P4ufAozD*8oNM3O0WSe~Z1gd0)FZfKt107TI&p?B$$t*lt1r04% zc)ANTbVM4k(Fiz9!us$~v?9gKtD7xF2-Hy@>2erwUw>_C5|{Mh=lo|%Gfef|Xt^Sc zV#Qu?Cd|X5rLfryGn|v7Vz+t(+UFq{EmY$O25Moc4Q4o_MiDHXCeeU-c>81o=MIoA zASq0CLub)z0lY3`1k#B(^$HL#B#Ef#Q*#XSFkO}L2Y}3!aF#Y=4`ymOD z%NcnJYG_qY#gZRU;Jgzceci1c8dme`CBQfB9|O_HOSM39aIHTKB02w#42*@weSoMS z0x;g!8$L8lLperHO<<(vzU8lvvWpQ3L%WLo*|WoT*be_Se*k<$&z8G_6=VPa002ov JPDHLkV1nH`CQASS From 5f776c7372ffecbbe959fbfa968c8c91efaf0061 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Jun 2025 09:41:08 +0000 Subject: [PATCH 061/162] Bump New CM fonts to version 7.0.2 (#6376) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30a4db7a7..347704b33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2863,7 +2863,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a" +source = "git+https://github.com/typst/typst-assets?rev=c74e539#c74e539b090070a0c66fd007c550f5b6d3b724bd" [[package]] name = "typst-cli" diff --git a/Cargo.toml b/Cargo.toml index bc563b980..0f871e211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c74e539" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" } arrayvec = "0.7.4" az = "1.2" From 1de2095f67c9719a973868618c3548dd6083f534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 4 Jun 2025 11:54:03 +0200 Subject: [PATCH 062/162] Add support for WebP images (#6311) --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/typst-ide/src/complete.rs | 4 +++- crates/typst-layout/src/image.rs | 1 + crates/typst-library/src/visualize/image/mod.rs | 4 ++-- crates/typst-library/src/visualize/image/raster.rs | 6 ++++++ crates/typst-svg/src/image.rs | 1 + docs/tutorial/1-writing.md | 2 +- tests/suite/visualize/image.typ | 2 +- 9 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 347704b33..a9b3756a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,6 +1215,7 @@ dependencies = [ "byteorder-lite", "color_quant", "gif", + "image-webp", "num-traits", "png", "zune-core", diff --git a/Cargo.toml b/Cargo.toml index 0f871e211..b4890e3c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ icu_provider_adapters = "1.4" icu_provider_blob = "1.4" icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" -image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } +image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif", "webp"] } indexmap = { version = "2", features = ["serde"] } infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 15b4296eb..4a36045ae 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -841,7 +841,9 @@ fn param_value_completions<'a>( /// Returns which file extensions to complete for the given parameter if any. fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> { Some(match (func.name(), param.name) { - (Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], + (Some("image"), "source") => { + &["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp"] + } (Some("csv"), "source") => &["csv"], (Some("plugin"), "source") => &["wasm"], (Some("cbor"), "source") => &["cbor"], diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 3e5b7d8bd..8136a25a3 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -147,6 +147,7 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), "gif" => return Ok(ExchangeFormat::Gif.into()), "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), + "webp" => return Ok(ExchangeFormat::Webp.into()), _ => {} } } diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 258eb96f3..f9e345e70 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -77,8 +77,8 @@ pub struct ImageElem { /// [`source`]($image.source) (even then, Typst will try to figure out the /// format automatically, but that's not always possible). /// - /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well - /// as raw pixel data. Embedding PDFs as images is + /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}`, + /// `{"webp"}` as well as raw pixel data. Embedding PDFs as images is /// [not currently supported](https://github.com/typst/typst/issues/145). /// /// When providing raw pixel data as the `source`, you must specify a diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 21d5b18fc..54f832bae 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -9,6 +9,7 @@ use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; +use image::codecs::webp::WebPDecoder; use image::{ guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, }; @@ -77,6 +78,7 @@ impl RasterImage { ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc), ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc), ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc), + ExchangeFormat::Webp => decode(WebPDecoder::new(cursor), icc), } .map_err(format_image_error)?; @@ -242,6 +244,8 @@ pub enum ExchangeFormat { /// Raster format that is typically used for short animated clips. Typst can /// load GIFs, but they will become static. Gif, + /// Raster format that supports both lossy and lossless compression. + Webp, } impl ExchangeFormat { @@ -257,6 +261,7 @@ impl From for image::ImageFormat { ExchangeFormat::Png => image::ImageFormat::Png, ExchangeFormat::Jpg => image::ImageFormat::Jpeg, ExchangeFormat::Gif => image::ImageFormat::Gif, + ExchangeFormat::Webp => image::ImageFormat::WebP, } } } @@ -269,6 +274,7 @@ impl TryFrom for ExchangeFormat { image::ImageFormat::Png => ExchangeFormat::Png, image::ImageFormat::Jpeg => ExchangeFormat::Jpg, image::ImageFormat::Gif => ExchangeFormat::Gif, + image::ImageFormat::WebP => ExchangeFormat::Webp, _ => bail!("format not yet supported"), }) } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index d74432026..1868ca39b 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -45,6 +45,7 @@ pub fn convert_image_to_base64_url(image: &Image) -> EcoString { ExchangeFormat::Png => "png", ExchangeFormat::Jpg => "jpeg", ExchangeFormat::Gif => "gif", + ExchangeFormat::Webp => "webp", }, raster.data(), ), diff --git a/docs/tutorial/1-writing.md b/docs/tutorial/1-writing.md index acc257830..d505d2d03 100644 --- a/docs/tutorial/1-writing.md +++ b/docs/tutorial/1-writing.md @@ -69,7 +69,7 @@ the first item of the list above by indenting it. ## Adding a figure { #figure } You think that your report would benefit from a figure. Let's add one. Typst -supports images in the formats PNG, JPEG, GIF, and SVG. To add an image file to +supports images in the formats PNG, JPEG, GIF, SVG, and WebP. To add an image file to your project, first open the _file panel_ by clicking the box icon in the left sidebar. Here, you can see a list of all files in your project. Currently, there is only one: The main Typst file you are writing in. To upload another file, diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 9a77870af..73c4feff8 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -243,7 +243,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-png-but-pixmap-format --- #image( read("/assets/images/tiger.jpg", encoding: none), - // Error: 11-18 expected "png", "jpg", "gif", dictionary, "svg", or auto + // Error: 11-18 expected "png", "jpg", "gif", "webp", dictionary, "svg", or auto format: "rgba8", ) From aee99408e1cb6e825992a43399597f5d1a937230 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 4 Jun 2025 10:14:24 +0000 Subject: [PATCH 063/162] Apply short fall consistently in math when stretching (#6377) --- crates/typst-layout/src/math/accent.rs | 2 +- crates/typst-layout/src/math/frac.rs | 4 ++-- crates/typst-layout/src/math/fragment.rs | 12 +++--------- crates/typst-layout/src/math/mat.rs | 4 ++-- crates/typst-layout/src/math/root.rs | 2 +- crates/typst-layout/src/math/stretch.rs | 11 ++++------- crates/typst-layout/src/math/text.rs | 2 +- crates/typst-layout/src/math/underover.rs | 2 +- tests/ref/gradient-math-conic.png | Bin 1721 -> 1642 bytes tests/ref/gradient-math-dir.png | Bin 2615 -> 2575 bytes tests/ref/gradient-math-mat.png | Bin 1560 -> 1557 bytes tests/ref/gradient-math-misc.png | Bin 2993 -> 3138 bytes tests/ref/gradient-math-radial.png | Bin 1641 -> 1606 bytes tests/ref/issue-1617-mat-align.png | Bin 3354 -> 3335 bytes .../issue-3774-math-call-empty-2d-args.png | Bin 1315 -> 1334 bytes tests/ref/math-accent-bottom-high-base.png | Bin 572 -> 567 bytes tests/ref/math-accent-bottom-wide-base.png | Bin 359 -> 351 bytes tests/ref/math-accent-wide-base.png | Bin 510 -> 506 bytes tests/ref/math-cases-gap.png | Bin 340 -> 354 bytes tests/ref/math-cases-linebreaks.png | Bin 506 -> 492 bytes tests/ref/math-cases.png | Bin 1281 -> 1228 bytes .../math-mat-align-explicit-alternating.png | Bin 1035 -> 927 bytes tests/ref/math-mat-align-explicit-left.png | Bin 989 -> 903 bytes tests/ref/math-mat-align-explicit-mixed.png | Bin 2523 -> 2454 bytes tests/ref/math-mat-align-explicit-right.png | Bin 976 -> 875 bytes tests/ref/math-mat-align-implicit.png | Bin 1046 -> 954 bytes tests/ref/math-mat-align-signed-numbers.png | Bin 2036 -> 2024 bytes tests/ref/math-mat-align.png | Bin 1564 -> 1531 bytes tests/ref/math-mat-augment-set.png | Bin 1810 -> 1714 bytes tests/ref/math-mat-augment.png | Bin 3631 -> 3563 bytes tests/ref/math-mat-baseline.png | Bin 818 -> 816 bytes tests/ref/math-mat-gap.png | Bin 496 -> 526 bytes tests/ref/math-mat-gaps.png | Bin 1309 -> 1311 bytes tests/ref/math-mat-linebreaks.png | Bin 651 -> 648 bytes tests/ref/math-mat-sparse.png | Bin 882 -> 956 bytes tests/ref/math-mat-spread.png | Bin 1814 -> 1796 bytes tests/ref/math-shorthands.png | Bin 1231 -> 1173 bytes .../math-vec-align-explicit-alternating.png | Bin 1035 -> 927 bytes tests/ref/math-vec-align.png | Bin 1098 -> 1126 bytes tests/ref/math-vec-gap.png | Bin 420 -> 436 bytes tests/ref/math-vec-linebreaks.png | Bin 651 -> 648 bytes tests/ref/math-vec-wide.png | Bin 620 -> 630 bytes 42 files changed, 15 insertions(+), 24 deletions(-) diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 53dfdf055..301606466 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -46,7 +46,7 @@ pub fn layout_accent( // wide in many case. let width = elem.size(styles).relative_to(base.width()); let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); - let variant = glyph.stretch_horizontal(ctx, width, short_fall); + let variant = glyph.stretch_horizontal(ctx, width - short_fall); let accent = variant.frame; let accent_attach = variant.accent_attach.0; diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 6d3caac45..2567349d0 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -110,12 +110,12 @@ fn layout_frac_like( if binom { let mut left = GlyphFragment::new(ctx, styles, '(', span) - .stretch_vertical(ctx, height, short_fall); + .stretch_vertical(ctx, height - short_fall); left.center_on_axis(ctx); ctx.push(left); ctx.push(FrameFragment::new(styles, frame)); let mut right = GlyphFragment::new(ctx, styles, ')', span) - .stretch_vertical(ctx, height, short_fall); + .stretch_vertical(ctx, height - short_fall); right.center_on_axis(ctx); ctx.push(right); } else { diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 85101c486..01fa6be4b 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -435,13 +435,8 @@ impl GlyphFragment { } /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical( - self, - ctx: &mut MathContext, - height: Abs, - short_fall: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, height, short_fall, Axis::Y) + pub fn stretch_vertical(self, ctx: &mut MathContext, height: Abs) -> VariantFragment { + stretch_glyph(ctx, self, height, Axis::Y) } /// Try to stretch a glyph to a desired width. @@ -449,9 +444,8 @@ impl GlyphFragment { self, ctx: &mut MathContext, width: Abs, - short_fall: Abs, ) -> VariantFragment { - stretch_glyph(ctx, self, width, short_fall, Axis::X) + stretch_glyph(ctx, self, width, Axis::X) } } diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index d678f8658..e509cecc7 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -314,7 +314,7 @@ fn layout_delimiters( if let Some(left) = left { let mut left = GlyphFragment::new(ctx, styles, left, span) - .stretch_vertical(ctx, target, short_fall); + .stretch_vertical(ctx, target - short_fall); left.align_on_axis(ctx, delimiter_alignment(left.c)); ctx.push(left); } @@ -323,7 +323,7 @@ fn layout_delimiters( if let Some(right) = right { let mut right = GlyphFragment::new(ctx, styles, right, span) - .stretch_vertical(ctx, target, short_fall); + .stretch_vertical(ctx, target - short_fall); right.align_on_axis(ctx, delimiter_alignment(right.c)); ctx.push(right); } diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index c7f41488e..32f527198 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -50,7 +50,7 @@ pub fn layout_root( // Layout root symbol. let target = radicand.height() + thickness + gap; let sqrt = GlyphFragment::new(ctx, styles, '√', span) - .stretch_vertical(ctx, target, Abs::zero()) + .stretch_vertical(ctx, target) .frame; // Layout the index. diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 6157d0c50..40f76da59 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -67,8 +67,7 @@ pub fn stretch_fragment( let mut variant = stretch_glyph( ctx, glyph, - stretch.relative_to(relative_to_size), - short_fall, + stretch.relative_to(relative_to_size) - short_fall, axis, ); @@ -120,7 +119,6 @@ pub fn stretch_glyph( ctx: &mut MathContext, mut base: GlyphFragment, target: Abs, - short_fall: Abs, axis: Axis, ) -> VariantFragment { // If the base glyph is good enough, use it. @@ -128,8 +126,7 @@ pub fn stretch_glyph( Axis::X => base.width, Axis::Y => base.height(), }; - let short_target = target - short_fall; - if short_target <= advance { + if target <= advance { return base.into_variant(); } @@ -153,13 +150,13 @@ pub fn stretch_glyph( for variant in construction.variants { best_id = variant.variant_glyph; best_advance = base.font.to_em(variant.advance_measurement).at(base.font_size); - if short_target <= best_advance { + if target <= best_advance { break; } } // This is either good or the best we've got. - if short_target <= best_advance || construction.assembly.is_none() { + if target <= best_advance || construction.assembly.is_none() { base.set_id(ctx, best_id); return base.into_variant(); } diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 7ecbcfbaf..e191ec170 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -159,7 +159,7 @@ fn layout_glyph( let mut variant = if math_size == MathSize::Display { let height = scaled!(ctx, styles, display_operator_min_height) .max(SQRT_2 * glyph.height()); - glyph.stretch_vertical(ctx, height, Abs::zero()) + glyph.stretch_vertical(ctx, height) } else { glyph.into_variant() }; diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index 5b6bd40eb..a24113c81 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -286,7 +286,7 @@ fn layout_underoverspreader( let body_class = body.class(); let body = body.into_fragment(styles); let glyph = GlyphFragment::new(ctx, styles, c, span); - let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); + let stretched = glyph.stretch_horizontal(ctx, body.width()); let mut rows = vec![]; let baseline = match position { diff --git a/tests/ref/gradient-math-conic.png b/tests/ref/gradient-math-conic.png index ffd3e8068f65a6c50e76af647605aa5b21005338..9bac6c3d4ced5297eabd91af61bcd7addfb2d4a8 100644 GIT binary patch delta 1638 zcmV-s2ATP}4eAV#7k@Yi0ssI2xn8U3000ItNkl&$@C`$=+QAxUFYZ{WKY11r?vo@~dI9}p?x8r>q$74HQ#vaf1 zBaw2L(U__gNu+3=j~-c;Y+t_f*W(#!<|PoP;w6%RgeIX05`UV6CZP!unuI1uXcGE= znqJRRSSz_o@@!tQj*lHw9pL z(&z9{P)ek#(|F_!9NI%NYRA6S`Dx<%>-Kccy$UZxP*; zE3LsHt0Wuu(P2xi*4BFH?*~JLGW+b%>f8gPPEnMg!Z4h{5ZCE~JTC%A0=~l1qF>XY z8!h3JJ}-dh56yOjxus}_6F5$U(!to)8!UjyNr!zW1AiP51gYA*>O3u+gownW1n(k!Rn&VY~0mu=ufW&t>GF@ zo_i-XGkP^@+PP@PTp>a8XM4SG&Ca9GWUJSIThwT1YM*<(*Mn10 zt1^Fl`{2xnmb`AfxLk#M^*bNXoghd!KK1=pU+?`h9=cXz-+%Am6)(u1Y`xOsM>iDR z9-IzCMUrswf^M+i@My{|Nl*)VGImpRecst(I)9KUq|3jbpE%p0`Najc=L;zr#|uhU z_?N#8qnn58>4Awr*YjxeX0%yn|7(Aky}Jb<>`5QIS57o@xp(pmhZds|F%o7k{VF)@ z%KSV!9mNtw)!Y`uciw=CB~VS|nl~qWP?1E*ub9yqyU*xzH;TOv9lVQH*InH4eqJF^;ci#!L%Y`sU+sI z@+DQPP!j$A+BUe=An}G zrKn^PfPQK#AEQ)$iIbH5!^P(UDI+$Ej#mnryXms5M6@laJGlsM)>YO)0DQxr^?yP7xbh3ZA=I}y> zwz+{r)V5P-vrwW#iHz9`An%IQvU^YpZ#bfxZUDX&Yax_|jALQjrL$-^G=Hr(TP%fty#|%o z)MIodNwF1^wSUTVd0gmlP^zRU_l#cPc;l@(?_-nfSD5{2R1!rEg;opxd_`bdiu!al zFffj8GDb(USOyS(VojUeP=BfA>8va2oi`?}0pJLoC~S@B0BAZiZQ7w$4UnmL!_o0s zbQ51PHu5C^_1s?CXSU7!|m$rku~eS?0=GNv`mBZcW^sHv4Td}{M4rkbNN5t8 kAfZWU5}F{PNoWGVzrc)+4>46(^b07*qoM6N<$f=GfJZvX%Q delta 1718 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 Z`7hW!#nL9z|C0a!002ovPDHLkV1jBn2Ydhk diff --git a/tests/ref/gradient-math-mat.png b/tests/ref/gradient-math-mat.png index ecf9530389ebe41ea2ccc67c00a20bce7c8c416d..d003d6d03a21e22a04c21308f168a31c09f9551e 100644 GIT binary patch delta 1553 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 literal 2993 zcmV;i3r_TjP)79tnaA<|c|~sqn2SL#21REsW`M!WAix9#+B6Ab$7$2VaqLVS$BmMaB{hzu*pebk zmMm(gY^@Y2ilj)1`@SEZ{k+R>9M>}FSPLnVU7rsKUFhsBLi^UT|9Yt2ZENZHZ}kc;WH)CkmQ2+++Qb~OJQ8au^!kHT zIb&>2r+XUuFL|O93fnI_V}2uE;b@bTh-{b74{e(~b)ax2|M<6~&|DyrhQkm|!?b!7 z+CZZwivT1f2kS~`)s~S8vpGTqhIUvwONXD=lj8KyQetB|edMhUUfSK7%>7-md`CaLqs9jz z`MIGbq?gSP+t&eX@CCovX(jZMn)t4I^j$eVQA~~JvtyaOx2EEl!Q`W;K6hmP?`mUc zOFt@c99!8nCl}NySL5MqefD8(ZmPWRX*4eyn-A0bYlYE|{Om@HRP(eJ=U$IV>$g#L8T8(92lys4%;6C_{RB6g@Ptw5&aMK9ki)x3JHt_ zba%{ZuYlrkCbu7Dp8i@|qaXH#(+7%HvpJI;O*8?N@U~y=a~l`sw8F&S(Gc&7DM` zzBQBSXrlMixc3;JPh!mn*AsBwqcu7y0-J1Zn=g$!{av{j_WSGW8T0k&N}|T+!xmb$ zG#m@86@UMW&3sJtxUy%3=1dk6nh-J{R|S_Hg_c0F1R;?w!g6}I>&S}3sw_-811Wjb zmCzQK=>~HI;#H1zFk2K;n&98plcIwI%b$IF=g$1gtNZA2>-v%Kt@F7P_tF2QzWcX! zGIYXKv@U(vt%+uEY?x>4qv>8pHxDcnx|{aO@1=|xv@9-CZ)rCublW1e-yf;NXqkum zuNnOmx^2nE{eW^CEt_}T{pJ{jZd=9N-mYH8D|9Fdssh}A{)1`>hdrf#!m^G~M-Iqlt&juCR#-=c_HggeIfTcv|Er^u-1Bp$b31 zF$Wd^11g#*P`1n#3}a0o{PEYjsNa=8al~$ z@QR$wOt5?O4NEtfvW{11XwX+#^!Xd1&=i_bXbMfC35BN6ghEqj3jJ3t#E(AHCT`Xi zvW|)-h9iGF(fH`=+VYx8p}Sz6?FLu^r>H1mbi78PyBs=a8xOLjsf0qIdmGvm+^?u# z2Fnz>=S#;vg{IIH`tLCGP6FvthoSo@^tr@ureWF_4xKc$p+xqYx3HcuDfFpCye5~z zw2$t-l&^j?o3HNG%d;6?FrJ2{155X5H9NgmF9wvo4}y)|ij!#IRWT1de{cc=TJ%mO zGmtHRozZ3}^hJcm()raAP9-5Y`s{*PMm7{dUz>{WS=jAmN2~goW}BYzZOr7$ zb(>Eg>a=w9F!Yz1$Xou%8|#@5pHyk0>6sygrqE}H9yS)2jBt%xXkv5B_{@nFjY4;a zBGmv(?AsYrGBgnqp19*XvTM7I;xYQ>aPuy1+`#QC7{vgFLYJY5W- z=dCCIxR82t1Lu*_HhJNRehnvCP?Cn=IEmo2>v*QTTnPPm(il6yc`RN!E%aPHp>d9v z)fBO?pwz1lg?=u7-B{Lz{hXcB8XrD^L<^xmYJH+hRo>rUJPc|S`g=@q(wDZR z$#5py$;BJP8~XJyTWx)f9v`p9B07bJaAfFRA1j&YOw_6~DQO>lD$mCUT3EqpvObY< z(}AT^+e7iJyMz;22G+;$ho3`o3f&R^V*bug*VZrZhc0_F?`|ts-0ZiH@WB|a+{Qca z;8#Dx<)2{nQ@YpDsr-?e_SA|ywfas)-zhnsyxdL07l%KBU=ZOwL_IL;6uM^^hh;rK zJ2R6)cSqTGJhMVVv%v&*0=Rb%p@%LVUa zRMH5~QRq$yE@SS}!Pf}@#V>;_T9dH~RP+1L*(|nK;9o%SA%*UO-H~K@uMVJe=YXdh z$kd_R&qH&Dy?I0yD0CNuX7klh1Ax9$GA1K(Gl`Odogh3O0I1~FjL^a9%gE+-i>sndXCXG#1 z*cAFK!No%Aae4C>9y6f{#g;`4I&Ch}fq+JOxqC-WW_a9crII@+h(I9*PkkJ}4g zXNvCL^^oOcb(Z3=-S!}b?x9M377!fPsy#rVdm4JKWUl9#$ALwmd#Isp(KACrp(!+l nCKQ@N6ADeCDKw$bf0h0p5MsaV_+?U<00000NkvXXu0mjf3JTd! diff --git a/tests/ref/gradient-math-radial.png b/tests/ref/gradient-math-radial.png index 8d0047bbe491b86029ba81088f498b5361008a15..97fb17e6fbb1778200ae2fc10226d5d4c74f190a 100644 GIT binary patch delta 1602 zcmV-I2EF;|48{zQ7k@Yi0ssI2xn8U3000IJNkl5RxiT#iA)y)?hFmuq!0CsYvZup#(Prp!&w^ix||+;rPh^a5rk1~?2c3mc0`1m z147-9P~BeAGHbXXt$C#AZU9vYg-*GF){R zy&(ncAX<5VM2h2*W%zqUWGCaL9<9DTDgNE?c!(L|x zJ>i3v8nBp(9tAr(;QH11h$uuj$&(both+Wc)|Dhf{iVQ2M*grutTf+u~jKLb{H@fK&F{ma(@|ip8r+4@#>6ogI+t-1o2had7*Lk-U**mgGM;FzaB&tc3yg^RC&bf(4e)P z$Q^tHyJ9UXo&UIZ&j-CGPs7$>hJ(*;!zM5w*gnOaeD>h0y&bfSOD^UR(u%NEp(K2M z0_-SCGRRiirH{Xfx1I@4+**w5U}MJ1kb~WeyMG~}sDc66$WQO@+1o(}kn1&njzdw{ zDwc)gN5M|4Q0p&Uw>?L979s_tf}KKECMRez%nmT5E8eLmI)S4}9PW~DWKaN%Fak{m zO#!s=qwAK&Vc^;UuwcdEq88~FZ`ua^l!;E{OVg|b7M`YP+wFyEPiAf~`%YB{25=(} z0)Mq#eK?Ey!JwG$EhQU-XuY!3fEF6`*+xi=S`uC0>)y9)*lZ5XmT>l&atrK)c@Y4v z4CPk^9n3naf}PdD@_Mr5Uj>6qnEUy|JzE_H|0GH07n`BsDI}F-3~0 zVhjl}U17mYDozFm-vcwBTsMvS`ceB$<$vO`z6@p|P6!nxGCcJ0T~iX6^*srfH}B#0 zgsXN543fd(=?}X1WY8>yJtG3x8GVuTy-azVV|$>ZcTEaM zPD)2$$LxWQ-W?3i9^=o#uE?)QXAZa=8Z@hOUuSA|Z3TC3bx6AQNx-ecZdeI(m%s2h zG-%?}?r>h#%tsN>F}xkt;VpCxgMY=sNUrLKQRfDIw0?CthrpoUFMV6Fa_W4l;+L4K zg9S6B=)UfM@5FeUqF|xQI@wufJdE{KEID73?cBLRS3W9?%}8J{K3rTMdNZMsB z6RtEFW(OJ424F_sf0I?Eytyn+tb#%JgGI8!I5+5l-h69a+O|?jrnb-QwSU82PgB&> z7}YHmLtYh-$8Oi#@cY`_@Ebpz7f&JCKlb8JN95Ev8~>G6fB%@)60QYg0s*#N$ZGB*@^2Mef42_5&g|wL*x)_pKO9BhdU6A=~pW({VR(a zY#-9j^vgo@TUithng&hLplQ%FXo?0+gQfuB9}DrB4(bZ{wEzGB07*qoM6N<$g8JnB AsQ>@~ delta 1637 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 RGY$X%002ovPDHLkV1glBKE(h4 literal 3354 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 diff --git a/tests/ref/issue-3774-math-call-empty-2d-args.png b/tests/ref/issue-3774-math-call-empty-2d-args.png index c1bf52d0040aee18b7b00eefb7ba55ab48dafe9d..52472d8dbeed2803c9dd5d075f7244d213abef66 100644 GIT binary patch delta 1328 zcmV-01<(4U3bqQ67k}&s00000n)p3i000F2Nkl z2j+U1%h~FpW*szh9dn`(P7F-cU|N{sW9mdvvj>NDqvnFrMQZqf&POoMkWMHuNfa5R z2!bFF;o-onHh;vz7Phd3(T8(9-k&1@ zU(A5R=k(#O_yDl1Qyt!S0pRRjb$D6`AT&)IzO-DLenlBxbyj*NT^V*)NpH?nh9`7M zedBcD?&m=^tXmbXj|N$Ey@id!4gbLns&HT^$c6`0VWK;wzKQ6fF zmfn6x86NjU>K~&E`=dZM+;2}2gA;C;0I~@Us&Hs1$bW`~RADBzOFLgc7oJ@%{VP@( zUY{?`-=GXvd?fv_Oc~}qKY-tjHvCf_z_Fw1a3CcF9dm=~Fy-Gs(hc-se)D=R+b4{L zEo@;64{+G$iwwNw+KsOx0q5+6%Q^b+qeTFpHmSqAyZ|Mc+Hmk~sVk%mAKf6`a#9&y zQ6#;Ps(%bmua(}3QHI9^q)$fc!oCq8i}Kl1#NdPTe^(1)wGLqawBFlFB2 z>-Gs_VGCQ>!UG&`X^9M+ezQ6=67b$!xb~es+<*8nz^XcRcuP4z*?Mib+a-+&D8q-g zN_YIC45wX@7A{qWU3aB5Zz;p0gVMlAUAR*`U__@qMGQ{3X&lJLH~mj;Ad{Mpimtq)dzL+(TDGSPGD|5`Y?C? zD1WK4PZ$eZ*uoYb*l@6}E*J?olu}#2A&5S_{RC9*)`#z{1Xy3K4zIZdaC4Rfd-pOD`{0hNsp^|9(Rm9vPAbqjcepQ6L-DVNVf*6Mi@rWMdyj=ms(z zU3hYnv}KAioZyvSU!V+skt6+Kmoofdj(_z1`^qr$n*mx9(S{4N0kTWfVLoYxs$}%x zhiijfu`TGs)L$>XZ=Wz0wy=dQJn-Ohho4Nr$F~!9-<~oD-(Cc?*U;O+{ z7JaWH&J>)#6Cf`f%$IqF;F!n#!!2)`fjzPG5A#8>5qRDAPVj*YBk<%JfYz*Kjemq$ zu+`QlV=W>MJU0fmw})V6xBC&kFWn3MTD7VJTNRA9L`N mEW&Q9-=3Q+Y+(xza`<2R*CZT0x74Zt0000*X)3Zn{;7k}*t00000QKatv000E)NklY`(#l!N@ahxN z+%?K@ak6w#p?@;W^j-k}bhP2U#{qJGQipwufbO}y>M(_SAh`&Am{ab8WA-FrVGCQ> z!u=c$2EzmIx^iuAIN+QuaPp`={C7OS(sp$?^D;pGPIcIYL>tasC0&=N45yrzo?WgC zzf&W<^|mrRrbp@LSUeSi4gM*yqp)ZtBK0Hy1-;a-;34QT=N;f&)@xlf3lm7L(GCVve4Mga|T_Zp?qRU=I3{1FXG{{D`gy{}s zD7x^ZW@+1GWjM|)Es0l#zsQpQxI-Ckntv^QuU;8uUMoP`e6-<M$!hp(+u5 zxMgjiJGu>hn1+(#dV7Siu!Svb;r<7gIs9Y_K9)hqeS6Xzd}{%HZ1qiJ@Qg0FzBP+S zuPeqBoUt!3o}%Y6PBC>jYO$GJgSg zJP`o6nv#@9s6EwT04|NdaW51bfRDY3<6OTPfHUXfxVTIMaQZ?Vw=mrRoHifF#itp7 z6I?tz_vU^4XRZVTaC#CCujl`7bW*woj1s(TrZYTqnE^OzqBA@(O6RZ62+uRVx;i;e z1KyB`;}SO*fa9m&xGC`}&lpsm@qe~N9G9@&0Gt(tfhTJXzzptif;*nZ1RQhP3C@o-0`L9I2~OQ_1n!y~8qNUVwfjb3KE1%BcXjQG zJm5+yX5hMb`rOQWhF~)D=wrFNFa@);o{+1YhdH=sDJOBUy07*qoM6N<$f@^djP5=M^ delta 71 zcmV-N0J#6R1iS>0BLV^gu_fLCIdF8^?rvGLa^>=sYu7Aav2tnKnE~o8UK;Hmk6Jux d@u6Fv002ovPDHLkV1j5o9?bv% diff --git a/tests/ref/math-accent-bottom-wide-base.png b/tests/ref/math-accent-bottom-wide-base.png index 0475b4856bd49b9a4f7663564747ca9635654028..fb4a1169b745905df726e50d90e56d03d9fe11bf 100644 GIT binary patch delta 102 zcmV-s0Ga>i0^b6VBmpLoCE!}00^~D{abOJu9HdRj^>EAT6)RUPTe)V%AgS%!Xnj0t@er^W0BVrlFPS4}qW}N^07*qo IM6N<$f`ErEX#fBK delta 110 zcmV-!0FnRS0_OsdBmpjwCE#MO0^}bggb%ENfP=J%xGu!1iRQl`Gt@JFb|7?WNZ>P1N-O{rBAKhywNNW2wS|5*EJOnHT0M8KIstp54 Q*Z=?k07*qoM6N<$g8n2lN&o-= diff --git a/tests/ref/math-accent-wide-base.png b/tests/ref/math-accent-wide-base.png index af716bf45f7e1f270278e5ecba92c4a1adf0bb4e..793ab30bd3b2318829cc63dc193a6f6b6399ba4a 100644 GIT binary patch delta 480 zcmV<60U!SU1NsAyB!9t4L_t(|+U=FyD??!%$Ndj36*nT~#w8`BCPnVZ+>w_eNm7y& zbCi;lw|RfLaAUccSs|2{=7ycaHnUC4OJQov#&+8I+Ih}ar(Hib7w2>FJl~$4`t4xBY%sS9D8Gkp)oAiwzY@o zh`rYwXEtA1SeA_h&Uz^Zmc6ucHy#_0JLiClnKC)BsSb3 zMkxp zOmKzYz;ZzT;=u#H^bBudsn$=l#QL&h>=*Sv4wlT1by-`0qgL&t339P>X1&dTp;a-j zoIu3$CoMA^3*5W>)C}NsiDYkY0(X0ehlFb*%8xRS6|o{ty7(6$ WXFuwI9Dtbs0000Q3X&G8t1#Sh&ok!r5uIL^?b zFFj)e*4coE&ZNoVIwvq*w>ee5`y(Gnxl!?{hY<_*^)mrq>Y7OPX8}e%RtprMmx3+6 zuS`C*o%AQw&3{b5^QVc53J#Smp=1QWDJ7a*G;pyn(g>>VN`}|T2;Bw&r>zWCdy!$K z%_#6w(W}7(s&MWiGqAs9s-~8X+Lj6ZVC0B?z{7DB%^Y_RjP|1<_?bQ_WCos)ahD*g z;JsuB+0VdiDEh=b8*=_0uETWJ=d8fyf^Dqlbe;9&OGQ*z>;U>C!NXqUEz11Cm_*U=Bz0nP(taZ%d`Ak#ovs=F!>WNeN(!Cr(^RXN&JBe;=UU;@pHk1Jq63 mojp1tFlzCr#iJIF1d9PQ^aSoH$y^8k00000 zpzKtY2V;$_vik0{%7=8e*wF+;rvAHZN@t7f3P5D}w;y73wm8xoLNBfvn8bCbYG4xA ztJ(jz(%ItVP(KjquchTmXNxB#3{XD4qx^kbP{gRkqZW@^JQ6Gh Y0KklU)%RWnK>z>%07*qoM6N<$g3u(OEdT%j diff --git a/tests/ref/math-cases-linebreaks.png b/tests/ref/math-cases-linebreaks.png index eb4971c46fb2d2a36a8b95324d3d1e08b7d99319..65b4e402575de10ef2513bb25a1d21443f3235ca 100644 GIT binary patch delta 479 zcmV<50U-YR1MCBk7k@Yi000006IWwm0005ANklj3#$+rU#$G;b@ow$+iiEDsTwfrU+i|&bQfPXCJn=_yV$c3vg{+E59 zA&f`8S7>Un>Uj`3qZP*gYV{7vdKO1Zi{DHC15u5LwCw_8dA-~Zx+zwQ4|U+~}Kuj{)#d;k9rxk@vO zU-;|<)2D0yCx0CItMUIt&HscKAC311fko>l(=Mq0cHnuJ zIM?w1#l`>M185n?H+8|(${BC||KG83-Ly|P)~xvdf8D;#v`$?AW$)pV`4}-UmBgFI zH8izYVkHiV@BfBrB_XI0gB`E zP5&QVq_f3FdqL#peLLyxO4Tp)o(#}X#O;7R12y>acyzdF)Z$T#M=c&$ivjQ3 V%;jb2;!OYm002ovPDHLkV1n{o0g(Uz delta 493 zcmVVP4Bt@3Ph~{FOY>~-6-Ym~#Fk@(%=gn7d8s##_ z|N9-h|Ka!jzuw_<_^dgPGQ4DaHy6D zPTURhgFj~rC+X$d*Zcb|6!U@Jf`%y^^#b7U_EF#qbX=t@;hh41RPS2=ydL}DDms+_ z!Qf*H+cWR{nSXlP#p9skEE^kT_Bu?A%yIA*vV%==kNz}Qp>q`(??MII3`luOhtmxV zHZX&+S@xjHmcBS8ICMZZCNQe*yTvW{Ia zo?K$2M~*ie0O?xdfm<&i8Ue;I67~?JUDnVX5>L}kre<*X_7pVUfpwTU6oFEhiU{JM6vkUXP!URf!8x6Ff_6g` zbvV!h9hAa&IY3*W?K4l>oV2AYB%IPL>*wzD=E>>#<(re7MSq0JqDVLrj)WuOg%0aK z8wpGJ>^qYLF@XSBjl&;mgkhbmVQoi#CzYq&*IR z8)`cjNd7B!8jq2~WZf0f10Kk3&yE|kL zA2>7`y`v2zx__$6h#nC00ort+w>J$Tcc=_?r6Logk@8pgk?w0g`-xo`cyRTjdJZ zyaKS{7R$OpGEGrE_=nTV*;pg*sqV1zw18w^`e<}a8(`O@mFynJ6CUX2y({wGG32NN zZT0oZeSh`WU7oOby_KNhaToMi@Ff_794>vuq!1ts>n1=7n^ahWgr_l7Cz$KO~kGacLmP@+&Ig5}2jRnVcu=9xpME02WmU!4}!B6}ykZ&@} zHNYn5a<1Yx>`q`UhqVyx0CID@?if{qjNybkKn!fjk1aQFOa-LqmXAOT!1OfU*60zr z1b_VKPZP+E8Uw;feSom_&Srr6c03Hws(-W{`YM1hrExc6b1 zfdoT<@ShtS5f!>T*ZuHoj%EZju74enV(sLUcyXes$48}7r5XmtH=a-m$1Oo7)Al#M z1R=tmFkm=A!sOTirC3)Ns=UBs4uX zi5MpN#v3w(zk34U>=4hi1>R~aqPReMKhGa?8%?Cu$r&)95wNp1+LwdN>X(0Kk2`z83 z-O-z$RZK%D!^R_7_mMa%DtmuFq&{ubowre2bw4EG=I6pRAk!;a!xQHAYEyW^^z0mo jd<=<%BjHH+{}%oSB0>?K?nN@?00000NkvXXu0mjf|ENs2 delta 1274 zcmVrYc>9Kdn^1CwRhJB?Y) zF5a>&ZrPTxON^VuOZ3Imd5g=y&27eLM1iP7MMRt(193zc-UUIFyL1d?C^8r#av3Tx zF4cmiEiKTN_O$)ZbCUI-bR7w&HjDk_MaCW&kcmQl}@0oH~Im24}LD++si+O zYyg#)x+fIQdmo_v%wmu=WK(#qL;5;Vd-nbG&&wVAb;t(*S8*!+-VTm0c^D{1;V}^ zYzHLQ)S@9J7D&`&{!40Ii62(xuIIqFjvYG+@XGI_hQNjY@>6&ueOR(|;>SIzx~ZM!J$RY(UfX`0u05Qdlzbu1?ue+VZC4uzZv#0Ta}!@-dqxEm z>3Q7n1s~xgb62OAu#FJz%Hi{OxaupMeEc|f%Xm^vEn}Tt0kG%QxzlZU#D3hfF02<= zbN18Ha!|I_$n!Y$Fs?MR6DE0=*x@I(6RY@TMmHMnd^cXemCLEv)iO zIAJP4y7l~iCmvJO>Yaq5ervn=y>oNwt$qoo&j!fK+_>s49&-lJ5=t?-KQ6DP*N>y7 zYwfaDJg)7|FRGti7+!MWR{PeXGyV#DGXmgtxdAJF3D?XB&;k)=-40Bc#t-iYCX8mY kDfkyA7!HPm;b$%UH_GKbHUIzs07*qoM6N<$g1Z$_L;wH) diff --git a/tests/ref/math-mat-align-explicit-alternating.png b/tests/ref/math-mat-align-explicit-alternating.png index 1ebcc7b6847d96c69e3e5787510299955d3c7003..52a51378be2516fb650de8c05e6d4e71b8ccac96 100644 GIT binary patch delta 918 zcmV;H18Mw=2%iU#7k@Yi000006IWwm000AKNkl^q*=mxlCou4 zip~<-_UZQp!9j=dQ+|*ad)>S*Kb&(ozl+~E{3qiQ!-?Uf>;_yG91_z zg(#VA{KMhRAqewZ5#=5>TOm;SHQwRKsxbcb#aZ|f#NqIQJd!R)sqN*LIuNVQqX&6= zJMSAycu9nF*r|pLM|H;*>K7%JN5JeD)K*h1b%C9TrGVN`)z`l~Kt1i|98P-y83C0o zl{#?FIxhs~W`F*E8};=ma~fh57w&RXU!FFkQ=8WE4Tq;jA!AK*d#a!H##tnJS-a*S zyYwM|EeObl}kKV1qrew!z|k+nnbDm8(*Dpj5N&RS!y zINyX}(aTb$x(7hjO|QBc%*uMM;UWbDJ9vd_I3ojsU4J;nHLTD>Fw<79;T3KO=2^it zoLm9HDw4T|C#WEpYQh+JV*cSeN&AQT$(|nou{vBJwNh>b@wDC2c8p|rk_NF3gbYq< z`TgJ~YVgF+S-pEOw-WJOf8MeWL^FwNczh;em1z$fsjV5dRBHdpp|jeYM`v{$4JQrh zAerMw@qeoOW+2bt1F-tl`xL4v3v(pa?{jg!xDIj54>oM|js6=KOTMg5*7~wL`Aw`} zjjr2PFlMPX>nGyyIqzv&!9P4j2f=hxxQ3H#5X_b|w*TX_5(sAAz%_hO2f@^bxQ2B~ z2(~kiYq)v?1WWO94fjq4F*z+DcRg7H)DI=Ot$!d!hYfO~2D=kP@@OK|7RSYQkmeEj*TXrCqc4DNi|f7xj8l4?)5Y58 zs48k^?XtS6%OK;c)#WV*Qz|{whFG86sx4?^QGs{3XFCMi{DXTKm1Phn=Pvk%VXlE7 scl6*MCY=zZGjZLN7)}f)hDG}ezuAM7E-%;Wc1a95W(nsUK|L$s|}skSC}!)2i!}m zFekIQy_!xO3VTgCH$Q6(5$<{z%WEMC&FcSdIIA{IR zGZY+7B)ve+xj_>8$2lITIY%X~s6dRXY9EzYRm)o3P(&qOX=W|Hv64!hZDB38WK)S3 zRR!m|i^n@Vyjzof5hBIAN@vOYIYT`oBiJ`9s+Nu*G0G2@I(NYk{aJSn5eB zK5y4vNq z7Avo^7V8SB#2V)JxN;|zxMnYF@s?9m;=6?eK6{vOs?9@)9FHZwy%#~U^|~!e7`FQf z`3?sm`ujGOIPxQGKNdyu$#7#((+4z~?o|PUo5E{kz1)A}1thGEZ+-_nbKiIgh<=Vx ziGR^DI8AKRFAu@!xoKAeaI97OR`YiKlDM3B&Zhr`gzlzN3vj6A;zpo13(<*B*Wryb zUr_cY5E7OQ8tZ-V52%eQfE|UxDwmx2zDmsUVn%-roo;=PLy5&-3}@8VP>Lh)+O_X+ zeiF?h5IXT+@Q6N@;^#0PErF#rGn07*qoM6N<$g0=VWJ^%m! diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png index cb9819248275a76b4ae40dff2472c993c64ee49d..09c5cb3d40a77a2dff1e7180a98355f326a8c716 100644 GIT binary patch delta 893 zcmV-@1A_eB2Zslc7k@Yi000006IWwm0009{NklPaYw zE|eh^(FM9-J=hg{u!|H5QV-dpEGlMV>NcChT*GX0Zd06kDle%`QnM66(oBt*Q%C9) zFO^R4()R8AKsd)Za1M43^{~&w@AG^3!-t*2`9Ew0{t+ue;eSv#6b^-D4NqN%jNF=* z8V*EzAtMv<0g2&9jgXc18foF-Gmx2BucWZ*BRiTHeE`BZ9OWPy7(F!O2wxqs(g-YGJ-&8RKgg5YuAEFpZpksbccLH*OW5aO8{JIh&*??9@x9>Brg zo|ANx+YB0Pa)C#6jD+yoF`7oE>cKFC7<%Hg=F*bQR6L-wHTD+|+X3D-kW*~7g2#__ z;$h!PD8Twb(QxZVD8SAZ(QwHjD8MnNXgE6t3J{Ym8h_5Z00p>^B^r)a~MmpWwZe-!BeOmSa$eIIEZV0V-dNy+V{s(^4(dytK;XjudoDb zy(boegkKR2pAG|I%SbelyKfg9#S&)jF5cC7I=ZX*bR^MTEkxL9iH~}EGjv85eSlqk zN5~wxDSz0(upSG?LrWd_9O&yh;rh#WY++m}qpUyJUS_s~&xpxcVx}dJsZE&#p9V*{ zrgORBxCkgfM4V_?9R&r5Qj3NS%6!KL(Xj0V6yUf`G+e(M3b3_NG~62o1z6oD8eUj4 z2icigwICYCv1Xc8Wht}d{(NmW2$Q3L#BBn6wST3%$w8yN2n17>79e*-LOiU~(Ny`3 z4sxl>cb4o^1PA?(siKLmEt}>C^RraIgDV2GbAKWHL z>rKlk^7k8-?Lk_!`uI}vVYh@Z&U%39EzsoD0EqnJK1|L{#Wp9vZ9?q#eu|>QS$b@! zV0P`@k6ZcgfYEc15^hdnM-kJQKF+$$QIXThdfe=+v;o{EL`%_YLpSRYv$MPyELNsI z!w)33NeUxj2(mI53u)nr1jxx;^n}DPJ|{y)ZgxOw7^9Gp(a_&bp>QZ13d_rHWFl3R TCZHZ^00000NkvXXu0mjf-nOI{ delta 980 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 U)b8MD5dZ)H07*qoM6N<$f^EX9@Bjb+ literal 2523 zcmV<12_*K3P)ZylHX;L)+<x3`=5vXw#RKd20!d{eC6uou{ZfqP)Cfdes*dm!Y&DKwn(cgp%Lq!tkUMXefMwFgzk24c+*?F#K#G z8Y+BN7#>=LhKhy?!-JBMiw!ELn=$589U?i!*r4-{11=i?N%EipK5@uCi;EeMtjN)g zMj-m1L0G%%-7)hlr#{dNo>&~7a)TuafF!*x-5HU+w!)dq2wVa)><=%FVR8`N(#DtD z7Y06!8esV0Xn+cTNmJz)hsJ(Gk8j7I5GCCgr3@dJwzJ{#f02iy9~Op(9Y}P`Syp=HV+8W%E-k{!4{uDGu$QD!2deFT$Cjj9 zfW!dVF=hnnDk|!+VkjvoX`!;BysiK6W65YJ`LPG@%k_-5;b(;5S2NI1%B#ZgUm_9d zjQXYtO0FR-66s3CoeN3Gg^m-3=O0C6o8GvToIS2ci$uB-Gq`c&Ix@Q#2*cam5%~Zn z7)f;nX^}`*Lec-Ga1k=gw+X}P;}CfbrWV>2KD>*v@yJY07lzF*m=PEbQ%dX#AKit1 z3cWgiWf6v1n25k1ER5~6D|~GO87t&}&qo~2n~N>FC0q>GeL<=FhY3iFM7k1NSGBH$ zUamToFjMbgODELig0Z2o5owV~S1K|a=~dx^TKL;_XkZBhRKxp&=}~cPG!zD={|6xUM655NksH?1*NtV{L9PrSWK0PP=e^a!9ZnX- z^w#$xJ6ZuJm}Gvh!u-p2`Z}|HfYaOeJ5<5}2T_EpUs@gn@FqClt>)e<+;4c-QI+tR z@08(+U9(1;ftO)wKDec-hOb0M&X0^NvKg)#B@Wk*Kp9@wg=ZA-7JS@H@XczITKHR? z&P}JQwHf9&_k`iF1<09Rht+op%(;7~g z!dlC&#Bo`KysHN!c~1`TsAnefk6VD`Q2y=F_RXu~=5xsHz6Rwk$;=I-Tkf*!_JwI? zv(Es-k0t@sL`qsyHYL9sLO0{lRVbuM_oXSrSETJC(9bi=!%2?{!-H-DbXH0%)D_Ms z=y|9X%P(r^t5LeIQ5mk0w%cgf)>R@8-yG<#FoCBl7a%DMz^S?P1rq{a`(VxFvd8NH zmcM1&{f--t`52Hi-=yNa=XVIiA3K(cm2!W7{{q5}1?u|`KaquovYrrzhv?9dZpef8 z$$a4!8p?iA82)24LY21xOC{1G(@}$fC8G_w^Y016E2+i2b_7;d zZ7xUVz)WFy;|@eVf(c!U>8<39D4gQ17^|({APirbhR6tTF0(5fw3E`Q$jpioh7H3I zc?Fyc>InA485 z^yu*0pgANVojQxcTb1Tt6 zjjI9m@TzPy@%;+*@YS#BQFUxSG=+qP0F)dKP9)7|Q8?DFt%;vLULG0P_cJER@!>8)|BS~%uQ<=9yF+C@)*(J;C8g0bWI zF7fb1eNetUP9;1!QyDfde`zhi%kW7q>^q2BxWi=HYBE`DhVv$i!={%}hCOQArey(d zz{DC-{W~pc;e%dY6TG}qY=(KUT^NpY0l9bJa|0Kn40*V;;Kc2{r3X#6!Y8ec`VLl{ z za|IwtALtO(EsA;NfMj`M>F?~DS0`*__on+Aluu6K^Ye7G?DMrR+?dSXF9#SNoC$zQ z!jXX1caGC7ebqS>x}^KMl;I9(+X8Ll>*V3g!NTw(i2#+yCCywtH7f~_WLbN~=)a~LA|{vnsh*|`yE=_`8aS#0pT=-H0U;iQlUI&elYy;c8Y7Of$9 zCo)@>3Bw@^5g7$js_hD&-A>6YWEKVq!_~tP`87<#<^JX0aZ^DE+ zyTYNK6waep<=cqEKKrqy={lb_FcDIgAT0`ePGIfsQX>55b$Fjj*zh5?d>=?%XmC0O zo@*kI7UbeLZ%M^kT;xGBy(St}!g$7_k%%3rg}df-qKUTI9jamS+|k5m`KX7n7-;i@FjL zf{HSvA}XN++J$Asg6tq21nH()l$~dGFsJD-!_2nkw#21xrIjlsvx5;t+QA~$%vH8T z?O+C#ZQlrQ_P`78Q@k(U>~rF77c4edDM~*#lP+S)Zw^jnY*k6%` z;Jd{!Edt;u-hW&JaKJ7RUfY6-vleF739D}gN1e?;((4z?ZxPLw&*Vnn-5mD0TY-jj ziSYPdRCJ&6I?KgXqSPPk1$^#HhuetORa$L#0uh^X<b~FJCI-f6VM9tf| zWLMt%zkg?Z-*vgL4ft5*y=X^Jq1?#*1l;!2FJ+^6vUCS`QQkQ19c{T=^+M*eL9ftwNc4~%)#{EQh17n6X!=$thU)kIr0B_D) zI{+y2RHE*cE6*>k$Z2;K07~ugFhF}U+Tnr%0Dtq~X%9e6V`vpX$ijl(sPfzmtQS|; zapnS$Mb1rtf+C4bk8%SRd`)x@rKY(tKhNwm*^zlhuu`-e({e?$1I`KTZ9TVV>on rqn+)@hUq~^J+aSCv2ZLL3+w9-Nq<$6)q=x?00000NkvXXu0mjf6dj#3 delta 967 zcmV;&133Ka2G9qP7k@bj00000*bA`7000A*NklZzc& z2&#z%U8rH%1+z&vODj#XizG=2-4G(EEVT$KEpSUqTTU>iWLE0jNb`mxX*sXkbenTt zwxe^KzyANTr~mlJ`6uU)5Bolx&-c&H;j?r2opazUf&Y{x5r1(+91%yvqKc=kh^e?X z%_S(nd^u(Ho7TDTTsI;MS^NAzgMoyE0+cAtd z8*R=nA4R1mkwLuDNfWEHd$!yq+Qbga)eIdfhucf#6~xZU9OI@(h<3TB}7X-2V}hUmQZPJ`^jZbV_*zRqWhjx}4s%z~Sn1j|VU(%zifz2^a$ozVuGIf_>q`tk1^V{`t@_ zjd(pgy|#HT&|~GM! diff --git a/tests/ref/math-mat-align-implicit.png b/tests/ref/math-mat-align-implicit.png index b184d9140a7eff77e8b69b544ddb31dabbe3f794..cd683315593520675aaf57c37cb30f714926a9e7 100644 GIT binary patch delta 945 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 FV1m!*%4h%p delta 2035 zcmV?)IPfYNzuA8}O z=6>{RZ0^p+xuWa3o4N74ygRK-%#5Uslq@l^RGQ2mgjoot0Xhx3RwJZDESX6DTz`ZR z#ZN)lRarJ+VSxZ)mjzj1cVEpn#@+pXcbDPwd4IFnneX#2{(pdZ%;)$^mc92XkLo5TCzpW& z8wl{#5b)-hc$EMn%=6XlM9qsS{|*yj}J z7hLdUg`|-|Na_QCm%7MsOat82B*jCVR+R&BpFZ*(fR?3H*sx5zy$!Sxklr$%zeI#% ztNGxPmuc|eJpgd!tWYrvjKB1g;1e$bfTdBWi3xuG7ztjx2i_GegQV1tASqG@q1_22 z_M7uEN;Lt#5Kb7G7lLtQn!~`|T(V%i@S!{*of}?MN`goJ>>MX-vkDDghaGo2Nq;aB z55beO@Hsm~4JT$eAnMxOZ(IP*Yyb`ZJ`$cT=V?wtRH;T&1yO~Xye0q(yNED0RD+|^ zb)drb7#EluSZpW5&~E^1u9tM6!?o=brhSb-Jz#=_0*V65FF2Ye-F1dNTW zZlc2@&t3&{!zu?E-lB!Oy0m17JAeE75s1rsrX4_WG8MKw0Z&e!Tc*?JUhN#cWj@|R zgx@*FccK3*nFhP=9pZzBr#ne-<$SMSWoLmguZ#r0n+DN|SNg#Fec(n2r6iN!*$3g3 z{_W+`MfsX_(nWb}iV*9C&iw=jAFTsE07GRuN-WHwC&ci~M0 zcxb{9IO`+*{20G&fmb!}t$&9&?aI#~?$f!g5YpWN3jDVU-jR9yVG{i6hrICKcoKYg z$#`LFuh5Vijw>X=*6HJg3-X2ZZg^Th2}Vq%XjZ=hNok)z(jo%@=oW(p*Dix6eah?7 zS-mkyI;&T2Hv(8yM}!gG364xrpu(-Iz|ry+G7L=)nA@fS9fqa_jDMMOl|7dNE3g79 z@C|}(AZFu(PwoJ*%u_tDeksUps$+*8k#>k{de08uoceV5X|Oa0fIZ5|4DZ_uN#|yE z!?)h52jQnHhgATue-A6{`kw>R=J9!g0AuK%E=Ks^8pr`7;V>f{bIN`8kJUmt zKKM*DBYgL;JSQIbJ%1ZqcOQJpzJF^Ae6sHTgL())88-Y~1EB}&nPAPMkf}wvOz`_l zAXD+%nc&wFAyZpkWrCx3LZ)^`v%$L{Q@hyUh&TwDGV2kz;ffUasZYBPp5h{SPF$bx zB?4!?7y&=Eo>YlW%mZVy8)lgu1UNEPE;u!k3BGgT7;u;R$A7xU1uwjl364sa>%{3% zJn#6#{1w#*n05CwzhipSzciwE92DQj55+;I;vNc)6F-*$naWTx!E2KsQ|l9$;Iudh z8FthnaQ33N;HRE~J@BN;V1o0Xf>7g|Um|d}<_P$yqgVw`GY>Ms#_4hmkb4>!Z<)RF zzys632FD&{hHQ0ep^sf6xbS60_^ajeoH$O)2xF!Vz=@~h$DGwO{Lbpc*&2BMVJ9n` zmkH3@@dY!C7%RwG7K0sbUJGiDs8$Xbwdo+1dLBG5Tp;FB7HkTvzzY0R^aE_NnIYHk R4VnM|002ovPDHLkV1fsk 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 KMNUMnLSTZSpzF8* diff --git a/tests/ref/math-mat-augment-set.png b/tests/ref/math-mat-augment-set.png index c5881b13976e15a853483b70e227573be1a4f367..1a66761591388a4bcd58dafec8824598b6a0000c 100644 GIT binary patch delta 1711 zcmV;g22lBu4zdl97k_C80{{R3#xNJ&0006#P)t-s|Ns9k%s`2WiL9)w>+9?N{r&s< z`@OxroSdA?%ggxq_^Yd{@bK{W_xJ7X?WLuqt*x!#;NYjHr@XwpOiWDW<>gjZR`K!i z+}zwlp`n-MppA`T4)zw>DTRS^D^z`(vudk}As-&c(prD|!v9Y$cwm3LA?(XijwY6DU zS?K8KXMbmBSXfx;>FN6V`ebBePEJlhKtMh|KHA#aLPA39?Ci?Q%1=*EP*6}eH#he7 z_B=d1Qc_YnIyyZ)J!oiXhK7dk@9#A=H90vsHa0f?{{H;@{D>*M#sB~WFiAu~RCwC$ z*=J8w2^5F%$B~&~7*UX-QUrE!7nNdRbrpNx-hX@Vz4x`3wZO{iBCLpt$bd@kAk55X z+uV|zT!t}olarX3`-?CBId7gg6Rzfh$K&xlxZ00m*|m%0RxUtUp#B(^dGknaNntm- zDh8nZ)e<}W@S^mG01{VaHL8l=2AJ=&!$ZWzFeI+dUNpV-7DzZY4&xXSS7j%PN^1ZK zr+>}B^j%=C)su#vl1+|qAPP}{xmMLP=ot%P2>)t@8I1iqh$(TscmOr=k9PxvDvk!N z@X~P8B}|Dc^K(C1mVSgV!uRyWmuPwSLpR}xAX>K7xe3>|p~Y9|CVbk5mZ?cM;mIkq z965~PkwxhPR=B+4S&*A^9X_J{H@*!+Mt|6Tl*mn%{EZdE{&ARzNmy>Ol*5r0c^XSt zZqC&bLCb}U0P!e9k+9sH>*59N?|96gF=mIuRUkLV(*KUh&U&=h3WusU29cXA<#2P4 zo1f#JX0$X!+=L?yXqoB+R?JJm3ZQceEyp|Dgijnt%Z)xaANBQL&~oZ448g_etACBK zy-ejR9|Pz5`Wv*2yypkVh1+J>ULcPWS%E#fQB$)4h|k#1YI`LK_x7Nty4LZmwpNmG z?OF6(8Up;&FpLqlR+4btDHP@A0r`QEd?RcxOY0Pl)qVg?Ih%mIb6pnY?M1NUjRVv|)1E}jF1zq0DB9F)8xt~gyZTog+TZ6i% z$`DEao!U@=vT!=|&)?|hY!oG?*Asuu)X9){#`Wrt~vfk zIRO)9*j{qu>J*AHTL1~SUx(-tBWy3Z;g5EsC;XfvJTeUb#Er1MKvu&(^nVm?afIoC zI~ZYmfvlJJqG$g?fY9msAf{_q+b)}7dx5O9Cs3334mf3GVutMn$lCuBTHgPFM0PmQ z@M^T|d(%z0ZarFdta20HyF>AB5xv!bvMEH(8?{XgSx6;e34L z7N%Er`mC_rWHq1DewR546Mx|NotxZbDSp(Axd01iAiBUAmYb~HM&(~$I`F=AbJeQM%TYp5)!A>9}3e$dM%^f~} z5Jlr7Kp#8~V6VBu@tPQl(wl&?@S3%Ny{vGkU|A58y>@OxQ}>Ag5Q|~3mKjbED2k)% z&~K?dK>j?0vZj-%gece5qArKowr*v%wcfiTkH_->`yczQPidWJ{mTFV002ovPDHLk FV1jcOdR+hj delta 1808 zcmV+r2k-c@4U!I!7k_IA0{{R3i}<$H00075P)t-s|NsB~{{H>_{rvp=`}_O)`uh3# z`S|$w_xJbq_V)Gl_4M@g^Yioa^78TV@$m5Q@9*#K?(XgF?d+9?4>gws~>FDU_ z=jZ3<=H}()<>ch#(^($dk<(a+D%&d$!w&CSfr%*)Hm%F4>g$;rsb$j8UW#>U2% z<)D`3pT)(+#KgqI!^6VD!ok78zrVk|y}i7=yt})*y1Kf#xw*KwxVN{rwzjslwY9Ue zv$C?Xv9YnQuYa$uuCA@Et*or9tE;Q3s;a4}sbbh~sHmu>rlzH(rK6*xp`oFmprD_h zpPrtcot>SWoSd4PnwgoIn3$NCmzS27mXwr~l9G~_eSCa;dwY9&dU|#NEijSF z1TSe?wrLugCSnCGl!O_gQcA@oyU}J|l3cTLkzxh9(8cclTlJmu&AfAjZFXng8E2gH z{PfKG!#=b7&Axl)TYP+cd~RLxk6M{Ei^RO1i!$kFW?E}#ATc>TL3C*;2Z|TkaIk7k z0DoYtEo(-VIUfTR$HF$;QZ(lk3}dZp9-6#$55SW)%w_l*!&qC^g(5RzfP}N-Fq8?5 zXZ?lfQG37>&WXcF1~8sA?njTaWu7p@FaR)~J#r2;0)J-#%x?W~z8ijV!O}*6@hszy zG_;ue7lvu2rMJ0Z7B0s!)_$Ik7OTHUFn{6ykyTfSF)R{`S zvk5JFV;C~lZ3x=%-Ah1bUW@f=zvI?|;#b{pMOzqTCMNG#GaQ4l7waEwb3YA9FAZZf&nBfGp|JsYoBr5(g*9^@Z9!K79lf_>3^Rw zlrF{%=M@(hA~P}NaBqJq3#4E9dt9>8rJ-SEVoH=h>Mo$NAqgshF1_&H2vAb#^53f` zrUW>>7cDk^g&}L>=1Jy^t-0J+4ql5tiWZB0#BemX=#U9pbEs$ugV%mrh!&ZDrg?U? zHIP*zmVP}8HDZhH1MKi*6Lu{@cz<3$dNkMBNA{74ChS^*aNW!3vEVa+I9$PmT}u$| zTZAH?7X!OjRPQ%oYYz9meYVVF`RCB2`T)S}QcT#ILsnki6py{O3RT`MOm0^fM$l!( ziKN1T$aIuh)@Y@&lEfS;N1Y;)pvb?a^6~NUxtY|G1ka&P`znNlKah)CP=6-02bhqJ zB|p~Z7xa?RT+Tw(*$&63)&?LOU^F(ah@eGx6vGG%;(raxOf1^1ea0QR zh40|9>!1^snV9|=lesRGgF{(OO_d4DOiVM}aXys=a=rsCj+{y*eCmkeM}3o4<_jOB z68`9UwAd2G;jTMC%nb*p;UE@!94*Sf!%+Lc+MS3w;fjth4r0IDj}}vZ$M7Y5ftdF{ zH?iweQR9a@fk|-~3V$Ltet706dc3h6#_Z(;;irzF$J#C!vzHTu-(QO&-(~{s1=EiM z*4%KQwk?Q*wLokFitO19OssxlGGNUOFA0Sn#=+XNHE0q%ou1qRNsk6lW%*0Vg%2)A ynca_ZBipudBhS>KPBlre#=oTU@$tEZ`4`!{o0>hWUX}m=00{s|MNUMnLSTZElAMwN diff --git a/tests/ref/math-mat-augment.png b/tests/ref/math-mat-augment.png index 0e2a42a241c8195744b2442e32bbaaffa1920ffe..306c4b1995f82e367be5442de9253dd9ae69e78e 100644 GIT binary patch literal 3563 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 B_7MO8 diff --git a/tests/ref/math-mat-baseline.png b/tests/ref/math-mat-baseline.png index d2f26621306cf21c89c4b80a0892857274b9da7a..01928f724d2784e7e1f60f56d014fb76371596d3 100644 GIT binary patch delta 806 zcmV+>1KIqt2CxQ@7k@qo00000#SVaf0008_Nklv8auqR4}@0$s&ViDZ+Jh%-!GRuWM%NT@n19-Xk4q&xgEPS;9J=@bR4025+J}UCn zIImFJy=o<%@*#mJlA>3p7jeM8c_Yghw%7} zST3BoxWnykw`IiI38MB;QzXKyzYIXkXRUMIO4V z^Xh#7%*_%BUyngk9@B-&=B(<2CQDk<*+u{wb~yps>_ihbo>=i4C0Fksg`wt!h-|nHg_^UAZetpP)Kb zb?QJNfPZK>WfH2w#{js}(e$<|O`vhmPQOVhl8ft3c6WDoe7iTLh7ToLf_~=B4H-D2$1uckbaCuCV#x{IeOo+8Gt3{QilW8>ja^2S~7Y` z3Sd-}NU13c)#(hOaN}b1b})doMbu1hMm4`tC`>{PI%~cIM*ACiwy_HU#~P%<Dtwi=ZyLD!Pp* z=pv#>Dq4l2E{deps34<_ii{Ss!oU_=h$4j2v7Eu0Qo_ozaw;{8)KpwT%TO^bmvTYP z(e#^{Cpdw8>Zsqqs}j!d`oP2goSQR>VKREuF!6@HVQ+ZEgnzX_&EUZxhru$2?8?WW zia-o87LKvfzruS^XbUa^;;eFE`w9lo;tQ|aA4LGHZ4nD!f64%If&s_zCV>0bq{7-P zCpztyENAKAYVh40%~YQb&@tB`6i(fW-kvONIX(C_lBf$$RKHXOMn| z9##=|8bFCUA%7H}lZ%dP69*Dr(A_~cQ~@YOb7jJ=v32O>BxIZ0=`KIEm@dw-PLXic zI6O4zyGS^5Iv$#tArgMP1P?8JEE10Q#Y4XFBH`!-7*lu>lm*d4y!gCsv0QlH%CNAo z&_lDQJLt=~K?zq+Ed#)mh@|*Ld@GdONBc!BQ9HwU_1JSh2(c(B$0h1Yr9Q zP<)!2*Fkk$@!*8ZFQVkW-k;nF4}Z<(5sDw?D~VV_--?E<3sLOH)K2nRORHOegogA9J zj6EXzbmNeP^};^+GX!CCukia9k^P6wgS0s16$TdnzaIhHJUrxM6A2ehp@*~zjs8sj zED~-RPj@x9!oSgvjxntwVJ0=Ar(M8}&g23>DSw-YOnB2v^vosn_Iqc2RFVxPWus6y zG6cOq+&C3U{^qABp;1EN%4O*JV#SI5wcy^U5E07r7f!UX(8=gWgM`d00aT)5^CdJg*L+EF7ICiMmeg(M>x=A(uo mS|KJz!_vHAZ`d0iG2y>=uN^j*@}lej0000r? zk^Cr1TpUC+Bsqv2{5*(@+CfZTMaz%!v&blgG7d^3cCh)eS{v@sgDwP8OMyJSNCC^$n zo5|p^J=S_sN`D6D7+C9VHW{4hVy*5>GFV|{tyTpY%$ZoLi6etEJgn7|k$x~|WUWSy z3|81!t4%=$XSrCbGm8w)yeu*zU2CzRu2>a>)s5eogmDa~{;WqgQVf31T$l=_Uw0&#lb-;BWDB<_^ zUxujil@x|W delta 483 zcmV<90UZ901n>ip7k@Pf00000ugP${0005ENklkhtA}BDSI(WNFGPnp&K8<^TWhCk#-+VCR3gL;wHZ zPoky8kK@3U39{Jk<^PLk0_ngJHlcF_-Xe7NNz2mp?}3HKd@QcbOA*EQKX&4 z7rp;uv3TXJ|2sgm(*>GY++7Ox+H8fX?~vkHMoV=kh*8r^GmB#<;gek!MKg=-*W;7j zV@orOmCoRky{t$xi#ea*lYPobGmF`u)!|3vWJt z%tbScmGRa;r+-vvX0gLAFtuaV0ul}3 z(qC0+*x0LE10-77{%1BJ#7=CQZ+H?&Z>pf7#V>t;#9aA#>)nt`lHaW+yMa{1OIr3M za>4QV{2!*w>3o`YVLm3{koXt-p0>Tk-P5s)SL~&A|9I;$to`FnqeBs+7LQsyYVq*5 Z7ywe-VaC$1f7Ac~002ovPDHLkV1ig@`9%N# diff --git a/tests/ref/math-mat-gaps.png b/tests/ref/math-mat-gaps.png index 405358776c3c42f9e20a7ac48ae152506cf3dfa1..95cd6cf113bbbedcb2fda0ac1af1bd39509cc87e 100644 GIT binary patch delta 1305 zcmV+!1?Kvl3ZDv)7k_IA00000nN1lx000E$Nkl1Kqlgn}bE5TV+*)YqC|&Q%k_VSfTVhfsVx5s7g5BlY2$nHYp` zHZwIB2(DHhjyz-(hLfKFWGzr0_U|wZ*Thl>xCW^XmrO!8(v8UV=_%Xy08BWeI2^MO zz_s-|TQdU1(SOm=)d0(56^CcUBW2eIio@eGkn-zT#o@ulNI5%5akx({QtJ9B4*On2 z%0*wrVV_c@l=>(R_pV1ueee4v>{Ei264g`tRv@LqS8>>{7AZA;io-+BAmy~b;&4z7 z8u$5b5UWCy{)*~HA_bs!Wy#np2$fAnW1XcqykQZ*`F}XZmm=b}SOUQ+yNYsaKgmO3CGg?dtlbZm>mK?^6Fj)l{%Fp!v z2zbNZuzxq~4c~*|Zj8B23U|$K!O+%Y=7htG!PtsYGs0PQ);L{c{IE@tZvguRjcDS6v2=Y=7(We;}+0(0yw)40mpD{#6dR`niw~ z+dl>_&OM1o^-`=$p(+f}S+W6Yz8RN{NL1i({|_aZc_+vK z2!BsQQy74pU2jN->xZM+KV^xX+Zsj=AGRJ~=R7n8fymiykq+;D9Rue^pgH&?ayE{U z4#&*FK%1XwC;oIbM*JGh3I}h)$iOpZg@ckX;-3eQ=Z~DEAn9=52#oZy0c1XgoY<$N z!_UTJr2j#)rncYEJ*T|hdLvx`oJ-R@4$X_7YQvh7U_4t4it%XM_Y#_I%P?zd#+^fB3v0zAwObD}{Wmm4vQsBdmJeGu0<^O^lG{$Cmw%16 z1MUDPn!1OP6CEZWZW{+2wUBDzw!crE8dL-vS%uEss{`Q5&xv~wE8l`9e2sjVg@xd- z?S1EW@JN3l5qPT^bOS2^qLN(?btBUK;W{)^l8_I#%mi@qUq{yfx{&<Q`1e3*<>gsx3<0lLJ+Xv#x#5PzvWen1a90_aJYgj@u=X4i2`?`i3Y91zpv1Mbd?cEkr!fb89(2jcV@2xlN4SU1)PxxO4781yvCZZiu P00000NkvXXu0mjfPUvZF delta 1303 zcmV+y1?c*p3Y`j&7k_OC00000PZatl000E!NklcPa0;jeqW?GhtmxUHynDfG;a*KJ19c7$ zXhcfm0G-2@a-@`7`fSH$*gRuD!vXh@QXimmctjaeE(Pfvo^S?5{?UVoxxPn{IZ@~E z{1kxSqM1;FP^uM0!a|+HJ68g@nq6Z%5V=*LhFPAVbAPzr}&7Z|EFm z#A8(9c?cY-xihgHaEwIn@Y)O@ZWGb#5h%?*5ONX7UZs1uc?NLRmSt^3#JMrl4$N-W zJE8K6);;0#OeE)z$><@TtPi`+(~u7k~2M%1?pglS_TO@ukxX;cgV5vgV_> z9p*&j+}v$3e*#P@K|UN&3$UaVd?$Ny4zTDts0OPq^(krTh@7?A4xspaw|ux}9{&i_ z$&YcsrA4TEp7{$Y->#MquS^kyX`0;w@MbHD+UJpzxIjJ}@<0%FZ0xQ9h{-@v5{#U~ zQ-7tyjiXVx7v_VP`{ONJ;{j5mQ526tPP$b(d~6DeQ)#J{cs;b)Y-Ipduc9anL5^*T zbT~c|4IKf7?Re=rv;?#m6%J2AOJJE%VQV^CMim69&OzH609cUSx z+h?PG>cYViXKY9~bSGMdzLYgz;iF?{3sCqSD)+P907uiWh9dUECKNGo z@?m02fQqyf4?fNuKLNbo3aY?50Q>nzLp_MxUZaM2?-26gwgn*eOB|q@a}vePFY*vd zJ*9>jQ;B?-tTcok%yt2){a>QGG=CM5>~=LwS1|sB*=8rnZV_b}lO zdbRG10paW4(3`u_7!YPpKH54yYBMCvzBV)+Y&3rxn_)9-hM%7B-&bRxJ`iZnT1o%_ N002ovPDHLkV1kEeWrhF% diff --git a/tests/ref/math-mat-linebreaks.png b/tests/ref/math-mat-linebreaks.png index 52ff0a8bbc61bba3604655887b7aeb893fc9115d..6666749dabf1fb1a75f67347472ee189f775a5eb 100644 GIT binary patch delta 636 zcmV-?0)zdF1&9TZ7k@Yi000006IWwm0006{Nkl? zsKBDYB3961&CpaTy2!Paixvn$vlz`*I53(naujW`)#wCE(FNJeUnvw43h7vwrO6`5 zwSg_SJ@TISz~Q|=`+9Spi|@1N>ccq~gRKIalnNczVI9_C5`TF9E^qe|Eu60V2RV7m zDk*&S7nFRnQ^Ngj^|N!75H{ujyRX4`OlDhVL9mkLf*$`z9Q&%m0#(%f#61&+EzLtlhlnMj$QxNsCupZIGC zJTq6JK9Y3izjeDjmdz<@b*TzPGwgPfaP!%fQ!KBd33yk0e`V;&Tash_Lt{Xx8Tl8^;Neo zVBAyMKe^I^rex;`KK(|tuRQQ)NrDPbyhbVfDiFhPWDKj(#2DbtY7AGmio3p+-3Ne} zk`Efg$2{V8G_yND4lzQSci3ZGvN*K WN34eLR6y+j0000Fp_9-l zGDtAGC{c8XR6@&wB3QQ48A}g}S`?Zq2AQdpA-F(8@h}obL~bT61Y6V&wl$2HZaQ@> zHn6R^Z-?Juy|=>c@!;=!d7j&c_c^=;Y*9N3wkRFeVIAIPuz&x)Haiz(;Ydq^wlv=T zDGE3B@6wi?&Fux7j(={VBfGavv&sGgK9vAi`3|s9E(ben0mdGU8UQNh#9*Txh!^wP z0$^(vgZC!@w~=P|3J@rkf&+yB@f+TwwkW{fA42eW82~YVJP1IkQwV-|5v8*>A=uoC zlBq)ou6c~oZGV#xT-}e-o$COEQ48lBE~*D0czO*0s>=Z0`T=033N2?KUoSrP_lr|K zXvNNemNifCQq9Lq7Ph|u=I1lKw0s8Ud|+A34?KE#*_4HwBGCKdxrxSZ_Y=(1bwMV^ zat!aQ0ie#8F49hZ0H}pfDZ_&t&v-74mjm2QA)7pnmFYhv*5Er3HQegV`v#Nc$v24I<; z90F1&5QDMW0m4auz7at*X7QFz>CPW%z<5%=m~ltqx>imX#p_tLd-~>#z<3 Ze*pLZl?g+Rl0X0e002ovPDHLkV1l@1Bn|)o diff --git a/tests/ref/math-mat-sparse.png b/tests/ref/math-mat-sparse.png index e9f0d948c363f982089c2fc19e5e2c57f3ae5ef1..c255fe3e59f72eeb47e723c5008be892d96f64da 100644 GIT binary patch delta 947 zcmV;k15Et#2D}H57k@kv;Lm;zwPk+tI!dW;AXW_R6-`|E( zbpbs%v@(iP$v=Zy@aj>FdPuMPR0Q)9_kdnc5}vQofm=2MFmiAKX*G9V`7(-Gy|q?o zb%oXe^)3y#a1x_rI*hZ0rJb18Z-s&xtj}OPPaVGKzzl}CC6XEb49c$o<(HM=-%n+Dqe`ffrI3b6{|Oky{Ik;`cm7|i0%C0Fqcj!x z2cxM|dBYq%7yS)@xdV#uiSNPaMTh4iAiCpryOUxJ#OY(^Ax2L-JY8_>^f&+|#}wgR zWoY%sPDQx#KeXCZp$PxH8?73xig4*(wAx#$2+yw+Cx36#7=SdTVt&AXJ`OJiRAWc(vQH-C00M$!$A9d?udr?1NHvhiIid0_(Mv5a88*x1=oT|oEsIEx=QO6LO&41`3)6ewZF(ZW_k|&G;qQVhvc;R}q zTE9>cE^a`phT;$H%Cv7stF3lLc;h;Zx`PmI^MEv^UU&e{=?DPMYDM@!82~LM?ckg_ zdyLnnTyKLF4tt|c09G7QguUNkRMpP#YG(40rsXKPhrhf;MR|*!h3cI=}{>Y%K&-G@Zu>}hJc#g%J9pt z0eXt7hWYo|{&ql~_S<}ux@*H|bl-w2xSK*wfMpZPFdI66Cqsi{aP0wrJR>j;5sW4V zhlU~OO#m1EM;#9TigLwc8ZgaAP^|tWIxH~tmr$y03wkhql=5f)lgPqZI19hEUIFYz VPg@A2?~4Ec002ovPDHLkV1ide%=Z8Q delta 872 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 delta 1812 zcmV+v2kZER4wep(7k^y{0{{R3S=&-&0008_P)t-s|NsB2tgMNNiTC&S?Ck9R{{H&< z`uX|!tE;R1{r%C=(aOroprD|GgM;|^_|nqSV`F2jt*vNiXyoMN)6>(>&(DjCi~IZg z#>U2deSN;ZzOS#Z{QUgj;NY5?nlm#qDk>_vy1Hg&X4~7_$bZPlgoK28dV2Qu_VMxY zLqkJlWo5Lqw8Fx|adC0f)YM&FUB$)4larI~?(XH~CcUe0+d_fbhxN;h4Mm>+;J9Nb_tjODgt?t6t z{PXwgxX|ODzxU$o%S(vjo4wzTwB@A3@y*`VY@XYItLwSZ?7r0f_xj$AwDQs5?ZMXC zeyaA~>F~a;E-o%5B_-9>)sc~r>bB3Rsi~KjmmeP=qN1Xno}OuGY0%KnS65f_)#Kse z;p5}uwY9aCm6hh^=IZL|etv#TOiWQxQJ9#RR8&-CWMm;BA#`+fv$M0nz`%lnf?;7{ z$H&Lk*4AojY91aQHa0ddFE3tRUZ|+3s;a8SIe&ajO--YtquScqIyySc%*=3baHOQ9 zIXO9xkB^3ihMk?Ay}iB3$;l%lBSAqyOG``H+1cys>nSNIb#--{o0~sBKc%ImH8nNC z!NJ7D#5XrL^z`&nQc|(8v9hwVU|?WxZ*Q)yuHxe2G&D5J%ggEM>Fw?9@bK{R^78NR z?|(QrIP>%KK0ZG6_4Qa-SZL#rg8%>n+DSw~RCwC$*mqEqX8_0XFOZNy0)h%ELzyZd z?k!k5?QYfDz4zXG@4ffld)hs!nO0oGFud7=L@H8In-Uxi)y`UC^)81=a&AK*-9>9&d-l zBhap2B0!%s5Q;`pT>z$1v@6IB4cq!MwHWx!i)QZOsIb!5ngpzfp>XPJgG% zLl3?OW(SfyeD3)d=yL0Avmm$}=s$ocmV8dPR@OtR6WBHx&YCrQ9-ut(@E^c2Q&5&C z@Zz&qUNId|Zn=3MU|A3-A&TGqbZh5!$X_Q8rZ8hRH{L)95x#6H%BCFdy^oof(`C8{ zJ5B+nW%VGOw+7eOxJgr9hlyKavRm!AeK{DSU%D8q+YKxvoZ9WclN6tu(K3(u{BR&~vORYz7q*=Ur5 zQhEBR$M3v@)U%N;zG&{Tke3cD%AtGfUjfV`D!y+<@-IdBZVhBPfr&@M`F|J80nffj zGb$@9p>zyN*Ru}7id9HdJsD`&z<7GNMzy!k3%*~R89NKHP3rGCIM6i7!ZEGL9H z8&KMiI_!i9o(wGhlqhN)0Ji*}{2-*PT8(fbKu$LQPf{7tv`s)*wWwZRX+?x_pMYk; z@j!b5jyDjjKh(`{0wzsHw}1T`pm_unUPqY^@S`8yD!v8Es}D#IYs4@7 z>u;?cAk-~>`6WPY+QUJww-c$o5I}93!_gVxJt`3Ev@RQf&K+$*5Nt)Gkkqz8>#g49 zcW(xvzBmy;ZQ8%%0Bj9=Y^Y6l*dw0M{;;sxG>2RMf!)brVYO)vvmOG3h1I4zyzZBu zw}4>pv$7dboAxlSzJEx`mShW{Hm%{mVx88-7vr??Be6%rNrSc_jmfrl0&>$I?w^^6 zG;XM8Eg-jezU&1c&>Qd7ULP#!?1gYpKB40uW_5gTb;r(4e}b?g-}(`tHqGHM9+UBU ztIJ@g(?k>|r*km6q}lB@qOjTRX~zFz27@6k{sVi|V->VecMYBZ0000MgpLj9Kv)$9%njG2?82_3 zZY;aFv2}A>wmPXR9RrJe{|1{yjav(|>dhQ8FhMj)i04Sa`<6 z`Q;obC7?uK#h~8IMD`rL{*q1EaK1L3I3EGE>vAn@vJxV->BOl7gQDSeotUc$vTiKc zJ(emFaJvJUuC<8lBT9yE%r!8Iuyzcv<{Ycnj0?NI_J_~@8vm1$Vd*UbS`cB9D;G#W zdx?aBTP>t*c8hS)?)^hbhEL=XkmdxM4@1pziGaI1 zkZC(F{+X5HR4^RhMS#%@d4jsF0QU4lKX}XsYZVLkq^P1ZAnwzB(FxaFi->X` zbz@aTnqqIKSU48Gw>vwUqZ000coX<`n-N*zwnQcwntze-+vfoG?!|XK-X|``>q&?B zRo&cMXVz6{KYMa8!tn5;9)H-|@v3K1*ip*OJ&ae{M(<_FkwCEOv=GPWh6uyW&j9}3 zg2~}?IjB~Z1H(D}`6d8QcADLk*xH>`2ttk)_ab;L!f@r|Q-&{EgjKdQCFt6iXI{kZmbV0^IXxOl{EopTVFtiP`x(cwWF1s?cl2P4< zP_h2wGK!2x%BCL`tRY!X94c&)F&b6dg0z41Zyg6THVb=f8-&{*u1g+zQevzWPjmj{19MY z9j{mjn(NNmiZEuTVGp6ccOcuJ+eX;1skp>22z2Km`v$*dWdK)iu(BB73^E6w7a^Se z(SN_ywiCUEYxpH2=zp&a8y>wpugiRZV7Nl#1#o_m4uW;51Vdk{56BfxudyLGf?Iuf zhwpuW>l)1`pK1;QT(ITq!-cu^83%un!>csHbHxDqcO(Q?!VtizdAppz-<(?=GMQ3b zuXhOuyS)fCSQmv2>+)IN+8aEvt_q=KnST#iro(;}Z0C2@H9+Hhisw(>>eA$DapL3^ z8gXK09Iea3hLZk$+(p!VnV9%cL%THhu_%hW8!CvU2vqgW19$Wa+3~KcivGlmB|+Diua8;OoLM zNS{;raLdyGBUuZxU5t0UFj)e6HgF}AJr&A_rN@S&^BnVCVwA$(%_**^grTY<_IFb( g91F+7|C{hXi6?DL=0_CF00000Ne4wvM6N<$g7?%qvj6}9 delta 1224 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 diff --git a/tests/ref/math-vec-align.png b/tests/ref/math-vec-align.png index 680d0936d936349a26c04c5dfe416595c6f9be56..07d58df722224d48cc19c4a5ef0fbed5e6d819c2 100644 GIT binary patch delta 1118 zcmV-k1fl!N2<8Zo7k@Yi000006IWwm000CnNklQDi|$ zVy}M+!-`OdNV0-n_ApZ_HMQk>kdGEUEF*er>tO{!!%4KttZ6Te9uQ&=3TazSvj`up zmBV~A-FDmW+}*o#<}W=$P=DAq zSaT6$yHzgQx_16z;^Rifo~Y*se_P(o?s#yHcF86-ZC29m>}57}DO#3h=7q)ve z`-5*!ahY$sYJ0gy4VQd-jN7SgO}0m^sl4EO2WS^(^N4+^?Co)MTBrkEX_I#>qsM1X zB9m?41TSgt1Ai7WYbAfEsxP?wpyXd>3w^+C^ZCHfQb2oqpt&XR+cnaaH67GRQl|uV z&2V_*6?GnQleAX?3ma;>&z}i!B_b){k8V~CvW$sNhU4D}TtVu-sn#06F zj1bTkCQihJ7igMM!~xF9L;JQ3aYLz|j)P}bkutOuP4{MVfRhUGLn~~cx;Q+I(}CI) zcv_Ui0e?=^;GILPpxQ-vx=0IZ4Z%}QA_q7;ft1RWYQJBaK$W5Lj^}%*Qw&vtn%Kh` zPpia@gz!O(INvbPh_4?RXvF3uX~h3Ccv>0`4z35y4adRZxuEsII5=$@2Y7ru4h}Mc zrjNwIBh$&E2I1iN@q?J^eYXdiYW?AXrrNk}@PBK3Kkwk{{=qNu)z$rjEys4~`N5Hy z0@{KbK-ClRbfOxxAsA0HBRRl}3;yurY6d+MgQqcPK%4*Yy$@DbY;y-+Hm z@k7t90PRt9py_@U2l%K8?OQj7l(!K$cx)Xhp&!vS{Sl%n<(1JFMAh~MLYUCmB}xApGS#yeJT0?nf#jPWiPZF z1LtbJ*Pa0bKU(XZpDYaQ9Ovc%uX+mF(gdwb21mAt>18!&8%f(_u=EBksGE&>AIh%S>--oda4+(tlbR9M$TLqhX_wq<7`_>bz1euxqxHwy{>w zvwF#=zM|8UWCbj#lD~e4PTwVhx?`Pu|D6hN&{uU%(HB8*#LBB=OsCSv`2{teve6=+ z^*m}hEhl!t`IesN{L|L6rq3B_%6;2A-}|TLExzrFV*k{+)WQ4zxb~?3Ew+U+9apI? zHtmS-q}b%707*qoM6N<$f-FQWvj6}9 delta 1090 zcmV-I1iky_2+9bM7k@bj00000*bA`7000CLNkl+L8T)LAxuVK z&8){V`#qTM%I5$dJdB_+k9WyZwFm|;sl+nD0H#N?IlxJI2rPw3?&?@LjaB1KRlsR} z5(hZ30DplMY?Qld6`Zb8$=w)?4X}M2;BY0;x9vXQCb{Q?dPkZ*d!Ivd@Q4rC^n$1| zoPWlT5;Y^txccDP5oWBtltjO4|J1V(p90wdjj0iG2PgM+kqw?bfWh!U@D5Dbo= z#Q|QO3xgG%a=#o6gQG9Yy;A{$b64{(^_RFq&42!vx-l!^oByRYpeE=^`d{U zS+DQ*54LpZJNdz3N(9z8a*pT1>0EM-$HA#GY|MVS{$+4RE`mx_49Zdwg}{oekh@)B z0n=ld9N=^8hwPyCoIKk-MIo~6;hccElC!1O}^iyuH9`}8t%fIl>h($07*qo IM6N<$g8pp?od5s; diff --git a/tests/ref/math-vec-gap.png b/tests/ref/math-vec-gap.png index e48b3e9022b8742f1a3c1557e809c8a1ddee127f..ccfb217111766e8c1f7ac89688022582a4b0cfcc 100644 GIT binary patch delta 423 zcmV;Y0a*T|1GEE>7k@Me00000WR;^C0004dNklUnzH-(~#KmFch!>#z<>!x@LRlP|LHS6>RqLDuvs3V+X>!`YY@h&d(U+hKq` z4}j%H5Vl{H;Fv`YerTlx)AA?=2j+gmi+(Y=V zZmUdoObhWtl6wuDCdw{I7jr6qmeiV?nINn|R@qPnTzU^K*S-j8}l^?u-P8N5jqVm%^>11(B8k)r|bh6k}7nQH;Nhgcj zGEn&$Z3AZUaF1gz^f>k!?m^v!mXA}q=w$KYK(wG=La(yg3QXOcTKNnT#|PeTQMJecg2JwEfL~ z1!3Ha{72vZ@%L5;-SBO6C}Py&QHw_{9x)aJ05|AziVz_-XVd@y002ovPDHLkV1ma0 B%pw2) diff --git a/tests/ref/math-vec-linebreaks.png b/tests/ref/math-vec-linebreaks.png index 52ff0a8bbc61bba3604655887b7aeb893fc9115d..6666749dabf1fb1a75f67347472ee189f775a5eb 100644 GIT binary patch delta 636 zcmV-?0)zdF1&9TZ7k@Yi000006IWwm0006{Nkl? zsKBDYB3961&CpaTy2!Paixvn$vlz`*I53(naujW`)#wCE(FNJeUnvw43h7vwrO6`5 zwSg_SJ@TISz~Q|=`+9Spi|@1N>ccq~gRKIalnNczVI9_C5`TF9E^qe|Eu60V2RV7m zDk*&S7nFRnQ^Ngj^|N!75H{ujyRX4`OlDhVL9mkLf*$`z9Q&%m0#(%f#61&+EzLtlhlnMj$QxNsCupZIGC zJTq6JK9Y3izjeDjmdz<@b*TzPGwgPfaP!%fQ!KBd33yk0e`V;&Tash_Lt{Xx8Tl8^;Neo zVBAyMKe^I^rex;`KK(|tuRQQ)NrDPbyhbVfDiFhPWDKj(#2DbtY7AGmio3p+-3Ne} zk`Efg$2{V8G_yND4lzQSci3ZGvN*K WN34eLR6y+j0000Fp_9-l zGDtAGC{c8XR6@&wB3QQ48A}g}S`?Zq2AQdpA-F(8@h}obL~bT61Y6V&wl$2HZaQ@> zHn6R^Z-?Juy|=>c@!;=!d7j&c_c^=;Y*9N3wkRFeVIAIPuz&x)Haiz(;Ydq^wlv=T zDGE3B@6wi?&Fux7j(={VBfGavv&sGgK9vAi`3|s9E(ben0mdGU8UQNh#9*Txh!^wP z0$^(vgZC!@w~=P|3J@rkf&+yB@f+TwwkW{fA42eW82~YVJP1IkQwV-|5v8*>A=uoC zlBq)ou6c~oZGV#xT-}e-o$COEQ48lBE~*D0czO*0s>=Z0`T=033N2?KUoSrP_lr|K zXvNNemNifCQq9Lq7Ph|u=I1lKw0s8Ud|+A34?KE#*_4HwBGCKdxrxSZ_Y=(1bwMV^ zat!aQ0ie#8F49hZ0H}pfDZ_&t&v-74mjm2QA)7pnmFYhv*5Er3HQegV`v#Nc$v24I<; z90F1&5QDMW0m4auz7at*X7QFz>CPW%z<5%=m~ltqx>imX#p_tLd-~>#z<3 Ze*pLZl?g+Rl0X0e002ovPDHLkV1l@1Bn|)o diff --git a/tests/ref/math-vec-wide.png b/tests/ref/math-vec-wide.png index 9dc887a8cd7640ae9f0ba0c7707c6bfca25d0c52..000e3cf2a6f608878d55cfe87a78114e69a49222 100644 GIT binary patch delta 618 zcmV-w0+s#j1oi}w7k@Yi000006IWwm0006#Nkl=?-`3T7r@lqvMXjke}0$Sz{GgsfIt(T}Y}GhJmd(-Ah( z<$k!mV>^AH;hu~8 z{t7f%FABj;X4D4j%x$PW-YEnhu1B}A)k1KI8{LwNV>0kYFZ!(v$iPKI=vUk$0~dZl zzYSg)c%c^{(tmK?G)1OqGTGaYegt6oRmq8M*EoIyfN?tj$t~2L%h$vsjlLhvz|3^I zxC)Q;V|J-9Oq6}i7yO{*ZlVTw(v;3krX{b)^nG_67!CRwg25O-+p*686guY&vu85e z0fYwAjgV=Dqjetu%bMp7mw(b31^n&ly46)DPrP{9s;=5w zM$6uOt(w~%!zd|mugPTct1$Q6RX;_4r=_pa6c3=S$Rz`>7(lNyoOv!F z2UGW!9F3NzBwVbKQid9@n>tlkpKVy07*qoM6N<$ Ef_r}@Z2$lO delta 608 zcmV-m0-yc%1ndNm7k@bj00000*bA`70006rNklygdj{E$oyo{@e?5QG%;J|3DBMfQ zDBKB4Xl8N6I(!ztv!d2HcsNxSdZ-rD%B1>rL+HyTu#3|JU_G zitc&Py7GU~N`Gouym|J5`$<50cQ**kM~h?Et0)%#pB+cFIDT4u;r~K(LB>N+d)5Mp zAMI^b!EJ32C)7cLnqwN}LGAwk<~4L5^IQj0`}h4nj_l)$>HpW;gIS6@DYrQC#zq{8 z>yoaI$0}rtpO?IeolUjG_5CicRQ>VZy?Z|(74T}P+JD{GXq%7OP7x~FmJG9!i?GE< zXk)R`c>)T=uhGV0d-UA$5Vbb?!Tg0b7Dq2a6Np%V!o4FzqZUYOBPxGeJF3O|Eofx% zaSzlc*6vxT7SFAwk;Q-HP$uq5R2?XUW8T3LJ{5j9bV zrM^Jo?pbc3U2oCvJxY20?k#eA*5?E5EWT2STW)7JEk^{}&*GAK7Wt2+V<2_cv5Wsm uensO^9~|cY{4hGsHEQvw#iJI}7ytl}=_J?)Agx#c0000 Date: Wed, 4 Jun 2025 14:31:06 +0100 Subject: [PATCH 064/162] Numbering implementation refactor (#6122) --- crates/typst-library/src/model/numbering.rs | 667 ++++++++++---------- tests/suite/model/numbering.typ | 42 +- 2 files changed, 354 insertions(+), 355 deletions(-) diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 320ed7d17..236ced361 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -9,7 +9,6 @@ use ecow::{eco_format, EcoString, EcoVec}; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{cast, func, Context, Func, Str, Value}; -use crate::text::Case; /// Applies a numbering to a sequence of numbers. /// @@ -381,40 +380,194 @@ impl NumberingKind { /// Apply the numbering to the given number. pub fn apply(self, n: u64) -> EcoString { match self { - Self::Arabic => eco_format!("{n}"), - Self::LowerRoman => roman_numeral(n, Case::Lower), - Self::UpperRoman => roman_numeral(n, Case::Upper), - Self::LowerGreek => greek_numeral(n, Case::Lower), - Self::UpperGreek => greek_numeral(n, Case::Upper), - Self::Symbol => { - if n == 0 { - return '-'.into(); - } - - const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; - let n_symbols = SYMBOLS.len() as u64; - let symbol = SYMBOLS[((n - 1) % n_symbols) as usize]; - let amount = ((n - 1) / n_symbols) + 1; - std::iter::repeat_n(symbol, amount.try_into().unwrap()).collect() + Self::Arabic => { + numeric(&['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], n) } - Self::Hebrew => hebrew_numeral(n), - - Self::LowerLatin => zeroless( - [ + Self::LowerRoman => additive( + &[ + ("m̅", 1000000), + ("d̅", 500000), + ("c̅", 100000), + ("l̅", 50000), + ("x̅", 10000), + ("v̅", 5000), + ("i̅v̅", 4000), + ("m", 1000), + ("cm", 900), + ("d", 500), + ("cd", 400), + ("c", 100), + ("xc", 90), + ("l", 50), + ("xl", 40), + ("x", 10), + ("ix", 9), + ("v", 5), + ("iv", 4), + ("i", 1), + ("n", 0), + ], + n, + ), + Self::UpperRoman => additive( + &[ + ("M̅", 1000000), + ("D̅", 500000), + ("C̅", 100000), + ("L̅", 50000), + ("X̅", 10000), + ("V̅", 5000), + ("I̅V̅", 4000), + ("M", 1000), + ("CM", 900), + ("D", 500), + ("CD", 400), + ("C", 100), + ("XC", 90), + ("L", 50), + ("XL", 40), + ("X", 10), + ("IX", 9), + ("V", 5), + ("IV", 4), + ("I", 1), + ("N", 0), + ], + n, + ), + Self::LowerGreek => additive( + &[ + ("͵θ", 9000), + ("͵η", 8000), + ("͵ζ", 7000), + ("͵ϛ", 6000), + ("͵ε", 5000), + ("͵δ", 4000), + ("͵γ", 3000), + ("͵β", 2000), + ("͵α", 1000), + ("ϡ", 900), + ("ω", 800), + ("ψ", 700), + ("χ", 600), + ("φ", 500), + ("υ", 400), + ("τ", 300), + ("σ", 200), + ("ρ", 100), + ("ϟ", 90), + ("π", 80), + ("ο", 70), + ("ξ", 60), + ("ν", 50), + ("μ", 40), + ("λ", 30), + ("κ", 20), + ("ι", 10), + ("θ", 9), + ("η", 8), + ("ζ", 7), + ("ϛ", 6), + ("ε", 5), + ("δ", 4), + ("γ", 3), + ("β", 2), + ("α", 1), + ("𐆊", 0), + ], + n, + ), + Self::UpperGreek => additive( + &[ + ("͵Θ", 9000), + ("͵Η", 8000), + ("͵Ζ", 7000), + ("͵Ϛ", 6000), + ("͵Ε", 5000), + ("͵Δ", 4000), + ("͵Γ", 3000), + ("͵Β", 2000), + ("͵Α", 1000), + ("Ϡ", 900), + ("Ω", 800), + ("Ψ", 700), + ("Χ", 600), + ("Φ", 500), + ("Υ", 400), + ("Τ", 300), + ("Σ", 200), + ("Ρ", 100), + ("Ϟ", 90), + ("Π", 80), + ("Ο", 70), + ("Ξ", 60), + ("Ν", 50), + ("Μ", 40), + ("Λ", 30), + ("Κ", 20), + ("Ι", 10), + ("Θ", 9), + ("Η", 8), + ("Ζ", 7), + ("Ϛ", 6), + ("Ε", 5), + ("Δ", 4), + ("Γ", 3), + ("Β", 2), + ("Α", 1), + ("𐆊", 0), + ], + n, + ), + Self::Hebrew => additive( + &[ + ("ת", 400), + ("ש", 300), + ("ר", 200), + ("ק", 100), + ("צ", 90), + ("פ", 80), + ("ע", 70), + ("ס", 60), + ("נ", 50), + ("מ", 40), + ("ל", 30), + ("כ", 20), + ("יט", 19), + ("יח", 18), + ("יז", 17), + ("טז", 16), + ("טו", 15), + ("י", 10), + ("ט", 9), + ("ח", 8), + ("ז", 7), + ("ו", 6), + ("ה", 5), + ("ד", 4), + ("ג", 3), + ("ב", 2), + ("א", 1), + ("-", 0), + ], + n, + ), + Self::LowerLatin => alphabetic( + &[ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ], n, ), - Self::UpperLatin => zeroless( - [ + Self::UpperLatin => alphabetic( + &[ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ], n, ), - Self::HiraganaAiueo => zeroless( - [ + Self::HiraganaAiueo => alphabetic( + &[ 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', @@ -423,8 +576,8 @@ impl NumberingKind { ], n, ), - Self::HiraganaIroha => zeroless( - [ + Self::HiraganaIroha => alphabetic( + &[ 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', 'を', 'わ', 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', 'む', 'う', 'ゐ', 'の', 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', @@ -433,8 +586,8 @@ impl NumberingKind { ], n, ), - Self::KatakanaAiueo => zeroless( - [ + Self::KatakanaAiueo => alphabetic( + &[ 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', @@ -443,8 +596,8 @@ impl NumberingKind { ], n, ), - Self::KatakanaIroha => zeroless( - [ + Self::KatakanaIroha => alphabetic( + &[ 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', 'ヲ', 'ワ', 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', 'ム', 'ウ', 'ヰ', 'ノ', 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', @@ -453,40 +606,40 @@ impl NumberingKind { ], n, ), - Self::KoreanJamo => zeroless( - [ + Self::KoreanJamo => alphabetic( + &[ 'ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ', ], n, ), - Self::KoreanSyllable => zeroless( - [ + Self::KoreanSyllable => alphabetic( + &[ '가', '나', '다', '라', '마', '바', '사', '아', '자', '차', '카', '타', '파', '하', ], n, ), - Self::BengaliLetter => zeroless( - [ + Self::BengaliLetter => alphabetic( + &[ 'ক', 'খ', 'গ', 'ঘ', 'ঙ', 'চ', 'ছ', 'জ', 'ঝ', 'ঞ', 'ট', 'ঠ', 'ড', 'ঢ', 'ণ', 'ত', 'থ', 'দ', 'ধ', 'ন', 'প', 'ফ', 'ব', 'ভ', 'ম', 'য', 'র', 'ল', 'শ', 'ষ', 'স', 'হ', ], n, ), - Self::CircledNumber => zeroless( - [ - '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', '⑭', - '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', '㉖', - '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', '㊲', - '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', '㊽', - '㊾', '㊿', + Self::CircledNumber => fixed( + &[ + '⓪', '①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩', '⑪', '⑫', '⑬', + '⑭', '⑮', '⑯', '⑰', '⑱', '⑲', '⑳', '㉑', '㉒', '㉓', '㉔', '㉕', + '㉖', '㉗', '㉘', '㉙', '㉚', '㉛', '㉜', '㉝', '㉞', '㉟', '㊱', + '㊲', '㊳', '㊴', '㊵', '㊶', '㊷', '㊸', '㊹', '㊺', '㊻', '㊼', + '㊽', '㊾', '㊿', ], n, ), Self::DoubleCircledNumber => { - zeroless(['⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n) + fixed(&['0', '⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽', '⓾'], n) } Self::LowerSimplifiedChinese => { @@ -502,306 +655,170 @@ impl NumberingKind { u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into() } - Self::EasternArabic => decimal('\u{0660}', n), - Self::EasternArabicPersian => decimal('\u{06F0}', n), - Self::DevanagariNumber => decimal('\u{0966}', n), - Self::BengaliNumber => decimal('\u{09E6}', n), - } - } -} - -/// Stringify an integer to a Hebrew number. -fn hebrew_numeral(mut n: u64) -> EcoString { - if n == 0 { - return '-'.into(); - } - let mut fmt = EcoString::new(); - 'outer: for (name, value) in [ - ('ת', 400), - ('ש', 300), - ('ר', 200), - ('ק', 100), - ('צ', 90), - ('פ', 80), - ('ע', 70), - ('ס', 60), - ('נ', 50), - ('מ', 40), - ('ל', 30), - ('כ', 20), - ('י', 10), - ('ט', 9), - ('ח', 8), - ('ז', 7), - ('ו', 6), - ('ה', 5), - ('ד', 4), - ('ג', 3), - ('ב', 2), - ('א', 1), - ] { - while n >= value { - match n { - 15 => fmt.push_str("ט״ו"), - 16 => fmt.push_str("ט״ז"), - _ => { - let append_geresh = n == value && fmt.is_empty(); - if n == value && !fmt.is_empty() { - fmt.push('״'); - } - fmt.push(name); - if append_geresh { - fmt.push('׳'); - } - - n -= value; - continue; - } + Self::EasternArabic => { + numeric(&['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'], n) } - break 'outer; - } - } - fmt -} - -/// Stringify an integer to a Roman numeral. -fn roman_numeral(mut n: u64, case: Case) -> EcoString { - if n == 0 { - return match case { - Case::Lower => 'n'.into(), - Case::Upper => 'N'.into(), - }; - } - - // Adapted from Yann Villessuzanne's roman.rs under the - // Unlicense, at https://github.com/linfir/roman.rs/ - let mut fmt = EcoString::new(); - for &(name, value) in &[ - ("M̅", 1000000), - ("D̅", 500000), - ("C̅", 100000), - ("L̅", 50000), - ("X̅", 10000), - ("V̅", 5000), - ("I̅V̅", 4000), - ("M", 1000), - ("CM", 900), - ("D", 500), - ("CD", 400), - ("C", 100), - ("XC", 90), - ("L", 50), - ("XL", 40), - ("X", 10), - ("IX", 9), - ("V", 5), - ("IV", 4), - ("I", 1), - ] { - while n >= value { - n -= value; - for c in name.chars() { - match case { - Case::Lower => fmt.extend(c.to_lowercase()), - Case::Upper => fmt.push(c), - } + Self::EasternArabicPersian => { + numeric(&['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'], n) } + Self::DevanagariNumber => { + numeric(&['०', '१', '२', '३', '४', '५', '६', '७', '८', '९'], n) + } + Self::BengaliNumber => { + numeric(&['০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯'], n) + } + Self::Symbol => symbolic(&['*', '†', '‡', '§', '¶', '‖'], n), } } - - fmt } -/// Stringify an integer to Greek numbers. +/// Stringify a number using symbols representing values. The decimal +/// representation of the number is recovered by summing over the values of the +/// symbols present. /// -/// Greek numbers use the Greek Alphabet to represent numbers; it is based on 10 -/// (decimal). Here we implement the single digit M power representation from -/// [The Greek Number Converter][convert] and also described in -/// [Greek Numbers][numbers]. -/// -/// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm -/// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/ -fn greek_numeral(n: u64, case: Case) -> EcoString { - let thousands = [ - ["͵α", "͵Α"], - ["͵β", "͵Β"], - ["͵γ", "͵Γ"], - ["͵δ", "͵Δ"], - ["͵ε", "͵Ε"], - ["͵ϛ", "͵Ϛ"], - ["͵ζ", "͵Ζ"], - ["͵η", "͵Η"], - ["͵θ", "͵Θ"], - ]; - let hundreds = [ - ["ρ", "Ρ"], - ["σ", "Σ"], - ["τ", "Τ"], - ["υ", "Υ"], - ["φ", "Φ"], - ["χ", "Χ"], - ["ψ", "Ψ"], - ["ω", "Ω"], - ["ϡ", "Ϡ"], - ]; - let tens = [ - ["ι", "Ι"], - ["κ", "Κ"], - ["λ", "Λ"], - ["μ", "Μ"], - ["ν", "Ν"], - ["ξ", "Ξ"], - ["ο", "Ο"], - ["π", "Π"], - ["ϙ", "Ϟ"], - ]; - let ones = [ - ["α", "Α"], - ["β", "Β"], - ["γ", "Γ"], - ["δ", "Δ"], - ["ε", "Ε"], - ["ϛ", "Ϛ"], - ["ζ", "Ζ"], - ["η", "Η"], - ["θ", "Θ"], - ]; - - if n == 0 { - // Greek Zero Sign - return '𐆊'.into(); - } - - let mut fmt = EcoString::new(); - let case = match case { - Case::Lower => 0, - Case::Upper => 1, - }; - - // Extract a list of decimal digits from the number - let mut decimal_digits: Vec = Vec::new(); - let mut n = n; - while n > 0 { - decimal_digits.push((n % 10) as usize); - n /= 10; - } - - // Pad the digits with leading zeros to ensure we can form groups of 4 - while decimal_digits.len() % 4 != 0 { - decimal_digits.push(0); - } - decimal_digits.reverse(); - - let mut m_power = decimal_digits.len() / 4; - - // M are used to represent 10000, M_power = 2 means 10000^2 = 10000 0000 - // The prefix of M is also made of Greek numerals but only be single digits, so it is 9 at max. This enables us - // to represent up to (10000)^(9 + 1) - 1 = 10^40 -1 (9,999,999,999,999,999,999,999,999,999,999,999,999,999) - let get_m_prefix = |m_power: usize| { - if m_power == 0 { - None - } else { - assert!(m_power <= 9); - // the prefix of M is a single digit lowercase - Some(ones[m_power - 1][0]) - } - }; - - let mut previous_has_number = false; - for chunk in decimal_digits.chunks_exact(4) { - // chunk must be exact 4 item - assert_eq!(chunk.len(), 4); - - m_power = m_power.saturating_sub(1); - - // `th`ousan, `h`undred, `t`en and `o`ne - let (th, h, t, o) = (chunk[0], chunk[1], chunk[2], chunk[3]); - if th + h + t + o == 0 { - continue; - } - - if previous_has_number { - fmt.push_str(", "); - } - - if let Some(m_prefix) = get_m_prefix(m_power) { - fmt.push_str(m_prefix); - fmt.push_str("Μ"); - } - if th != 0 { - let thousand_digit = thousands[th - 1][case]; - fmt.push_str(thousand_digit); - } - if h != 0 { - let hundred_digit = hundreds[h - 1][case]; - fmt.push_str(hundred_digit); - } - if t != 0 { - let ten_digit = tens[t - 1][case]; - fmt.push_str(ten_digit); - } - if o != 0 { - let one_digit = ones[o - 1][case]; - fmt.push_str(one_digit); - } - // if we do not have thousan, we need to append 'ʹ' at the end. - if th == 0 { - fmt.push_str("ʹ"); - } - previous_has_number = true; - } - fmt -} - -/// Stringify a number using a base-N counting system with no zero digit. -/// -/// This is best explained by example. Suppose our digits are 'A', 'B', and 'C'. -/// We would get the following: +/// Consider the situation where ['I': 1, 'IV': 4, 'V': 5], /// /// ```text -/// 1 => "A" -/// 2 => "B" -/// 3 => "C" -/// 4 => "AA" -/// 5 => "AB" -/// 6 => "AC" -/// 7 => "BA" -/// 8 => "BB" -/// 9 => "BC" -/// 10 => "CA" -/// 11 => "CB" -/// 12 => "CC" -/// 13 => "AAA" -/// etc. +/// 1 => 'I' +/// 2 => 'II' +/// 3 => 'III' +/// 4 => 'IV' +/// 5 => 'V' +/// 6 => 'VI' +/// 7 => 'VII' +/// 8 => 'VIII' /// ``` /// -/// You might be familiar with this scheme from the way spreadsheet software -/// tends to label its columns. -fn zeroless(alphabet: [char; N_DIGITS], mut n: u64) -> EcoString { +/// where this is the start of the familiar Roman numeral system. +fn additive(symbols: &[(&str, u64)], mut n: u64) -> EcoString { + if n == 0 { + if let Some(&(symbol, 0)) = symbols.last() { + return symbol.into(); + } + return '0'.into(); + } + + let mut s = EcoString::new(); + for (symbol, weight) in symbols { + if *weight == 0 || *weight > n { + continue; + } + let reps = n / weight; + for _ in 0..reps { + s.push_str(symbol); + } + + n -= weight * reps; + if n == 0 { + return s; + } + } + s +} + +/// Stringify a number using a base-n (where n is the number of provided +/// symbols) system without a zero symbol. +/// +/// Consider the situation where ['A', 'B', 'C'] are the provided symbols, +/// +/// ```text +/// 1 => 'A' +/// 2 => 'B' +/// 3 => 'C' +/// 4 => 'AA +/// 5 => 'AB' +/// 6 => 'AC' +/// 7 => 'BA' +/// ... +/// ``` +/// +/// This system is commonly used in spreadsheet software. +fn alphabetic(symbols: &[char], mut n: u64) -> EcoString { + let n_digits = symbols.len() as u64; if n == 0 { return '-'.into(); } - let n_digits = N_DIGITS as u64; - let mut cs = EcoString::new(); - while n > 0 { + let mut s = EcoString::new(); + while n != 0 { n -= 1; - cs.push(alphabet[(n % n_digits) as usize]); + s.push(symbols[(n % n_digits) as usize]); n /= n_digits; } - cs.chars().rev().collect() + s.chars().rev().collect() } -/// Stringify a number using a base-10 counting system with a zero digit. +/// Stringify a number using the symbols provided, defaulting to the arabic +/// representation when the number is greater than the number of symbols. /// -/// This function assumes that the digits occupy contiguous codepoints. -fn decimal(start: char, mut n: u64) -> EcoString { - if n == 0 { - return start.into(); +/// Consider the situation where ['0', 'A', 'B', 'C'] are the provided symbols, +/// +/// ```text +/// 0 => '0' +/// 1 => 'A' +/// 2 => 'B' +/// 3 => 'C' +/// 4 => '4' +/// ... +/// n => 'n' +/// ``` +fn fixed(symbols: &[char], n: u64) -> EcoString { + let n_digits = symbols.len() as u64; + if n < n_digits { + return symbols[(n) as usize].into(); } - let mut cs = EcoString::new(); - while n > 0 { - cs.push(char::from_u32((start as u32) + ((n % 10) as u32)).unwrap()); - n /= 10; - } - cs.chars().rev().collect() + eco_format!("{n}") +} + +/// Stringify a number using a base-n (where n is the number of provided +/// symbols) system with a zero symbol. +/// +/// Consider the situation where ['0', '1', '2'] are the provided symbols, +/// +/// ```text +/// 0 => '0' +/// 1 => '1' +/// 2 => '2' +/// 3 => '10' +/// 4 => '11' +/// 5 => '12' +/// 6 => '20' +/// ... +/// ``` +/// +/// which is the familiar trinary counting system. +fn numeric(symbols: &[char], mut n: u64) -> EcoString { + let n_digits = symbols.len() as u64; + if n == 0 { + return symbols[0].into(); + } + let mut s = EcoString::new(); + while n != 0 { + s.push(symbols[(n % n_digits) as usize]); + n /= n_digits; + } + s.chars().rev().collect() +} + +/// Stringify a number using repeating symbols. +/// +/// Consider the situation where ['A', 'B', 'C'] are the provided symbols, +/// +/// ```text +/// 0 => '-' +/// 1 => 'A' +/// 2 => 'B' +/// 3 => 'C' +/// 4 => 'AA' +/// 5 => 'BB' +/// 6 => 'CC' +/// 7 => 'AAA' +/// ... +/// ``` +fn symbolic(symbols: &[char], n: u64) -> EcoString { + let n_digits = symbols.len() as u64; + if n == 0 { + return '-'.into(); + } + EcoString::from(symbols[((n - 1) % n_digits) as usize]) + .repeat((n.div_ceil(n_digits)) as usize) } diff --git a/tests/suite/model/numbering.typ b/tests/suite/model/numbering.typ index 6af989ff1..2d6a3d6a6 100644 --- a/tests/suite/model/numbering.typ +++ b/tests/suite/model/numbering.typ @@ -19,50 +19,32 @@ // Greek. #t( pat: "α", - "𐆊", "αʹ", "βʹ", "γʹ", "δʹ", "εʹ", "ϛʹ", "ζʹ", "ηʹ", "θʹ", "ιʹ", - "ιαʹ", "ιβʹ", "ιγʹ", "ιδʹ", "ιεʹ", "ιϛʹ", "ιζʹ", "ιηʹ", "ιθʹ", "κʹ", - 241, "σμαʹ", - 999, "ϡϙθʹ", + "𐆊", "α", "β", "γ", "δ", "ε", "ϛ", "ζ", "η", "θ", "ι", + "ια", "ιβ", "ιγ", "ιδ", "ιε", "ιϛ", "ιζ", "ιη", "ιθ", "κ", + 241, "σμα", + 999, "ϡϟθ", 1005, "͵αε", - 1999, "͵αϡϙθ", - 2999, "͵βϡϙθ", + 1999, "͵αϡϟθ", + 2999, "͵βϡϟθ", 3000, "͵γ", - 3398, "͵γτϙη", + 3398, "͵γτϟη", 4444, "͵δυμδ", 5683, "͵εχπγ", 9184, "͵θρπδ", - 9999, "͵θϡϙθ", - 20000, "αΜβʹ", - 20001, "αΜβʹ, αʹ", - 97554, "αΜθʹ, ͵ζφνδ", - 99999, "αΜθʹ, ͵θϡϙθ", - 1000000, "αΜρʹ", - 1000001, "αΜρʹ, αʹ", - 1999999, "αΜρϙθʹ, ͵θϡϙθ", - 2345678, "αΜσλδʹ, ͵εχοη", - 9999999, "αΜϡϙθʹ, ͵θϡϙθ", - 10000000, "αΜ͵α", - 90000001, "αΜ͵θ, αʹ", - 100000000, "βΜαʹ", - 1000000000, "βΜιʹ", - 2000000000, "βΜκʹ", - 2000000001, "βΜκʹ, αʹ", - 2000010001, "βΜκʹ, αΜαʹ, αʹ", - 2056839184, "βΜκʹ, αΜ͵εχπγ, ͵θρπδ", - 12312398676, "βΜρκγʹ, αΜ͵ασλθ, ͵ηχοϛ", + 9999, "͵θϡϟθ", ) #t( pat: sym.Alpha, - "𐆊", "Αʹ", "Βʹ", "Γʹ", "Δʹ", "Εʹ", "Ϛʹ", "Ζʹ", "Ηʹ", "Θʹ", "Ιʹ", - "ΙΑʹ", "ΙΒʹ", "ΙΓʹ", "ΙΔʹ", "ΙΕʹ", "ΙϚʹ", "ΙΖʹ", "ΙΗʹ", "ΙΘʹ", "Κʹ", - 241, "ΣΜΑʹ", + "𐆊", "Α", "Β", "Γ", "Δ", "Ε", "Ϛ", "Ζ", "Η", "Θ", "Ι", + "ΙΑ", "ΙΒ", "ΙΓ", "ΙΔ", "ΙΕ", "ΙϚ", "ΙΖ", "ΙΗ", "ΙΘ", "Κ", + 241, "ΣΜΑ", ) // Symbols. #t(pat: "*", "-", "*", "†", "‡", "§", "¶", "‖", "**") // Hebrew. -#t(pat: "א", step: 2, 9, "ט׳", "י״א", "י״ג") +#t(pat: "א", step: 2, 9, "ט", "יא", "יג", 15, "טו", 16, "טז") // Chinese. #t(pat: "一", step: 2, 9, "九", "十一", "十三", "十五", "十七", "十九") From 6725061841e327227a49f90134136264a5b8c584 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 9 Jun 2025 10:46:29 -0300 Subject: [PATCH 065/162] Pin colspan and rowspan for blank cells (#6401) --- crates/typst-library/src/layout/grid/mod.rs | 9 ++++++++- crates/typst-library/src/model/table.rs | 9 ++++++++- .../ref/issue-6399-grid-cell-colspan-set-rule.png | Bin 0 -> 232 bytes .../ref/issue-6399-grid-cell-rowspan-set-rule.png | Bin 0 -> 232 bytes tests/suite/layout/grid/colspan.typ | 4 ++++ tests/suite/layout/grid/rowspan.typ | 4 ++++ 6 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/ref/issue-6399-grid-cell-colspan-set-rule.png create mode 100644 tests/ref/issue-6399-grid-cell-rowspan-set-rule.png diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index 6616c3311..369df11ee 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -755,7 +755,14 @@ impl Show for Packed { impl Default for Packed { fn default() -> Self { - Packed::new(GridCell::new(Content::default())) + Packed::new( + // Explicitly set colspan and rowspan to ensure they won't be + // overridden by set rules (default cells are created after + // colspans and rowspans are processed in the resolver) + GridCell::new(Content::default()) + .with_colspan(NonZeroUsize::ONE) + .with_rowspan(NonZeroUsize::ONE), + ) } } diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 6f4461bd4..373230897 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -770,7 +770,14 @@ impl Show for Packed { impl Default for Packed { fn default() -> Self { - Packed::new(TableCell::new(Content::default())) + Packed::new( + // Explicitly set colspan and rowspan to ensure they won't be + // overridden by set rules (default cells are created after + // colspans and rowspans are processed in the resolver) + TableCell::new(Content::default()) + .with_colspan(NonZeroUsize::ONE) + .with_rowspan(NonZeroUsize::ONE), + ) } } diff --git a/tests/ref/issue-6399-grid-cell-colspan-set-rule.png b/tests/ref/issue-6399-grid-cell-colspan-set-rule.png new file mode 100644 index 0000000000000000000000000000000000000000..a40eda78dc1708901754f8c1ce78df5e1456bd85 GIT binary patch literal 232 zcmVP)z*yZ?bhY?VIE=NYmcAAT^~PQKe|$;bk39t~ z(J%jRT{)Fb7Mp(B-ul;Of9(7{A-^WBe&N2~@hE*P4*P$s=!*N($KUtV{9iC*UDngb zzv*J}O~3#9GXJQ){PTQM$^RMsPxZck{y-Ot7nD4oS^oM!(fnUs)!&-xf2}TGIa+d! iT0Cm;sKuieBLM(bBzWrxf}HRG0000P)z*yZ?bhY?VIE=NYmcAAT^~PQKe|$;bk39t~ z(J%jRT{)Fb7Mp(B-ul;Of9(7{A-^WBe&N2~@hE*P4*P$s=!*N($KUtV{9iC*UDngb zzv*J}O~3#9GXJQ){PTQM$^RMsPxZck{y-Ot7nD4oS^oM!(fnUs)!&-xf2}TGIa+d! iT0Cm;sKuieBLM(bBzWrxf}HRG0000 Date: Mon, 9 Jun 2025 09:48:55 -0400 Subject: [PATCH 066/162] Clean up some parser comments (#6398) --- crates/typst-syntax/src/parser.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index ecd0d78a5..a68815806 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -1571,10 +1571,10 @@ struct Token { prev_end: usize, } -/// Information about a newline if present (currently only relevant in Markup). +/// Information about newlines in a group of trivia. #[derive(Debug, Clone, Copy)] struct Newline { - /// The column of the start of our token in its line. + /// The column of the start of the next token in its line. column: Option, /// Whether any of our newlines were paragraph breaks. parbreak: bool, @@ -1587,7 +1587,7 @@ enum AtNewline { Continue, /// Stop at any newline. Stop, - /// Continue only if there is no continuation with `else` or `.` (Code only). + /// Continue only if there is a continuation with `else` or `.` (Code only). ContextualContinue, /// Stop only at a parbreak, not normal newlines (Markup only). StopParBreak, @@ -1610,9 +1610,10 @@ impl AtNewline { }, AtNewline::StopParBreak => parbreak, AtNewline::RequireColumn(min_col) => { - // Don't stop if this newline doesn't start a column (this may - // be checked on the boundary of lexer modes, since we only - // report a column in Markup). + // When the column is `None`, the newline doesn't start a + // column, and we continue parsing. This may happen on the + // boundary of lexer modes, since we only report a column in + // Markup. column.is_some_and(|column| column <= min_col) } } From df4c08f852ba3342e69caa721067804a7152e166 Mon Sep 17 00:00:00 2001 From: cAttte <26514199+cAttte@users.noreply.github.com> Date: Mon, 9 Jun 2025 11:16:47 -0300 Subject: [PATCH 067/162] Autocomplete fixes for math mode (#6415) --- crates/typst-ide/src/complete.rs | 16 +++++++++++++++- crates/typst-ide/src/utils.rs | 4 +++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 4a36045ae..a042b1640 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -298,13 +298,20 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { return false; } - // Start of an interpolated identifier: "#|". + // Start of an interpolated identifier: "$#|$". if ctx.leaf.kind() == SyntaxKind::Hash { ctx.from = ctx.cursor; code_completions(ctx, true); return true; } + // Behind existing interpolated identifier: "$#pa|$". + if ctx.leaf.kind() == SyntaxKind::Ident { + ctx.from = ctx.leaf.offset(); + code_completions(ctx, true); + return true; + } + // Behind existing atom or identifier: "$a|$" or "$abc|$". if matches!( ctx.leaf.kind(), @@ -1666,6 +1673,13 @@ mod tests { test("#{() .a}", -2).must_include(["at", "any", "all"]); } + /// Test that autocomplete in math uses the correct global scope. + #[test] + fn test_autocomplete_math_scope() { + test("$#col$", -2).must_include(["colbreak"]).must_exclude(["colon"]); + test("$col$", -2).must_include(["colon"]).must_exclude(["colbreak"]); + } + /// Test that the `before_window` doesn't slice into invalid byte /// boundaries. #[test] diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs index 887e851f9..13de402ba 100644 --- a/crates/typst-ide/src/utils.rs +++ b/crates/typst-ide/src/utils.rs @@ -114,7 +114,9 @@ pub fn globals<'a>(world: &'a dyn IdeWorld, leaf: &LinkedNode) -> &'a Scope { | Some(SyntaxKind::Math) | Some(SyntaxKind::MathFrac) | Some(SyntaxKind::MathAttach) - ); + ) && leaf + .prev_leaf() + .is_none_or(|prev| !matches!(prev.kind(), SyntaxKind::Hash)); let library = world.library(); if in_math { From 2a3746c51de9231436013a2885a6d7096b0e4028 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 9 Jun 2025 17:25:33 +0300 Subject: [PATCH 068/162] Update docs for gradient.repeat (#6385) --- crates/typst-library/src/visualize/gradient.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 45f388ccd..5d7859a37 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -549,7 +549,7 @@ impl Gradient { } /// Repeats this gradient a given number of times, optionally mirroring it - /// at each repetition. + /// at every second repetition. /// /// ```example /// #circle( @@ -564,7 +564,17 @@ impl Gradient { &self, /// The number of times to repeat the gradient. repetitions: Spanned, - /// Whether to mirror the gradient at each repetition. + /// Whether to mirror the gradient at every second repetition, i.e., + /// the first instance (and all odd ones) stays unchanged. + /// + /// ```example + /// #circle( + /// radius: 40pt, + /// fill: gradient + /// .conic(green, black) + /// .repeat(2, mirror: true) + /// ) + /// ``` #[named] #[default(false)] mirror: bool, From e632bffc2ed4c005e5e989b527a05e87f077a8a0 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:34:39 +0300 Subject: [PATCH 069/162] Document how to escape lr delimiter auto-scaling (#6410) Co-authored-by: Laurenz --- docs/reference/groups.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 8fea3a1f2..e5aa7e999 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -112,11 +112,18 @@ a few more functions that create delimiter pairings for absolute, ceiled, and floored values as well as norms. + To prevent a delimiter from being matched by Typst, and thus auto-scaled, + escape it with a backslash. To instead disable auto-scaling completely, use + `{set math.lr(size: 1em)}`. + # Example ```example $ [a, b/2] $ $ lr(]sum_(x=1)^n], size: #50%) x $ $ abs((x + y) / 2) $ + $ \{ (x / y) \} $ + #set math.lr(size: 1em) + $ { (a / b), a, b in (0; 1/2] } $ ``` - name: calc From 82da96ed957a68017e092e2606226b45c34324f1 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Tue, 10 Jun 2025 05:11:27 -0400 Subject: [PATCH 070/162] Improve number lexing (#5969) --- crates/typst-syntax/src/lexer.rs | 154 ++++++++++++++++-------------- tests/ref/double-percent.png | Bin 496 -> 0 bytes tests/suite/foundations/float.typ | 8 +- tests/suite/layout/length.typ | 36 +++++-- tests/suite/layout/relative.typ | 7 +- 5 files changed, 118 insertions(+), 87 deletions(-) delete mode 100644 tests/ref/double-percent.png diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index ac69eb616..7d363d7b5 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -807,86 +807,96 @@ impl Lexer<'_> { } } - fn number(&mut self, mut start: usize, c: char) -> SyntaxKind { + fn number(&mut self, start: usize, first_c: char) -> SyntaxKind { // Handle alternative integer bases. - let mut base = 10; - if c == '0' { - if self.s.eat_if('b') { - base = 2; - } else if self.s.eat_if('o') { - base = 8; - } else if self.s.eat_if('x') { - base = 16; - } - if base != 10 { - start = self.s.cursor(); - } - } - - // Read the first part (integer or fractional depending on `first`). - self.s.eat_while(if base == 16 { - char::is_ascii_alphanumeric - } else { - char::is_ascii_digit - }); - - // Read the fractional part if not already done. - // Make sure not to confuse a range for the decimal separator. - if c != '.' - && !self.s.at("..") - && !self.s.scout(1).is_some_and(is_id_start) - && self.s.eat_if('.') - && base == 10 - { - self.s.eat_while(char::is_ascii_digit); - } - - // Read the exponent. - if !self.s.at("em") && self.s.eat_if(['e', 'E']) && base == 10 { - self.s.eat_if(['+', '-']); - self.s.eat_while(char::is_ascii_digit); - } - - // Read the suffix. - let suffix_start = self.s.cursor(); - if !self.s.eat_if('%') { - self.s.eat_while(char::is_ascii_alphanumeric); - } - - let number = self.s.get(start..suffix_start); - let suffix = self.s.from(suffix_start); - - let kind = if i64::from_str_radix(number, base).is_ok() { - SyntaxKind::Int - } else if base == 10 && number.parse::().is_ok() { - SyntaxKind::Float - } else { - return self.error(match base { - 2 => eco_format!("invalid binary number: 0b{}", number), - 8 => eco_format!("invalid octal number: 0o{}", number), - 16 => eco_format!("invalid hexadecimal number: 0x{}", number), - _ => eco_format!("invalid number: {}", number), - }); + let base = match first_c { + '0' if self.s.eat_if('b') => 2, + '0' if self.s.eat_if('o') => 8, + '0' if self.s.eat_if('x') => 16, + _ => 10, }; - if suffix.is_empty() { - return kind; + // Read the initial digits. + if base == 16 { + self.s.eat_while(char::is_ascii_alphanumeric); + } else { + self.s.eat_while(char::is_ascii_digit); } - if !matches!( - suffix, - "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%" - ) { - return self.error(eco_format!("invalid number suffix: {}", suffix)); + // Read floating point digits and exponents. + let mut is_float = false; + if base == 10 { + // Read digits following a dot. Make sure not to confuse a spread + // operator or a method call for the decimal separator. + if first_c == '.' { + is_float = true; // We already ate the trailing digits above. + } else if !self.s.at("..") + && !self.s.scout(1).is_some_and(is_id_start) + && self.s.eat_if('.') + { + is_float = true; + self.s.eat_while(char::is_ascii_digit); + } + + // Read the exponent. + if !self.s.at("em") && self.s.eat_if(['e', 'E']) { + is_float = true; + self.s.eat_if(['+', '-']); + self.s.eat_while(char::is_ascii_digit); + } } - if base != 10 { - let kind = self.error(eco_format!("invalid base-{base} prefix")); - self.hint("numbers with a unit cannot have a base prefix"); - return kind; - } + let number = self.s.from(start); + let suffix = self.s.eat_while(|c: char| c.is_ascii_alphanumeric() || c == '%'); - SyntaxKind::Numeric + let mut suffix_result = match suffix { + "" => Ok(None), + "pt" | "mm" | "cm" | "in" | "deg" | "rad" | "em" | "fr" | "%" => Ok(Some(())), + _ => Err(eco_format!("invalid number suffix: {suffix}")), + }; + + let number_result = if is_float && number.parse::().is_err() { + // The only invalid case should be when a float lacks digits after + // the exponent: e.g. `1.2e`, `2.3E-`, or `1EM`. + Err(eco_format!("invalid floating point number: {number}")) + } else if base == 10 { + Ok(()) + } else { + let name = match base { + 2 => "binary", + 8 => "octal", + 16 => "hexadecimal", + _ => unreachable!(), + }; + // The index `[2..]` skips the leading `0b`/`0o`/`0x`. + match i64::from_str_radix(&number[2..], base) { + Ok(_) if suffix.is_empty() => Ok(()), + Ok(value) => { + if suffix_result.is_ok() { + suffix_result = Err(eco_format!( + "try using a decimal number: {value}{suffix}" + )); + } + Err(eco_format!("{name} numbers cannot have a suffix")) + } + Err(_) => Err(eco_format!("invalid {name} number: {number}")), + } + }; + + // Return our number or write an error with helpful hints. + match (number_result, suffix_result) { + // Valid numbers :D + (Ok(()), Ok(None)) if is_float => SyntaxKind::Float, + (Ok(()), Ok(None)) => SyntaxKind::Int, + (Ok(()), Ok(Some(()))) => SyntaxKind::Numeric, + // Invalid numbers :( + (Err(number_err), Err(suffix_err)) => { + let err = self.error(number_err); + self.hint(suffix_err); + err + } + (Ok(()), Err(msg)) | (Err(msg), Ok(_)) => self.error(msg), + } } fn string(&mut self) -> SyntaxKind { diff --git a/tests/ref/double-percent.png b/tests/ref/double-percent.png deleted file mode 100644 index 61a0d6143cd1615b0fa0051d0442b32be6fd2491..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 496 zcmV+=2f`SbJhrlzKag@yFj3(}_~!2H?CkmK@#wC} z)^MQTk+#!kn$&BZ+=Z^zaiQj?#p$ujwY9a5j*hIXtmfwC)^eiSeW~fQ&FJXp>gwu| zk&(y8$MMbHoSdBU^75mjqwT@g;g-6}%F6cO>gu-7`||eg?(V$2yr7_<{{H@khK6)> zbh5It#KgqGDRSzy&iUx@&|Q?$VwdpA+xFh+_xJby`~2XNx8aw%>a@-K@b&-x{>PGM zX#fBKrb$FWRCwC$(?t%$KoCUHa+sN!nVFdxY~TMVk>bQRm`IWOt-g9ws|F#2{4FP1O>0000 Date: Tue, 10 Jun 2025 14:46:27 +0200 Subject: [PATCH 071/162] Report errors in external files (#6308) Co-authored-by: Laurenz --- Cargo.lock | 3 + Cargo.toml | 1 + crates/typst-cli/src/compile.rs | 4 +- crates/typst-cli/src/timings.rs | 2 +- crates/typst-cli/src/world.rs | 23 +- crates/typst-layout/Cargo.toml | 1 + crates/typst-layout/src/image.rs | 14 +- crates/typst-library/Cargo.toml | 1 + crates/typst-library/src/diag.rs | 303 ++++++++++++- crates/typst-library/src/foundations/bytes.rs | 11 + .../typst-library/src/foundations/plugin.rs | 4 +- crates/typst-library/src/loading/cbor.rs | 4 +- crates/typst-library/src/loading/csv.rs | 38 +- crates/typst-library/src/loading/json.rs | 13 +- crates/typst-library/src/loading/mod.rs | 49 ++- crates/typst-library/src/loading/read.rs | 15 +- crates/typst-library/src/loading/toml.rs | 28 +- crates/typst-library/src/loading/xml.rs | 11 +- crates/typst-library/src/loading/yaml.rs | 23 +- .../typst-library/src/model/bibliography.rs | 119 +++--- crates/typst-library/src/text/raw.rs | 81 ++-- .../typst-library/src/visualize/image/mod.rs | 20 +- .../typst-library/src/visualize/image/svg.rs | 26 +- crates/typst-syntax/Cargo.toml | 1 + crates/typst-syntax/src/lib.rs | 2 + crates/typst-syntax/src/lines.rs | 402 ++++++++++++++++++ crates/typst-syntax/src/source.rs | 326 +------------- tests/src/collect.rs | 98 ++++- tests/src/run.rs | 74 ++-- tests/src/world.rs | 21 +- tests/suite/loading/csv.typ | 4 +- tests/suite/loading/json.typ | 2 +- tests/suite/loading/read.typ | 2 +- tests/suite/loading/toml.typ | 2 +- tests/suite/loading/xml.typ | 2 +- tests/suite/loading/yaml.typ | 2 +- tests/suite/scripting/import.typ | 1 + tests/suite/visualize/image.typ | 4 +- 38 files changed, 1165 insertions(+), 572 deletions(-) create mode 100644 crates/typst-syntax/src/lines.rs diff --git a/Cargo.lock b/Cargo.lock index a9b3756a6..b699d2450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3039,6 +3039,7 @@ dependencies = [ "icu_provider_blob", "icu_segmenter", "kurbo", + "memchr", "rustybuzz", "smallvec", "ttf-parser", @@ -3112,6 +3113,7 @@ dependencies = [ "unicode-segmentation", "unscanny", "usvg", + "utf8_iter", "wasmi", "xmlwriter", ] @@ -3200,6 +3202,7 @@ dependencies = [ name = "typst-syntax" version = "0.13.1" dependencies = [ + "comemo", "ecow", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index b4890e3c1..b548245fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,7 @@ unicode-segmentation = "1" unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } usvg = { version = "0.45", default-features = false, features = ["text"] } +utf8_iter = "1.0.4" walkdir = "2" wasmi = "0.40.0" web-sys = "0.3" diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 4edb4c323..207bb7d09 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -16,7 +16,7 @@ use typst::diag::{ use typst::foundations::{Datetime, Smart}; use typst::html::HtmlDocument; use typst::layout::{Frame, Page, PageRanges, PagedDocument}; -use typst::syntax::{FileId, Source, Span}; +use typst::syntax::{FileId, Lines, Span}; use typst::WorldExt; use typst_pdf::{PdfOptions, PdfStandards, Timestamp}; @@ -696,7 +696,7 @@ fn label(world: &SystemWorld, span: Span) -> Option> { impl<'a> codespan_reporting::files::Files<'a> for SystemWorld { type FileId = FileId; type Name = String; - type Source = Source; + type Source = Lines; fn name(&'a self, id: FileId) -> CodespanResult { let vpath = id.vpath(); diff --git a/crates/typst-cli/src/timings.rs b/crates/typst-cli/src/timings.rs index 9f017dc12..3d10bbc67 100644 --- a/crates/typst-cli/src/timings.rs +++ b/crates/typst-cli/src/timings.rs @@ -85,6 +85,6 @@ fn resolve_span(world: &SystemWorld, span: Span) -> Option<(String, u32)> { let id = span.id()?; let source = world.source(id).ok()?; let range = source.range(span)?; - let line = source.byte_to_line(range.start)?; + let line = source.lines().byte_to_line(range.start)?; Some((format!("{id:?}"), line as u32 + 1)) } diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 2da03d4d5..f63d34b63 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -9,7 +9,7 @@ use ecow::{eco_format, EcoString}; use parking_lot::Mutex; use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; -use typst::syntax::{FileId, Source, VirtualPath}; +use typst::syntax::{FileId, Lines, Source, VirtualPath}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; use typst::{Library, World}; @@ -181,10 +181,20 @@ impl SystemWorld { } } - /// Lookup a source file by id. + /// Lookup line metadata for a file by id. #[track_caller] - pub fn lookup(&self, id: FileId) -> Source { - self.source(id).expect("file id does not point to any source file") + pub fn lookup(&self, id: FileId) -> Lines { + self.slot(id, |slot| { + if let Some(source) = slot.source.get() { + let source = source.as_ref().expect("file is not valid"); + source.lines() + } else if let Some(bytes) = slot.file.get() { + let bytes = bytes.as_ref().expect("file is not valid"); + Lines::try_from(bytes).expect("file is not valid utf-8") + } else { + panic!("file id does not point to any source file"); + } + }) } } @@ -339,6 +349,11 @@ impl SlotCell { self.accessed = false; } + /// Gets the contents of the cell. + fn get(&self) -> Option<&FileResult> { + self.data.as_ref() + } + /// Gets the contents of the cell or initialize them. fn get_or_init( &mut self, diff --git a/crates/typst-layout/Cargo.toml b/crates/typst-layout/Cargo.toml index 438e09e43..cc355a3db 100644 --- a/crates/typst-layout/Cargo.toml +++ b/crates/typst-layout/Cargo.toml @@ -30,6 +30,7 @@ icu_provider_adapters = { workspace = true } icu_provider_blob = { workspace = true } icu_segmenter = { workspace = true } kurbo = { workspace = true } +memchr = { workspace = true } rustybuzz = { workspace = true } smallvec = { workspace = true } ttf-parser = { workspace = true } diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 8136a25a3..a8f4a0c81 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -1,6 +1,6 @@ use std::ffi::OsStr; -use typst_library::diag::{warning, At, SourceResult, StrResult}; +use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult}; use typst_library::engine::Engine; use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; @@ -27,17 +27,17 @@ pub fn layout_image( // Take the format that was explicitly defined, or parse the extension, // or try to detect the format. - let Derived { source, derived: data } = &elem.source; + let Derived { source, derived: loaded } = &elem.source; let format = match elem.format(styles) { Smart::Custom(v) => v, - Smart::Auto => determine_format(source, data).at(span)?, + Smart::Auto => determine_format(source, &loaded.data).at(span)?, }; // Warn the user if the image contains a foreign object. Not perfect // because the svg could also be encoded, but that's an edge case. if format == ImageFormat::Vector(VectorFormat::Svg) { let has_foreign_object = - data.as_str().is_ok_and(|s| s.contains(" ImageKind::Raster( RasterImage::new( - data.clone(), + loaded.data.clone(), format, elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), ) @@ -61,11 +61,11 @@ pub fn layout_image( ), ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( SvgImage::with_fonts( - data.clone(), + loaded.data.clone(), engine.world, &families(styles).map(|f| f.as_str()).collect::>(), ) - .at(span)?, + .within(loaded)?, ), }; diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index b210637a8..f4b219882 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -66,6 +66,7 @@ unicode-normalization = { workspace = true } unicode-segmentation = { workspace = true } unscanny = { workspace = true } usvg = { workspace = true } +utf8_iter = { workspace = true } wasmi = { workspace = true } xmlwriter = { workspace = true } diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index 49cbd02c6..41b92ed65 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -1,17 +1,20 @@ //! Diagnostics. -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Display, Formatter, Write as _}; use std::io; use std::path::{Path, PathBuf}; use std::str::Utf8Error; use std::string::FromUtf8Error; +use az::SaturatingAs; use comemo::Tracked; use ecow::{eco_vec, EcoVec}; use typst_syntax::package::{PackageSpec, PackageVersion}; -use typst_syntax::{Span, Spanned, SyntaxError}; +use typst_syntax::{Lines, Span, Spanned, SyntaxError}; +use utf8_iter::ErrorReportingUtf8Chars; use crate::engine::Engine; +use crate::loading::{LoadSource, Loaded}; use crate::{World, WorldExt}; /// Early-return with a [`StrResult`] or [`SourceResult`]. @@ -148,7 +151,7 @@ pub struct Warned { pub warnings: EcoVec, } -/// An error or warning in a source file. +/// An error or warning in a source or text file. /// /// The contained spans will only be detached if any of the input source files /// were detached. @@ -568,31 +571,287 @@ impl From for EcoString { } } +/// A result type with a data-loading-related error. +pub type LoadResult = Result; + +/// A call site independent error that occurred during data loading. This avoids +/// polluting the memoization with [`Span`]s and [`FileId`]s from source files. +/// Can be turned into a [`SourceDiagnostic`] using the [`LoadedWithin::within`] +/// method available on [`LoadResult`]. +/// +/// [`FileId`]: typst_syntax::FileId +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct LoadError { + /// The position in the file at which the error occured. + pos: ReportPos, + /// Must contain a message formatted like this: `"failed to do thing (cause)"`. + message: EcoString, +} + +impl LoadError { + /// Creates a new error from a position in a file, a base message + /// (e.g. `failed to parse JSON`) and a concrete error (e.g. `invalid + /// number`) + pub fn new( + pos: impl Into, + message: impl std::fmt::Display, + error: impl std::fmt::Display, + ) -> Self { + Self { + pos: pos.into(), + message: eco_format!("{message} ({error})"), + } + } +} + +impl From for LoadError { + fn from(err: Utf8Error) -> Self { + let start = err.valid_up_to(); + let end = start + err.error_len().unwrap_or(0); + LoadError::new( + start..end, + "failed to convert to string", + "file is not valid utf-8", + ) + } +} + +/// Convert a [`LoadResult`] to a [`SourceResult`] by adding the [`Loaded`] +/// context. +pub trait LoadedWithin { + /// Report an error, possibly in an external file. + fn within(self, loaded: &Loaded) -> SourceResult; +} + +impl LoadedWithin for Result +where + E: Into, +{ + fn within(self, loaded: &Loaded) -> SourceResult { + self.map_err(|err| { + let LoadError { pos, message } = err.into(); + load_err_in_text(loaded, pos, message) + }) + } +} + +/// Report an error, possibly in an external file. This will delegate to +/// [`load_err_in_invalid_text`] if the data isn't valid utf-8. +fn load_err_in_text( + loaded: &Loaded, + pos: impl Into, + mut message: EcoString, +) -> EcoVec { + let pos = pos.into(); + // This also does utf-8 validation. Only report an error in an external + // file if it is human readable (valid utf-8), otherwise fall back to + // `load_err_in_invalid_text`. + let lines = Lines::try_from(&loaded.data); + match (loaded.source.v, lines) { + (LoadSource::Path(file_id), Ok(lines)) => { + if let Some(range) = pos.range(&lines) { + let span = Span::from_range(file_id, range); + return eco_vec![SourceDiagnostic::error(span, message)]; + } + + // Either `ReportPos::None` was provided, or resolving the range + // from the line/column failed. If present report the possibly + // wrong line/column in the error message anyway. + let span = Span::from_range(file_id, 0..loaded.data.len()); + if let Some(pair) = pos.line_col(&lines) { + message.pop(); + let (line, col) = pair.numbers(); + write!(&mut message, " at {line}:{col})").ok(); + } + eco_vec![SourceDiagnostic::error(span, message)] + } + (LoadSource::Bytes, Ok(lines)) => { + if let Some(pair) = pos.line_col(&lines) { + message.pop(); + let (line, col) = pair.numbers(); + write!(&mut message, " at {line}:{col})").ok(); + } + eco_vec![SourceDiagnostic::error(loaded.source.span, message)] + } + _ => load_err_in_invalid_text(loaded, pos, message), + } +} + +/// Report an error (possibly from an external file) that isn't valid utf-8. +fn load_err_in_invalid_text( + loaded: &Loaded, + pos: impl Into, + mut message: EcoString, +) -> EcoVec { + let line_col = pos.into().try_line_col(&loaded.data).map(|p| p.numbers()); + match (loaded.source.v, line_col) { + (LoadSource::Path(file), _) => { + message.pop(); + if let Some(package) = file.package() { + write!( + &mut message, + " in {package}{}", + file.vpath().as_rooted_path().display() + ) + .ok(); + } else { + write!(&mut message, " in {}", file.vpath().as_rootless_path().display()) + .ok(); + }; + if let Some((line, col)) = line_col { + write!(&mut message, ":{line}:{col}").ok(); + } + message.push(')'); + } + (LoadSource::Bytes, Some((line, col))) => { + message.pop(); + write!(&mut message, " at {line}:{col})").ok(); + } + (LoadSource::Bytes, None) => (), + } + eco_vec![SourceDiagnostic::error(loaded.source.span, message)] +} + +/// A position at which an error was reported. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub enum ReportPos { + /// Contains a range, and a line/column pair. + Full(std::ops::Range, LineCol), + /// Contains a range. + Range(std::ops::Range), + /// Contains a line/column pair. + LineCol(LineCol), + #[default] + None, +} + +impl From> for ReportPos { + fn from(value: std::ops::Range) -> Self { + Self::Range(value.start.saturating_as()..value.end.saturating_as()) + } +} + +impl From for ReportPos { + fn from(value: LineCol) -> Self { + Self::LineCol(value) + } +} + +impl ReportPos { + /// Creates a position from a pre-existing range and line-column pair. + pub fn full(range: std::ops::Range, pair: LineCol) -> Self { + let range = range.start.saturating_as()..range.end.saturating_as(); + Self::Full(range, pair) + } + + /// Tries to determine the byte range for this position. + fn range(&self, lines: &Lines) -> Option> { + match self { + ReportPos::Full(range, _) => Some(range.start as usize..range.end as usize), + ReportPos::Range(range) => Some(range.start as usize..range.end as usize), + &ReportPos::LineCol(pair) => { + let i = + lines.line_column_to_byte(pair.line as usize, pair.col as usize)?; + Some(i..i) + } + ReportPos::None => None, + } + } + + /// Tries to determine the line/column for this position. + fn line_col(&self, lines: &Lines) -> Option { + match self { + &ReportPos::Full(_, pair) => Some(pair), + ReportPos::Range(range) => { + let (line, col) = lines.byte_to_line_column(range.start as usize)?; + Some(LineCol::zero_based(line, col)) + } + &ReportPos::LineCol(pair) => Some(pair), + ReportPos::None => None, + } + } + + /// Either gets the line/column pair, or tries to compute it from possibly + /// invalid utf-8 data. + fn try_line_col(&self, bytes: &[u8]) -> Option { + match self { + &ReportPos::Full(_, pair) => Some(pair), + ReportPos::Range(range) => { + LineCol::try_from_byte_pos(range.start as usize, bytes) + } + &ReportPos::LineCol(pair) => Some(pair), + ReportPos::None => None, + } + } +} + +/// A line/column pair. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct LineCol { + /// The 0-based line. + line: u32, + /// The 0-based column. + col: u32, +} + +impl LineCol { + /// Constructs the line/column pair from 0-based indices. + pub fn zero_based(line: usize, col: usize) -> Self { + Self { + line: line.saturating_as(), + col: col.saturating_as(), + } + } + + /// Constructs the line/column pair from 1-based numbers. + pub fn one_based(line: usize, col: usize) -> Self { + Self::zero_based(line.saturating_sub(1), col.saturating_sub(1)) + } + + /// Try to compute a line/column pair from possibly invalid utf-8 data. + pub fn try_from_byte_pos(pos: usize, bytes: &[u8]) -> Option { + let bytes = &bytes[..pos]; + let mut line = 0; + #[allow(clippy::double_ended_iterator_last)] + let line_start = memchr::memchr_iter(b'\n', bytes) + .inspect(|_| line += 1) + .last() + .map(|i| i + 1) + .unwrap_or(bytes.len()); + + let col = ErrorReportingUtf8Chars::new(&bytes[line_start..]).count(); + Some(LineCol::zero_based(line, col)) + } + + /// Returns the 0-based line/column indices. + pub fn indices(&self) -> (usize, usize) { + (self.line as usize, self.col as usize) + } + + /// Returns the 1-based line/column numbers. + pub fn numbers(&self) -> (usize, usize) { + (self.line as usize + 1, self.col as usize + 1) + } +} + /// Format a user-facing error message for an XML-like file format. -pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> EcoString { - match error { - roxmltree::Error::UnexpectedCloseTag(expected, actual, pos) => { - eco_format!( - "failed to parse {format} (found closing tag '{actual}' \ - instead of '{expected}' in line {})", - pos.row - ) +pub fn format_xml_like_error(format: &str, error: roxmltree::Error) -> LoadError { + let pos = LineCol::one_based(error.pos().row as usize, error.pos().col as usize); + let message = match error { + roxmltree::Error::UnexpectedCloseTag(expected, actual, _) => { + eco_format!("failed to parse {format} (found closing tag '{actual}' instead of '{expected}')") } - roxmltree::Error::UnknownEntityReference(entity, pos) => { - eco_format!( - "failed to parse {format} (unknown entity '{entity}' in line {})", - pos.row - ) + roxmltree::Error::UnknownEntityReference(entity, _) => { + eco_format!("failed to parse {format} (unknown entity '{entity}')") } - roxmltree::Error::DuplicatedAttribute(attr, pos) => { - eco_format!( - "failed to parse {format} (duplicate attribute '{attr}' in line {})", - pos.row - ) + roxmltree::Error::DuplicatedAttribute(attr, _) => { + eco_format!("failed to parse {format} (duplicate attribute '{attr}')") } roxmltree::Error::NoRootNode => { eco_format!("failed to parse {format} (missing root node)") } err => eco_format!("failed to parse {format} ({err})"), - } + }; + + LoadError { pos: pos.into(), message } } diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index d633c99ad..180dcdad5 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; +use typst_syntax::Lines; use typst_utils::LazyHash; use crate::diag::{bail, StrResult}; @@ -286,6 +287,16 @@ impl Serialize for Bytes { } } +impl TryFrom<&Bytes> for Lines { + type Error = Utf8Error; + + #[comemo::memoize] + fn try_from(value: &Bytes) -> Result, Utf8Error> { + let text = value.as_str()?; + Ok(Lines::new(text.to_string())) + } +} + /// Any type that can back a byte buffer. trait Bytelike: Send + Sync { fn as_bytes(&self) -> &[u8]; diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index 31f8cd732..a04443bf4 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -151,8 +151,8 @@ pub fn plugin( /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - Plugin::module(data).at(source.span) + let loaded = source.load(engine.world)?; + Plugin::module(loaded.data).at(source.span) } #[scope] diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index aa14c5c77..d95f73844 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -23,8 +23,8 @@ pub fn cbor( /// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - ciborium::from_reader(data.as_slice()) + let loaded = source.load(engine.world)?; + ciborium::from_reader(loaded.data.as_slice()) .map_err(|err| eco_format!("failed to parse CBOR ({err})")) .at(source.span) } diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6afb5baeb..d5b54a06c 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -1,7 +1,7 @@ -use ecow::{eco_format, EcoString}; +use az::SaturatingAs; use typst_syntax::Spanned; -use crate::diag::{bail, At, SourceResult}; +use crate::diag::{bail, LineCol, LoadError, LoadedWithin, ReportPos, SourceResult}; use crate::engine::Engine; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -44,7 +44,7 @@ pub fn csv( #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let data = source.load(engine.world)?; + let loaded = source.load(engine.world)?; let mut builder = ::csv::ReaderBuilder::new(); let has_headers = row_type == RowType::Dict; @@ -53,7 +53,7 @@ pub fn csv( // Counting lines from 1 by default. let mut line_offset: usize = 1; - let mut reader = builder.from_reader(data.as_slice()); + let mut reader = builder.from_reader(loaded.data.as_slice()); let mut headers: Option<::csv::StringRecord> = None; if has_headers { @@ -62,9 +62,9 @@ pub fn csv( headers = Some( reader .headers() + .cloned() .map_err(|err| format_csv_error(err, 1)) - .at(source.span)? - .clone(), + .within(&loaded)?, ); } @@ -74,7 +74,7 @@ pub fn csv( // incorrect with `has_headers` set to `false`. See issue: // https://github.com/BurntSushi/rust-csv/issues/184 let line = line + line_offset; - let row = result.map_err(|err| format_csv_error(err, line)).at(source.span)?; + let row = result.map_err(|err| format_csv_error(err, line)).within(&loaded)?; let item = if let Some(headers) = &headers { let mut dict = Dict::new(); for (field, value) in headers.iter().zip(&row) { @@ -164,15 +164,23 @@ cast! { } /// Format the user-facing CSV error message. -fn format_csv_error(err: ::csv::Error, line: usize) -> EcoString { +fn format_csv_error(err: ::csv::Error, line: usize) -> LoadError { + let msg = "failed to parse CSV"; + let pos = (err.kind().position()) + .map(|pos| { + let start = pos.byte().saturating_as(); + ReportPos::from(start..start) + }) + .unwrap_or(LineCol::one_based(line, 1).into()); match err.kind() { - ::csv::ErrorKind::Utf8 { .. } => "file is not valid utf-8".into(), - ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { - eco_format!( - "failed to parse CSV (found {len} instead of \ - {expected_len} fields in line {line})" - ) + ::csv::ErrorKind::Utf8 { .. } => { + LoadError::new(pos, msg, "file is not valid utf-8") } - _ => eco_format!("failed to parse CSV ({err})"), + ::csv::ErrorKind::UnequalLengths { expected_len, len, .. } => { + let err = + format!("found {len} instead of {expected_len} fields in line {line}"); + LoadError::new(pos, msg, err) + } + _ => LoadError::new(pos, "failed to parse CSV", err), } } diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index aa908cca4..7d0732ba0 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -1,7 +1,7 @@ use ecow::eco_format; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, LineCol, LoadError, LoadedWithin, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -54,10 +54,13 @@ pub fn json( /// A [path]($syntax/#paths) to a JSON file or raw JSON bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(source.span) + let loaded = source.load(engine.world)?; + serde_json::from_slice(loaded.data.as_slice()) + .map_err(|err| { + let pos = LineCol::one_based(err.line(), err.column()); + LoadError::new(pos, "failed to parse JSON", err) + }) + .within(&loaded) } #[scope] diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index c57e02888..67f4be834 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -17,7 +17,7 @@ mod yaml_; use comemo::Tracked; use ecow::EcoString; -use typst_syntax::Spanned; +use typst_syntax::{FileId, Spanned}; pub use self::cbor_::*; pub use self::csv_::*; @@ -74,39 +74,44 @@ pub trait Load { } impl Load for Spanned { - type Output = Bytes; + type Output = Loaded; - fn load(&self, world: Tracked) -> SourceResult { + fn load(&self, world: Tracked) -> SourceResult { self.as_ref().load(world) } } impl Load for Spanned<&DataSource> { - type Output = Bytes; + type Output = Loaded; - fn load(&self, world: Tracked) -> SourceResult { + fn load(&self, world: Tracked) -> SourceResult { match &self.v { DataSource::Path(path) => { let file_id = self.span.resolve_path(path).at(self.span)?; - world.file(file_id).at(self.span) + let data = world.file(file_id).at(self.span)?; + let source = Spanned::new(LoadSource::Path(file_id), self.span); + Ok(Loaded::new(source, data)) + } + DataSource::Bytes(data) => { + let source = Spanned::new(LoadSource::Bytes, self.span); + Ok(Loaded::new(source, data.clone())) } - DataSource::Bytes(bytes) => Ok(bytes.clone()), } } } impl Load for Spanned> { - type Output = Vec; + type Output = Vec; - fn load(&self, world: Tracked) -> SourceResult> { + fn load(&self, world: Tracked) -> SourceResult { self.as_ref().load(world) } } impl Load for Spanned<&OneOrMultiple> { - type Output = Vec; + type Output = Vec; - fn load(&self, world: Tracked) -> SourceResult> { + fn load(&self, world: Tracked) -> SourceResult { self.v .0 .iter() @@ -115,6 +120,28 @@ impl Load for Spanned<&OneOrMultiple> { } } +/// Data loaded from a [`DataSource`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Loaded { + /// Details about where `data` was loaded from. + pub source: Spanned, + /// The loaded data. + pub data: Bytes, +} + +impl Loaded { + pub fn new(source: Spanned, bytes: Bytes) -> Self { + Self { source, data: bytes } + } +} + +/// A loaded [`DataSource`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum LoadSource { + Path(FileId), + Bytes, +} + /// A value that can be read from a file. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Readable { diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index 32dadc799..91e6e4366 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -1,11 +1,10 @@ use ecow::EcoString; use typst_syntax::Spanned; -use crate::diag::{At, FileError, SourceResult}; +use crate::diag::{LoadedWithin, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, Cast}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads plain text or data from a file. /// @@ -36,14 +35,10 @@ pub fn read( #[default(Some(Encoding::Utf8))] encoding: Option, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; + let loaded = path.map(DataSource::Path).load(engine.world)?; Ok(match encoding { - None => Readable::Bytes(data), - Some(Encoding::Utf8) => { - Readable::Str(data.to_str().map_err(FileError::from).at(span)?) - } + None => Readable::Bytes(loaded.data), + Some(Encoding::Utf8) => Readable::Str(loaded.data.to_str().within(&loaded)?), }) } diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index f04b2e746..a4252feca 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -1,7 +1,7 @@ -use ecow::{eco_format, EcoString}; -use typst_syntax::{is_newline, Spanned}; +use ecow::eco_format; +use typst_syntax::Spanned; -use crate::diag::{At, FileError, SourceResult}; +use crate::diag::{At, LoadError, LoadedWithin, ReportPos, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -32,11 +32,9 @@ pub fn toml( /// A [path]($syntax/#paths) to a TOML file or raw TOML bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - let raw = data.as_str().map_err(FileError::from).at(source.span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(source.span) + let loaded = source.load(engine.world)?; + let raw = loaded.data.as_str().within(&loaded)?; + ::toml::from_str(raw).map_err(format_toml_error).within(&loaded) } #[scope] @@ -71,15 +69,7 @@ impl toml { } /// Format the user-facing TOML error message. -fn format_toml_error(error: ::toml::de::Error, raw: &str) -> EcoString { - if let Some(head) = error.span().and_then(|range| raw.get(..range.start)) { - let line = head.lines().count(); - let column = 1 + head.chars().rev().take_while(|&c| !is_newline(c)).count(); - eco_format!( - "failed to parse TOML ({} at line {line} column {column})", - error.message(), - ) - } else { - eco_format!("failed to parse TOML ({})", error.message()) - } +fn format_toml_error(error: ::toml::de::Error) -> LoadError { + let pos = error.span().map(ReportPos::from).unwrap_or_default(); + LoadError::new(pos, "failed to parse TOML", error.message()) } diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index e76c4e9cf..0023c5df5 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -1,8 +1,7 @@ -use ecow::EcoString; use roxmltree::ParsingOptions; use typst_syntax::Spanned; -use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; +use crate::diag::{format_xml_like_error, LoadError, LoadedWithin, SourceResult}; use crate::engine::Engine; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -61,14 +60,14 @@ pub fn xml( /// A [path]($syntax/#paths) to an XML file or raw XML bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - let text = data.as_str().map_err(FileError::from).at(source.span)?; + let loaded = source.load(engine.world)?; + let text = loaded.data.as_str().within(&loaded)?; let document = roxmltree::Document::parse_with_options( text, ParsingOptions { allow_dtd: true, ..Default::default() }, ) .map_err(format_xml_error) - .at(source.span)?; + .within(&loaded)?; Ok(convert_xml(document.root())) } @@ -111,6 +110,6 @@ fn convert_xml(node: roxmltree::Node) -> Value { } /// Format the user-facing XML error message. -fn format_xml_error(error: roxmltree::Error) -> EcoString { +fn format_xml_error(error: roxmltree::Error) -> LoadError { format_xml_like_error("XML", error) } diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 3f48113e8..0edf1f901 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -1,7 +1,7 @@ use ecow::eco_format; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, LineCol, LoadError, LoadedWithin, ReportPos, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; use crate::loading::{DataSource, Load, Readable}; @@ -44,10 +44,10 @@ pub fn yaml( /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes. source: Spanned, ) -> SourceResult { - let data = source.load(engine.world)?; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(source.span) + let loaded = source.load(engine.world)?; + serde_yaml::from_slice(loaded.data.as_slice()) + .map_err(format_yaml_error) + .within(&loaded) } #[scope] @@ -76,3 +76,16 @@ impl yaml { .at(span) } } + +/// Format the user-facing YAML error message. +pub fn format_yaml_error(error: serde_yaml::Error) -> LoadError { + let pos = error + .location() + .map(|loc| { + let line_col = LineCol::one_based(loc.line(), loc.column()); + let range = loc.index()..loc.index(); + ReportPos::full(range, line_col) + }) + .unwrap_or_default(); + LoadError::new(pos, "failed to parse YAML", error) +} diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 51e3b03b0..114356575 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -19,7 +19,10 @@ use smallvec::{smallvec, SmallVec}; use typst_syntax::{Span, Spanned}; use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; -use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::diag::{ + bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, + SourceResult, StrResult, +}; use crate::engine::{Engine, Sink}; use crate::foundations::{ elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, @@ -31,7 +34,7 @@ use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sides, Sizing, TrackSizings, }; -use crate::loading::{DataSource, Load}; +use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded}; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, Url, @@ -294,24 +297,21 @@ impl Bibliography { world: Tracked, sources: Spanned>, ) -> SourceResult, Self>> { - let data = sources.load(world)?; - let bibliography = Self::decode(&sources.v, &data).at(sources.span)?; + let loaded = sources.load(world)?; + let bibliography = Self::decode(&loaded)?; Ok(Derived::new(sources.v, bibliography)) } /// Decode a bibliography from loaded data sources. #[comemo::memoize] #[typst_macros::time(name = "load bibliography")] - fn decode( - sources: &OneOrMultiple, - data: &[Bytes], - ) -> StrResult { + fn decode(data: &[Loaded]) -> SourceResult { let mut map = IndexMap::new(); let mut duplicates = Vec::::new(); // We might have multiple bib/yaml files - for (source, data) in sources.0.iter().zip(data) { - let library = decode_library(source, data)?; + for d in data.iter() { + let library = decode_library(d)?; for entry in library { match map.entry(Label::new(PicoStr::intern(entry.key()))) { indexmap::map::Entry::Vacant(vacant) => { @@ -325,7 +325,11 @@ impl Bibliography { } if !duplicates.is_empty() { - bail!("duplicate bibliography keys: {}", duplicates.join(", ")); + // TODO: Store spans of entries for duplicate key error messages. + // Requires hayagriva entries to store their location, which should + // be fine, since they are 1kb anyway. + let span = data.first().unwrap().source.span; + bail!(span, "duplicate bibliography keys: {}", duplicates.join(", ")); } Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data))))) @@ -351,36 +355,47 @@ impl Debug for Bibliography { } /// Decode on library from one data source. -fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { - let src = data.as_str().map_err(FileError::from)?; +fn decode_library(loaded: &Loaded) -> SourceResult { + let data = loaded.data.as_str().within(loaded)?; - if let DataSource::Path(path) = source { + if let LoadSource::Path(file_id) = loaded.source.v { // If we got a path, use the extension to determine whether it is // YAML or BibLaTeX. - let ext = Path::new(path.as_str()) + let ext = file_id + .vpath() + .as_rooted_path() .extension() .and_then(OsStr::to_str) .unwrap_or_default(); match ext.to_lowercase().as_str() { - "yml" | "yaml" => hayagriva::io::from_yaml_str(src) - .map_err(|err| eco_format!("failed to parse YAML ({err})")), - "bib" => hayagriva::io::from_biblatex_str(src) - .map_err(|errors| format_biblatex_error(src, Some(path), errors)), - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + "yml" | "yaml" => hayagriva::io::from_yaml_str(data) + .map_err(format_yaml_error) + .within(loaded), + "bib" => hayagriva::io::from_biblatex_str(data) + .map_err(format_biblatex_error) + .within(loaded), + _ => bail!( + loaded.source.span, + "unknown bibliography format (must be .yml/.yaml or .bib)" + ), } } else { // If we just got bytes, we need to guess. If it can be decoded as // hayagriva YAML, we'll use that. - let haya_err = match hayagriva::io::from_yaml_str(src) { + let haya_err = match hayagriva::io::from_yaml_str(data) { Ok(library) => return Ok(library), Err(err) => err, }; // If it can be decoded as BibLaTeX, we use that isntead. - let bib_errs = match hayagriva::io::from_biblatex_str(src) { - Ok(library) => return Ok(library), - Err(err) => err, + let bib_errs = match hayagriva::io::from_biblatex_str(data) { + // If the file is almost valid yaml, but contains no `@` character + // it will be successfully parsed as an empty BibLaTeX library, + // since BibLaTeX does support arbitrary text outside of entries. + Ok(library) if !library.is_empty() => return Ok(library), + Ok(_) => None, + Err(err) => Some(err), }; // If neither decoded correctly, check whether `:` or `{` appears @@ -388,7 +403,7 @@ fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { // and emit the more appropriate error. let mut yaml = 0; let mut biblatex = 0; - for c in src.chars() { + for c in data.chars() { match c { ':' => yaml += 1, '{' => biblatex += 1, @@ -396,37 +411,33 @@ fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { } } - if yaml > biblatex { - bail!("failed to parse YAML ({haya_err})") - } else { - Err(format_biblatex_error(src, None, bib_errs)) + match bib_errs { + Some(bib_errs) if biblatex >= yaml => { + Err(format_biblatex_error(bib_errs)).within(loaded) + } + _ => Err(format_yaml_error(haya_err)).within(loaded), } } } /// Format a BibLaTeX loading error. -fn format_biblatex_error( - src: &str, - path: Option<&str>, - errors: Vec, -) -> EcoString { - let Some(error) = errors.first() else { - return match path { - Some(path) => eco_format!("failed to parse BibLaTeX file ({path})"), - None => eco_format!("failed to parse BibLaTeX"), - }; +fn format_biblatex_error(errors: Vec) -> LoadError { + // TODO: return multiple errors? + let Some(error) = errors.into_iter().next() else { + // TODO: can this even happen, should we just unwrap? + return LoadError::new( + ReportPos::None, + "failed to parse BibLaTeX", + "something went wrong", + ); }; - let (span, msg) = match error { - BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), - BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), + let (range, msg) = match error { + BibLaTeXError::Parse(error) => (error.span, error.kind.to_string()), + BibLaTeXError::Type(error) => (error.span, error.kind.to_string()), }; - let line = src.get(..span.start).unwrap_or_default().lines().count(); - match path { - Some(path) => eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})"), - None => eco_format!("failed to parse BibLaTeX ({line}: {msg})"), - } + LoadError::new(range, "failed to parse BibLaTeX", msg) } /// A loaded CSL style. @@ -442,8 +453,8 @@ impl CslStyle { let style = match &source { CslSource::Named(style) => Self::from_archived(*style), CslSource::Normal(source) => { - let data = Spanned::new(source, span).load(world)?; - Self::from_data(data).at(span)? + let loaded = Spanned::new(source, span).load(world)?; + Self::from_data(&loaded.data).within(&loaded)? } }; Ok(Derived::new(source, style)) @@ -464,16 +475,18 @@ impl CslStyle { /// Load a CSL style from file contents. #[comemo::memoize] - pub fn from_data(data: Bytes) -> StrResult { - let text = data.as_str().map_err(FileError::from)?; + pub fn from_data(bytes: &Bytes) -> LoadResult { + let text = bytes.as_str()?; citationberg::IndependentStyle::from_xml(text) .map(|style| { Self(Arc::new(ManuallyHash::new( style, - typst_utils::hash128(&(TypeId::of::(), data)), + typst_utils::hash128(&(TypeId::of::(), bytes)), ))) }) - .map_err(|err| eco_format!("failed to load CSL style ({err})")) + .map_err(|err| { + LoadError::new(ReportPos::None, "failed to load CSL style", err) + }) } /// Get the underlying independent style. diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index d5c07424d..f2485e16b 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -3,15 +3,17 @@ use std::ops::Range; use std::sync::{Arc, LazyLock}; use comemo::Tracked; -use ecow::{eco_format, EcoString, EcoVec}; -use syntect::highlighting as synt; -use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; +use ecow::{EcoString, EcoVec}; +use syntect::highlighting::{self as synt}; +use syntect::parsing::{ParseSyntaxError, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; use typst_syntax::{split_newlines, LinkedNode, Span, Spanned}; use typst_utils::ManuallyHash; use unicode_segmentation::UnicodeSegmentation; use super::Lang; -use crate::diag::{At, FileError, SourceResult, StrResult}; +use crate::diag::{ + LineCol, LoadError, LoadResult, LoadedWithin, ReportPos, SourceResult, +}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, @@ -539,40 +541,29 @@ impl RawSyntax { world: Tracked, sources: Spanned>, ) -> SourceResult, Vec>> { - let data = sources.load(world)?; - let list = sources - .v - .0 + let loaded = sources.load(world)?; + let list = loaded .iter() - .zip(&data) - .map(|(source, data)| Self::decode(source, data)) - .collect::>() - .at(sources.span)?; + .map(|data| Self::decode(&data.data).within(data)) + .collect::>()?; Ok(Derived::new(sources.v, list)) } /// Decode a syntax from a loaded source. #[comemo::memoize] #[typst_macros::time(name = "load syntaxes")] - fn decode(source: &DataSource, data: &Bytes) -> StrResult { - let src = data.as_str().map_err(FileError::from)?; - let syntax = SyntaxDefinition::load_from_str(src, false, None).map_err( - |err| match source { - DataSource::Path(path) => { - eco_format!("failed to parse syntax file `{path}` ({err})") - } - DataSource::Bytes(_) => { - eco_format!("failed to parse syntax ({err})") - } - }, - )?; + fn decode(bytes: &Bytes) -> LoadResult { + let str = bytes.as_str()?; + + let syntax = SyntaxDefinition::load_from_str(str, false, None) + .map_err(format_syntax_error)?; let mut builder = SyntaxSetBuilder::new(); builder.add(syntax); Ok(RawSyntax(Arc::new(ManuallyHash::new( builder.build(), - typst_utils::hash128(data), + typst_utils::hash128(bytes), )))) } @@ -582,6 +573,24 @@ impl RawSyntax { } } +fn format_syntax_error(error: ParseSyntaxError) -> LoadError { + let pos = syntax_error_pos(&error); + LoadError::new(pos, "failed to parse syntax", error) +} + +fn syntax_error_pos(error: &ParseSyntaxError) -> ReportPos { + match error { + ParseSyntaxError::InvalidYaml(scan_error) => { + let m = scan_error.marker(); + ReportPos::full( + m.index()..m.index(), + LineCol::one_based(m.line(), m.col() + 1), + ) + } + _ => ReportPos::None, + } +} + /// A loaded syntect theme. #[derive(Debug, Clone, PartialEq, Hash)] pub struct RawTheme(Arc>); @@ -592,18 +601,18 @@ impl RawTheme { world: Tracked, source: Spanned, ) -> SourceResult> { - let data = source.load(world)?; - let theme = Self::decode(&data).at(source.span)?; + let loaded = source.load(world)?; + let theme = Self::decode(&loaded.data).within(&loaded)?; Ok(Derived::new(source.v, theme)) } /// Decode a theme from bytes. #[comemo::memoize] - fn decode(data: &Bytes) -> StrResult { - let mut cursor = std::io::Cursor::new(data.as_slice()); - let theme = synt::ThemeSet::load_from_reader(&mut cursor) - .map_err(|err| eco_format!("failed to parse theme ({err})"))?; - Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(data))))) + fn decode(bytes: &Bytes) -> LoadResult { + let mut cursor = std::io::Cursor::new(bytes.as_slice()); + let theme = + synt::ThemeSet::load_from_reader(&mut cursor).map_err(format_theme_error)?; + Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(bytes))))) } /// Get the underlying syntect theme. @@ -612,6 +621,14 @@ impl RawTheme { } } +fn format_theme_error(error: syntect::LoadingError) -> LoadError { + let pos = match &error { + syntect::LoadingError::ParseSyntax(err, _) => syntax_error_pos(err), + _ => ReportPos::None, + }; + LoadError::new(pos, "failed to parse theme", error) +} + /// A highlighted line of raw text. /// /// This is a helper element that is synthesized by [`raw`] elements. diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index f9e345e70..f5109798b 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -22,7 +22,7 @@ use crate::foundations::{ Smart, StyleChain, }; use crate::layout::{BlockElem, Length, Rel, Sizing}; -use crate::loading::{DataSource, Load, Readable}; +use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::model::Figurable; use crate::text::LocalName; @@ -65,10 +65,10 @@ pub struct ImageElem { #[required] #[parse( let source = args.expect::>("source")?; - let data = source.load(engine.world)?; - Derived::new(source.v, data) + let loaded = source.load(engine.world)?; + Derived::new(source.v, loaded) )] - pub source: Derived, + pub source: Derived, /// The image's format. /// @@ -154,8 +154,8 @@ pub struct ImageElem { /// to `{auto}`, Typst will try to extract an ICC profile from the image. #[parse(match args.named::>>("icc")? { Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({ - let data = Spanned::new(&source, span).load(engine.world)?; - Derived::new(source, data) + let loaded = Spanned::new(&source, span).load(engine.world)?; + Derived::new(source, loaded.data) })), Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), None => None, @@ -173,7 +173,7 @@ impl ImageElem { pub fn decode( span: Span, /// The data to decode as an image. Can be a string for SVGs. - data: Readable, + data: Spanned, /// The image's format. Detected automatically by default. #[named] format: Option>, @@ -193,8 +193,10 @@ impl ImageElem { #[named] scaling: Option>, ) -> StrResult { - let bytes = data.into_bytes(); - let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); + let bytes = data.v.into_bytes(); + let loaded = + Loaded::new(Spanned::new(LoadSource::Bytes, data.span), bytes.clone()); + let source = Derived::new(DataSource::Bytes(bytes), loaded); let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index 9bf1ead0d..1a3f6d474 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -3,10 +3,9 @@ use std::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; use comemo::Tracked; -use ecow::EcoString; use siphasher::sip128::{Hasher128, SipHasher13}; -use crate::diag::{format_xml_like_error, StrResult}; +use crate::diag::{format_xml_like_error, LoadError, LoadResult, ReportPos}; use crate::foundations::Bytes; use crate::layout::Axes; use crate::text::{ @@ -30,7 +29,7 @@ impl SvgImage { /// Decode an SVG image without fonts. #[comemo::memoize] #[typst_macros::time(name = "load svg")] - pub fn new(data: Bytes) -> StrResult { + pub fn new(data: Bytes) -> LoadResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) @@ -43,7 +42,7 @@ impl SvgImage { data: Bytes, world: Tracked, families: &[&str], - ) -> StrResult { + ) -> LoadResult { let book = world.book(); let resolver = Mutex::new(FontResolver::new(world, book, families)); let tree = usvg::Tree::from_data( @@ -125,16 +124,15 @@ fn tree_size(tree: &usvg::Tree) -> Axes { } /// Format the user-facing SVG decoding error message. -fn format_usvg_error(error: usvg::Error) -> EcoString { - match error { - usvg::Error::NotAnUtf8Str => "file is not valid utf-8".into(), - usvg::Error::MalformedGZip => "file is not compressed correctly".into(), - usvg::Error::ElementsLimitReached => "file is too large".into(), - usvg::Error::InvalidSize => { - "failed to parse SVG (width, height, or viewbox is invalid)".into() - } - usvg::Error::ParsingFailed(error) => format_xml_like_error("SVG", error), - } +fn format_usvg_error(error: usvg::Error) -> LoadError { + let error = match error { + usvg::Error::NotAnUtf8Str => "file is not valid utf-8", + usvg::Error::MalformedGZip => "file is not compressed correctly", + usvg::Error::ElementsLimitReached => "file is too large", + usvg::Error::InvalidSize => "width, height, or viewbox is invalid", + usvg::Error::ParsingFailed(error) => return format_xml_like_error("SVG", error), + }; + LoadError::new(ReportPos::None, "failed to parse SVG", error) } /// Provides Typst's fonts to usvg. diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml index 263595bd4..c20f6a087 100644 --- a/crates/typst-syntax/Cargo.toml +++ b/crates/typst-syntax/Cargo.toml @@ -15,6 +15,7 @@ readme = { workspace = true } [dependencies] typst-timing = { workspace = true } typst-utils = { workspace = true } +comemo = { workspace = true } ecow = { workspace = true } serde = { workspace = true } toml = { workspace = true } diff --git a/crates/typst-syntax/src/lib.rs b/crates/typst-syntax/src/lib.rs index 5e7b710fc..1249f88e9 100644 --- a/crates/typst-syntax/src/lib.rs +++ b/crates/typst-syntax/src/lib.rs @@ -7,6 +7,7 @@ mod file; mod highlight; mod kind; mod lexer; +mod lines; mod node; mod parser; mod path; @@ -22,6 +23,7 @@ pub use self::lexer::{ is_id_continue, is_id_start, is_ident, is_newline, is_valid_label_literal_id, link_prefix, split_newlines, }; +pub use self::lines::Lines; pub use self::node::{LinkedChildren, LinkedNode, Side, SyntaxError, SyntaxNode}; pub use self::parser::{parse, parse_code, parse_math}; pub use self::path::VirtualPath; diff --git a/crates/typst-syntax/src/lines.rs b/crates/typst-syntax/src/lines.rs new file mode 100644 index 000000000..fa1e77563 --- /dev/null +++ b/crates/typst-syntax/src/lines.rs @@ -0,0 +1,402 @@ +use std::hash::{Hash, Hasher}; +use std::iter::zip; +use std::ops::Range; +use std::sync::Arc; + +use crate::is_newline; + +/// A text buffer and metadata about lines. +#[derive(Clone)] +pub struct Lines(Arc>); + +#[derive(Clone)] +struct Repr { + lines: Vec, + text: T, +} + +/// Metadata about a line. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct Line { + /// The UTF-8 byte offset where the line starts. + byte_idx: usize, + /// The UTF-16 codepoint offset where the line starts. + utf16_idx: usize, +} + +impl> Lines { + /// Create from the text buffer and compute the line metadata. + pub fn new(text: T) -> Self { + let lines = lines(text.as_ref()); + Lines(Arc::new(Repr { lines, text })) + } + + /// The text as a string slice. + pub fn text(&self) -> &str { + self.0.text.as_ref() + } + + /// Get the length of the file in UTF-8 encoded bytes. + pub fn len_bytes(&self) -> usize { + self.0.text.as_ref().len() + } + + /// Get the length of the file in UTF-16 code units. + pub fn len_utf16(&self) -> usize { + let last = self.0.lines.last().unwrap(); + last.utf16_idx + len_utf16(&self.text()[last.byte_idx..]) + } + + /// Get the length of the file in lines. + pub fn len_lines(&self) -> usize { + self.0.lines.len() + } + + /// Return the index of the UTF-16 code unit at the byte index. + pub fn byte_to_utf16(&self, byte_idx: usize) -> Option { + let line_idx = self.byte_to_line(byte_idx)?; + let line = self.0.lines.get(line_idx)?; + let head = self.text().get(line.byte_idx..byte_idx)?; + Some(line.utf16_idx + len_utf16(head)) + } + + /// Return the index of the line that contains the given byte index. + pub fn byte_to_line(&self, byte_idx: usize) -> Option { + (byte_idx <= self.text().len()).then(|| { + match self.0.lines.binary_search_by_key(&byte_idx, |line| line.byte_idx) { + Ok(i) => i, + Err(i) => i - 1, + } + }) + } + + /// Return the index of the column at the byte index. + /// + /// The column is defined as the number of characters in the line before the + /// byte index. + pub fn byte_to_column(&self, byte_idx: usize) -> Option { + let line = self.byte_to_line(byte_idx)?; + let start = self.line_to_byte(line)?; + let head = self.text().get(start..byte_idx)?; + Some(head.chars().count()) + } + + /// Return the index of the line and column at the byte index. + pub fn byte_to_line_column(&self, byte_idx: usize) -> Option<(usize, usize)> { + let line = self.byte_to_line(byte_idx)?; + let start = self.line_to_byte(line)?; + let head = self.text().get(start..byte_idx)?; + let col = head.chars().count(); + Some((line, col)) + } + + /// Return the byte index at the UTF-16 code unit. + pub fn utf16_to_byte(&self, utf16_idx: usize) -> Option { + let line = self.0.lines.get( + match self.0.lines.binary_search_by_key(&utf16_idx, |line| line.utf16_idx) { + Ok(i) => i, + Err(i) => i - 1, + }, + )?; + + let text = self.text(); + let mut k = line.utf16_idx; + for (i, c) in text[line.byte_idx..].char_indices() { + if k >= utf16_idx { + return Some(line.byte_idx + i); + } + k += c.len_utf16(); + } + + (k == utf16_idx).then_some(text.len()) + } + + /// Return the byte position at which the given line starts. + pub fn line_to_byte(&self, line_idx: usize) -> Option { + self.0.lines.get(line_idx).map(|line| line.byte_idx) + } + + /// Return the range which encloses the given line. + pub fn line_to_range(&self, line_idx: usize) -> Option> { + let start = self.line_to_byte(line_idx)?; + let end = self.line_to_byte(line_idx + 1).unwrap_or(self.text().len()); + Some(start..end) + } + + /// Return the byte index of the given (line, column) pair. + /// + /// The column defines the number of characters to go beyond the start of + /// the line. + pub fn line_column_to_byte( + &self, + line_idx: usize, + column_idx: usize, + ) -> Option { + let range = self.line_to_range(line_idx)?; + let line = self.text().get(range.clone())?; + let mut chars = line.chars(); + for _ in 0..column_idx { + chars.next(); + } + Some(range.start + (line.len() - chars.as_str().len())) + } +} + +impl Lines { + /// Fully replace the source text. + /// + /// This performs a naive (suffix/prefix-based) diff of the old and new text + /// to produce the smallest single edit that transforms old into new and + /// then calls [`edit`](Self::edit) with it. + /// + /// Returns whether any changes were made. + pub fn replace(&mut self, new: &str) -> bool { + let Some((prefix, suffix)) = self.replacement_range(new) else { + return false; + }; + + let old = self.text(); + let replace = prefix..old.len() - suffix; + let with = &new[prefix..new.len() - suffix]; + self.edit(replace, with); + + true + } + + /// Returns the common prefix and suffix lengths. + /// Returns [`None`] if the old and new strings are equal. + pub fn replacement_range(&self, new: &str) -> Option<(usize, usize)> { + let old = self.text(); + + let mut prefix = + zip(old.bytes(), new.bytes()).take_while(|(x, y)| x == y).count(); + + if prefix == old.len() && prefix == new.len() { + return None; + } + + while !old.is_char_boundary(prefix) || !new.is_char_boundary(prefix) { + prefix -= 1; + } + + let mut suffix = zip(old[prefix..].bytes().rev(), new[prefix..].bytes().rev()) + .take_while(|(x, y)| x == y) + .count(); + + while !old.is_char_boundary(old.len() - suffix) + || !new.is_char_boundary(new.len() - suffix) + { + suffix += 1; + } + + Some((prefix, suffix)) + } + + /// Edit the source file by replacing the given range. + /// + /// Returns the range in the new source that was ultimately reparsed. + /// + /// The method panics if the `replace` range is out of bounds. + #[track_caller] + pub fn edit(&mut self, replace: Range, with: &str) { + let start_byte = replace.start; + let start_utf16 = self.byte_to_utf16(start_byte).unwrap(); + let line = self.byte_to_line(start_byte).unwrap(); + + let inner = Arc::make_mut(&mut self.0); + + // Update the text itself. + inner.text.replace_range(replace.clone(), with); + + // Remove invalidated line starts. + inner.lines.truncate(line + 1); + + // Handle adjoining of \r and \n. + if inner.text[..start_byte].ends_with('\r') && with.starts_with('\n') { + inner.lines.pop(); + } + + // Recalculate the line starts after the edit. + inner.lines.extend(lines_from( + start_byte, + start_utf16, + &inner.text[start_byte..], + )); + } +} + +impl Hash for Lines { + fn hash(&self, state: &mut H) { + self.0.text.hash(state); + } +} + +impl> AsRef for Lines { + fn as_ref(&self) -> &str { + self.0.text.as_ref() + } +} + +/// Create a line vector. +fn lines(text: &str) -> Vec { + std::iter::once(Line { byte_idx: 0, utf16_idx: 0 }) + .chain(lines_from(0, 0, text)) + .collect() +} + +/// Compute a line iterator from an offset. +fn lines_from( + byte_offset: usize, + utf16_offset: usize, + text: &str, +) -> impl Iterator + '_ { + let mut s = unscanny::Scanner::new(text); + let mut utf16_idx = utf16_offset; + + std::iter::from_fn(move || { + s.eat_until(|c: char| { + utf16_idx += c.len_utf16(); + is_newline(c) + }); + + if s.done() { + return None; + } + + if s.eat() == Some('\r') && s.eat_if('\n') { + utf16_idx += 1; + } + + Some(Line { byte_idx: byte_offset + s.cursor(), utf16_idx }) + }) +} + +/// The number of code units this string would use if it was encoded in +/// UTF16. This runs in linear time. +fn len_utf16(string: &str) -> usize { + string.chars().map(char::len_utf16).sum() +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST: &str = "ä\tcde\nf💛g\r\nhi\rjkl"; + + #[test] + fn test_source_file_new() { + let lines = Lines::new(TEST); + assert_eq!( + lines.0.lines, + [ + Line { byte_idx: 0, utf16_idx: 0 }, + Line { byte_idx: 7, utf16_idx: 6 }, + Line { byte_idx: 15, utf16_idx: 12 }, + Line { byte_idx: 18, utf16_idx: 15 }, + ] + ); + } + + #[test] + fn test_source_file_pos_to_line() { + let lines = Lines::new(TEST); + assert_eq!(lines.byte_to_line(0), Some(0)); + assert_eq!(lines.byte_to_line(2), Some(0)); + assert_eq!(lines.byte_to_line(6), Some(0)); + assert_eq!(lines.byte_to_line(7), Some(1)); + assert_eq!(lines.byte_to_line(8), Some(1)); + assert_eq!(lines.byte_to_line(12), Some(1)); + assert_eq!(lines.byte_to_line(21), Some(3)); + assert_eq!(lines.byte_to_line(22), None); + } + + #[test] + fn test_source_file_pos_to_column() { + let lines = Lines::new(TEST); + assert_eq!(lines.byte_to_column(0), Some(0)); + assert_eq!(lines.byte_to_column(2), Some(1)); + assert_eq!(lines.byte_to_column(6), Some(5)); + assert_eq!(lines.byte_to_column(7), Some(0)); + assert_eq!(lines.byte_to_column(8), Some(1)); + assert_eq!(lines.byte_to_column(12), Some(2)); + } + + #[test] + fn test_source_file_utf16() { + #[track_caller] + fn roundtrip(lines: &Lines<&str>, byte_idx: usize, utf16_idx: usize) { + let middle = lines.byte_to_utf16(byte_idx).unwrap(); + let result = lines.utf16_to_byte(middle).unwrap(); + assert_eq!(middle, utf16_idx); + assert_eq!(result, byte_idx); + } + + let lines = Lines::new(TEST); + roundtrip(&lines, 0, 0); + roundtrip(&lines, 2, 1); + roundtrip(&lines, 3, 2); + roundtrip(&lines, 8, 7); + roundtrip(&lines, 12, 9); + roundtrip(&lines, 21, 18); + assert_eq!(lines.byte_to_utf16(22), None); + assert_eq!(lines.utf16_to_byte(19), None); + } + + #[test] + fn test_source_file_roundtrip() { + #[track_caller] + fn roundtrip(lines: &Lines<&str>, byte_idx: usize) { + let line = lines.byte_to_line(byte_idx).unwrap(); + let column = lines.byte_to_column(byte_idx).unwrap(); + let result = lines.line_column_to_byte(line, column).unwrap(); + assert_eq!(result, byte_idx); + } + + let lines = Lines::new(TEST); + roundtrip(&lines, 0); + roundtrip(&lines, 7); + roundtrip(&lines, 12); + roundtrip(&lines, 21); + } + + #[test] + fn test_source_file_edit() { + // This tests only the non-parser parts. The reparsing itself is + // tested separately. + #[track_caller] + fn test(prev: &str, range: Range, with: &str, after: &str) { + let reference = Lines::new(after); + + let mut edited = Lines::new(prev.to_string()); + edited.edit(range.clone(), with); + assert_eq!(edited.text(), reference.text()); + assert_eq!(edited.0.lines, reference.0.lines); + + let mut replaced = Lines::new(prev.to_string()); + replaced.replace(&{ + let mut s = prev.to_string(); + s.replace_range(range, with); + s + }); + assert_eq!(replaced.text(), reference.text()); + assert_eq!(replaced.0.lines, reference.0.lines); + } + + // Test inserting at the beginning. + test("abc\n", 0..0, "hi\n", "hi\nabc\n"); + test("\nabc", 0..0, "hi\r", "hi\r\nabc"); + + // Test editing in the middle. + test(TEST, 4..16, "❌", "ä\tc❌i\rjkl"); + + // Test appending. + test("abc\ndef", 7..7, "hi", "abc\ndefhi"); + test("abc\ndef\n", 8..8, "hi", "abc\ndef\nhi"); + + // Test appending with adjoining \r and \n. + test("abc\ndef\r", 8..8, "\nghi", "abc\ndef\r\nghi"); + + // Test removing everything. + test(TEST, 0..21, "", ""); + } +} diff --git a/crates/typst-syntax/src/source.rs b/crates/typst-syntax/src/source.rs index 6ff94c73f..514cb9a4a 100644 --- a/crates/typst-syntax/src/source.rs +++ b/crates/typst-syntax/src/source.rs @@ -2,14 +2,14 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; -use std::iter::zip; use std::ops::Range; use std::sync::Arc; use typst_utils::LazyHash; +use crate::lines::Lines; use crate::reparser::reparse; -use crate::{is_newline, parse, FileId, LinkedNode, Span, SyntaxNode, VirtualPath}; +use crate::{parse, FileId, LinkedNode, Span, SyntaxNode, VirtualPath}; /// A source file. /// @@ -24,9 +24,8 @@ pub struct Source(Arc); #[derive(Clone)] struct Repr { id: FileId, - text: LazyHash, root: LazyHash, - lines: Vec, + lines: LazyHash>, } impl Source { @@ -37,8 +36,7 @@ impl Source { root.numberize(id, Span::FULL).unwrap(); Self(Arc::new(Repr { id, - lines: lines(&text), - text: LazyHash::new(text), + lines: LazyHash::new(Lines::new(text)), root: LazyHash::new(root), })) } @@ -58,9 +56,14 @@ impl Source { self.0.id } + /// The whole source as a string slice. + pub fn lines(&self) -> Lines { + Lines::clone(&self.0.lines) + } + /// The whole source as a string slice. pub fn text(&self) -> &str { - &self.0.text + self.0.lines.text() } /// Slice out the part of the source code enclosed by the range. @@ -77,29 +80,12 @@ impl Source { /// Returns the range in the new source that was ultimately reparsed. pub fn replace(&mut self, new: &str) -> Range { let _scope = typst_timing::TimingScope::new("replace source"); - let old = self.text(); - let mut prefix = - zip(old.bytes(), new.bytes()).take_while(|(x, y)| x == y).count(); - - if prefix == old.len() && prefix == new.len() { + let Some((prefix, suffix)) = self.0.lines.replacement_range(new) else { return 0..0; - } - - while !old.is_char_boundary(prefix) || !new.is_char_boundary(prefix) { - prefix -= 1; - } - - let mut suffix = zip(old[prefix..].bytes().rev(), new[prefix..].bytes().rev()) - .take_while(|(x, y)| x == y) - .count(); - - while !old.is_char_boundary(old.len() - suffix) - || !new.is_char_boundary(new.len() - suffix) - { - suffix += 1; - } + }; + let old = self.text(); let replace = prefix..old.len() - suffix; let with = &new[prefix..new.len() - suffix]; self.edit(replace, with) @@ -112,48 +98,28 @@ impl Source { /// The method panics if the `replace` range is out of bounds. #[track_caller] pub fn edit(&mut self, replace: Range, with: &str) -> Range { - let start_byte = replace.start; - let start_utf16 = self.byte_to_utf16(start_byte).unwrap(); - let line = self.byte_to_line(start_byte).unwrap(); - let inner = Arc::make_mut(&mut self.0); - // Update the text itself. - inner.text.replace_range(replace.clone(), with); - - // Remove invalidated line starts. - inner.lines.truncate(line + 1); - - // Handle adjoining of \r and \n. - if inner.text[..start_byte].ends_with('\r') && with.starts_with('\n') { - inner.lines.pop(); - } - - // Recalculate the line starts after the edit. - inner.lines.extend(lines_from( - start_byte, - start_utf16, - &inner.text[start_byte..], - )); + // Update the text and lines. + inner.lines.edit(replace.clone(), with); // Incrementally reparse the replaced range. - reparse(&mut inner.root, &inner.text, replace, with.len()) + reparse(&mut inner.root, inner.lines.text(), replace, with.len()) } /// Get the length of the file in UTF-8 encoded bytes. pub fn len_bytes(&self) -> usize { - self.text().len() + self.0.lines.len_bytes() } /// Get the length of the file in UTF-16 code units. pub fn len_utf16(&self) -> usize { - let last = self.0.lines.last().unwrap(); - last.utf16_idx + len_utf16(&self.0.text[last.byte_idx..]) + self.0.lines.len_utf16() } /// Get the length of the file in lines. pub fn len_lines(&self) -> usize { - self.0.lines.len() + self.0.lines.len_lines() } /// Find the node with the given span. @@ -171,85 +137,6 @@ impl Source { pub fn range(&self, span: Span) -> Option> { Some(self.find(span)?.range()) } - - /// Return the index of the UTF-16 code unit at the byte index. - pub fn byte_to_utf16(&self, byte_idx: usize) -> Option { - let line_idx = self.byte_to_line(byte_idx)?; - let line = self.0.lines.get(line_idx)?; - let head = self.0.text.get(line.byte_idx..byte_idx)?; - Some(line.utf16_idx + len_utf16(head)) - } - - /// Return the index of the line that contains the given byte index. - pub fn byte_to_line(&self, byte_idx: usize) -> Option { - (byte_idx <= self.0.text.len()).then(|| { - match self.0.lines.binary_search_by_key(&byte_idx, |line| line.byte_idx) { - Ok(i) => i, - Err(i) => i - 1, - } - }) - } - - /// Return the index of the column at the byte index. - /// - /// The column is defined as the number of characters in the line before the - /// byte index. - pub fn byte_to_column(&self, byte_idx: usize) -> Option { - let line = self.byte_to_line(byte_idx)?; - let start = self.line_to_byte(line)?; - let head = self.get(start..byte_idx)?; - Some(head.chars().count()) - } - - /// Return the byte index at the UTF-16 code unit. - pub fn utf16_to_byte(&self, utf16_idx: usize) -> Option { - let line = self.0.lines.get( - match self.0.lines.binary_search_by_key(&utf16_idx, |line| line.utf16_idx) { - Ok(i) => i, - Err(i) => i - 1, - }, - )?; - - let mut k = line.utf16_idx; - for (i, c) in self.0.text[line.byte_idx..].char_indices() { - if k >= utf16_idx { - return Some(line.byte_idx + i); - } - k += c.len_utf16(); - } - - (k == utf16_idx).then_some(self.0.text.len()) - } - - /// Return the byte position at which the given line starts. - pub fn line_to_byte(&self, line_idx: usize) -> Option { - self.0.lines.get(line_idx).map(|line| line.byte_idx) - } - - /// Return the range which encloses the given line. - pub fn line_to_range(&self, line_idx: usize) -> Option> { - let start = self.line_to_byte(line_idx)?; - let end = self.line_to_byte(line_idx + 1).unwrap_or(self.0.text.len()); - Some(start..end) - } - - /// Return the byte index of the given (line, column) pair. - /// - /// The column defines the number of characters to go beyond the start of - /// the line. - pub fn line_column_to_byte( - &self, - line_idx: usize, - column_idx: usize, - ) -> Option { - let range = self.line_to_range(line_idx)?; - let line = self.get(range.clone())?; - let mut chars = line.chars(); - for _ in 0..column_idx { - chars.next(); - } - Some(range.start + (line.len() - chars.as_str().len())) - } } impl Debug for Source { @@ -261,7 +148,7 @@ impl Debug for Source { impl Hash for Source { fn hash(&self, state: &mut H) { self.0.id.hash(state); - self.0.text.hash(state); + self.0.lines.hash(state); self.0.root.hash(state); } } @@ -271,176 +158,3 @@ impl AsRef for Source { self.text() } } - -/// Metadata about a line. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -struct Line { - /// The UTF-8 byte offset where the line starts. - byte_idx: usize, - /// The UTF-16 codepoint offset where the line starts. - utf16_idx: usize, -} - -/// Create a line vector. -fn lines(text: &str) -> Vec { - std::iter::once(Line { byte_idx: 0, utf16_idx: 0 }) - .chain(lines_from(0, 0, text)) - .collect() -} - -/// Compute a line iterator from an offset. -fn lines_from( - byte_offset: usize, - utf16_offset: usize, - text: &str, -) -> impl Iterator + '_ { - let mut s = unscanny::Scanner::new(text); - let mut utf16_idx = utf16_offset; - - std::iter::from_fn(move || { - s.eat_until(|c: char| { - utf16_idx += c.len_utf16(); - is_newline(c) - }); - - if s.done() { - return None; - } - - if s.eat() == Some('\r') && s.eat_if('\n') { - utf16_idx += 1; - } - - Some(Line { byte_idx: byte_offset + s.cursor(), utf16_idx }) - }) -} - -/// The number of code units this string would use if it was encoded in -/// UTF16. This runs in linear time. -fn len_utf16(string: &str) -> usize { - string.chars().map(char::len_utf16).sum() -} - -#[cfg(test)] -mod tests { - use super::*; - - const TEST: &str = "ä\tcde\nf💛g\r\nhi\rjkl"; - - #[test] - fn test_source_file_new() { - let source = Source::detached(TEST); - assert_eq!( - source.0.lines, - [ - Line { byte_idx: 0, utf16_idx: 0 }, - Line { byte_idx: 7, utf16_idx: 6 }, - Line { byte_idx: 15, utf16_idx: 12 }, - Line { byte_idx: 18, utf16_idx: 15 }, - ] - ); - } - - #[test] - fn test_source_file_pos_to_line() { - let source = Source::detached(TEST); - assert_eq!(source.byte_to_line(0), Some(0)); - assert_eq!(source.byte_to_line(2), Some(0)); - assert_eq!(source.byte_to_line(6), Some(0)); - assert_eq!(source.byte_to_line(7), Some(1)); - assert_eq!(source.byte_to_line(8), Some(1)); - assert_eq!(source.byte_to_line(12), Some(1)); - assert_eq!(source.byte_to_line(21), Some(3)); - assert_eq!(source.byte_to_line(22), None); - } - - #[test] - fn test_source_file_pos_to_column() { - let source = Source::detached(TEST); - assert_eq!(source.byte_to_column(0), Some(0)); - assert_eq!(source.byte_to_column(2), Some(1)); - assert_eq!(source.byte_to_column(6), Some(5)); - assert_eq!(source.byte_to_column(7), Some(0)); - assert_eq!(source.byte_to_column(8), Some(1)); - assert_eq!(source.byte_to_column(12), Some(2)); - } - - #[test] - fn test_source_file_utf16() { - #[track_caller] - fn roundtrip(source: &Source, byte_idx: usize, utf16_idx: usize) { - let middle = source.byte_to_utf16(byte_idx).unwrap(); - let result = source.utf16_to_byte(middle).unwrap(); - assert_eq!(middle, utf16_idx); - assert_eq!(result, byte_idx); - } - - let source = Source::detached(TEST); - roundtrip(&source, 0, 0); - roundtrip(&source, 2, 1); - roundtrip(&source, 3, 2); - roundtrip(&source, 8, 7); - roundtrip(&source, 12, 9); - roundtrip(&source, 21, 18); - assert_eq!(source.byte_to_utf16(22), None); - assert_eq!(source.utf16_to_byte(19), None); - } - - #[test] - fn test_source_file_roundtrip() { - #[track_caller] - fn roundtrip(source: &Source, byte_idx: usize) { - let line = source.byte_to_line(byte_idx).unwrap(); - let column = source.byte_to_column(byte_idx).unwrap(); - let result = source.line_column_to_byte(line, column).unwrap(); - assert_eq!(result, byte_idx); - } - - let source = Source::detached(TEST); - roundtrip(&source, 0); - roundtrip(&source, 7); - roundtrip(&source, 12); - roundtrip(&source, 21); - } - - #[test] - fn test_source_file_edit() { - // This tests only the non-parser parts. The reparsing itself is - // tested separately. - #[track_caller] - fn test(prev: &str, range: Range, with: &str, after: &str) { - let reference = Source::detached(after); - - let mut edited = Source::detached(prev); - edited.edit(range.clone(), with); - assert_eq!(edited.text(), reference.text()); - assert_eq!(edited.0.lines, reference.0.lines); - - let mut replaced = Source::detached(prev); - replaced.replace(&{ - let mut s = prev.to_string(); - s.replace_range(range, with); - s - }); - assert_eq!(replaced.text(), reference.text()); - assert_eq!(replaced.0.lines, reference.0.lines); - } - - // Test inserting at the beginning. - test("abc\n", 0..0, "hi\n", "hi\nabc\n"); - test("\nabc", 0..0, "hi\r", "hi\r\nabc"); - - // Test editing in the middle. - test(TEST, 4..16, "❌", "ä\tc❌i\rjkl"); - - // Test appending. - test("abc\ndef", 7..7, "hi", "abc\ndefhi"); - test("abc\ndef\n", 8..8, "hi", "abc\ndef\nhi"); - - // Test appending with adjoining \r and \n. - test("abc\ndef\r", 8..8, "\nghi", "abc\ndef\r\nghi"); - - // Test removing everything. - test(TEST, 0..21, "", ""); - } -} diff --git a/tests/src/collect.rs b/tests/src/collect.rs index 84af04d2d..173488b01 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -7,7 +7,9 @@ use std::sync::LazyLock; use ecow::{eco_format, EcoString}; use typst_syntax::package::PackageVersion; -use typst_syntax::{is_id_continue, is_ident, is_newline, FileId, Source, VirtualPath}; +use typst_syntax::{ + is_id_continue, is_ident, is_newline, FileId, Lines, Source, VirtualPath, +}; use unscanny::Scanner; /// Collects all tests from all files. @@ -79,6 +81,8 @@ impl Display for FileSize { pub struct Note { pub pos: FilePos, pub kind: NoteKind, + /// The file [`Self::range`] belongs to. + pub file: FileId, pub range: Option>, pub message: String, } @@ -341,9 +345,28 @@ impl<'a> Parser<'a> { let kind: NoteKind = head.parse().ok()?; self.s.eat_if(' '); + let mut file = None; + if self.s.eat_if('"') { + let path = self.s.eat_until(|c| is_newline(c) || c == '"'); + if !self.s.eat_if('"') { + self.error("expected closing quote after file path"); + return None; + } + + let vpath = VirtualPath::new(path); + file = Some(FileId::new(None, vpath)); + + self.s.eat_if(' '); + } + let mut range = None; if self.s.at('-') || self.s.at(char::is_numeric) { - range = self.parse_range(source); + if let Some(file) = file { + range = self.parse_range_external(file); + } else { + range = self.parse_range(source); + } + if range.is_none() { self.error("range is malformed"); return None; @@ -359,11 +382,78 @@ impl<'a> Parser<'a> { Some(Note { pos: FilePos::new(self.path, self.line), kind, + file: file.unwrap_or(source.id()), range, message, }) } + #[cfg(not(feature = "default"))] + fn parse_range_external(&mut self, _file: FileId) -> Option> { + panic!("external file ranges are not expected when testing `typst_syntax`"); + } + + /// Parse a range in an external file, optionally abbreviated as just a position + /// if the range is empty. + #[cfg(feature = "default")] + fn parse_range_external(&mut self, file: FileId) -> Option> { + use typst::foundations::Bytes; + + use crate::world::{read, system_path}; + + let path = match system_path(file) { + Ok(path) => path, + Err(err) => { + self.error(err.to_string()); + return None; + } + }; + + let bytes = match read(&path) { + Ok(data) => Bytes::new(data), + Err(err) => { + self.error(err.to_string()); + return None; + } + }; + + let start = self.parse_line_col()?; + let lines = Lines::try_from(&bytes).expect( + "errors shouldn't be annotated for files \ + that aren't human readable (not valid utf-8)", + ); + let range = if self.s.eat_if('-') { + let (line, col) = start; + let start = lines.line_column_to_byte(line, col); + let (line, col) = self.parse_line_col()?; + let end = lines.line_column_to_byte(line, col); + Option::zip(start, end).map(|(a, b)| a..b) + } else { + let (line, col) = start; + lines.line_column_to_byte(line, col).map(|i| i..i) + }; + if range.is_none() { + self.error("range is out of bounds"); + } + range + } + + /// Parses absolute `line:column` indices in an external file. + fn parse_line_col(&mut self) -> Option<(usize, usize)> { + let line = self.parse_number()?; + if !self.s.eat_if(':') { + self.error("positions in external files always require both `:`"); + return None; + } + let col = self.parse_number()?; + if line < 0 || col < 0 { + self.error("line and column numbers must be positive"); + return None; + } + + Some(((line as usize).saturating_sub(1), (col as usize).saturating_sub(1))) + } + /// Parse a range, optionally abbreviated as just a position if the range /// is empty. fn parse_range(&mut self, source: &Source) -> Option> { @@ -389,13 +479,13 @@ impl<'a> Parser<'a> { let line_idx = (line_idx_in_test + comments).checked_add_signed(line_delta)?; let column_idx = if column < 0 { // Negative column index is from the back. - let range = source.line_to_range(line_idx)?; + let range = source.lines().line_to_range(line_idx)?; text[range].chars().count().saturating_add_signed(column) } else { usize::try_from(column).ok()?.checked_sub(1)? }; - source.line_column_to_byte(line_idx, column_idx) + source.lines().line_column_to_byte(line_idx, column_idx) } /// Parse a number. diff --git a/tests/src/run.rs b/tests/src/run.rs index 4d08362cf..a34e38db5 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -10,10 +10,11 @@ use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform}; use typst::visualize::Color; use typst::{Document, WorldExt}; use typst_pdf::PdfOptions; +use typst_syntax::FileId; use crate::collect::{Attr, FileSize, NoteKind, Test}; use crate::logger::TestResult; -use crate::world::TestWorld; +use crate::world::{system_path, TestWorld}; /// Runs a single test. /// @@ -117,7 +118,7 @@ impl<'a> Runner<'a> { if seen { continue; } - let note_range = self.format_range(¬e.range); + let note_range = self.format_range(note.file, ¬e.range); if first { log!(self, "not emitted"); first = false; @@ -208,10 +209,6 @@ impl<'a> Runner<'a> { /// Compare a subset of notes with a given kind against diagnostics of /// that same kind. fn check_diagnostic(&mut self, kind: NoteKind, diag: &SourceDiagnostic) { - // Ignore diagnostics from other sources than the test file itself. - if diag.span.id().is_some_and(|id| id != self.test.source.id()) { - return; - } // TODO: remove this once HTML export is stable if diag.message == "html export is under active development and incomplete" { return; @@ -219,11 +216,11 @@ impl<'a> Runner<'a> { let message = diag.message.replace("\\", "/"); let range = self.world.range(diag.span); - self.validate_note(kind, range.clone(), &message); + self.validate_note(kind, diag.span.id(), range.clone(), &message); // Check hints. for hint in &diag.hints { - self.validate_note(NoteKind::Hint, range.clone(), hint); + self.validate_note(NoteKind::Hint, diag.span.id(), range.clone(), hint); } } @@ -235,15 +232,18 @@ impl<'a> Runner<'a> { fn validate_note( &mut self, kind: NoteKind, + file: Option, range: Option>, message: &str, ) { // Try to find perfect match. + let file = file.unwrap_or(self.test.source.id()); if let Some((i, _)) = self.test.notes.iter().enumerate().find(|&(i, note)| { !self.seen[i] && note.kind == kind && note.range == range && note.message == message + && note.file == file }) { self.seen[i] = true; return; @@ -257,7 +257,7 @@ impl<'a> Runner<'a> { && (note.range == range || note.message == message) }) else { // Not even a close match, diagnostic is not annotated. - let diag_range = self.format_range(&range); + let diag_range = self.format_range(file, &range); log!(into: self.not_annotated, " {kind}: {diag_range} {}", message); return; }; @@ -267,10 +267,10 @@ impl<'a> Runner<'a> { // Range is wrong. if range != note.range { - let note_range = self.format_range(¬e.range); - let note_text = self.text_for_range(¬e.range); - let diag_range = self.format_range(&range); - let diag_text = self.text_for_range(&range); + let note_range = self.format_range(note.file, ¬e.range); + let note_text = self.text_for_range(note.file, ¬e.range); + let diag_range = self.format_range(file, &range); + let diag_text = self.text_for_range(file, &range); log!(self, "mismatched range ({}):", note.pos); log!(self, " message | {}", note.message); log!(self, " annotated | {note_range:<9} | {note_text}"); @@ -286,39 +286,49 @@ impl<'a> Runner<'a> { } /// Display the text for a range. - fn text_for_range(&self, range: &Option>) -> String { + fn text_for_range(&self, file: FileId, range: &Option>) -> String { let Some(range) = range else { return "No text".into() }; if range.is_empty() { - "(empty)".into() - } else { - format!("`{}`", self.test.source.text()[range.clone()].replace('\n', "\\n")) + return "(empty)".into(); } + + let lines = self.world.lookup(file); + lines.text()[range.clone()].replace('\n', "\\n").replace('\r', "\\r") } /// Display a byte range as a line:column range. - fn format_range(&self, range: &Option>) -> String { + fn format_range(&self, file: FileId, range: &Option>) -> String { let Some(range) = range else { return "No range".into() }; + + let mut preamble = String::new(); + if file != self.test.source.id() { + preamble = format!("\"{}\" ", system_path(file).unwrap().display()); + } + if range.start == range.end { - self.format_pos(range.start) + format!("{preamble}{}", self.format_pos(file, range.start)) } else { - format!("{}-{}", self.format_pos(range.start,), self.format_pos(range.end,)) + format!( + "{preamble}{}-{}", + self.format_pos(file, range.start), + self.format_pos(file, range.end) + ) } } /// Display a position as a line:column pair. - fn format_pos(&self, pos: usize) -> String { - if let (Some(line_idx), Some(column_idx)) = - (self.test.source.byte_to_line(pos), self.test.source.byte_to_column(pos)) - { - let line = self.test.pos.line + line_idx; - let column = column_idx + 1; - if line == 1 { - format!("{column}") - } else { - format!("{line}:{column}") - } + fn format_pos(&self, file: FileId, pos: usize) -> String { + let lines = self.world.lookup(file); + + let res = lines.byte_to_line_column(pos).map(|(line, col)| (line + 1, col + 1)); + let Some((line, col)) = res else { + return "oob".into(); + }; + + if line == 1 { + format!("{col}") } else { - "oob".into() + format!("{line}:{col}") } } } diff --git a/tests/src/world.rs b/tests/src/world.rs index fe2bd45ea..bc3e690b2 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -20,6 +20,7 @@ use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::utils::{singleton, LazyHash}; use typst::visualize::Color; use typst::{Feature, Library, World}; +use typst_syntax::Lines; /// A world that provides access to the tests environment. #[derive(Clone)] @@ -84,6 +85,22 @@ impl TestWorld { let mut map = self.base.slots.lock(); f(map.entry(id).or_insert_with(|| FileSlot::new(id))) } + + /// Lookup line metadata for a file by id. + #[track_caller] + pub fn lookup(&self, id: FileId) -> Lines { + self.slot(id, |slot| { + if let Some(source) = slot.source.get() { + let source = source.as_ref().expect("file is not valid"); + source.lines() + } else if let Some(bytes) = slot.file.get() { + let bytes = bytes.as_ref().expect("file is not valid"); + Lines::try_from(bytes).expect("file is not valid utf-8") + } else { + panic!("file id does not point to any source file"); + } + }) + } } /// Shared foundation of all test worlds. @@ -149,7 +166,7 @@ impl FileSlot { } /// The file system path for a file ID. -fn system_path(id: FileId) -> FileResult { +pub(crate) fn system_path(id: FileId) -> FileResult { let root: PathBuf = match id.package() { Some(spec) => format!("tests/packages/{}-{}", spec.name, spec.version).into(), None => PathBuf::new(), @@ -159,7 +176,7 @@ fn system_path(id: FileId) -> FileResult { } /// Read a file. -fn read(path: &Path) -> FileResult> { +pub(crate) fn read(path: &Path) -> FileResult> { // Resolve asset. if let Ok(suffix) = path.strip_prefix("assets/") { return typst_dev_assets::get(&suffix.to_string_lossy()) diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ index 6f57ec458..046345bec 100644 --- a/tests/suite/loading/csv.typ +++ b/tests/suite/loading/csv.typ @@ -18,12 +18,12 @@ #csv("nope.csv") --- csv-invalid --- -// Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3) +// Error: "/assets/data/bad.csv" 3:1 failed to parse CSV (found 3 instead of 2 fields in line 3) #csv("/assets/data/bad.csv") --- csv-invalid-row-type-dict --- // Test error numbering with dictionary rows. -// Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3) +// Error: "/assets/data/bad.csv" 3:1 failed to parse CSV (found 3 instead of 2 fields in line 3) #csv("/assets/data/bad.csv", row-type: dictionary) --- csv-invalid-delimiter --- diff --git a/tests/suite/loading/json.typ b/tests/suite/loading/json.typ index c8df1ff6e..9e433992d 100644 --- a/tests/suite/loading/json.typ +++ b/tests/suite/loading/json.typ @@ -6,7 +6,7 @@ #test(data.at(2).weight, 150) --- json-invalid --- -// Error: 7-30 failed to parse JSON (expected value at line 3 column 14) +// Error: "/assets/data/bad.json" 3:14 failed to parse JSON (expected value at line 3 column 14) #json("/assets/data/bad.json") --- json-decode-deprecated --- diff --git a/tests/suite/loading/read.typ b/tests/suite/loading/read.typ index b5c9c0892..57bfc1d5c 100644 --- a/tests/suite/loading/read.typ +++ b/tests/suite/loading/read.typ @@ -8,5 +8,5 @@ #let data = read("/assets/text/missing.txt") --- read-invalid-utf-8 --- -// Error: 18-40 file is not valid utf-8 +// Error: 18-40 failed to convert to string (file is not valid utf-8 in assets/text/bad.txt:1:1) #let data = read("/assets/text/bad.txt") diff --git a/tests/suite/loading/toml.typ b/tests/suite/loading/toml.typ index a4318a015..9d65da452 100644 --- a/tests/suite/loading/toml.typ +++ b/tests/suite/loading/toml.typ @@ -37,7 +37,7 @@ )) --- toml-invalid --- -// Error: 7-30 failed to parse TOML (expected `.`, `=` at line 1 column 16) +// Error: "/assets/data/bad.toml" 1:16-2:1 failed to parse TOML (expected `.`, `=`) #toml("/assets/data/bad.toml") --- toml-decode-deprecated --- diff --git a/tests/suite/loading/xml.typ b/tests/suite/loading/xml.typ index 933f3c480..eed7db0ae 100644 --- a/tests/suite/loading/xml.typ +++ b/tests/suite/loading/xml.typ @@ -24,7 +24,7 @@ ),)) --- xml-invalid --- -// Error: 6-28 failed to parse XML (found closing tag 'data' instead of 'hello' in line 3) +// Error: "/assets/data/bad.xml" 3:0 failed to parse XML (found closing tag 'data' instead of 'hello') #xml("/assets/data/bad.xml") --- xml-decode-deprecated --- diff --git a/tests/suite/loading/yaml.typ b/tests/suite/loading/yaml.typ index a8089052c..ad171c6ef 100644 --- a/tests/suite/loading/yaml.typ +++ b/tests/suite/loading/yaml.typ @@ -13,7 +13,7 @@ #test(data.at("1"), "ok") --- yaml-invalid --- -// Error: 7-30 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18) +// Error: "/assets/data/bad.yaml" 2:1 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18) #yaml("/assets/data/bad.yaml") --- yaml-decode-deprecated --- diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ index 49b66ee56..382e444cc 100644 --- a/tests/suite/scripting/import.typ +++ b/tests/suite/scripting/import.typ @@ -334,6 +334,7 @@ --- import-cyclic-in-other-file --- // Cyclic import in other file. +// Error: "tests/suite/scripting/modules/cycle2.typ" 2:9-2:21 cyclic import #import "./modules/cycle1.typ": * This is never reached. diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 73c4feff8..45c70c4b8 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -167,7 +167,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image("/assets/plugins/hello.wasm") --- image-bad-svg --- -// Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4) +// Error: "/assets/images/bad.svg" 4:3 failed to parse SVG (found closing tag 'g' instead of 'style') #image("/assets/images/bad.svg") --- image-decode-svg --- @@ -176,7 +176,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image.decode(``.text, format: "svg") --- image-decode-bad-svg --- -// Error: 2-168 failed to parse SVG (missing root node) +// Error: 15-152 failed to parse SVG (missing root node at 1:1) // Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") From 7c7b962b98a09c1baabdd03ff4ccad8f6d817b37 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:41:16 -0300 Subject: [PATCH 072/162] Table multiple headers and subheaders (#6168) --- crates/typst-layout/src/grid/layouter.rs | 598 +++++++++++------ crates/typst-layout/src/grid/lines.rs | 29 +- crates/typst-layout/src/grid/repeated.rs | 496 +++++++++++++-- crates/typst-layout/src/grid/rowspans.rs | 245 ++++--- crates/typst-library/src/foundations/int.rs | 17 +- crates/typst-library/src/layout/grid/mod.rs | 13 +- .../typst-library/src/layout/grid/resolve.rs | 487 +++++++++----- crates/typst-library/src/model/table.rs | 70 +- crates/typst-syntax/src/span.rs | 13 +- crates/typst-utils/src/lib.rs | 11 +- crates/typst-utils/src/pico.rs | 5 +- ...grid-footer-non-repeatable-unbreakable.png | Bin 0 -> 365 bytes .../grid-footer-repeatable-unbreakable.png | Bin 0 -> 340 bytes .../grid-header-and-large-auto-contiguous.png | Bin 0 -> 894 bytes .../grid-header-and-rowspan-contiguous-1.png | Bin 0 -> 815 bytes .../grid-header-and-rowspan-contiguous-2.png | Bin 0 -> 815 bytes tests/ref/grid-header-multiple.png | Bin 0 -> 214 bytes ...header-non-repeating-orphan-prevention.png | Bin 0 -> 453 bytes ...id-header-not-at-first-row-two-columns.png | Bin 0 -> 176 bytes tests/ref/grid-header-not-at-first-row.png | Bin 0 -> 176 bytes tests/ref/grid-header-not-at-the-top.png | Bin 0 -> 605 bytes tests/ref/grid-header-replace-doesnt-fit.png | Bin 0 -> 559 bytes tests/ref/grid-header-replace-orphan.png | Bin 0 -> 559 bytes tests/ref/grid-header-replace.png | Bin 0 -> 692 bytes tests/ref/grid-header-skip.png | Bin 0 -> 432 bytes ...-header-too-large-non-repeating-orphan.png | Bin 0 -> 372 bytes ...arge-repeating-orphan-not-at-first-row.png | Bin 0 -> 398 bytes ...too-large-repeating-orphan-with-footer.png | Bin 0 -> 576 bytes ...grid-header-too-large-repeating-orphan.png | Bin 0 -> 321 bytes ...-subheaders-alone-no-orphan-prevention.png | Bin 0 -> 254 bytes ...alone-with-footer-no-orphan-prevention.png | Bin 0 -> 378 bytes .../ref/grid-subheaders-alone-with-footer.png | Bin 0 -> 319 bytes ...gutter-and-footer-no-orphan-prevention.png | Bin 0 -> 382 bytes ...alone-with-gutter-no-orphan-prevention.png | Bin 0 -> 254 bytes tests/ref/grid-subheaders-alone.png | Bin 0 -> 256 bytes ...ders-basic-non-consecutive-with-footer.png | Bin 0 -> 279 bytes .../grid-subheaders-basic-non-consecutive.png | Bin 0 -> 256 bytes tests/ref/grid-subheaders-basic-replace.png | Bin 0 -> 321 bytes .../ref/grid-subheaders-basic-with-footer.png | Bin 0 -> 256 bytes tests/ref/grid-subheaders-basic.png | Bin 0 -> 210 bytes tests/ref/grid-subheaders-colorful.png | Bin 0 -> 11005 bytes tests/ref/grid-subheaders-demo.png | Bin 0 -> 5064 bytes ...multi-page-row-right-after-with-footer.png | Bin 0 -> 1207 bytes ...-subheaders-multi-page-row-right-after.png | Bin 0 -> 1127 bytes ...-subheaders-multi-page-row-with-footer.png | Bin 0 -> 1345 bytes tests/ref/grid-subheaders-multi-page-row.png | Bin 0 -> 1173 bytes ...d-subheaders-multi-page-rowspan-gutter.png | Bin 0 -> 1560 bytes ...headers-multi-page-rowspan-right-after.png | Bin 0 -> 1421 bytes ...headers-multi-page-rowspan-with-footer.png | Bin 0 -> 1190 bytes .../grid-subheaders-multi-page-rowspan.png | Bin 0 -> 1048 bytes .../grid-subheaders-non-repeat-replace.png | Bin 0 -> 878 bytes tests/ref/grid-subheaders-non-repeat.png | Bin 0 -> 614 bytes ...repeating-header-before-multi-page-row.png | Bin 0 -> 410 bytes ...eaders-non-repeating-orphan-prevention.png | Bin 0 -> 347 bytes ...s-non-repeating-replace-didnt-fit-once.png | Bin 0 -> 895 bytes ...ubheaders-non-repeating-replace-orphan.png | Bin 0 -> 964 bytes tests/ref/grid-subheaders-repeat-gutter.png | Bin 0 -> 503 bytes ...grid-subheaders-repeat-non-consecutive.png | Bin 0 -> 599 bytes ...bheaders-repeat-replace-didnt-fit-once.png | Bin 0 -> 877 bytes ...ubheaders-repeat-replace-double-orphan.png | Bin 0 -> 950 bytes ...-repeat-replace-gutter-orphan-at-child.png | Bin 0 -> 806 bytes ...repeat-replace-gutter-orphan-at-gutter.png | Bin 0 -> 758 bytes .../grid-subheaders-repeat-replace-gutter.png | Bin 0 -> 782 bytes ...headers-repeat-replace-multiple-levels.png | Bin 0 -> 877 bytes .../grid-subheaders-repeat-replace-orphan.png | Bin 0 -> 939 bytes ...-subheaders-repeat-replace-short-lived.png | Bin 0 -> 795 bytes ...ders-repeat-replace-with-footer-orphan.png | Bin 0 -> 961 bytes ...-subheaders-repeat-replace-with-footer.png | Bin 0 -> 992 bytes tests/ref/grid-subheaders-repeat-replace.png | Bin 0 -> 953 bytes ...aders-repeat-short-lived-also-replaces.png | Bin 0 -> 899 bytes .../grid-subheaders-repeat-with-footer.png | Bin 0 -> 584 bytes tests/ref/grid-subheaders-repeat.png | Bin 0 -> 472 bytes ...subheaders-repeating-orphan-prevention.png | Bin 0 -> 347 bytes ...aders-short-lived-no-orphan-prevention.png | Bin 0 -> 287 bytes ...large-non-repeating-orphan-before-auto.png | Bin 0 -> 460 bytes ...e-non-repeating-orphan-before-relative.png | Bin 0 -> 542 bytes ...too-large-repeating-orphan-before-auto.png | Bin 0 -> 525 bytes ...large-repeating-orphan-before-relative.png | Bin 0 -> 437 bytes tests/ref/html/multi-header-inside-table.html | 69 ++ tests/ref/html/multi-header-table.html | 49 ++ ...59-column-override-stays-inside-header.png | Bin 0 -> 674 bytes tests/suite/layout/grid/footers.typ | 23 + tests/suite/layout/grid/headers.typ | 171 ++++- tests/suite/layout/grid/html.typ | 75 +++ tests/suite/layout/grid/subheaders.typ | 602 ++++++++++++++++++ 85 files changed, 2453 insertions(+), 520 deletions(-) create mode 100644 tests/ref/grid-footer-non-repeatable-unbreakable.png create mode 100644 tests/ref/grid-footer-repeatable-unbreakable.png create mode 100644 tests/ref/grid-header-and-large-auto-contiguous.png create mode 100644 tests/ref/grid-header-and-rowspan-contiguous-1.png create mode 100644 tests/ref/grid-header-and-rowspan-contiguous-2.png create mode 100644 tests/ref/grid-header-multiple.png create mode 100644 tests/ref/grid-header-non-repeating-orphan-prevention.png create mode 100644 tests/ref/grid-header-not-at-first-row-two-columns.png create mode 100644 tests/ref/grid-header-not-at-first-row.png create mode 100644 tests/ref/grid-header-not-at-the-top.png create mode 100644 tests/ref/grid-header-replace-doesnt-fit.png create mode 100644 tests/ref/grid-header-replace-orphan.png create mode 100644 tests/ref/grid-header-replace.png create mode 100644 tests/ref/grid-header-skip.png create mode 100644 tests/ref/grid-header-too-large-non-repeating-orphan.png create mode 100644 tests/ref/grid-header-too-large-repeating-orphan-not-at-first-row.png create mode 100644 tests/ref/grid-header-too-large-repeating-orphan-with-footer.png create mode 100644 tests/ref/grid-header-too-large-repeating-orphan.png create mode 100644 tests/ref/grid-subheaders-alone-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone-with-footer-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone-with-footer.png create mode 100644 tests/ref/grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone-with-gutter-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-alone.png create mode 100644 tests/ref/grid-subheaders-basic-non-consecutive-with-footer.png create mode 100644 tests/ref/grid-subheaders-basic-non-consecutive.png create mode 100644 tests/ref/grid-subheaders-basic-replace.png create mode 100644 tests/ref/grid-subheaders-basic-with-footer.png create mode 100644 tests/ref/grid-subheaders-basic.png create mode 100644 tests/ref/grid-subheaders-colorful.png create mode 100644 tests/ref/grid-subheaders-demo.png create mode 100644 tests/ref/grid-subheaders-multi-page-row-right-after-with-footer.png create mode 100644 tests/ref/grid-subheaders-multi-page-row-right-after.png create mode 100644 tests/ref/grid-subheaders-multi-page-row-with-footer.png create mode 100644 tests/ref/grid-subheaders-multi-page-row.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan-gutter.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan-right-after.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan-with-footer.png create mode 100644 tests/ref/grid-subheaders-multi-page-rowspan.png create mode 100644 tests/ref/grid-subheaders-non-repeat-replace.png create mode 100644 tests/ref/grid-subheaders-non-repeat.png create mode 100644 tests/ref/grid-subheaders-non-repeating-header-before-multi-page-row.png create mode 100644 tests/ref/grid-subheaders-non-repeating-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-non-repeating-replace-didnt-fit-once.png create mode 100644 tests/ref/grid-subheaders-non-repeating-replace-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-gutter.png create mode 100644 tests/ref/grid-subheaders-repeat-non-consecutive.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-didnt-fit-once.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-double-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-gutter-orphan-at-child.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-gutter-orphan-at-gutter.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-gutter.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-multiple-levels.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-short-lived.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-with-footer-orphan.png create mode 100644 tests/ref/grid-subheaders-repeat-replace-with-footer.png create mode 100644 tests/ref/grid-subheaders-repeat-replace.png create mode 100644 tests/ref/grid-subheaders-repeat-short-lived-also-replaces.png create mode 100644 tests/ref/grid-subheaders-repeat-with-footer.png create mode 100644 tests/ref/grid-subheaders-repeat.png create mode 100644 tests/ref/grid-subheaders-repeating-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-short-lived-no-orphan-prevention.png create mode 100644 tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-auto.png create mode 100644 tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-relative.png create mode 100644 tests/ref/grid-subheaders-too-large-repeating-orphan-before-auto.png create mode 100644 tests/ref/grid-subheaders-too-large-repeating-orphan-before-relative.png create mode 100644 tests/ref/html/multi-header-inside-table.html create mode 100644 tests/ref/html/multi-header-table.html create mode 100644 tests/ref/issue-5359-column-override-stays-inside-header.png create mode 100644 tests/suite/layout/grid/subheaders.typ diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 99b85eddb..42fe38dbe 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,7 +3,9 @@ use std::fmt::Debug; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Resolve, StyleChain}; -use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable}; +use typst_library::layout::grid::resolve::{ + Cell, CellGrid, Header, LinePosition, Repeatable, +}; use typst_library::layout::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -30,10 +32,8 @@ pub struct GridLayouter<'a> { pub(super) rcols: Vec, /// The sum of `rcols`. pub(super) width: Abs, - /// Resolve row sizes, by region. + /// Resolved row sizes, by region. pub(super) rrows: Vec>, - /// Rows in the current region. - pub(super) lrows: Vec, /// The amount of unbreakable rows remaining to be laid out in the /// current unbreakable row group. While this is positive, no region breaks /// should occur. @@ -41,24 +41,155 @@ pub struct GridLayouter<'a> { /// Rowspans not yet laid out because not all of their spanned rows were /// laid out yet. pub(super) rowspans: Vec, - /// The initial size of the current region before we started subtracting. - pub(super) initial: Size, + /// Grid layout state for the current region. + pub(super) current: Current, /// Frames for finished regions. pub(super) finished: Vec, + /// The amount and height of header rows on each finished region. + pub(super) finished_header_rows: Vec, /// Whether this is an RTL grid. pub(super) is_rtl: bool, - /// The simulated header height. - /// This field is reset in `layout_header` and properly updated by + /// Currently repeating headers, one per level. Sorted by increasing + /// levels. + /// + /// Note that some levels may be absent, in particular level 0, which does + /// not exist (so all levels are >= 1). + pub(super) repeating_headers: Vec<&'a Header>, + /// Headers, repeating or not, awaiting their first successful layout. + /// Sorted by increasing levels. + pub(super) pending_headers: &'a [Repeatable

], + /// Next headers to be processed. + pub(super) upcoming_headers: &'a [Repeatable
], + /// State of the row being currently laid out. + /// + /// This is kept as a field to avoid passing down too many parameters from + /// `layout_row` into called functions, which would then have to pass them + /// down to `push_row`, which reads these values. + pub(super) row_state: RowState, + /// The span of the grid element. + pub(super) span: Span, +} + +/// Grid layout state for the current region. This should be reset or updated +/// on each region break. +pub(super) struct Current { + /// The initial size of the current region before we started subtracting. + pub(super) initial: Size, + /// The height of the region after repeated headers were placed and footers + /// prepared. This also includes pending repeating headers from the start, + /// even if they were not repeated yet, since they will be repeated in the + /// next region anyway (bar orphan prevention). + /// + /// This is used to quickly tell if any additional space in the region has + /// been occupied since then, meaning that additional space will become + /// available after a region break (see + /// [`GridLayouter::may_progress_with_repeats`]). + pub(super) initial_after_repeats: Abs, + /// Whether `layouter.regions.may_progress()` was `true` at the top of the + /// region. + pub(super) could_progress_at_top: bool, + /// Rows in the current region. + pub(super) lrows: Vec, + /// The amount of repeated header rows at the start of the current region. + /// Thus, excludes rows from pending headers (which were placed for the + /// first time). + /// + /// Note that `repeating_headers` and `pending_headers` can change if we + /// find a new header inside the region (not at the top), so this field + /// is required to access information from the top of the region. + /// + /// This information is used on finish region to calculate the total height + /// of resolved header rows at the top of the region, which is used by + /// multi-page rowspans so they can properly skip the header rows at the + /// top of each region during layout. + pub(super) repeated_header_rows: usize, + /// The end bound of the row range of the last repeating header at the + /// start of the region. + /// + /// The last row might have disappeared from layout due to being empty, so + /// this is how we can become aware of where the last header ends without + /// having to check the vector of rows. Line layout uses this to determine + /// when to prioritize the last lines under a header. + /// + /// A value of zero indicates no repeated headers were placed. + pub(super) last_repeated_header_end: usize, + /// Stores the length of `lrows` before a sequence of rows equipped with + /// orphan prevention was laid out. In this case, if no more rows without + /// orphan prevention are laid out after those rows before the region ends, + /// the rows will be removed, and there may be an attempt to place them + /// again in the new region. Effectively, this is the mechanism used for + /// orphan prevention of rows. + /// + /// At the moment, this is only used by repeated headers (they aren't laid + /// out if alone in the region) and by new headers, which are moved to the + /// `pending_headers` vector and so will automatically be placed again + /// until they fit and are not orphans in at least one region (or exactly + /// one, for non-repeated headers). + pub(super) lrows_orphan_snapshot: Option, + /// The height of effectively repeating headers, that is, ignoring + /// non-repeating pending headers, in the current region. + /// + /// This is used by multi-page auto rows so they can inform cell layout on + /// how much space should be taken by headers if they break across regions. + /// In particular, non-repeating headers only occupy the initial region, + /// but disappear on new regions, so they can be ignored. + /// + /// This field is reset on each new region and properly updated by /// `layout_auto_row` and `layout_relative_row`, and should not be read /// before all header rows are fully laid out. It is usually fine because /// header rows themselves are unbreakable, and unbreakable rows do not /// need to read this field at all. - pub(super) header_height: Abs, + /// + /// This height is not only computed at the beginning of the region. It is + /// updated whenever a new header is found, subtracting the height of + /// headers which stopped repeating and adding the height of all new + /// headers. + pub(super) repeating_header_height: Abs, + /// The height for each repeating header that was placed in this region. + /// Note that this includes headers not at the top of the region, before + /// their first repetition (pending headers), and excludes headers removed + /// by virtue of a new, conflicting header being found (short-lived + /// headers). + /// + /// This is used to know how much to update `repeating_header_height` by + /// when finding a new header and causing existing repeating headers to + /// stop. + pub(super) repeating_header_heights: Vec, /// The simulated footer height for this region. + /// /// The simulation occurs before any rows are laid out for a region. pub(super) footer_height: Abs, - /// The span of the grid element. - pub(super) span: Span, +} + +/// Data about the row being laid out right now. +#[derive(Debug, Default)] +pub(super) struct RowState { + /// If this is `Some`, this will be updated by the currently laid out row's + /// height if it is auto or relative. This is used for header height + /// calculation. + pub(super) current_row_height: Option, + /// This is `true` when laying out non-short lived headers and footers. + /// That is, headers and footers which are not immediately followed or + /// preceded (respectively) by conflicting headers and footers of same or + /// lower level, or the end or start of the table (respectively), which + /// would cause them to never repeat, even once. + /// + /// If this is `false`, the next row to be laid out will remove an active + /// orphan snapshot and will flush pending headers, as there is no risk + /// that they will be orphans anymore. + pub(super) in_active_repeatable: bool, +} + +/// Data about laid out repeated header rows for a specific finished region. +#[derive(Debug, Default)] +pub(super) struct FinishedHeaderRowInfo { + /// The amount of repeated headers at the top of the region. + pub(super) repeated_amount: usize, + /// The end bound of the row range of the last repeated header at the top + /// of the region. + pub(super) last_repeated_header_end: usize, + /// The total height of repeated headers at the top of the region. + pub(super) repeated_height: Abs, } /// Details about a resulting row piece. @@ -114,14 +245,27 @@ impl<'a> GridLayouter<'a> { rcols: vec![Abs::zero(); grid.cols.len()], width: Abs::zero(), rrows: vec![], - lrows: vec![], unbreakable_rows_left: 0, rowspans: vec![], - initial: regions.size, finished: vec![], + finished_header_rows: vec![], is_rtl: TextElem::dir_in(styles) == Dir::RTL, - header_height: Abs::zero(), - footer_height: Abs::zero(), + repeating_headers: vec![], + upcoming_headers: &grid.headers, + pending_headers: Default::default(), + row_state: RowState::default(), + current: Current { + initial: regions.size, + initial_after_repeats: regions.size.y, + could_progress_at_top: regions.may_progress(), + lrows: vec![], + repeated_header_rows: 0, + last_repeated_header_end: 0, + lrows_orphan_snapshot: None, + repeating_header_height: Abs::zero(), + repeating_header_heights: vec![], + footer_height: Abs::zero(), + }, span, } } @@ -130,38 +274,57 @@ impl<'a> GridLayouter<'a> { pub fn layout(mut self, engine: &mut Engine) -> SourceResult { self.measure_columns(engine)?; - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - // Ensure rows in the first region will be aware of the possible - // presence of the footer. - self.prepare_footer(footer, engine, 0)?; - if matches!(self.grid.header, None | Some(Repeatable::NotRepeated(_))) { - // No repeatable header, so we won't subtract it later. - self.regions.size.y -= self.footer_height; + if let Some(footer) = &self.grid.footer { + if footer.repeated { + // Ensure rows in the first region will be aware of the + // possible presence of the footer. + self.prepare_footer(footer, engine, 0)?; + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; } } - for y in 0..self.grid.rows.len() { - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if y < header.end { - if y == 0 { - self.layout_header(header, engine, 0)?; - self.regions.size.y -= self.footer_height; - } + let mut y = 0; + let mut consecutive_header_count = 0; + while y < self.grid.rows.len() { + if let Some(next_header) = self.upcoming_headers.get(consecutive_header_count) + { + if next_header.range.contains(&y) { + self.place_new_headers(&mut consecutive_header_count, engine)?; + y = next_header.range.end; + // Skip header rows during normal layout. continue; } } - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - if y >= footer.start { + if let Some(footer) = &self.grid.footer { + if footer.repeated && y >= footer.start { if y == footer.start { self.layout_footer(footer, engine, self.finished.len())?; + self.flush_orphans(); } + y = footer.end; continue; } } self.layout_row(y, engine, 0)?; + + // After the first non-header row is placed, pending headers are no + // longer orphans and can repeat, so we move them to repeating + // headers. + // + // Note that this is usually done in `push_row`, since the call to + // `layout_row` above might trigger region breaks (for multi-page + // auto rows), whereas this needs to be called as soon as any part + // of a row is laid out. However, it's possible a row has no + // visible output and thus does not push any rows even though it + // was successfully laid out, in which case we additionally flush + // here just in case. + self.flush_orphans(); + + y += 1; } self.finish_region(engine, true)?; @@ -184,12 +347,46 @@ impl<'a> GridLayouter<'a> { self.render_fills_strokes() } - /// Layout the given row. + /// Layout a row with a certain initial state, returning the final state. + #[inline] + pub(super) fn layout_row_with_state( + &mut self, + y: usize, + engine: &mut Engine, + disambiguator: usize, + initial_state: RowState, + ) -> SourceResult { + // Keep a copy of the previous value in the stack, as this function can + // call itself recursively (e.g. if a region break is triggered and a + // header is placed), so we shouldn't outright overwrite it, but rather + // save and later restore the state when back to this call. + let previous = std::mem::replace(&mut self.row_state, initial_state); + + // Keep it as a separate function to allow inlining the return below, + // as it's usually not needed. + self.layout_row_internal(y, engine, disambiguator)?; + + Ok(std::mem::replace(&mut self.row_state, previous)) + } + + /// Layout the given row with the default row state. + #[inline] pub(super) fn layout_row( &mut self, y: usize, engine: &mut Engine, disambiguator: usize, + ) -> SourceResult<()> { + self.layout_row_with_state(y, engine, disambiguator, RowState::default())?; + Ok(()) + } + + /// Layout the given row using the current state. + pub(super) fn layout_row_internal( + &mut self, + y: usize, + engine: &mut Engine, + disambiguator: usize, ) -> SourceResult<()> { // Skip to next region if current one is full, but only for content // rows, not for gutter rows, and only if we aren't laying out an @@ -206,13 +403,18 @@ impl<'a> GridLayouter<'a> { } // Don't layout gutter rows at the top of a region. - if is_content_row || !self.lrows.is_empty() { + if is_content_row || !self.current.lrows.is_empty() { match self.grid.rows[y] { Sizing::Auto => self.layout_auto_row(engine, disambiguator, y)?, Sizing::Rel(v) => { self.layout_relative_row(engine, disambiguator, v, y)? } - Sizing::Fr(v) => self.lrows.push(Row::Fr(v, y, disambiguator)), + Sizing::Fr(v) => { + if !self.row_state.in_active_repeatable { + self.flush_orphans(); + } + self.current.lrows.push(Row::Fr(v, y, disambiguator)) + } } } @@ -225,8 +427,13 @@ impl<'a> GridLayouter<'a> { fn render_fills_strokes(mut self) -> SourceResult { let mut finished = std::mem::take(&mut self.finished); let frame_amount = finished.len(); - for ((frame_index, frame), rows) in - finished.iter_mut().enumerate().zip(&self.rrows) + for (((frame_index, frame), rows), finished_header_rows) in + finished.iter_mut().enumerate().zip(&self.rrows).zip( + self.finished_header_rows + .iter() + .map(Some) + .chain(std::iter::repeat(None)), + ) { if self.rcols.is_empty() || rows.is_empty() { continue; @@ -347,7 +554,8 @@ impl<'a> GridLayouter<'a> { let hline_indices = rows .iter() .map(|piece| piece.y) - .chain(std::iter::once(self.grid.rows.len())); + .chain(std::iter::once(self.grid.rows.len())) + .enumerate(); // Converts a row to the corresponding index in the vector of // hlines. @@ -372,7 +580,7 @@ impl<'a> GridLayouter<'a> { }; let mut prev_y = None; - for (y, dy) in hline_indices.zip(hline_offsets) { + for ((i, y), dy) in hline_indices.zip(hline_offsets) { // Position of lines below the row index in the previous iteration. let expected_prev_line_position = prev_y .map(|prev_y| { @@ -383,47 +591,40 @@ impl<'a> GridLayouter<'a> { }) .unwrap_or(LinePosition::Before); - // FIXME: In the future, directly specify in 'self.rrows' when - // we place a repeated header rather than its original rows. - // That would let us remove most of those verbose checks, both - // in 'lines.rs' and here. Those checks also aren't fully - // accurate either, since they will also trigger when some rows - // have been removed between the header and what's below it. - let is_under_repeated_header = self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .zip(prev_y) - .is_some_and(|(header, prev_y)| { - // Note: 'y == header.end' would mean we're right below - // the NON-REPEATED header, so that case should return - // false. - prev_y < header.end && y > header.end - }); + // Header's lines at the bottom have priority when repeated. + // This will store the end bound of the last header if the + // current iteration is calculating lines under it. + let last_repeated_header_end_above = match finished_header_rows { + Some(info) if prev_y.is_some() && i == info.repeated_amount => { + Some(info.last_repeated_header_end) + } + _ => None, + }; // If some grid rows were omitted between the previous resolved // row and the current one, we ensure lines below the previous // row don't "disappear" and are considered, albeit with less // priority. However, don't do this when we're below a header, // as it must have more priority instead of less, so it is - // chained later instead of before. The exception is when the + // chained later instead of before (stored in the + // 'header_hlines' variable below). The exception is when the // last row in the header is removed, in which case we append // both the lines under the row above us and also (later) the // lines under the header's (removed) last row. - let prev_lines = prev_y - .filter(|prev_y| { - prev_y + 1 != y - && (!is_under_repeated_header - || self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| prev_y + 1 != header.end)) - }) - .map(|prev_y| get_hlines_at(prev_y + 1)) - .unwrap_or(&[]); + let prev_lines = match prev_y { + Some(prev_y) + if prev_y + 1 != y + && last_repeated_header_end_above.is_none_or( + |last_repeated_header_end| { + prev_y + 1 != last_repeated_header_end + }, + ) => + { + get_hlines_at(prev_y + 1) + } + + _ => &[], + }; let expected_hline_position = expected_line_position(y, y == self.grid.rows.len()); @@ -441,15 +642,13 @@ impl<'a> GridLayouter<'a> { }; let mut expected_header_line_position = LinePosition::Before; - let header_hlines = if let Some((Repeatable::Repeated(header), prev_y)) = - self.grid.header.as_ref().zip(prev_y) - { - if is_under_repeated_header - && (!self.grid.has_gutter + let header_hlines = match (last_repeated_header_end_above, prev_y) { + (Some(header_end_above), Some(prev_y)) + if !self.grid.has_gutter || matches!( self.grid.rows[prev_y], Sizing::Rel(length) if length.is_zero() - )) + ) => { // For lines below a header, give priority to the // lines originally below the header rather than @@ -468,15 +667,13 @@ impl<'a> GridLayouter<'a> { // column-gutter is specified, for example. In that // case, we still repeat the line under the gutter. expected_header_line_position = expected_line_position( - header.end, - header.end == self.grid.rows.len(), + header_end_above, + header_end_above == self.grid.rows.len(), ); - get_hlines_at(header.end) - } else { - &[] + get_hlines_at(header_end_above) } - } else { - &[] + + _ => &[], }; // The effective hlines to be considered at this row index are @@ -529,6 +726,7 @@ impl<'a> GridLayouter<'a> { grid, rows, local_top_y, + last_repeated_header_end_above, in_last_region, y, x, @@ -941,15 +1139,9 @@ impl<'a> GridLayouter<'a> { let frame = self.layout_single_row(engine, disambiguator, first, y)?; self.push_row(frame, y, true); - if self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| y < header.end) - { - // Add to header height. - self.header_height += first; + if let Some(row_height) = &mut self.row_state.current_row_height { + // Add to header height, as we are in a header row. + *row_height += first; } return Ok(()); @@ -958,19 +1150,21 @@ impl<'a> GridLayouter<'a> { // Expand all but the last region. // Skip the first region if the space is eaten up by an fr row. let len = resolved.len(); - for ((i, region), target) in self - .regions - .iter() - .enumerate() - .zip(&mut resolved[..len - 1]) - .skip(self.lrows.iter().any(|row| matches!(row, Row::Fr(..))) as usize) + for ((i, region), target) in + self.regions + .iter() + .enumerate() + .zip(&mut resolved[..len - 1]) + .skip(self.current.lrows.iter().any(|row| matches!(row, Row::Fr(..))) + as usize) { // Subtract header and footer heights from the region height when - // it's not the first. + // it's not the first. Ignore non-repeating headers as they only + // appear on the first region by definition. target.set_max( region.y - if i > 0 { - self.header_height + self.footer_height + self.current.repeating_header_height + self.current.footer_height } else { Abs::zero() }, @@ -1181,25 +1375,19 @@ impl<'a> GridLayouter<'a> { let resolved = v.resolve(self.styles).relative_to(self.regions.base().y); let frame = self.layout_single_row(engine, disambiguator, resolved, y)?; - if self - .grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|header| y < header.end) - { - // Add to header height. - self.header_height += resolved; + if let Some(row_height) = &mut self.row_state.current_row_height { + // Add to header height, as we are in a header row. + *row_height += resolved; } // Skip to fitting region, but only if we aren't part of an unbreakable - // row group. We use 'in_last_with_offset' so our 'in_last' call - // properly considers that a header and a footer would be added on each - // region break. + // row group. We use 'may_progress_with_repeats' to stop trying if we + // would skip to a region with the same height and where the same + // headers would be repeated. let height = frame.height(); while self.unbreakable_rows_left == 0 && !self.regions.size.y.fits(height) - && !in_last_with_offset(self.regions, self.header_height + self.footer_height) + && self.may_progress_with_repeats() { self.finish_region(engine, false)?; @@ -1323,8 +1511,13 @@ impl<'a> GridLayouter<'a> { /// will be pushed for this particular row. It can be `false` for rows /// spanning multiple regions. fn push_row(&mut self, frame: Frame, y: usize, is_last: bool) { + if !self.row_state.in_active_repeatable { + // There is now a row after the rows equipped with orphan + // prevention, so no need to keep moving them anymore. + self.flush_orphans(); + } self.regions.size.y -= frame.height(); - self.lrows.push(Row::Frame(frame, y, is_last)); + self.current.lrows.push(Row::Frame(frame, y, is_last)); } /// Finish rows for one region. @@ -1333,68 +1526,73 @@ impl<'a> GridLayouter<'a> { engine: &mut Engine, last: bool, ) -> SourceResult<()> { + // The latest rows have orphan prevention (headers) and no other rows + // were placed, so remove those rows and try again in a new region, + // unless this is the last region. + if let Some(orphan_snapshot) = self.current.lrows_orphan_snapshot.take() { + if !last { + self.current.lrows.truncate(orphan_snapshot); + self.current.repeated_header_rows = + self.current.repeated_header_rows.min(orphan_snapshot); + + if orphan_snapshot == 0 { + // Removed all repeated headers. + self.current.last_repeated_header_end = 0; + } + } + } + if self + .current .lrows .last() .is_some_and(|row| self.grid.is_gutter_track(row.index())) { // Remove the last row in the region if it is a gutter row. - self.lrows.pop().unwrap(); + self.current.lrows.pop().unwrap(); + self.current.repeated_header_rows = + self.current.repeated_header_rows.min(self.current.lrows.len()); } - // If no rows other than the footer have been laid out so far, and - // there are rows beside the footer, then don't lay it out at all. - // This check doesn't apply, and is thus overridden, when there is a - // header. - let mut footer_would_be_orphan = self.lrows.is_empty() - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) - && self - .grid - .footer - .as_ref() - .and_then(Repeatable::as_repeated) - .is_some_and(|footer| footer.start != 0); - - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - if self.grid.rows.len() > header.end - && self - .grid - .footer - .as_ref() - .and_then(Repeatable::as_repeated) - .is_none_or(|footer| footer.start != header.end) - && self.lrows.last().is_some_and(|row| row.index() < header.end) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) - { - // Header and footer would be alone in this region, but there are more - // rows beyond the header and the footer. Push an empty region. - self.lrows.clear(); - footer_would_be_orphan = true; - } - } + // If no rows other than the footer have been laid out so far + // (e.g. due to header orphan prevention), and there are rows + // beside the footer, then don't lay it out at all. + // + // It is worth noting that the footer is made non-repeatable at + // the grid resolving stage if it is short-lived, that is, if + // it is at the start of the table (or right after headers at + // the start of the table). + // + // TODO(subfooters): explicitly check for short-lived footers. + // TODO(subfooters): widow prevention for non-repeated footers with a + // similar mechanism / when implementing multiple footers. + let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated) + && self.current.lrows.is_empty() + && self.current.could_progress_at_top; let mut laid_out_footer_start = None; - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - // Don't layout the footer if it would be alone with the header in - // the page, and don't layout it twice. - if !footer_would_be_orphan - && self.lrows.iter().all(|row| row.index() < footer.start) - { - laid_out_footer_start = Some(footer.start); - self.layout_footer(footer, engine, self.finished.len())?; + if !footer_would_be_widow { + if let Some(footer) = &self.grid.footer { + // Don't layout the footer if it would be alone with the header + // in the page (hence the widow check), and don't layout it + // twice (check below). + // + // TODO(subfooters): this check can be replaced by a vector of + // repeating footers in the future, and/or some "pending + // footers" vector for footers we're about to place. + if footer.repeated + && self.current.lrows.iter().all(|row| row.index() < footer.start) + { + laid_out_footer_start = Some(footer.start); + self.layout_footer(footer, engine, self.finished.len())?; + } } } // Determine the height of existing rows in the region. let mut used = Abs::zero(); let mut fr = Fr::zero(); - for row in &self.lrows { + for row in &self.current.lrows { match row { Row::Frame(frame, _, _) => used += frame.height(), Row::Fr(v, _, _) => fr += *v, @@ -1403,9 +1601,9 @@ impl<'a> GridLayouter<'a> { // Determine the size of the grid in this region, expanding fully if // there are fr rows. - let mut size = Size::new(self.width, used).min(self.initial); - if fr.get() > 0.0 && self.initial.y.is_finite() { - size.y = self.initial.y; + let mut size = Size::new(self.width, used).min(self.current.initial); + if fr.get() > 0.0 && self.current.initial.y.is_finite() { + size.y = self.current.initial.y; } // The frame for the region. @@ -1413,9 +1611,10 @@ impl<'a> GridLayouter<'a> { let mut pos = Point::zero(); let mut rrows = vec![]; let current_region = self.finished.len(); + let mut repeated_header_row_height = Abs::zero(); // Place finished rows and layout fractional rows. - for row in std::mem::take(&mut self.lrows) { + for (i, row) in std::mem::take(&mut self.current.lrows).into_iter().enumerate() { let (frame, y, is_last) = match row { Row::Frame(frame, y, is_last) => (frame, y, is_last), Row::Fr(v, y, disambiguator) => { @@ -1426,6 +1625,9 @@ impl<'a> GridLayouter<'a> { }; let height = frame.height(); + if i < self.current.repeated_header_rows { + repeated_header_row_height += height; + } // Ensure rowspans which span this row will have enough space to // be laid out over it later. @@ -1504,7 +1706,11 @@ impl<'a> GridLayouter<'a> { // we have to check the same index again in the next // iteration. let rowspan = self.rowspans.remove(i); - self.layout_rowspan(rowspan, Some((&mut output, &rrows)), engine)?; + self.layout_rowspan( + rowspan, + Some((&mut output, repeated_header_row_height)), + engine, + )?; } else { i += 1; } @@ -1515,21 +1721,40 @@ impl<'a> GridLayouter<'a> { pos.y += height; } - self.finish_region_internal(output, rrows); + self.finish_region_internal( + output, + rrows, + FinishedHeaderRowInfo { + repeated_amount: self.current.repeated_header_rows, + last_repeated_header_end: self.current.last_repeated_header_end, + repeated_height: repeated_header_row_height, + }, + ); if !last { + self.current.repeated_header_rows = 0; + self.current.last_repeated_header_end = 0; + self.current.repeating_header_height = Abs::zero(); + self.current.repeating_header_heights.clear(); + let disambiguator = self.finished.len(); - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { + if let Some(footer) = + self.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { self.prepare_footer(footer, engine, disambiguator)?; } - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - // Add a header to the new region. - self.layout_header(header, engine, disambiguator)?; - } - // Ensure rows don't try to overrun the footer. - self.regions.size.y -= self.footer_height; + // Note that header layout will only subtract this again if it has + // to skip regions to fit headers, so there is no risk of + // subtracting this twice. + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; + + if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() { + // Add headers to the new region. + self.layout_active_headers(engine)?; + } } Ok(()) @@ -1541,11 +1766,26 @@ impl<'a> GridLayouter<'a> { &mut self, output: Frame, resolved_rows: Vec, + header_row_info: FinishedHeaderRowInfo, ) { self.finished.push(output); self.rrows.push(resolved_rows); self.regions.next(); - self.initial = self.regions.size; + self.current.initial = self.regions.size; + + // Repeats haven't been laid out yet, so in the meantime, this will + // represent the initial height after repeats laid out so far, and will + // be gradually updated when preparing footers and repeating headers. + self.current.initial_after_repeats = self.current.initial.y; + + self.current.could_progress_at_top = self.regions.may_progress(); + + if !self.grid.headers.is_empty() { + self.finished_header_rows.push(header_row_info); + } + + // Ensure orphan prevention is handled before resolving rows. + debug_assert!(self.current.lrows_orphan_snapshot.is_none()); } } @@ -1560,13 +1800,3 @@ pub(super) fn points( offset }) } - -/// Checks if the first region of a sequence of regions is the last usable -/// region, assuming that the last region will always be occupied by some -/// specific offset height, even after calling `.next()`, due to some -/// additional logic which adds content automatically on each region turn (in -/// our case, headers). -pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { - regions.backlog.is_empty() - && regions.last.is_none_or(|height| regions.size.y + offset == height) -} diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 7549673f1..d5da7e263 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -391,10 +391,12 @@ pub fn vline_stroke_at_row( /// /// This function assumes columns are sorted by increasing `x`, and rows are /// sorted by increasing `y`. +#[allow(clippy::too_many_arguments)] pub fn hline_stroke_at_column( grid: &CellGrid, rows: &[RowPiece], local_top_y: Option, + header_end_above: Option, in_last_region: bool, y: usize, x: usize, @@ -499,17 +501,15 @@ pub fn hline_stroke_at_column( // Top border stroke and header stroke are generally prioritized, unless // they don't have explicit hline overrides and one or more user-provided // hlines would appear at the same position, which then are prioritized. - let top_stroke_comes_from_header = grid - .header - .as_ref() - .and_then(Repeatable::as_repeated) - .zip(local_top_y) - .is_some_and(|(header, local_top_y)| { - // Ensure the row above us is a repeated header. - // FIXME: Make this check more robust when headers at arbitrary - // positions are added. - local_top_y < header.end && y > header.end - }); + let top_stroke_comes_from_header = header_end_above.zip(local_top_y).is_some_and( + |(last_repeated_header_end, local_top_y)| { + // Check if the last repeated header row is above this line. + // + // Note that `y == last_repeated_header_end` is impossible for a + // strictly repeated header (not in its original position). + local_top_y < last_repeated_header_end && y > last_repeated_header_end + }, + ); // Prioritize the footer's top stroke as well where applicable. let bottom_stroke_comes_from_footer = grid @@ -637,7 +637,7 @@ mod test { }, vec![], vec![], - None, + vec![], None, entries, ) @@ -1175,7 +1175,7 @@ mod test { }, vec![], vec![], - None, + vec![], None, entries, ) @@ -1268,6 +1268,7 @@ mod test { grid, &rows, y.checked_sub(1), + None, true, y, x, @@ -1461,6 +1462,7 @@ mod test { grid, &rows, y.checked_sub(1), + None, true, y, x, @@ -1506,6 +1508,7 @@ mod test { grid, &rows, if y == 4 { Some(2) } else { y.checked_sub(1) }, + None, true, y, x, diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 22d2a09ef..8db33df5e 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,57 +1,446 @@ +use std::ops::Deref; + use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; use typst_library::layout::{Abs, Axes, Frame, Regions}; -use super::layouter::GridLayouter; +use super::layouter::{GridLayouter, RowState}; use super::rowspans::UnbreakableRowGroup; -impl GridLayouter<'_> { - /// Layouts the header's rows. - /// Skips regions as necessary. - pub fn layout_header( +impl<'a> GridLayouter<'a> { + /// Checks whether a region break could help a situation where we're out of + /// space for the next row. The criteria are: + /// + /// 1. If we could progress at the top of the region, that indicates the + /// region has a backlog, or (if we're at the first region) a region break + /// is at all possible (`regions.last` is `Some()`), so that's sufficient. + /// + /// 2. Otherwise, we may progress if another region break is possible + /// (`regions.last` is still `Some()`) and non-repeating rows have been + /// placed, since that means the space they occupy will be available in the + /// next region. + #[inline] + pub fn may_progress_with_repeats(&self) -> bool { + // TODO(subfooters): check below isn't enough to detect non-repeating + // footers... we can also change 'initial_after_repeats' to stop being + // calculated if there were any non-repeating footers. + self.current.could_progress_at_top + || self.regions.last.is_some() + && self.regions.size.y != self.current.initial_after_repeats + } + + pub fn place_new_headers( + &mut self, + consecutive_header_count: &mut usize, + engine: &mut Engine, + ) -> SourceResult<()> { + *consecutive_header_count += 1; + let (consecutive_headers, new_upcoming_headers) = + self.upcoming_headers.split_at(*consecutive_header_count); + + if new_upcoming_headers.first().is_some_and(|next_header| { + consecutive_headers.last().is_none_or(|latest_header| { + !latest_header.short_lived + && next_header.range.start == latest_header.range.end + }) && !next_header.short_lived + }) { + // More headers coming, so wait until we reach them. + return Ok(()); + } + + self.upcoming_headers = new_upcoming_headers; + *consecutive_header_count = 0; + + let [first_header, ..] = consecutive_headers else { + self.flush_orphans(); + return Ok(()); + }; + + // Assuming non-conflicting headers sorted by increasing y, this must + // be the header with the lowest level (sorted by increasing levels). + let first_level = first_header.level; + + // Stop repeating conflicting headers, even if the new headers are + // short-lived or won't repeat. + // + // If we go to a new region before the new headers fit alongside their + // children (or in general, for short-lived), the old headers should + // not be displayed anymore. + let first_conflicting_pos = + self.repeating_headers.partition_point(|h| h.level < first_level); + self.repeating_headers.truncate(first_conflicting_pos); + + // Ensure upcoming rows won't see that these headers will occupy any + // space in future regions anymore. + for removed_height in + self.current.repeating_header_heights.drain(first_conflicting_pos..) + { + self.current.repeating_header_height -= removed_height; + } + + // Layout short-lived headers immediately. + if consecutive_headers.last().is_some_and(|h| h.short_lived) { + // No chance of orphans as we're immediately placing conflicting + // headers afterwards, which basically are not headers, for all intents + // and purposes. It is therefore guaranteed that all new headers have + // been placed at least once. + self.flush_orphans(); + + // Layout each conflicting header independently, without orphan + // prevention (as they don't go into 'pending_headers'). + // These headers are short-lived as they are immediately followed by a + // header of the same or lower level, such that they never actually get + // to repeat. + self.layout_new_headers(consecutive_headers, true, engine)?; + } else { + // Let's try to place pending headers at least once. + // This might be a waste as we could generate an orphan and thus have + // to try to place old and new headers all over again, but that happens + // for every new region anyway, so it's rather unavoidable. + let snapshot_created = + self.layout_new_headers(consecutive_headers, false, engine)?; + + // Queue the new headers for layout. They will remain in this + // vector due to orphan prevention. + // + // After the first subsequent row is laid out, move to repeating, as + // it's then confirmed the headers won't be moved due to orphan + // prevention anymore. + self.pending_headers = consecutive_headers; + + if !snapshot_created { + // Region probably couldn't progress. + // + // Mark new pending headers as final and ensure there isn't a + // snapshot. + self.flush_orphans(); + } + } + + Ok(()) + } + + /// Lays out rows belonging to a header, returning the calculated header + /// height only for that header. Indicates to the laid out rows that they + /// should inform their laid out heights if appropriate (auto or fixed + /// size rows only). + #[inline] + fn layout_header_rows( &mut self, header: &Header, engine: &mut Engine, disambiguator: usize, - ) -> SourceResult<()> { - let header_rows = - self.simulate_header(header, &self.regions, engine, disambiguator)?; - let mut skipped_region = false; - while self.unbreakable_rows_left == 0 - && !self.regions.size.y.fits(header_rows.height + self.footer_height) - && self.regions.may_progress() - { - // Advance regions without any output until we can place the - // header and the footer. - self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); - skipped_region = true; + as_short_lived: bool, + ) -> SourceResult { + let mut header_height = Abs::zero(); + for y in header.range.clone() { + header_height += self + .layout_row_with_state( + y, + engine, + disambiguator, + RowState { + current_row_height: Some(Abs::zero()), + in_active_repeatable: !as_short_lived, + }, + )? + .current_row_height + .unwrap_or_default(); + } + Ok(header_height) + } + + /// This function should be called each time an additional row has been + /// laid out in a region to indicate that orphan prevention has succeeded. + /// + /// It removes the current orphan snapshot and flushes pending headers, + /// such that a non-repeating header won't try to be laid out again + /// anymore, and a repeating header will begin to be part of + /// `repeating_headers`. + pub fn flush_orphans(&mut self) { + self.current.lrows_orphan_snapshot = None; + self.flush_pending_headers(); + } + + /// Indicates all currently pending headers have been successfully placed + /// once, since another row has been placed after them, so they are + /// certainly not orphans. + pub fn flush_pending_headers(&mut self) { + if self.pending_headers.is_empty() { + return; } - // Reset the header height for this region. - // It will be re-calculated when laying out each header row. - self.header_height = Abs::zero(); - - if let Some(Repeatable::Repeated(footer)) = &self.grid.footer { - if skipped_region { - // Simulate the footer again; the region's 'full' might have - // changed. - self.footer_height = self - .simulate_footer(footer, &self.regions, engine, disambiguator)? - .height; + for header in self.pending_headers { + if header.repeated { + // Vector remains sorted by increasing levels: + // - 'pending_headers' themselves are sorted, since we only + // push non-mutually-conflicting headers at a time. + // - Before pushing new pending headers in + // 'layout_new_pending_headers', we truncate repeating headers + // to remove anything with the same or higher levels as the + // first pending header. + // - Assuming it was sorted before, that truncation only keeps + // elements with a lower level. + // - Therefore, by pushing this header to the end, it will have + // a level larger than all the previous headers, and is thus + // in its 'correct' position. + self.repeating_headers.push(header); } } - // Header is unbreakable. + self.pending_headers = Default::default(); + } + + /// Lays out the rows of repeating and pending headers at the top of the + /// region. + /// + /// Assumes the footer height for the current region has already been + /// calculated. Skips regions as necessary to fit all headers and all + /// footers. + pub fn layout_active_headers(&mut self, engine: &mut Engine) -> SourceResult<()> { + // Generate different locations for content in headers across its + // repetitions by assigning a unique number for each one. + let disambiguator = self.finished.len(); + + let header_height = self.simulate_header_height( + self.repeating_headers + .iter() + .copied() + .chain(self.pending_headers.iter().map(Repeatable::deref)), + &self.regions, + engine, + disambiguator, + )?; + + // We already take the footer into account below. + // While skipping regions, footer height won't be automatically + // re-calculated until the end. + let mut skipped_region = false; + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(header_height) + && self.may_progress_with_repeats() + { + // Advance regions without any output until we can place the + // header and the footer. + self.finish_region_internal( + Frame::soft(Axes::splat(Abs::zero())), + vec![], + Default::default(), + ); + + // TODO(layout model): re-calculate heights of headers and footers + // on each region if 'full' changes? (Assuming height doesn't + // change for now...) + // + // Would remove the footer height update below (move it here). + skipped_region = true; + + self.regions.size.y -= self.current.footer_height; + self.current.initial_after_repeats = self.regions.size.y; + } + + if let Some(footer) = &self.grid.footer { + if footer.repeated && skipped_region { + // Simulate the footer again; the region's 'full' might have + // changed. + self.regions.size.y += self.current.footer_height; + self.current.footer_height = self + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height; + self.regions.size.y -= self.current.footer_height; + } + } + + let repeating_header_rows = + total_header_row_count(self.repeating_headers.iter().copied()); + + let pending_header_rows = + total_header_row_count(self.pending_headers.iter().map(Repeatable::deref)); + + // Group of headers is unbreakable. // Thus, no risk of 'finish_region' being recursively called from // within 'layout_row'. - self.unbreakable_rows_left += header.end; - for y in 0..header.end { - self.layout_row(y, engine, disambiguator)?; + self.unbreakable_rows_left += repeating_header_rows + pending_header_rows; + + self.current.last_repeated_header_end = + self.repeating_headers.last().map(|h| h.range.end).unwrap_or_default(); + + // Reset the header height for this region. + // It will be re-calculated when laying out each header row. + self.current.repeating_header_height = Abs::zero(); + self.current.repeating_header_heights.clear(); + + debug_assert!(self.current.lrows.is_empty()); + debug_assert!(self.current.lrows_orphan_snapshot.is_none()); + let may_progress = self.may_progress_with_repeats(); + + if may_progress { + // Enable orphan prevention for headers at the top of the region. + // Otherwise, we will flush pending headers below, after laying + // them out. + // + // It is very rare for this to make a difference as we're usually + // at the 'last' region after the first skip, at which the snapshot + // is handled by 'layout_new_headers'. Either way, we keep this + // here for correctness. + self.current.lrows_orphan_snapshot = Some(self.current.lrows.len()); } + + // Use indices to avoid double borrow. We don't mutate headers in + // 'layout_row' so this is fine. + let mut i = 0; + while let Some(&header) = self.repeating_headers.get(i) { + let header_height = + self.layout_header_rows(header, engine, disambiguator, false)?; + self.current.repeating_header_height += header_height; + + // We assume that this vector will be sorted according + // to increasing levels like 'repeating_headers' and + // 'pending_headers' - and, in particular, their union, as this + // vector is pushed repeating heights from both. + // + // This is guaranteed by: + // 1. We always push pending headers after repeating headers, + // as we assume they don't conflict because we remove + // conflicting repeating headers when pushing a new pending + // header. + // + // 2. We push in the same order as each. + // + // 3. This vector is also modified when pushing a new pending + // header, where we remove heights for conflicting repeating + // headers which have now stopped repeating. They are always at + // the end and new pending headers respect the existing sort, + // so the vector will remain sorted. + self.current.repeating_header_heights.push(header_height); + + i += 1; + } + + self.current.repeated_header_rows = self.current.lrows.len(); + self.current.initial_after_repeats = self.regions.size.y; + + let mut has_non_repeated_pending_header = false; + for header in self.pending_headers { + if !header.repeated { + self.current.initial_after_repeats = self.regions.size.y; + has_non_repeated_pending_header = true; + } + let header_height = + self.layout_header_rows(header, engine, disambiguator, false)?; + if header.repeated { + self.current.repeating_header_height += header_height; + self.current.repeating_header_heights.push(header_height); + } + } + + if !has_non_repeated_pending_header { + self.current.initial_after_repeats = self.regions.size.y; + } + + if !may_progress { + // Flush pending headers immediately, as placing them again later + // won't help. + self.flush_orphans(); + } + Ok(()) } + /// Lays out headers found for the first time during row layout. + /// + /// If 'short_lived' is true, these headers are immediately followed by + /// a conflicting header, so it is assumed they will not be pushed to + /// pending headers. + /// + /// Returns whether orphan prevention was successfully setup, or couldn't + /// due to short-lived headers or the region couldn't progress. + pub fn layout_new_headers( + &mut self, + headers: &'a [Repeatable
], + short_lived: bool, + engine: &mut Engine, + ) -> SourceResult { + // At first, only consider the height of the given headers. However, + // for upcoming regions, we will have to consider repeating headers as + // well. + let header_height = self.simulate_header_height( + headers.iter().map(Repeatable::deref), + &self.regions, + engine, + 0, + )?; + + while self.unbreakable_rows_left == 0 + && !self.regions.size.y.fits(header_height) + && self.may_progress_with_repeats() + { + // Note that, after the first region skip, the new headers will go + // at the top of the region, but after the repeating headers that + // remained (which will be automatically placed in 'finish_region'). + self.finish_region(engine, false)?; + } + + // Remove new headers at the end of the region if the upcoming row + // doesn't fit. + // TODO(subfooters): what if there is a footer right after it? + let should_snapshot = !short_lived + && self.current.lrows_orphan_snapshot.is_none() + && self.may_progress_with_repeats(); + + if should_snapshot { + // If we don't enter this branch while laying out non-short lived + // headers, that means we will have to immediately flush pending + // headers and mark them as final, since trying to place them in + // the next page won't help get more space. + self.current.lrows_orphan_snapshot = Some(self.current.lrows.len()); + } + + let mut at_top = self.regions.size.y == self.current.initial_after_repeats; + + self.unbreakable_rows_left += + total_header_row_count(headers.iter().map(Repeatable::deref)); + + for header in headers { + let header_height = self.layout_header_rows(header, engine, 0, false)?; + + // Only store this header height if it is actually going to + // become a pending header. Otherwise, pretend it's not a + // header... This is fine for consumers of 'header_height' as + // it is guaranteed this header won't appear in a future + // region, so multi-page rows and cells can effectively ignore + // this header. + if !short_lived && header.repeated { + self.current.repeating_header_height += header_height; + self.current.repeating_header_heights.push(header_height); + if at_top { + self.current.initial_after_repeats = self.regions.size.y; + } + } else { + at_top = false; + } + } + + Ok(should_snapshot) + } + + /// Calculates the total expected height of several headers. + pub fn simulate_header_height<'h: 'a>( + &self, + headers: impl IntoIterator, + regions: &Regions<'_>, + engine: &mut Engine, + disambiguator: usize, + ) -> SourceResult { + let mut height = Abs::zero(); + for header in headers { + height += + self.simulate_header(header, regions, engine, disambiguator)?.height; + } + Ok(height) + } + /// Simulate the header's group of rows. pub fn simulate_header( &self, @@ -66,8 +455,8 @@ impl GridLayouter<'_> { // assume that the amount of unbreakable rows following the first row // in the header will be precisely the rows in the header. self.simulate_unbreakable_row_group( - 0, - Some(header.end), + header.range.start, + Some(header.range.end - header.range.start), regions, engine, disambiguator, @@ -91,11 +480,22 @@ impl GridLayouter<'_> { { // Advance regions without any output until we can place the // footer. - self.finish_region_internal(Frame::soft(Axes::splat(Abs::zero())), vec![]); + self.finish_region_internal( + Frame::soft(Axes::splat(Abs::zero())), + vec![], + Default::default(), + ); skipped_region = true; } - self.footer_height = if skipped_region { + // TODO(subfooters): Consider resetting header height etc. if we skip + // region. (Maybe move that step to `finish_region_internal`.) + // + // That is unnecessary at the moment as 'prepare_footers' is only + // called at the start of the region, so header height is always zero + // and no headers were placed so far, but what about when we can have + // footers in the middle of the region? Let's think about this then. + self.current.footer_height = if skipped_region { // Simulate the footer again; the region's 'full' might have // changed. self.simulate_footer(footer, &self.regions, engine, disambiguator)? @@ -118,12 +518,22 @@ impl GridLayouter<'_> { // Ensure footer rows have their own height available. // Won't change much as we're creating an unbreakable row group // anyway, so this is mostly for correctness. - self.regions.size.y += self.footer_height; + self.regions.size.y += self.current.footer_height; + let repeats = self.grid.footer.as_ref().is_some_and(|f| f.repeated); let footer_len = self.grid.rows.len() - footer.start; self.unbreakable_rows_left += footer_len; + for y in footer.start..self.grid.rows.len() { - self.layout_row(y, engine, disambiguator)?; + self.layout_row_with_state( + y, + engine, + disambiguator, + RowState { + in_active_repeatable: repeats, + ..Default::default() + }, + )?; } Ok(()) @@ -144,10 +554,18 @@ impl GridLayouter<'_> { // in the footer will be precisely the rows in the footer. self.simulate_unbreakable_row_group( footer.start, - Some(self.grid.rows.len() - footer.start), + Some(footer.end - footer.start), regions, engine, disambiguator, ) } } + +/// The total amount of rows in the given list of headers. +#[inline] +pub fn total_header_row_count<'h>( + headers: impl IntoIterator, +) -> usize { + headers.into_iter().map(|h| h.range.end - h.range.start).sum() +} diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 5ab0417d8..02ea14813 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -4,7 +4,7 @@ use typst_library::foundations::Resolve; use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; -use super::layouter::{in_last_with_offset, points, Row, RowPiece}; +use super::layouter::{points, Row}; use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. @@ -90,10 +90,10 @@ pub struct CellMeasurementData<'layouter> { impl GridLayouter<'_> { /// Layout a rowspan over the already finished regions, plus the current - /// region's frame and resolved rows, if it wasn't finished yet (because - /// we're being called from `finish_region`, but note that this function is - /// also called once after all regions are finished, in which case - /// `current_region_data` is `None`). + /// region's frame and height of resolved header rows, if it wasn't + /// finished yet (because we're being called from `finish_region`, but note + /// that this function is also called once after all regions are finished, + /// in which case `current_region_data` is `None`). /// /// We need to do this only once we already know the heights of all /// spanned rows, which is only possible after laying out the last row @@ -101,7 +101,7 @@ impl GridLayouter<'_> { pub fn layout_rowspan( &mut self, rowspan_data: Rowspan, - current_region_data: Option<(&mut Frame, &[RowPiece])>, + current_region_data: Option<(&mut Frame, Abs)>, engine: &mut Engine, ) -> SourceResult<()> { let Rowspan { @@ -146,11 +146,31 @@ impl GridLayouter<'_> { // Push the layouted frames directly into the finished frames. let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; - let (current_region, current_rrows) = current_region_data.unzip(); - for ((i, finished), frame) in self + let (current_region, current_header_row_height) = current_region_data.unzip(); + + // Clever trick to process finished header rows: + // - If there are grid headers, the vector will be filled with one + // finished header row height per region, so, chaining with the height + // for the current one, we get the header row height for each region. + // + // - But if there are no grid headers, the vector will be empty, so in + // theory the regions and resolved header row heights wouldn't match. + // But that's fine - 'current_header_row_height' can only be either + // 'Some(zero)' or 'None' in such a case, and for all other rows we + // append infinite zeros. That is, in such a case, the resolved header + // row height is always zero, so that's our fallback. + let finished_header_rows = self + .finished_header_rows + .iter() + .map(|info| info.repeated_height) + .chain(current_header_row_height) + .chain(std::iter::repeat(Abs::zero())); + + for ((i, (finished, header_dy)), frame) in self .finished .iter_mut() .chain(current_region.into_iter()) + .zip(finished_header_rows) .skip(first_region) .enumerate() .zip(fragment) @@ -162,22 +182,9 @@ impl GridLayouter<'_> { } else { // The rowspan continuation starts after the header (thus, // at a position after the sum of the laid out header - // rows). - if let Some(Repeatable::Repeated(header)) = &self.grid.header { - let header_rows = self - .rrows - .get(i) - .map(Vec::as_slice) - .or(current_rrows) - .unwrap_or(&[]) - .iter() - .take_while(|row| row.y < header.end); - - header_rows.map(|row| row.height).sum() - } else { - // Without a header, start at the very top of the region. - Abs::zero() - } + // rows). Without a header, this is zero, so the rowspan can + // start at the very top of the region as usual. + header_dy }; finished.push_frame(Point::new(dx, dy), frame); @@ -231,15 +238,13 @@ impl GridLayouter<'_> { // current row is dynamic and depends on the amount of upcoming // unbreakable cells (with or without a rowspan setting). let mut amount_unbreakable_rows = None; - if let Some(Repeatable::NotRepeated(header)) = &self.grid.header { - if current_row < header.end { - // Non-repeated header, so keep it unbreakable. - amount_unbreakable_rows = Some(header.end); - } - } - if let Some(Repeatable::NotRepeated(footer)) = &self.grid.footer { - if current_row >= footer.start { + if let Some(footer) = &self.grid.footer { + if !footer.repeated && current_row >= footer.start { // Non-repeated footer, so keep it unbreakable. + // + // TODO(subfooters): This will become unnecessary + // once non-repeated footers are treated differently and + // have widow prevention. amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start); } } @@ -254,10 +259,7 @@ impl GridLayouter<'_> { // Skip to fitting region. while !self.regions.size.y.fits(row_group.height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(engine, false)?; } @@ -396,16 +398,29 @@ impl GridLayouter<'_> { // auto rows don't depend on the backlog, as they only span one // region. if breakable - && (matches!(self.grid.header, Some(Repeatable::Repeated(_))) - || matches!(self.grid.footer, Some(Repeatable::Repeated(_)))) + && (!self.repeating_headers.is_empty() + || !self.pending_headers.is_empty() + || matches!(&self.grid.footer, Some(footer) if footer.repeated)) { // Subtract header and footer height from all upcoming regions // when measuring the cell, including the last repeated region. // // This will update the 'custom_backlog' vector with the // updated heights of the upcoming regions. + // + // We predict that header height will only include that of + // repeating headers, as we can assume non-repeating headers in + // the first region have been successfully placed, unless + // something didn't fit on the first region of the auto row, + // but we will only find that out after measurement, and if + // that happens, we discard the measurement and try again. let mapped_regions = self.regions.map(&mut custom_backlog, |size| { - Size::new(size.x, size.y - self.header_height - self.footer_height) + Size::new( + size.x, + size.y + - self.current.repeating_header_height + - self.current.footer_height, + ) }); // Callees must use the custom backlog instead of the current @@ -459,6 +474,7 @@ impl GridLayouter<'_> { // Height of the rowspan covered by spanned rows in the current // region. let laid_out_height: Abs = self + .current .lrows .iter() .filter_map(|row| match row { @@ -506,7 +522,12 @@ impl GridLayouter<'_> { .iter() .copied() .chain(std::iter::once(if breakable { - self.initial.y - self.header_height - self.footer_height + // Here we are calculating the available height for a + // rowspan from the top of the current region, so + // we have to use initial header heights (note that + // header height can change in the middle of the + // region). + self.current.initial_after_repeats } else { // When measuring unbreakable auto rows, infinite // height is available for content to expand. @@ -518,11 +539,13 @@ impl GridLayouter<'_> { // rowspan's already laid out heights with the current // region's height and current backlog to ensure a good // level of accuracy in the measurements. - let backlog = self - .regions - .backlog - .iter() - .map(|&size| size - self.header_height - self.footer_height); + // + // Assume only repeating headers will survive starting at + // the next region. + let backlog = self.regions.backlog.iter().map(|&size| { + size - self.current.repeating_header_height + - self.current.footer_height + }); heights_up_to_current_region.chain(backlog).collect::>() } else { @@ -536,10 +559,10 @@ impl GridLayouter<'_> { height = *rowspan_height; backlog = None; full = rowspan_full; - last = self - .regions - .last - .map(|size| size - self.header_height - self.footer_height); + last = self.regions.last.map(|size| { + size - self.current.repeating_header_height + - self.current.footer_height + }); } else { // The rowspan started in the current region, as its vector // of heights in regions is currently empty. @@ -741,10 +764,11 @@ impl GridLayouter<'_> { simulated_regions.next(); disambiguator += 1; - // Subtract the initial header and footer height, since that's the - // height we used when subtracting from the region backlog's + // Subtract the repeating header and footer height, since that's + // the height we used when subtracting from the region backlog's // heights while measuring cells. - simulated_regions.size.y -= self.header_height + self.footer_height; + simulated_regions.size.y -= + self.current.repeating_header_height + self.current.footer_height; } if let Some(original_last_resolved_size) = last_resolved_size { @@ -876,12 +900,8 @@ impl GridLayouter<'_> { // which, when used and combined with upcoming spanned rows, covers all // of the requested rowspan height, we give up. for _attempt in 0..5 { - let rowspan_simulator = RowspanSimulator::new( - disambiguator, - simulated_regions, - self.header_height, - self.footer_height, - ); + let rowspan_simulator = + RowspanSimulator::new(disambiguator, simulated_regions, &self.current); let total_spanned_height = rowspan_simulator.simulate_rowspan_layout( y, @@ -963,7 +983,8 @@ impl GridLayouter<'_> { { extra_amount_to_grow -= simulated_regions.size.y.max(Abs::zero()); simulated_regions.next(); - simulated_regions.size.y -= self.header_height + self.footer_height; + simulated_regions.size.y -= + self.current.repeating_header_height + self.current.footer_height; disambiguator += 1; } simulated_regions.size.y -= extra_amount_to_grow; @@ -980,10 +1001,17 @@ struct RowspanSimulator<'a> { finished: usize, /// The state of regions during the simulation. regions: Regions<'a>, - /// The height of the header in the currently simulated region. + /// The total height of headers in the currently simulated region. header_height: Abs, - /// The height of the footer in the currently simulated region. + /// The total height of footers in the currently simulated region. footer_height: Abs, + /// Whether `self.regions.may_progress()` was `true` at the top of the + /// region, indicating we can progress anywhere in the current region, + /// even right after a repeated header. + could_progress_at_top: bool, + /// Available height after laying out repeated headers at the top of the + /// currently simulated region. + initial_after_repeats: Abs, /// The total spanned height so far in the simulation. total_spanned_height: Abs, /// Height of the latest spanned gutter row in the simulation. @@ -997,14 +1025,19 @@ impl<'a> RowspanSimulator<'a> { fn new( finished: usize, regions: Regions<'a>, - header_height: Abs, - footer_height: Abs, + current: &super::layouter::Current, ) -> Self { Self { finished, regions, - header_height, - footer_height, + // There can be no new headers or footers within a multi-page + // rowspan, since headers and footers are unbreakable, so + // assuming the repeating header height and footer height + // won't change is safe. + header_height: current.repeating_header_height, + footer_height: current.footer_height, + could_progress_at_top: current.could_progress_at_top, + initial_after_repeats: current.initial_after_repeats, total_spanned_height: Abs::zero(), latest_spanned_gutter_height: Abs::zero(), } @@ -1053,10 +1086,7 @@ impl<'a> RowspanSimulator<'a> { 0, )?; while !self.regions.size.y.fits(row_group.height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(layouter, engine)?; } @@ -1078,10 +1108,7 @@ impl<'a> RowspanSimulator<'a> { let mut skipped_region = false; while unbreakable_rows_left == 0 && !self.regions.size.y.fits(height) - && !in_last_with_offset( - self.regions, - self.header_height + self.footer_height, - ) + && self.may_progress_with_repeats() { self.finish_region(layouter, engine)?; @@ -1127,23 +1154,37 @@ impl<'a> RowspanSimulator<'a> { // our simulation checks what happens AFTER the auto row, so we can // just use the original backlog from `self.regions`. let disambiguator = self.finished; - let header_height = - if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { - layouter - .simulate_header(header, &self.regions, engine, disambiguator)? - .height - } else { - Abs::zero() - }; - let footer_height = - if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { - layouter - .simulate_footer(footer, &self.regions, engine, disambiguator)? - .height - } else { - Abs::zero() - }; + let (repeating_headers, header_height) = if !layouter.repeating_headers.is_empty() + || !layouter.pending_headers.is_empty() + { + // Only repeating headers have survived after the first region + // break. + let repeating_headers = layouter.repeating_headers.iter().copied().chain( + layouter.pending_headers.iter().filter_map(Repeatable::as_repeated), + ); + + let header_height = layouter.simulate_header_height( + repeating_headers.clone(), + &self.regions, + engine, + disambiguator, + )?; + + (Some(repeating_headers), header_height) + } else { + (None, Abs::zero()) + }; + + let footer_height = if let Some(footer) = + layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { + layouter + .simulate_footer(footer, &self.regions, engine, disambiguator)? + .height + } else { + Abs::zero() + }; let mut skipped_region = false; @@ -1156,19 +1197,24 @@ impl<'a> RowspanSimulator<'a> { skipped_region = true; } - if let Some(Repeatable::Repeated(header)) = &layouter.grid.header { + if let Some(repeating_headers) = repeating_headers { self.header_height = if skipped_region { // Simulate headers again, at the new region, as // the full region height may change. - layouter - .simulate_header(header, &self.regions, engine, disambiguator)? - .height + layouter.simulate_header_height( + repeating_headers, + &self.regions, + engine, + disambiguator, + )? } else { header_height }; } - if let Some(Repeatable::Repeated(footer)) = &layouter.grid.footer { + if let Some(footer) = + layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated) + { self.footer_height = if skipped_region { // Simulate footers again, at the new region, as // the full region height may change. @@ -1185,6 +1231,7 @@ impl<'a> RowspanSimulator<'a> { // header or footer (as an invariant, any rowspans spanning any header // or footer rows are fully contained within that header's or footer's rows). self.regions.size.y -= self.header_height + self.footer_height; + self.initial_after_repeats = self.regions.size.y; Ok(()) } @@ -1201,8 +1248,18 @@ impl<'a> RowspanSimulator<'a> { self.regions.next(); self.finished += 1; + self.could_progress_at_top = self.regions.may_progress(); self.simulate_header_footer_layout(layouter, engine) } + + /// Similar to [`GridLayouter::may_progress_with_repeats`] but for rowspan + /// simulation. + #[inline] + fn may_progress_with_repeats(&self) -> bool { + self.could_progress_at_top + || self.regions.last.is_some() + && self.regions.size.y != self.initial_after_repeats + } } /// Subtracts some size from the end of a vector of sizes. diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index 83a89bf8a..f65641ff1 100644 --- a/crates/typst-library/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -1,4 +1,6 @@ -use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; +use std::num::{ + NonZeroI64, NonZeroIsize, NonZeroU32, NonZeroU64, NonZeroUsize, ParseIntError, +}; use ecow::{eco_format, EcoString}; use smallvec::SmallVec; @@ -482,3 +484,16 @@ cast! { "number too large" })?, } + +cast! { + NonZeroU32, + self => Value::Int(self.get() as _), + v: i64 => v + .try_into() + .and_then(|v: u32| v.try_into()) + .map_err(|_| if v <= 0 { + "number must be positive" + } else { + "number too large" + })?, +} diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index 369df11ee..52621c647 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -1,6 +1,6 @@ pub mod resolve; -use std::num::NonZeroUsize; +use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::Arc; use comemo::Track; @@ -468,6 +468,17 @@ pub struct GridHeader { #[default(true)] pub repeat: bool, + /// The level of the header. Must not be zero. + /// + /// This allows repeating multiple headers at once. Headers with different + /// levels can repeat together, as long as they have ascending levels. + /// + /// Notably, when a header with a lower level starts repeating, all higher + /// or equal level headers stop repeating (they are "replaced" by the new + /// header). + #[default(NonZeroU32::ONE)] + pub level: NonZeroU32, + /// The cells and lines within the header. #[variadic] pub children: Vec, diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index bad25b474..baf6b7383 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -1,5 +1,5 @@ -use std::num::NonZeroUsize; -use std::ops::Range; +use std::num::{NonZeroU32, NonZeroUsize}; +use std::ops::{Deref, DerefMut, Range}; use std::sync::Arc; use ecow::eco_format; @@ -48,6 +48,7 @@ pub fn grid_to_cellgrid<'a>( let children = elem.children.iter().map(|child| match child { GridChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), + level: header.level(styles), span: header.span(), items: header.children.iter().map(resolve_item), }, @@ -101,6 +102,7 @@ pub fn table_to_cellgrid<'a>( let children = elem.children.iter().map(|child| match child { TableChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), + level: header.level(styles), span: header.span(), items: header.children.iter().map(resolve_item), }, @@ -426,8 +428,20 @@ pub struct Line { /// A repeatable grid header. Starts at the first row. #[derive(Debug)] pub struct Header { - /// The index after the last row included in this header. - pub end: usize, + /// The range of rows included in this header. + pub range: Range, + /// The header's level. + /// + /// Higher level headers repeat together with lower level headers. If a + /// lower level header stops repeating, all higher level headers do as + /// well. + pub level: u32, + /// Whether this header cannot be repeated nor should have orphan + /// prevention because it would be about to cease repetition, either + /// because it is followed by headers of conflicting levels, or because + /// it is at the end of the table (possibly followed by some footers at the + /// end). + pub short_lived: bool, } /// A repeatable grid footer. Stops at the last row. @@ -435,32 +449,56 @@ pub struct Header { pub struct Footer { /// The first row included in this footer. pub start: usize, + /// The index after the last row included in this footer. + pub end: usize, + /// The footer's level. + /// + /// Used similarly to header level. + pub level: u32, } -/// A possibly repeatable grid object. +impl Footer { + /// The footer's range of included rows. + #[inline] + pub fn range(&self) -> Range { + self.start..self.end + } +} + +/// A possibly repeatable grid child (header or footer). +/// /// It still exists even when not repeatable, but must not have additional /// considerations by grid layout, other than for consistency (such as making /// a certain group of rows unbreakable). -pub enum Repeatable { - Repeated(T), - NotRepeated(T), +pub struct Repeatable { + inner: T, + + /// Whether the user requested the child to repeat. + pub repeated: bool, +} + +impl Deref for Repeatable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Repeatable { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } impl Repeatable { - /// Gets the value inside this repeatable, regardless of whether - /// it repeats. - pub fn unwrap(&self) -> &T { - match self { - Self::Repeated(repeated) => repeated, - Self::NotRepeated(not_repeated) => not_repeated, - } - } - /// Returns `Some` if the value is repeated, `None` otherwise. + #[inline] pub fn as_repeated(&self) -> Option<&T> { - match self { - Self::Repeated(repeated) => Some(repeated), - Self::NotRepeated(_) => None, + if self.repeated { + Some(&self.inner) + } else { + None } } } @@ -617,7 +655,7 @@ impl<'a> Entry<'a> { /// Any grid child, which can be either a header or an item. pub enum ResolvableGridChild { - Header { repeat: bool, span: Span, items: I }, + Header { repeat: bool, level: NonZeroU32, span: Span, items: I }, Footer { repeat: bool, span: Span, items: I }, Item(ResolvableGridItem), } @@ -638,8 +676,8 @@ pub struct CellGrid<'a> { /// Gutter rows are not included. /// Contains up to 'rows_without_gutter.len() + 1' vectors of lines. pub hlines: Vec>, - /// The repeatable header of this grid. - pub header: Option>, + /// The repeatable headers of this grid. + pub headers: Vec>, /// The repeatable footer of this grid. pub footer: Option>, /// Whether this grid has gutters. @@ -654,7 +692,7 @@ impl<'a> CellGrid<'a> { cells: impl IntoIterator>, ) -> Self { let entries = cells.into_iter().map(Entry::Cell).collect(); - Self::new_internal(tracks, gutter, vec![], vec![], None, None, entries) + Self::new_internal(tracks, gutter, vec![], vec![], vec![], None, entries) } /// Generates the cell grid, given the tracks and resolved entries. @@ -663,7 +701,7 @@ impl<'a> CellGrid<'a> { gutter: Axes<&[Sizing]>, vlines: Vec>, hlines: Vec>, - header: Option>, + headers: Vec>, footer: Option>, entries: Vec>, ) -> Self { @@ -717,7 +755,7 @@ impl<'a> CellGrid<'a> { entries, vlines, hlines, - header, + headers, footer, has_gutter, } @@ -852,6 +890,11 @@ impl<'a> CellGrid<'a> { self.cols.len() } } + + #[inline] + pub fn has_repeated_headers(&self) -> bool { + self.headers.iter().any(|h| h.repeated) + } } /// Resolves and positions all cells in the grid before creating it. @@ -937,6 +980,12 @@ struct RowGroupData { span: Span, kind: RowGroupKind, + /// Whether this header or footer may repeat. + repeat: bool, + + /// Level of this header or footer. + repeatable_level: NonZeroU32, + /// Start of the range of indices of hlines at the top of the row group. /// This is always the first index after the last hline before we started /// building the row group - any upcoming hlines would appear at least at @@ -984,14 +1033,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> { let mut pending_vlines: Vec<(Span, Line)> = vec![]; let has_gutter = self.gutter.any(|tracks| !tracks.is_empty()); - let mut header: Option
= None; - let mut repeat_header = false; + let mut headers: Vec> = vec![]; // Stores where the footer is supposed to end, its span, and the // actual footer structure. let mut footer: Option<(usize, Span, Footer)> = None; let mut repeat_footer = false; + // If true, there has been at least one cell besides headers and + // footers. When false, footers at the end are forced to not repeat. + let mut at_least_one_cell = false; + // We can't just use the cell's index in the 'cells' vector to // determine its automatic position, since cells could have arbitrary // positions, so the position of a cell in 'cells' can differ from its @@ -1008,6 +1060,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // automatically-positioned cell. let mut auto_index: usize = 0; + // The next header after the latest auto-positioned cell. This is used + // to avoid checking for collision with headers that were already + // skipped. + let mut next_header = 0; + // We have to rebuild the grid to account for fixed cell positions. // // Create at least 'children.len()' positions, since there could be at @@ -1028,12 +1085,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { columns, &mut pending_hlines, &mut pending_vlines, - &mut header, - &mut repeat_header, + &mut headers, &mut footer, &mut repeat_footer, &mut auto_index, + &mut next_header, &mut resolved_cells, + &mut at_least_one_cell, child, )?; } @@ -1049,13 +1107,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { row_amount, )?; - let (header, footer) = self.finalize_headers_and_footers( + let footer = self.finalize_headers_and_footers( has_gutter, - header, - repeat_header, + &mut headers, footer, repeat_footer, row_amount, + at_least_one_cell, )?; Ok(CellGrid::new_internal( @@ -1063,7 +1121,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { self.gutter, vlines, hlines, - header, + headers, footer, resolved_cells, )) @@ -1083,12 +1141,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> { columns: usize, pending_hlines: &mut Vec<(Span, Line, bool)>, pending_vlines: &mut Vec<(Span, Line)>, - header: &mut Option
, - repeat_header: &mut bool, + headers: &mut Vec>, footer: &mut Option<(usize, Span, Footer)>, repeat_footer: &mut bool, auto_index: &mut usize, + next_header: &mut usize, resolved_cells: &mut Vec>>, + at_least_one_cell: &mut bool, child: ResolvableGridChild, ) -> SourceResult<()> where @@ -1112,7 +1171,32 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // position than it would usually be if it would be in a non-empty // row, so we must step a local index inside headers and footers // instead, and use a separate counter outside them. - let mut local_auto_index = *auto_index; + let local_auto_index = if matches!(child, ResolvableGridChild::Item(_)) { + auto_index + } else { + // Although 'usize' is Copy, we need to be explicit here that we + // aren't reborrowing the original auto index but rather making a + // mutable copy of it using 'clone'. + &mut (*auto_index).clone() + }; + + // NOTE: usually, if 'next_header' were to be updated inside a row + // group (indicating a header was skipped by a cell), that would + // indicate a collision between the row group and that header, which + // is an error. However, the exception is for the first auto cell of + // the row group, which may skip headers while searching for a position + // where to begin the row group in the first place. + // + // Therefore, we cannot safely share the counter in the row group with + // the counter used by auto cells outside, as it might update it in a + // valid situation, whereas it must not, since its auto cells use a + // different auto index counter and will have seen different headers, + // so we copy the next header counter while inside a row group. + let local_next_header = if matches!(child, ResolvableGridChild::Item(_)) { + next_header + } else { + &mut (*next_header).clone() + }; // The first row in which this table group can fit. // @@ -1123,23 +1207,19 @@ impl<'x> CellGridResolver<'_, '_, 'x> { let mut first_available_row = 0; let (header_footer_items, simple_item) = match child { - ResolvableGridChild::Header { repeat, span, items, .. } => { - if header.is_some() { - bail!(span, "cannot have more than one header"); - } - + ResolvableGridChild::Header { repeat, level, span, items, .. } => { row_group_data = Some(RowGroupData { range: None, span, kind: RowGroupKind::Header, + repeat, + repeatable_level: level, top_hlines_start: pending_hlines.len(), top_hlines_end: None, }); - *repeat_header = repeat; - first_available_row = - find_next_empty_row(resolved_cells, local_auto_index, columns); + find_next_empty_row(resolved_cells, *local_auto_index, columns); // If any cell in the header is automatically positioned, // have it skip to the next empty row. This is to avoid @@ -1150,7 +1230,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // latest auto-position cell, since each auto-position cell // always occupies the first available position after the // previous one. Therefore, this will be >= auto_index. - local_auto_index = first_available_row * columns; + *local_auto_index = first_available_row * columns; (Some(items), None) } @@ -1162,21 +1242,27 @@ impl<'x> CellGridResolver<'_, '_, 'x> { row_group_data = Some(RowGroupData { range: None, span, + repeat, kind: RowGroupKind::Footer, + repeatable_level: NonZeroU32::ONE, top_hlines_start: pending_hlines.len(), top_hlines_end: None, }); - *repeat_footer = repeat; - first_available_row = - find_next_empty_row(resolved_cells, local_auto_index, columns); + find_next_empty_row(resolved_cells, *local_auto_index, columns); - local_auto_index = first_available_row * columns; + *local_auto_index = first_available_row * columns; (Some(items), None) } - ResolvableGridChild::Item(item) => (None, Some(item)), + ResolvableGridChild::Item(item) => { + if matches!(item, ResolvableGridItem::Cell(_)) { + *at_least_one_cell = true; + } + + (None, Some(item)) + } }; let items = header_footer_items.into_iter().flatten().chain(simple_item); @@ -1191,7 +1277,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // gutter. skip_auto_index_through_fully_merged_rows( resolved_cells, - &mut local_auto_index, + local_auto_index, columns, ); @@ -1266,7 +1352,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // automatically positioned cell. Same for footers. local_auto_index .checked_sub(1) - .filter(|_| local_auto_index > first_available_row * columns) + .filter(|_| *local_auto_index > first_available_row * columns) .map_or(0, |last_auto_index| last_auto_index % columns + 1) }); if end.is_some_and(|end| end.get() < start) { @@ -1295,10 +1381,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> { cell_y, colspan, rowspan, - header.as_ref(), + headers, footer.as_ref(), resolved_cells, - &mut local_auto_index, + local_auto_index, + local_next_header, first_available_row, columns, row_group_data.is_some(), @@ -1350,7 +1437,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { ); if top_hlines_end.is_none() - && local_auto_index > first_available_row * columns + && *local_auto_index > first_available_row * columns { // Auto index was moved, so upcoming auto-pos hlines should // no longer appear at the top. @@ -1437,7 +1524,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> { None => { // Empty header/footer: consider the header/footer to be // at the next empty row after the latest auto index. - local_auto_index = first_available_row * columns; + *local_auto_index = first_available_row * columns; let group_start = first_available_row; let group_end = group_start + 1; @@ -1454,8 +1541,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // 'find_next_empty_row' will skip through any existing headers // and footers without having to loop through them each time. // Cells themselves, unfortunately, still have to. - assert!(resolved_cells[local_auto_index].is_none()); - resolved_cells[local_auto_index] = + assert!(resolved_cells[*local_auto_index].is_none()); + resolved_cells[*local_auto_index] = Some(Entry::Cell(self.resolve_cell( T::default(), 0, @@ -1483,21 +1570,38 @@ impl<'x> CellGridResolver<'_, '_, 'x> { match row_group.kind { RowGroupKind::Header => { - if group_range.start != 0 { - bail!( - row_group.span, - "header must start at the first row"; - hint: "remove any rows before the header" - ); - } - - *header = Some(Header { - // Later on, we have to correct this number in case there + let data = Header { + // Later on, we have to correct this range in case there // is gutter. But only once all cells have been analyzed // and the header has fully expanded in the fixup loop // below. - end: group_range.end, - }); + range: group_range, + + level: row_group.repeatable_level.get(), + + // This can only change at a later iteration, if we + // find a conflicting header or footer right away. + short_lived: false, + }; + + // Mark consecutive headers right before this one as short + // lived if they would have a higher or equal level, as + // then they would immediately stop repeating during + // layout. + let mut consecutive_header_start = data.range.start; + for conflicting_header in + headers.iter_mut().rev().take_while(move |h| { + let conflicts = h.range.end == consecutive_header_start + && h.level >= data.level; + + consecutive_header_start = h.range.start; + conflicts + }) + { + conflicting_header.short_lived = true; + } + + headers.push(Repeatable { inner: data, repeated: row_group.repeat }); } RowGroupKind::Footer => { @@ -1514,15 +1618,14 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // before the footer might not be included as part of // the footer if it is contained within the header. start: group_range.start, + end: group_range.end, + level: 1, }, )); + + *repeat_footer = row_group.repeat; } } - } else { - // The child was a single cell outside headers or footers. - // Therefore, 'local_auto_index' for this table child was - // simply an alias for 'auto_index', so we update it as needed. - *auto_index = local_auto_index; } Ok(()) @@ -1689,47 +1792,64 @@ impl<'x> CellGridResolver<'_, '_, 'x> { fn finalize_headers_and_footers( &self, has_gutter: bool, - header: Option
, - repeat_header: bool, + headers: &mut [Repeatable
], footer: Option<(usize, Span, Footer)>, repeat_footer: bool, row_amount: usize, - ) -> SourceResult<(Option>, Option>)> { - let header = header - .map(|mut header| { - // Repeat the gutter below a header (hence why we don't - // subtract 1 from the gutter case). - // Don't do this if there are no rows under the header. - if has_gutter { - // - 'header.end' is always 'last y + 1'. The header stops - // before that row. - // - Therefore, '2 * header.end' will be 2 * (last y + 1), - // which is the adjusted index of the row before which the - // header stops, meaning it will still stop right before it - // even with gutter thanks to the multiplication below. - // - This means that it will span all rows up to - // '2 * (last y + 1) - 1 = 2 * last y + 1', which equates - // to the index of the gutter row right below the header, - // which is what we want (that gutter spacing should be - // repeated across pages to maintain uniformity). - header.end *= 2; + at_least_one_cell: bool, + ) -> SourceResult>> { + // Mark consecutive headers right before the end of the table, or the + // final footer, as short lived, given that there are no normal rows + // after them, so repeating them is pointless. + // + // It is important to do this BEFORE we update header and footer ranges + // due to gutter below as 'row_amount' doesn't consider gutter. + // + // TODO(subfooters): take the last footer if it is at the end and + // backtrack through consecutive footers until the first one in the + // sequence is found. If there is no footer at the end, there are no + // haeders to turn short-lived. + let mut consecutive_header_start = + footer.as_ref().map(|(_, _, f)| f.start).unwrap_or(row_amount); + for header_at_the_end in headers.iter_mut().rev().take_while(move |h| { + let at_the_end = h.range.end == consecutive_header_start; - // If the header occupies the entire grid, ensure we don't - // include an extra gutter row when it doesn't exist, since - // the last row of the header is at the very bottom, - // therefore '2 * last y + 1' is not a valid index. - let row_amount = (2 * row_amount).saturating_sub(1); - header.end = header.end.min(row_amount); - } - header - }) - .map(|header| { - if repeat_header { - Repeatable::Repeated(header) - } else { - Repeatable::NotRepeated(header) - } - }); + consecutive_header_start = h.range.start; + at_the_end + }) { + header_at_the_end.short_lived = true; + } + + // Repeat the gutter below a header (hence why we don't + // subtract 1 from the gutter case). + // Don't do this if there are no rows under the header. + if has_gutter { + for header in &mut *headers { + // Index of first y is doubled, as each row before it + // receives a gutter row below. + header.range.start *= 2; + + // - 'header.end' is always 'last y + 1'. The header stops + // before that row. + // - Therefore, '2 * header.end' will be 2 * (last y + 1), + // which is the adjusted index of the row before which the + // header stops, meaning it will still stop right before it + // even with gutter thanks to the multiplication below. + // - This means that it will span all rows up to + // '2 * (last y + 1) - 1 = 2 * last y + 1', which equates + // to the index of the gutter row right below the header, + // which is what we want (that gutter spacing should be + // repeated across pages to maintain uniformity). + header.range.end *= 2; + + // If the header occupies the entire grid, ensure we don't + // include an extra gutter row when it doesn't exist, since + // the last row of the header is at the very bottom, + // therefore '2 * last y + 1' is not a valid index. + let row_amount = (2 * row_amount).saturating_sub(1); + header.range.end = header.range.end.min(row_amount); + } + } let footer = footer .map(|(footer_end, footer_span, mut footer)| { @@ -1737,8 +1857,17 @@ impl<'x> CellGridResolver<'_, '_, 'x> { bail!(footer_span, "footer must end at the last row"); } - let header_end = - header.as_ref().map(Repeatable::unwrap).map(|header| header.end); + // TODO(subfooters): will need a global slice of headers and + // footers for when we have multiple footers + // Alternatively, never include the gutter in the footer's + // range and manually add it later on layout. This would allow + // laying out the gutter as part of both the header and footer, + // and, if the page only has headers, the gutter row below the + // header is automatically removed (as it becomes the last), so + // only the gutter above the footer is kept, ensuring the same + // gutter row isn't laid out two times in a row. When laying + // out the footer for real, the mechanism can be disabled. + let last_header_end = headers.last().map(|header| header.range.end); if has_gutter { // Convert the footer's start index to post-gutter coordinates. @@ -1747,23 +1876,38 @@ impl<'x> CellGridResolver<'_, '_, 'x> { // Include the gutter right before the footer, unless there is // none, or the gutter is already included in the header (no // rows between the header and the footer). - if header_end != Some(footer.start) { + if last_header_end != Some(footer.start) { footer.start = footer.start.saturating_sub(1); } + + // Adapt footer end but DO NOT include the gutter below it, + // if it exists. Calculation: + // - Starts as 'last y + 1'. + // - The result will be + // 2 * (last_y + 1) - 1 = 2 * last_y + 1, + // which is the new index of the last footer row plus one, + // meaning we do exclude any gutter below this way. + // + // It also keeps us within the total amount of rows, so we + // don't need to '.min()' later. + footer.end = (2 * footer.end).saturating_sub(1); } Ok(footer) }) .transpose()? .map(|footer| { - if repeat_footer { - Repeatable::Repeated(footer) - } else { - Repeatable::NotRepeated(footer) + // Don't repeat footers when the table only has headers and + // footers. + // TODO(subfooters): Switch this to marking the last N + // consecutive footers as short lived. + Repeatable { + inner: footer, + repeated: repeat_footer && at_least_one_cell, } }); - Ok((header, footer)) + Ok(footer) } /// Resolves the cell's fields based on grid-wide properties. @@ -1934,28 +2078,28 @@ fn expand_row_group( /// Check if a cell's fixed row would conflict with a header or footer. fn check_for_conflicting_cell_row( - header: Option<&Header>, + headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, cell_y: usize, rowspan: usize, ) -> HintedStrResult<()> { - if let Some(header) = header { - // TODO: check start (right now zero, always satisfied) - if cell_y < header.end { - bail!( - "cell would conflict with header spanning the same position"; - hint: "try moving the cell or the header" - ); - } + // NOTE: y + rowspan >, not >=, header.start, to check if the rowspan + // enters the header. For example, consider a rowspan of 1: if + // `y + 1 = header.start` holds, that means `y < header.start`, and it + // only occupies one row (`y`), so the cell is actually not in + // conflict. + if headers + .iter() + .any(|header| cell_y < header.range.end && cell_y + rowspan > header.range.start) + { + bail!( + "cell would conflict with header spanning the same position"; + hint: "try moving the cell or the header" + ); } - if let Some((footer_end, _, footer)) = footer { - // NOTE: y + rowspan >, not >=, footer.start, to check if the rowspan - // enters the footer. For example, consider a rowspan of 1: if - // `y + 1 = footer.start` holds, that means `y < footer.start`, and it - // only occupies one row (`y`), so the cell is actually not in - // conflict. - if cell_y < *footer_end && cell_y + rowspan > footer.start { + if let Some((_, _, footer)) = footer { + if cell_y < footer.end && cell_y + rowspan > footer.start { bail!( "cell would conflict with footer spanning the same position"; hint: "try reducing the cell's rowspan or moving the footer" @@ -1981,10 +2125,11 @@ fn resolve_cell_position( cell_y: Smart, colspan: usize, rowspan: usize, - header: Option<&Header>, + headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, resolved_cells: &[Option], auto_index: &mut usize, + next_header: &mut usize, first_available_row: usize, columns: usize, in_row_group: bool, @@ -2005,12 +2150,14 @@ fn resolve_cell_position( // Note that the counter ignores any cells with fixed positions, // but automatically-positioned cells will avoid conflicts by // simply skipping existing cells, headers and footers. - let resolved_index = find_next_available_position::( - header, + let resolved_index = find_next_available_position( + headers, footer, resolved_cells, columns, *auto_index, + next_header, + false, )?; // Ensure the next cell with automatic position will be @@ -2046,7 +2193,7 @@ fn resolve_cell_position( // footer (but only if it isn't already in one, otherwise there // will already be a separate check). if !in_row_group { - check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; + check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; } cell_index(cell_x, cell_y) @@ -2063,12 +2210,28 @@ fn resolve_cell_position( // requested column ('Some(None)') or an out of bounds position // ('None'), in which case we'd create a new row to place this // cell in. - find_next_available_position::( - header, + find_next_available_position( + headers, footer, resolved_cells, columns, initial_index, + // Make our own copy of the 'next_header' counter, since it + // should only be updated by auto cells. However, we cannot + // start with the same value as we are searching from the + // start, and not from 'auto_index', so auto cells might + // have skipped some headers already which this cell will + // also need to skip. + // + // We could, in theory, keep a separate 'next_header' + // counter for cells with fixed columns. But then we would + // need one for every column, and much like how there isn't + // an index counter for each column either, the potential + // speed gain seems less relevant for a less used feature. + // Still, it is something to consider for the future if + // this turns out to be a bottleneck in important cases. + &mut 0, + true, ) } } @@ -2078,7 +2241,7 @@ fn resolve_cell_position( // footer (but only if it isn't already in one, otherwise there // will already be a separate check). if !in_row_group { - check_for_conflicting_cell_row(header, footer, cell_y, rowspan)?; + check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; } // Let's find the first column which has that row available. @@ -2110,13 +2273,18 @@ fn resolve_cell_position( /// Finds the first available position after the initial index in the resolved /// grid of cells. Skips any non-absent positions (positions which already /// have cells specified by the user) as well as any headers and footers. +/// +/// When `skip_rows` is true, one row is skipped on each iteration, preserving +/// the column. That is used to find a position for a fixed column cell. #[inline] -fn find_next_available_position( - header: Option<&Header>, +fn find_next_available_position( + headers: &[Repeatable
], footer: Option<&(usize, Span, Footer)>, resolved_cells: &[Option>], columns: usize, initial_index: usize, + next_header: &mut usize, + skip_rows: bool, ) -> HintedStrResult { let mut resolved_index = initial_index; @@ -2126,7 +2294,7 @@ fn find_next_available_position( // determine where this cell will be placed. An out of // bounds position (thus `None`) is also a valid new // position (only requires expanding the vector). - if SKIP_ROWS { + if skip_rows { // Skip one row at a time (cell chose its column, so we don't // change it). resolved_index = @@ -2139,24 +2307,33 @@ fn find_next_available_position( // would become impractically large before this overflows. resolved_index += 1; } - } else if let Some(header) = - header.filter(|header| resolved_index < header.end * columns) + } else if let Some(header) = headers + .get(*next_header) + .filter(|header| resolved_index >= header.range.start * columns) { // Skip header (can't place a cell inside it from outside it). - resolved_index = header.end * columns; + // No changes needed if we already passed this header (which + // also triggers this branch) - in that case, we only update the + // counter. + if resolved_index < header.range.end * columns { + resolved_index = header.range.end * columns; - if SKIP_ROWS { - // Ensure the cell's chosen column is kept after the - // header. - resolved_index += initial_index % columns; + if skip_rows { + // Ensure the cell's chosen column is kept after the + // header. + resolved_index += initial_index % columns; + } } + + // From now on, only check the headers afterwards. + *next_header += 1; } else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| { resolved_index >= footer.start * columns && resolved_index < *end * columns }) { // Skip footer, for the same reason. resolved_index = *footer_end * columns; - if SKIP_ROWS { + if skip_rows { resolved_index += initial_index % columns; } } else { diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 373230897..dcc77b0dc 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -1,4 +1,4 @@ -use std::num::NonZeroUsize; +use std::num::{NonZeroU32, NonZeroUsize}; use std::sync::Arc; use typst_utils::NonZeroExt; @@ -292,16 +292,61 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { elem(tag::tr, Content::sequence(row)) }; + // TODO(subfooters): similarly to headers, take consecutive footers from + // the end for 'tfoot'. let footer = grid.footer.map(|ft| { - let rows = rows.drain(ft.unwrap().start..); + let rows = rows.drain(ft.start..); elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) }); - let header = grid.header.map(|hd| { - let rows = rows.drain(..hd.unwrap().end); - elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row)))) - }); - let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row))); + // Store all consecutive headers at the start in 'thead'. All remaining + // headers are just 'th' rows across the table body. + let mut consecutive_header_end = 0; + let first_mid_table_header = grid + .headers + .iter() + .take_while(|hd| { + let is_consecutive = hd.range.start == consecutive_header_end; + consecutive_header_end = hd.range.end; + + is_consecutive + }) + .count(); + + let (y_offset, header) = if first_mid_table_header > 0 { + let removed_header_rows = + grid.headers.get(first_mid_table_header - 1).unwrap().range.end; + let rows = rows.drain(..removed_header_rows); + + ( + removed_header_rows, + Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))), + ) + } else { + (0, None) + }; + + // TODO: Consider improving accessibility properties of multi-level headers + // inside tables in the future, e.g. indicating which columns they are + // relative to and so on. See also: + // https://www.w3.org/WAI/tutorials/tables/multi-level/ + let mut next_header = first_mid_table_header; + let mut body = + Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| { + let y = relative_y + y_offset; + if let Some(current_header) = + grid.headers.get(next_header).filter(|h| h.range.contains(&y)) + { + if y + 1 == current_header.range.end { + next_header += 1; + } + + tr(tag::th, row) + } else { + tr(tag::td, row) + } + })); + if header.is_some() || footer.is_some() { body = elem(tag::tbody, body); } @@ -492,6 +537,17 @@ pub struct TableHeader { #[default(true)] pub repeat: bool, + /// The level of the header. Must not be zero. + /// + /// This allows repeating multiple headers at once. Headers with different + /// levels can repeat together, as long as they have ascending levels. + /// + /// Notably, when a header with a lower level starts repeating, all higher + /// or equal level headers stop repeating (they are "replaced" by the new + /// header). + #[default(NonZeroU32::ONE)] + pub level: NonZeroU32, + /// The cells and lines within the header. #[variadic] pub children: Vec, diff --git a/crates/typst-syntax/src/span.rs b/crates/typst-syntax/src/span.rs index 3618b8f2f..b383ec27f 100644 --- a/crates/typst-syntax/src/span.rs +++ b/crates/typst-syntax/src/span.rs @@ -71,10 +71,7 @@ impl Span { /// Create a span that does not point into any file. pub const fn detached() -> Self { - match NonZeroU64::new(Self::DETACHED) { - Some(v) => Self(v), - None => unreachable!(), - } + Self(NonZeroU64::new(Self::DETACHED).unwrap()) } /// Create a new span from a file id and a number. @@ -111,11 +108,9 @@ impl Span { /// Pack a file ID and the low bits into a span. const fn pack(id: FileId, low: u64) -> Self { let bits = ((id.into_raw().get() as u64) << Self::FILE_ID_SHIFT) | low; - match NonZeroU64::new(bits) { - Some(v) => Self(v), - // The file ID is non-zero. - None => unreachable!(), - } + + // The file ID is non-zero. + Self(NonZeroU64::new(bits).unwrap()) } /// Whether the span is detached. diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index b346a8096..abe6423df 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -26,7 +26,7 @@ pub use once_cell; use std::fmt::{Debug, Formatter}; use std::hash::Hash; use std::iter::{Chain, Flatten, Rev}; -use std::num::NonZeroUsize; +use std::num::{NonZeroU32, NonZeroUsize}; use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::sync::Arc; @@ -66,10 +66,11 @@ pub trait NonZeroExt { } impl NonZeroExt for NonZeroUsize { - const ONE: Self = match Self::new(1) { - Some(v) => v, - None => unreachable!(), - }; + const ONE: Self = Self::new(1).unwrap(); +} + +impl NonZeroExt for NonZeroU32 { + const ONE: Self = Self::new(1).unwrap(); } /// Extra methods for [`Arc`]. diff --git a/crates/typst-utils/src/pico.rs b/crates/typst-utils/src/pico.rs index 2c80d37de..ce43667e9 100644 --- a/crates/typst-utils/src/pico.rs +++ b/crates/typst-utils/src/pico.rs @@ -95,10 +95,7 @@ impl PicoStr { } }; - match NonZeroU64::new(value) { - Some(value) => Ok(Self(value)), - None => unreachable!(), - } + Ok(Self(NonZeroU64::new(value).unwrap())) } /// Resolve to a decoded string. diff --git a/tests/ref/grid-footer-non-repeatable-unbreakable.png b/tests/ref/grid-footer-non-repeatable-unbreakable.png new file mode 100644 index 0000000000000000000000000000000000000000..59d72201f664f253d4ead37d769a26c148473b66 GIT binary patch literal 365 zcmV-z0h0cSP)959>O9UARv13v5=HY8(@bcbh zJ{?svs%BKpsG3nVqiU$Bh6pekhNEq4daL=mwBaHlHD_;JO(CS_&4h*Cgw)jSzUxj% z&6TE|MK}Ksr#5zV>1$+gPIgfEtEY#snnU@7)ttzG_M4y@$MtS62%7VMdOu+`6IKyW zbIj>b>JI{HKE1j0Z*ZiatdJGRV6qq%HDNm8$iSCx#FbJ1H_h8lNX>!v|J{Vt6mKDH zSAWU>mxR>(%&XWlbi&YI&ITEna$`|*e9d8Guy^5TTVhnrkW&KyP3_2YTbJIr00000 LNkvXXu0mjf3g)NX literal 0 HcmV?d00001 diff --git a/tests/ref/grid-footer-repeatable-unbreakable.png b/tests/ref/grid-footer-repeatable-unbreakable.png new file mode 100644 index 0000000000000000000000000000000000000000..0fa30f773390598874caadec81d03a8bfbd76820 GIT binary patch literal 340 zcmV-a0jvIrP)qr%<|VX4`=$T z**EnaAvK5hohT)w=I@5NK7`avSpU|GVEV~h;&^k&gkh*w2r}^E#iAxVbTu-VCO#E8*d$^GB9n6O>*rV~;V|2^pip>ku@ynBNq{Zy5_KnAB} zv8Zw55l051?8MkLywp6|@RX36+j;L|38|^yOxUja9>QuK+chmE?5}TwBn%TA8ju0j mw#0<^dSp-$FxqCLq8b39-U1g;aIg&k0000*Rn_wHat8+oR#w)fOP7B6@`aa|S4c=G zCMM?beXcEiGkXVG$P>&&$hWV`J;@@9*mB3JwnT_V(VgWy_2iGt$!1 zu3o(=CMK4Zm8GJhlAoU+9UZ-M=gyfkXYSasLqFIg) z>{&@kiI%F6BS?NwD( zdU|?HOibUteXFglJ#pg1zJ2@H+1Yt`cwWAISy53@Q&Z#N;lanp$HBn?3<`dJ{tq8M z?8w%d1`Hh&PZ!6Kid%2*oXoo%Akp^lKASqLRMfML0vexf&bMr2nKflu;p5-?MJ8-A zv99tHsJ-`QQ>BXcJ(-_1ik|iH%cI3oBmewcy84yN?pL=z96EaM)4StJvF^JsuG85! zr@O-bhrmm%Zu@^bYs!j0-+l0#>4EX$i^mEC3-psr?QLGpwYX^#-#&j{5epzAT`)$SaMX@^zAD7Lss?(|KDcbL6 zo`3Y|)T=Mt@_w9s@N4V8smsrAI-D zg9!!_JZ#NS>bu;$9P`hc=7(S3TzPKwRZR%+@~f_Pl-Fer>|1>v(>ZGGp}b{H(dYNPa4IT4G_s1D|Wa8(9JfG z$N`z6^4V$iO*t$)1d)*e2%4gcgH)bZ_* sfBSBoywAP8zxT0CRq>nzMqan{StqxdENxr^%v}r&p00i_>zopr0GIBM6951J literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-and-rowspan-contiguous-1.png b/tests/ref/grid-header-and-rowspan-contiguous-1.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf2cb9ca2f1398eae307c4d89639794495ce82c GIT binary patch literal 815 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iFM>v>)tfNQeqJSKx0G|-o|NsBj*Vi{RH2ilo zQ&(4aaBy&TcGlC=Q&v_kEiH|Wjg5$iC@U)q4Gk3+7thPfD=#lsRaITQcyVK6U#Y6F)uIgmoHzIE?wH*-X0SZBP1lm$;o;7@?|C_rf=WAef|2?)zuZ~PDVyX9v&V( zK0bDKb~ZLPX=&-dfB$aTvSr4M8EI*0SFc`;j*jNy;z~+NI&c)oRUAs4ZT;g$kV%-+)?#Jp%=X&Ja zcb!T)uOIHWZrwat+bD*bRo8FJ=`p=?Tl2hq`Q6{&XFYrOA#w7$yO~>VN3MQ-wlLOy z#?saII+?GWm-qdoXL9k~_3vv>R&2AB_E&qH(*G-Ci}v>YwY|~X-yhi-_ip~>=lX@k zxwgM1O8+yd`ugeE*X;1khvn_})P1aZ{b}l(f<5(@ZK|s@KOKI5&rL*JoZ&ok^T7lI zi9i1jUcQ?a_wDw|d1s5QRrk;3OU*ccTa!Je0VK=A23CZV`pp(@`9SLy?=RO+t3JoA zoW6xWS4YD?Zc?S}XRBwWXI{^^Zn!=`tkZg~Iz literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-and-rowspan-contiguous-2.png b/tests/ref/grid-header-and-rowspan-contiguous-2.png new file mode 100644 index 0000000000000000000000000000000000000000..29bc411d1cbf611682cb463daed918d201d3b788 GIT binary patch literal 815 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iFM>v>)tfNQeqJY%@0G|-o|NsAg`0$~=zP_QM z;lHEV?Af#V`1oGFeCh1$oRE+(VZwy?_;_}9_I>;I)z;RYIB~+k!C~prrBzi`OiWDQ zzI|(NZ`ae)`|{%4_Vn~Td-m+kojcy% z-oe4aSFc`8OG}$EV}^{3OmuW~e}6w07gu3nVNz1knKNg$Y}ry$QsU+1B`q!e_wQd7 z6_xz_{H&}jF)^{Ot}Zqlq7s;Vw7E+0RBeDUH14-Zd8MMX_bjfaN^2L}f*0QmX&uh%jg0>kCC zr;B4q#jUq@j^_ynO0+$!Pdo5HO6D@}!teJKzsRwB%;|jpKg!`#(I&@>mcp9nU(Yq~ zPdV8gKkKK{q;K)-uck=0&wbA9>i=5tBe7bSVDPS(A8d~r=w?O&sPHMYWw6W{H>yjuKYTC&XF z6TI>B?(Hdk{A;WB&4=FQ@9x#@`S&EN%tG#t{yh6?sh!%IgAj#%~U^VzCd7FxP%-?o7Nx$7TB~-xkS?QVA zGp-x1PmVjjr>|1>vsD=j#GcvL3$15fXT)a|Ozt~ldGUd}8P{GC;Al3Q%+@jgci60mKVf4DH z{%QVSIf-uB>$~?#NXY-6f6v}Q%s;|ZKuo^aYRa!<1)YCio?Y5MG1T+V-8-LC9CYl; zUe(-di1?HI&}Kg4#vQf4wa+tdtf+r^>cQ;Ax$EFgn8U!3yk*+FZN_PRAP0N8`njxg HN@xNA-aTI_ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-non-repeating-orphan-prevention.png b/tests/ref/grid-header-non-repeating-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..d0dbc59748ce0e8462187568367c72b122387107 GIT binary patch literal 453 zcmeAS@N?(olHy`uVBq!ia0vp^6+k?L14uA%c^CCCFfjIbx;TbZ+y*RqZXeQh$X>hg>89Mje8Hk~*2HhJV$}T*Br<=6ar@>~ z4-S5wvi*x}6oaau``TSA=D#^mdr;<-JHPh5wC=B8{arsa*>~p7|F5`7s)pr|a_9g3 z4tb02D}DG_bVi#=y7Q-)poP)YJdsBBIo(skg+8RP+bJ;4|H@!gaQMs)MW*eCg~hw7 zj;b@2A1u{nRBu~W{`t++%zHDHJ`_K%+syIiwu>9fx0V*GeJlF%Iu3ZuPx~3I0`{N{ z?}lWccmIo>d$XkF!7^q^iFe9^7E8iA4JNHQEdv@jsG0~9}=u6{1-oD!MmDLvn{Rnn3D?czT5o{NOk@`w`ewV;>U%57`?8l zf13YSPNG}(`tH3F67v7&-?MiR^N%nU5R)&qn(`}ILFeC>XP5S0yp;(xpGl0F;ZSmDLvn{Rnn3D?czT5o{NOk@`w`ewV;>U%57`?8l zf13YSPNG}(`tH3F67v7&-?MiR^N%nU5R)&qn(`}ILFeC>XP5S0yp;(xpGl0F;ZS&ZD-1Lkf5%VV8QpMfvfojI4J8JW2Ml8^|Gccd!?Qs2 z|0~90w#ly^yt(j?zhT;}{ni&sSG{%c5PjoYTWcnGp`_T~HH?cXfc1Vv^Yv%#%iNQq z^W~22(u(kxSh;>OfBK81U78L|KV_^` zuvVJi^}!pfg<;d5M`YToCTxDtdwT2UM{7-0nf25Dm3JJF|Gesw52wX_U6aS}F7JyH zl(_7hnAZ&nkL8T#K!G#6eX+y(&7QV7^*lPVTA_UZHy@wfuuQwXm_0Yn!Fy+dx$^eU zmvlovWL&!bgU8|W^W^&JF&nv*S3O9Vy`2Ajt<&X=*C*En#;#76J#b{*d5eFW{vGkE zj7jy45R55&e)q2Z(WR5;Wj*Kluy}=hSAs>ti92G#Qi@Fc^XE@$be^ur%&yOQ@T&W* m)CUC?rxYBJV(VcpZ$5)_#*EtAZ*=B^(v7F9pUXO@geCx~>kM50 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-replace-doesnt-fit.png b/tests/ref/grid-header-replace-doesnt-fit.png new file mode 100644 index 0000000000000000000000000000000000000000..a06ce0e965d7a7984fd2985dc2af298d7eb4e287 GIT binary patch literal 559 zcmV+~0?_@5P)W-(+^l8q*YA-5)S&7GO~TG;pl^giPGuHN0Jp0~a|PY{R_#4y7QGt4l<48Jz` z9$4vdiic(8!1*t|aM&&j@d1)SIIP+DUcdQ6FSrk(Ewf=4y`P82Zw0}F`fb>>`hByh=Kz>!cD?-q zS)Y0!f#*jpZ6vVYHIP99n>$ZaN#I)5Q1}`J+);=tP{0di01ced-R>uW3s5;tG-{)k z1TIC1n*w$zI7_n5nBVX@V^{q@SOKTC>;2q^7sE) zJ^Lv)8WC9_4z}rh8bpjp9X6Jn_~fUk4Ze0C^vY&v$ya4?GWidEO8ZC4^yy84fo19$4vd ziic(8!1)ioaM&&j^8u1UIIP)tX9XlhjLdhJU!@F)g13`L!c>4{IDS8^OA0jJiGu5v zyltN^AsO(=tc({o>cdQ6FSHNgEwleFdOr@2-wJ{U_1my%_4{U1&jB#e>$ZaN#I)5(El|GxT6qPpnw<302(-@yWLL$7oc*QXw*h630#U2 zHwEl!=$hg4mSKh&W|(1y8D==BV5;zr8D^N_mw*F!9-D4e6m0tt*^+O^Q%p%CO`>4@ zt%B#P_35op3*50(4WwnmqL$kif5%OVg|nh>zH};2v^7sE* zJ^L;<8WC9_4z}r0_&fjr literal 0 HcmV?d00001 diff --git a/tests/ref/grid-header-replace.png b/tests/ref/grid-header-replace.png new file mode 100644 index 0000000000000000000000000000000000000000..dafecd7f48f6e81d378d703d92d3ec8f7d558f09 GIT binary patch literal 692 zcmV;l0!#ggP)000003QKZ~0007eNklFkL?*3O2qTwXFILl>^zbcE7k+JzffYRkUEM$-gbuyz<_IxFERQ9)Z_t z|0Z`fjR6lE1~Pwuyzlx;0{6SREhKPipzSybEITKTlE6;g-5o;|Fk+bEMSx+38D@Af z!c>QQ%rL_Ya|Xg02xlORDg)6-L>b77YN7!Ws($%{20rjc`bhJ{Xum-OTY4zq{)28q z>VNgzIV=k9%2t~y19QbAsr9NT7`YelPWf#1UZMkp!K#Zuu@)Zv3ID6j`yPvhPwtAZ zX(=tn-b)Y*=avN(eV-=8!t2h!wK4otSt8$ux=I}EF&owOQ$YBNGafQGj)>g3Y62u% zX7uT45;&s!UO)n0>xG^KHa6qDp8|fn=gyHz3K)sW7=JA?%rL_YGt9pRxW^1L%<#hC zH0B?#ZBP{KU6E(1@XZySg&o($r9K7>D?PJ!O|9tErwrY-MLvqu=g(@V=9u`@XUnOe zA}|^g3a8hWr&>1+POdLul%Wa|_f8KfSCJBtXu$cxv43NM*+hrPfx&TWIj_xLc<*FdBw+u7PFvAQp%rL_l a1^)wiJV+UqAlozm0000*0w!K~a1rE=Xd_Jeo?N!f@o}Q-yOs_(~0v51<1^f$mN%g5aSyjG8hV!&= zS-E$JTzx@GP>1@VQU<2BogjwG*035#SB0rz|N4tA1gMy*`rStlXRm;iGEWXit;;Jc zeVk3Nb+f?X>8DN>*kanOz2$%>`ij>j4){z5fCcVx1bEU+W)cVtcz1$K&aDzdvWqp^BMfln`UJ1W7cPKXMKRzqU8T0L*-1P|cgoFg) ziZ6xH(+}KC%8Y0F{)f?5Wl#NEQ_h^&nzsU+KxyR)p}qN@54MC_OgeBlAt9LQ{!y!0 z9)ITV%NGVo8(aL#@qQ3;$ZFz&$NqsTa{J$wa{hVDsSlO>du#fElM%0CrX1a_%lSv> zWtNg)Vf(ZW76Su=4>pHZJ?w96Y)of-t}FP)UgV%;KUdt>*nZUy^NPhpZA)J`GRa%K oYhzk>@5iJVJ)m8$dxbmpplQm-Q84I7?mo>NAxj@vkZr&Oz~6Sx2tT}Kkd!L1I0h5^KsUQ+s>CaZ2iKQnE==`mIld~3)bnG7Zp^+buOb9=OXGw4Z$7PN(W^3lEcC-V z^SjrBqjz?z2`vxa&FsP?Az>jgC;q!?!NW7NryK}%7wNp}yg!;rKJsOm5+e_f43A!3 zt!ILH_zc0+!rlH%`6+fL6Hl}R=ejJ?`*44f?8F1svyV+ZFikmX>*e^JQw}^+_~tRU bA=~`F-L1AqubdV?0R^BM;3C~2?nKBx(AtD}@E?hz4f|DFox2AN< z@O3R(VAuDK|Izt}pMAF1K9H+BU;ndAWZ`mV9=7I#st;P-uDh`O`}Ns9oh9%7`}Hml zq?POW+#Z~~wcC6D8BQLy>5bFlOZ;6QoEN@--sQng_pe?o>-PSy*I?SdZokb_kA{N} zftt(WgSu)RIq$o#uQ~&AiCY06vuHlHFq%6G%r?j3By8)h!Q0%*hnHGFQJqtN}_~?P~s^`C=x3+da@uz5%=&< z{WiaI1D`sb(^vHXjfyZ~!h{J6z~{Vc0)a=1xds9&!R!wNcDB}^5P0Tkn3sGf!h{Kn zz}KR81%Zc?sSW}kV`)qzJk8w~1eP7;e@22ZVZuW2z3SgW;J!RIK;TO@{R@G8^*tsM zZL9RhNDwAWSPX836$sqx$QB5ED}f>rs3_kMxMs}1GZKUe6BdK}$rA+b)_ZnP7E@{n z94#C$k+7+AZ;(hx2GCUxj41dcV!A?L{lhJRR-Fk!VC_w_MZlXxe8xHve%(9)Jo+!- Tc1)(d00000NkvXXu0mjf6li}x literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-no-orphan-prevention.png b/tests/ref/grid-subheaders-alone-no-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..17bf3fe4f5ff3f740c0251c94b008db961dde494 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^6+oQL0VEh^`bt^@siU4Qjv*Dd-rg|eI^-bYdQsTjvu%utTLj)*j)>gF?s!4Xdc&n-PB|t^Z)m(?YVnLS6MQ!L$v)mWNe>R+@o?*I zXhZ_pjQLg8p1bOt4!`mWy0xFRoqf`hANRC=2(kw?^KOQ;hXz};- z+4B#S&dY!KE&cA;`GPwi3*z0UJY!&H&KI2H`QzBTeYyuai_hPPyv6jUI!Wg%>z>+z zZ$I?+nI7AgXxXK|bMm&*A0dxat*u$ln?86Epj57TUG(;c{8yJhFZuIl;}d44J%&G$ z{vWt;Z!N3EoBrOf%VfSaG{V7I%c#ea%*@Qei9Zgq?OAq+=YO}BsG8R)-vvU>Zl5B8 z8dW`Bhs-#*dqv2MqqPyIGec%{{(kc9X9GiUqQx6VphgG?PF!Jd;J|?p2`wIj4Yn22 zzP{Ef*m}7tpz-jT^HaJ0M7@Yxb>QswKi&skv-q?=+`!HQ1ACG`aIGn}4SZr|y9X4g Mp00i_>zopr0A*vRQ~&?~ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-with-footer.png b/tests/ref/grid-subheaders-alone-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..41bf88bc4050ab6cf3efb38110bbfca682df2040 GIT binary patch literal 319 zcmeAS@N?(olHy`uVBq!ia0vp^6+rC90VEid>iI7-Ffg)tx;TbZ+)Dmo@6eEVDc1KY*VFH;MfoNbK^v=>f4%KKGA$Z|4Jc5B6qh(D#7hmSID{PCuDwI*}Y z$Jw0Rd)bdX+I6@5E|AjpweHUAOIse;y)t{F_H+)T2R)0XAHBMC{X|Ev`x!F)!QItW zY$qN$|71@2*!d@YKl`zy<6rai*^V9mzx+G@grmY?ye&tM&*5A8PlBV{e(%mN`CFby ztrJUcUE{#^uJp?$3mzhY<29|TbYb1Z)(Ex>v Mr>mdKI;Vst0B7=#3IG5A literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention.png b/tests/ref/grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..7d6cc45e08422ec2e97decd1c56ca13a6738b0d7 GIT binary patch literal 382 zcmeAS@N?(olHy`uVBq!ia0vp^6+pb414uCZPe1;Jfq~K0)5S5Q;?~=nhTcqx5(hp; zXXj}|3|Jfzv#slZI+++9Im-ynGfsg?y|$1^fdxjMRDlx8{V`tis~M~hmQ z>*WO(zc+sV&-1=_|HB>A+kaaxGT}2YDCl|MbIAPJtGi5dEWY(vdPO(X`uBM5WjxM* z?p^&^`KkMIBfdRzf43k{)-Gnw$zyH@lX)#}O+7L9ROLalKQpVo+MK+<-)E;oLpa;~ zoc9u&OEVeyZr3J9IZv}qKmvm9*%ikR95_&-UZ=^V|I+X7d;6*HL{y(D1)Q|E-T61X zQPtyh$?|iXSClM2SsHOFvt)T=_492&V_*8|R0H+FK#BS;VFLq$yPfY?c=p&;O!<0R zt6*c`?!d;wXO7S0`t#$qO=#n4e|vt$`NBX&sr(XfVDan&&*?s{bs0Za_JIP{)78&q Iol`;+0M_TLTmS$7 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-alone-with-gutter-no-orphan-prevention.png b/tests/ref/grid-subheaders-alone-with-gutter-no-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..17bf3fe4f5ff3f740c0251c94b008db961dde494 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^6+oQL0VEh^`bt^@siU4Qjv*Dd-rg|eI^-bYdQsTjvu%utTLj)*j)>gF?s!4Xdc&n-PB|t^Z)m(?YVnLS6MQ!L$v)mWNe>R+@o?*I zXhZ_pjQLg8p1bOt4!`mWy0xFRoqf`hANRC=g*KIEGZ*O8#N*(2#iLQDC>b zuAG~gzek1x+r`f>Qwy7%ZH)}H7fwIQ`&C28axzeMYsHL+Kc$(6k1}rj@uqjRCUerq z*__;a*^fNhb+`O3kka~|sU-R_YjvfEM{5$`IqrzdlEk}>f;amDof}`7h@6IpzTb@X* z6H9Mh_kNXKe4~+t(9ill5`M(dH{j43l5lALX)`G6Cc@Pgg&ebxsLQ E0Q2l`5C8xG literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-basic-non-consecutive-with-footer.png b/tests/ref/grid-subheaders-basic-non-consecutive-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..6f2a57beb3810ea4a91f861eee59c1ddd837d9b0 GIT binary patch literal 279 zcmeAS@N?(olHy`uVBq!ia0vp^6+mpt0VEjKpDfq|r0#jTIEGZ*O8#N*(2#iLQDC>b zuH4p&D@jH=|8{M8_^2VG=G(EWQy4e?INQ>Ck2$IE?z`RZn3FzET{hR%+&yGfUDfyV zd#^Jmew11}|0A>T74F0HczjNlo_hJFA!3jD=hkam8#h>(zdOFw9jK-V)2!xokcfC zRH}Y@_s_l817YzRt)KA&6VwaCo(xRMb;{}NfLOdY^M%&6fv`9??#H!917Y!rd%i^a_bKQ_ky8vu*{8Gl*!Wk7Tnl9y5j29i=DFC~jXl4OvQ@>Yo9p+qSRilUyL z;~ospFZrJv_|*5D|2ap(Kuba}!2}ab@E7p25?Vmu$!2ARz}Hk!4S|DQP6dIN;f`zj zSEu^K@w*EGZphwdOzNWH$RO}oIf1}E+gY|ozz)-EeL%n?Cup2Tx=bAd8<}MQN?neF zV9yi*&+O?k{kcSeBl1_%EC66Z@u)r^sgs!kW{>aE0|sV(hJo8d47>}JRuFjM;T;gz z*8*K2(HMI};G4$VphrR!xE4*gA@E$#?lGyeIZSF!&$1A>pD*NToCqeE;2(fL8PQ`V Tbe%SA00000NkvXXu0mjfM4^Wm literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-basic-with-footer.png b/tests/ref/grid-subheaders-basic-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..5216561464d16b3123ef947a8c3bb6017e1176c7 GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=V0VEiX-Dr9Oq>g*KIEGZ*O8#N*(2#iLQDC>b zuH4p&D@jH=|8{M8_^2VG=G(EWQy4e?INQ>Ck2$IE?z`RZfK=!2bBks(Cw^S`htcb* z`ltDSN{ylpKG5-it0WtYvt0}*d6?Fc6d3I^PV3>98tnIZ9I(8TS z?XPyw(Yt5l;CMIw)BmRp5r2%=E4;eJu=LfL!_3Ja*G~f47kxBnF5|`>^>_b20#X$} z|2J~Jl8t*l_Y}KI;oLiAPyc0zr$OEOM~s=_W=GUd;ai98Kwk57^>bP0l+XkK)WLR} literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-basic.png b/tests/ref/grid-subheaders-basic.png new file mode 100644 index 0000000000000000000000000000000000000000..5a64680757e4120bc89ebd153bf23a5593c53c8c GIT binary patch literal 210 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=P0VEg%8WVp4shOTGjv*Ddl7HAcG$dYm6xi*q zE4Q`cN|KSzzg=4%K5B@l`F8B;6vmA|&bGAPV@@i(`)>C;Al3Q%+@jgci60mKVf4DH z{%QVSIf-uB>$~?#NXY-6f6v}Q%s;|ZKuo^aYRa!<1)YCio?Y567-pS2YkRGOj@^ZS z`>P#v^zIosINpu_^#5r?#2@4JN#`?WXTuyIVa~u{{%wZoLVd6IAjf*T`njxgN@xNA Dt|(dA literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-colorful.png b/tests/ref/grid-subheaders-colorful.png new file mode 100644 index 0000000000000000000000000000000000000000..38bb4ad8b88a2f3735b6f7eac0863b2e9102df58 GIT binary patch literal 11005 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 JY8kVz{{uIvZ$tn9 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-demo.png b/tests/ref/grid-subheaders-demo.png new file mode 100644 index 0000000000000000000000000000000000000000..ec1cd998c9aa4e74c7ecc16758a7c172d998cebd GIT binary patch literal 5064 zcmY+IbyO5u*N10@?vR0@K_op#{F z0NhDoOwhiPUOHu$8-4FKx@}xO zX@C6N?BYt2T<2QF5_IQG7@vK~_S&hixz^1uC2ppCxah-0#PINeQwpEv@?urg)K@;G zeJ>q9iFdpoK-7xlh%kb_w&TLN?yxQJJ1#$o`2illNp+ojV>Uzm@{jpHrva05+F0He zqb4X1qvhzFwd`XPbV*@CSh5&F;;0sZuJ-awg$3Z+H*IU=1>5?V+4!`b6>Br_i4RF{1hGoP!Sp73wI|b?*+}kd!eXE! zTY-oKK}KQgzKw%sCDIQxSN(@=!fCK73nKyRfa%o4H~3djXvM!TTb~NFsUQ>hhO~^d zi9s^JLcVCxGB?M|Ea6R67lwoz-Me(woPuu7@;C_N<+k(Zag8{v)3UrFF_H>io08WE zDv1~6a@(Ys`W+u@I9P_i;o6;Sk)_HjQ6;SRsZ03k;zVux{XjFoa`9{LGdc4%&E9)M zf5zw7m^XodG!*v`gdt?~5^S$0t<|r^n0^x?^1(uCa`8PmjtJwpvFUDCH{Z9kIX7eI z>o;f1(fLTKV=-wv5!DMD9=Ya3Pd+8;njKqTF#yG1Ri8TrGzS7+`uOx+Zig71nxyG# z)`pPLtQ+}%TO~rU5b@%z)`C-hVkVt4pbV zi;>kb1D(f+@F=6co5a_7nOAlw+lFGCpY0W`d{Oy|;44cXor?yW=|f9*E6R&|GU7Af zC+SU$HTdy_S;jwlDvs{aGhietKyj2EbDLHtc8z3&481>%L4H-$N@eA3J;7pQ{py02 zORa-WjeJk9Zs$)O5m|r?CwHsD(F+nkFcFj@fb>4T$mMeR0l_Ya*Pi)8c z4!MaeWR13XNI(}Ay8&6xgm@^hOHiCDdpze{OE&dh7adcT=#xlZsQI{rWrNa67~D&B z9IUN;@ScI9AsI|HFGK1DL<7+10uD4apaFnSjos9rdI}XQ#@h-!G7B|-BGWq_cBjdT zRg`;;uX1Tg%&qM;_GFGyz6`7TGUs8SDx~tP*X*1(N|G(p=d)ymIO~jV#HW~8o?T=H zx1;O`dp%fUPZ?XV3#&SeI~qV4$4 zQGwyALl_Ncdg9T}!!EnroloEjPsWOHVgsL*s!DpvfUc~E?m0%#U*TI9$B4`jiz1R# z`D!uj8h6;@9DE#&sTPU`A!ypfw@6Ri0@m0Q6VT|Leso5}Crwnn5K%Wb;k|A`rMrD{ zRIRwmi;!%#AAH0IpV#*-&X@M@MC7^>$hXMKUV$m8AG~@stoX38DW+5sX}dS4Kg2h< zA;xe%yvJ;QJ#H9xbloS$fRc%yrzauLueG0+Jv$LXQE}ss0rFe7|3D-CKbE9SU3ug~ zZz!7z^`e(N9Q{eyqzJc{-OJ$Xzm{#Hk6g|Cf$xdqre5KFG^C;%X(31T+llDK1iEpE zi*hNTh1PZF1v;ksyUzwTkMMRze;*Ft*+3dqJ4^Y>GPP+uVG#OF*J=)~krdz$_giGN!FQy!jYn?b53L+M-( zju(sxL-*Bu1$O?JRPLq3ct|GdRKX|yrK_bb&wX#R?| zQSdq%Cu6ZV)-D@O2oMhmx#~#Wyo=Ugr<}EEnR_uAsNMIaBhd{%pXX81k+o3^nAr`? zTwC^6x#)M==kM$+31Q4GO_|RF)@0TePRud6t>&AtRy8(r-q{E>pE|Cu_)MD(tqQPsTVPxOO#V8UT(%rhrLr6O*f8(XI zV#c8K4>-k~z#yZ%A@UJj{O1cBf}w%l*mwt4>q~R;r_O`4TpnJ}yk9JwSU28+D~=?-ikqW;n66>1)b^F?NDa$5I!`5z>_ojb-221s+ATrgv&t z(!@u9aO{XVqUF>Mq z;#&gPeD|c7fUC+!7(h+`VT6rjf&XC+HvmyMExeN&1}aAIVfzVu0etJwaP2=nm7bF7 z-FF{+em0K#uz!MAueDh2mD+q1N0qkoRRULraVvy$_Sv8#P0?l4=(0-ZZ6f-KiK%FX z3E=T@Lc{VTU}*VC_Or+hnYM4&hHRNkU$mhyuOAL$nu}%kQ$gB{ zc6=Ua2#Ln6B+FJntwKdf>>rG5v_byp@XL%v@SkzAw z12>f57(2){oqkiHHYh+#_Oa(fDT2>YKE{B6+3P5AxhR;440e!3ib)oGF@K8l-94`S zWQB{!JpMcOu?Jg-Ld9b`p(dW`=NW%+;$Tvzf}dPJ*H|HJG^VfWv?|u@O+}K7q>@fN#q>?Q_EY7w<;jh!#(r018?WQ2KH!I!u!&B&vvh}!P#8G%2Gupl5AI7L#oKUzK z2fo(XsK7QNwoQ&Cr}97>N3-|c8R_hQHOf9}`XgUm8-tJ^-XM<)JHZkAiSe=^0k6Cz8}wTpP?B!*anYEHsWpFV%cM4bj8eKHE+=_ z{MEVe-12G2OpI}{EgdjN@oVkq4m-nTjp~W{I8^|%t*yo*ioOL3r;QSXXmBL`d{B4a zVM35{;$q89&{ptFceK5va@<>FPw!>U+(?sdKfhLBEQ5&9n#~9M<(^ri8v2L+V@aV+ zchg*;OeCdTzvCN%jG&4Bxq}%3VKkJLE#|O|37JUaR^K5LlT!QT(%EUwap%7Ix>R1a zJvddw+=xELI98(6WJ>`#A#ZID?? z{WVj zmWMzo?*SOIEy(lYuHPxzZsB6d(!G*pDupWS3FS$JBRTk)^H+5FAiViA)?H*)hmKoj zvfJ?lQi59FUHbPEt?}h&XX_}L2QN>pRrkc#`>KNe#9ArT6xp5H^-qGHPa&lHUeN@8 z#pAnSJbUXY1AR9v#d+>k6L=hpQ+zk3(eu$qDFW2qa+jy-!hCsLFA7Di2`D0o6Y`|J zWLOI3+(8Hpo*YDye!NU1v0U)xasQ%Cq=hTVb(IRNYs_`YC9rDX1eP^HALp<5 zv5~z7$Fc8{kKx^cEaOqf0HZK?jqwCqBooR!Q+cF|4ec2^MZ(36?p=mKT_YxxA~9L? z!U|EL}(N38X>oZN$dGc0sZ(eTRV=JKJ*FdF zy4$t&HJYL+!D>(aL}bVf z?{HxPq!1w{+vhf`zWh8~P2$oSI#GPA9D@67P8m@f-e^X7w{M##&am~m(zNo_9yR=t zo!|54I$dhp0O1>Y<8s;Xqj}RXyGVJ`)|=>}cH5oHw`DgO?3^jF?YCrgIs6w{{|C=@ zMGC4a1o5jdl5fIvPLi!3V`xnM?u#;1TkTTw5-Hi@X1V{;S3493h>>Bk)J+&fLicY( z&n7#jWgK%o)n8N03-j(dND2sWj)9Z? zf^>)i2GlF=OYhV6gqZ@-Kei}`;hYL0=_G;3PVsp?Img%(e-%!>d3fL3IFjov%1Ss^ zHS5ua@TrOGcin)y&-vc3+D-jpk%<5yn++8Hs}DrO%3GDrxGt;0rwHmb=|S$eeAtDw z(3xEB^MF!`r(Eo}P`1O4LlSt5(7)KCBR#CGs`Elu?eE2%>(}G< zYBybuHc_wBe=$PXCCny5HE}sn?iz)LeBUWkJAs{-JojA&zD$P2L6RLbb~9pQvn`W2 zw4UUs=&zfw-U^6tjqH#-ClZ%0tNlC>K=PIh?IXPEjVdw%j$k5(9QASp-xA~PFfCqB zONyp)k)ZGt8vmMg=kVPw&b$q}4%5F3f(j^e;Mn)I>munygcnT#I{HL%GLue78P6Fp zY|q0<>EVv1o?fk;W%H0hu0^;E2JU?mG|}#WHd;WpTl4W4wxxXzCd-^~v&K@tR8RW1 j*7}$1`nQR)!QBuMBd+Bi5Ip<)&kpcdSxf0X+%oh(-6nnI literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row-right-after-with-footer.png b/tests/ref/grid-subheaders-multi-page-row-right-after-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..119a2c22b0e9484962bc258edd4a7fb488f092b5 GIT binary patch literal 1207 zcmV;o1W5adP)FMd#*498kKp7bsczAegYirNX&+F^!$;rvSzP>LnFH%xc zPEJnZ;^I0wI&E!jhlhu>w6vL-ne_DZ;Nalf+uMPGfni}`_xJZ9At5$4Hqg+}jg5`_ z`}^kR=0-+FudlCJSy{os!L_xue0+R4IXQWGdCbhrn3$McTwLep=dG=+*VosMj*i^i z+}hgOS65eqgM+xZxNK}}u&}U7N=k-?hU7Xhuq-7}QBlgu%Duh4X=!Qm^Yg2#tIp2O zh=_>l>gq#7Ln$dK^*%H4@$r(9k`WOR;o;$;qN3m5-!wEdUteG4<>e?SC}w76V`F0` zCMGd4F(4oyH#ax#?(SV(T`VjtJUl!}NlB5Bk!WaWv9Yo6@bIjxta^HSq@<*jl$4*J zpMrvdg@uKqqoZ+gao*nE{QUftm6g=g)KgPaDk>^gR#vB{r#m}4etv$tyStZ{mu_xu zv$M0+)zyiKiE?ssxw*OB-QD^5`RM5A`1tr`Wo1W4M@vgfp`oFVkB_RVs*sS7*x12L?Ck8#&CNkULH73crKP3W+1d5=_5S|;00000_NVdy00MVOL_t(|+U?p^ zS6e|Ch2h;O5~RTzS^~64i%Y%K-QA74QN!J(1q#FejAScQbip_SzXQpr2V_*f|f^iDh9CLqB(V}gh~ zMNvq&d(Sm(gMjLMx2+Nabb83pO-=Y^!r$;w5xVOBM?zuiLkO^!4D=(#j6+1yI+70w z6LFXzVRQsCj6c%SAz|D97-pDZh8bpj{B9(oE#a>a-aHWzhh!X*ZK@$zcvWafW)@q%=no!{39lbMkz4#_wq(<1!r z)=h~p;Jj^yjIaU;1B0Ix_$)aFW8-{&E^L-17qk$6g#~4C2{jG)5(;|oq@+%HnuEgXRjusSNcK>VKl86?OFvXrObbY8b{q9J$u)vwg1< z)|%S#qtJw2frMmPmV%~}2E>9*Q56`AKoe*5=c<%Yqy31ztoeM~W$a%GFG33q#}HOt zSdjh!Z%}zjZHX_$TN4|fUyf5Fikm|p^?_(4aJ_v*RPe>>@VAUzn4~*_$Ox{ zKm%=8uByPrBud+#yXtCjA%uea(7+mGEWY=PedDHBxMvPBW@ifd*hfS}L_|cik$;(T VJ^Ny!_+9`2002ovPDHLkV1fl)U(5gi literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row-right-after.png b/tests/ref/grid-subheaders-multi-page-row-right-after.png new file mode 100644 index 0000000000000000000000000000000000000000..c9e30869c4111d653e4a9045c70bd03108b62d61 GIT binary patch literal 1127 zcmV-t1ep7YP)StYpW@=;US3|&(b0f_fU>f(o}Qkgqoaq1hu`1dy}i9@X=$mcsc~^} zQ&Ur2U0qsQT5xc1=H}+x+uO>@%0WRvudlDRwzkjD&&kQju&}T^JUo_xJbx{ryc%O(Y~F zsHmu1TwIKdjE;_u@bK^(8ylRQob>ec`#&|xGb-HN+?$)5>pL+X9v)y|V5_UE$H&K@ zprA}lOlD?gjg5_hf`T+OG?|&1XlQ7LhKA?o=T=r$`uh63yu9k_>ZhltuCA_XYHE^_ zlEJ~j_V)HSH#aOSEJj8~Zf*(m{>FMd0mzQ&MbB~XYbaZsX!^8Rc`K6_$&CSjA z_4WMx{Mp&r|3NnM%CzeM00KWrL_t(|+U?rMQ(93R#qkrGf=U-qBm%Z*VlU~v_uhLq zz1oc?LShIO1SyZ2KQ%M%;y&+ZhVxmx?RVbY-MI%uMD#x?!WFNVcJdWSK>wAz6jk@j!N~0d-#oJ(l1jA-jK|*Q{GSV5WLPGXyXoQm+h8bpcev_W?*ZPGC(n)Cxtl%Ed-d8-yL0apRM6e&Y(zYG79Hay tWuLnc^rJn>`vwsa5fKp)5fKr|!yiHa)bcA|uw4KE002ovPDHLkV1i+!I$i(( literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row-with-footer.png b/tests/ref/grid-subheaders-multi-page-row-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..a440eac40523ab7dc093fe540dee131366f606b1 GIT binary patch literal 1345 zcmV-H1-|-;P)8SpI={J?d|Qbu&~q9)1soH zm6esfy}fO1ZR_jndwY9`h=^ljW6H|PprD|Ff`X=|raL=3<>loW85yjstYTtft*xy< zKtSs1>RMV_tE;QJy1I#piC$h_X=!PwsHiF`Dr#zKzP`RMFE8HS-qzOE*VorsSy_#Z zjq~&KaBy(q;^LN;mU?=6wzjr1GBTW;oEsY(adB}pG&DXwK2A9R(PfScq zl9H0e#l_Cf&QnuUii(O>R#sC@3hDl$3*mgP53@+uPfH zeSKYBU14Ei#Kgo+O-)KlN;Ne#q@<*qo13+@wRU!P;NakfhK6fvYq+?$zrVlZINJvQJfPg|mLYJ49 zrKP2di;K6nx6RGX*x1FM?L_4fAmK|w*n z!otSJ#&2(L$jHb5002(Q6gmI^0~Sd{K~#9!?b=maR8bfO;N`$Dz%UX+t8_Qmq=Jgw z-GzY?N=r*9-5oRjQ!mf3f57(vv);RPwVrb>_Ql={L_~CuY)()7g@(ELg(YZMUJO49 z4UrKpCp1JA9zPZUe^TNKtp^R- zQgPdJD0ti1&C`#;lm9n3q@nF#CJl+u1Buwc0DZK z^+ziZu`?Te!?*a!Sd&n5L`>7(qdUD160TQ`HSaB+=!S%mJeKjO=IF*KE zJ09rpdScx=#oNC}Ms>azQ^88bi$PGZJGrxNhk`%LD>nF-{()I&=kzBI1yYLoy;F4#_wq zk*|Va}C?m{aMj2~P`*|~pI9HeyZ^pwdB-9pYMiEoF ziR)$*F>ZIRnNdb!#HtxZeE-(&*b}oI5Has*Yxa$^gU=x2F?0-m3{un zM$XSbGm6O1+ln`%h_q8dnPxoPqGF0t%_t%+_DZ@LWn8*!Mj4?;H=@iaqM-4$(>LZN zB|}E%W)d{mMxf&zAFicrv_MB{2OqA9h=_=Yh)CCeRs*%|C#X8f00000NkvXXu0mjf D`7EVb literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-row.png b/tests/ref/grid-subheaders-multi-page-row.png new file mode 100644 index 0000000000000000000000000000000000000000..637ca3fb10f78af61372f9fdc88a74a7b3a19d0a GIT binary patch literal 1173 zcmV;G1Zw+U3^`T5=5-TeIg!otFrmzSBDnMX%Qxw*NCiHUM@a#U1Q`1ttJ($fC^ z{+7AJoyy9} z)6>(Yrl$7x_NS+(goK3SuUGVVmYinzil$2>{X zDk>^!YHF30l}JcPq@<*Fc6Qg-*I8Lvjg5`7v$Jk)Zg_Zjyu7^c@9$SvSG2UWNl8h> z#KgmX?;}FMc9OG}H3i-(7Y)YR1b`}?D#qvq!3+1c6s{r&$zHit9?(EtDfi%CR5RCwC$+C^7e zVHAbo-56eiyIZMHv`XFGixg^5>PV5|9vqSz>F@3ECf}TM+GIboJ$hF%xGQo%L`46S ziSeo5&@lO{a2p!d^9%N&VJp{@1`UCd^rK-Uk_Voo&MJF0dLmUGNO;)oUI^Q>__kGA z2@wU)XAtF1Rk;NbHS?2*QcxL$h|<=T?k?EeSLPt%W}CO`6{2K7hQo_f~7RLfHG*8rg$F7+b^DPzi^g)#`-N1D$Z^4V|#(8o&n# z!wfUbFvAQpEF0!OfJ8*5BTD$r6A|%{jE7|Z)FIhon$jUzvJ*NS&XA=O{yF=7MduYG zWl1TVp9cl5`Q#!m6#Q7v%0^<@EY5+3RX+}(Vfiz3EG=RW8r&ZT24Uk*iVQQ%FvAQp z%rL_YGt4l<3^U9y!@O=HA|f7=@sNy&h=*i6B;z3&56O5)#zQh5lJSs?hh*_CVooXN z&JS#xQ$*v1H$HR9sJs|4r-+)0`Z9Bhs6C!nYR>o;sID6^r-)$a{ir!*ynSa*8AyO7=9ICyWlkA$U#yen n6frxtTVhOpIwB$>qQmePlc&co57D5?00000NkvXXu0mjfH6mT? literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan-gutter.png b/tests/ref/grid-subheaders-multi-page-rowspan-gutter.png new file mode 100644 index 0000000000000000000000000000000000000000..53beeb02e4183db6499534946ce75b5180d79e52 GIT binary patch literal 1560 zcmV+z2Iu*SP)wlL_|b3Ha6DQ)<{T5aBy(N#l_p(+s@9; zK|w($CnrEaK!SpT#KgqCy}gW#jP331@bK_7G&Fg6d2MZNTU%TD`uduhnj#`17#J9t znVGS%v1w^(?(Xh|g@x+kRH^z`&SJw4*$;x#ojr>CbQBO^02GtA7)O-)T185uD# zF-l5G!NI|-tgJXVIBIHYVq#*Ro}QMLmXni{;NakVe0*A3TBD<*=jZ49`}=iub==(C zj*gDW$;syC=AE6LqN1XEdwa03u-Dhu;o;%3va)k?b98icrlzLx@$rw3kByCuSy@@j z%gY7^231v6>FMd{=;(@yit_UE)6>(bsi}j5gIHKt-{0SagoL!TwD|b={QUgn_3U)YQ~yXlR$0m(bAAFE1|#2M4IAs8?54|3Nl-+>KQL00XE=L_t(|+U?qDQ&VRc z#__Ahh)D<`2|GwC$|8shwHBo;uDEOWec$(eb!(Mc!78GtAjlS3k{nBlCHV0=sWXX> zhj;QkH__`i!<+lfeV@XFwe8A<-KC+H}}82Hb1~;C~Hs z9_V4+QCWcHIo#upwS;DFH6A|nO2mQ|6SJy`|4&1+$)CGieB}@k5z)eUOb9=DA|g&; zIe}eN6WGuip$TmLI)MqS?4E0VZEb}}xD0hgQhTm3q435PFfU%RbxT9y{t0YCXacJk z=kGHjT6C`bUJMBZStx;ot5L|fehniK;3=-09OKOS(Pj0czf)o(Q040VL1diZk6>=f zP&l=U?HDgKK2AukVmk+|Gv`f*yK;bIM-zP)Oox-J*p3r`b;*U{bB4mHRqWoTK=90} zz7t(%4TbF!SVLhuh8bSK@S*M7M8f&S!8VaFin@Om37<-QepDo!IC=5AYCcX3GrSPt zz(%3)3tpjcc4+4gnedSpTla~Cy~x`y6#k-EAZ!)3p6ZFESjNH=L$gD(X)eCGGCb^9 zrgGR$JE27|OBJ-8^7%(7BH{#=6Idc5;sllxSWaL$f#n326Idc5PGC8KZNxJG0000< KMNUMnLSTZPN*i4O literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan-right-after.png b/tests/ref/grid-subheaders-multi-page-rowspan-right-after.png new file mode 100644 index 0000000000000000000000000000000000000000..5fe1eacf2221bbddcc5933baeb8cf9ad3ab4767c GIT binary patch literal 1421 zcmV;81#+7khsnpce*x1;Mi;K6nx1OG!va+&hXlVWY{o>-{qobpEczAbrcW!QO ze}8{*adB&FYw79f+S=MyR#u0Hhu`1d%*@P*iHSTsJeir9yu7^d@bImzt;)*Ewzjs< z&(C9HW3H~QK|w)aU|_$$zley4q@<*Fc6OAMlpzLXJ=&tW@b!GOgT9@ z#KgoiGc(}e;Cp*}_xJZCBqZ(a?JX@WJv}}9KQ;99^p1{>oSdA5goL`fy2>*uQBhH* zrl#vVF|4et<>lo~O-=9b?>;_0jEsyXCMHi$Pgz-6(9qCIN=hjyDdXegFE1}HE-uBz z#jC5UprD|$v$MXwz9J$bH#aw2TwGUISA~Uz$H&Lp+uNO;oisEwEG#U7f`YHFuW)d1 zT3TALu&{=PhPb%6o12??d3nLX!A3?#l9G~|nwqDlr)6bjYHDiL)zxHVWWBw;fPjF} z(b1}^s`&W$US3`*Dk@!FT~kw2_V)JZ=;)W1myeH+b8~Y`OG}}lp^%V}=H}+Kw6w{| z$!Te6xw*M=a&mrten&?~ySuyH-QD{7`uX|!pP!#}bacbR!`a!{`}_O+{QUnxHZdQB zasU7W3rR#lRCwC$+E-H(VE~2UV-g_-goHq%lu!h0pkOZu_JY0l-h1zr4$`YAy>}ud zBv}%Fn={-vk{R#z`*1kV#ooTN^9TC-Aj9amV+=A_ zi++wwVm5i;@S4Lg-Jrv{!|a8c?zcG)Ok%pB&z;t&38<=nX$XAvEwT7RU8_Tb;H%v3 zg1Q>)U2S^!+UkJ1bgeD=X!uHo$~0QQmpA{N>fsJkGkm0egzDjTtWzL?MvJt~lEPbd zLxQ!5>1mi{ypSSzOH3OWQ2!x&}l@5 zm&61ib(J6H>Yx=F-Wz30L{^J-Ty%IK0?53Qc2F-mT(Jlfmo5Vsu7}P~V7PN>!X8w% z*S^kv8~DmD-~=Y4B|1pf040hnDPBf=p= z;4&c6tBM`ZABnTq(tVgfnO2>OG3Sn-?X=B_jh|pUHx01XMxf`c`4P3YgLCRmQ z4UJn6dhUEm>LF*+3fsf{JnN?_2r#_Hx*TDa6Ii`|Qdt!pY-vEAgYO$eL_|bHL_|bH bBp-eQltzz?4j@AQ00000NkvXXu0mjf^hMJa literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan-with-footer.png b/tests/ref/grid-subheaders-multi-page-rowspan-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..b91046675c4e7b691ee56ce05c30bed9dd747eb5 GIT binary patch literal 1190 zcmV;X1X=ruP)+1bLv!tUFMdaySsjVespwnw6wH@goKZekNNres;a7Ta&o!3xr2j) z+S=M*Utg7#l`1MKtgNiOyu6>EpOTW2-rnAGb92DJz=wy2ZfKPvGcz;b;NT)6B2-jVA0HpJwY9}EDa*^t@bK{Q@$vNZ^oWRv&d$!1l$2|0Yb7Nm z!^6XyCm?ej80zZk#KgpAW@b4#Ia5LnFYoW~c6N5-*J3Bk=?d|gN^8Nk&V`F2WprE9rq{ha^LPA2Orlxy)dl?xS`}_NXf`aDe=J@#d z($dnmx3`Oni<_I9{QUf~va;*z>p(z2o}QlM$Y8y4#?L_6!U6AVAZ>c~dAnehUId z?%Z}F&jcYNIc^+ptQGCGf~tU^S&+km`M^xbXBK9w-~KOM|I)tLF5eX0?x zs4B`nl~`QZ)AX-%^3lWR2K-0A{)Ru=eSudU65-mN_!(CiPOU}E7=CXKs16&~QK*07 zcD+^|*0loFk1-F`;now$t2gdi^r7LC80c3RhRM+g&1^!rtM)>j#8Yi@;9^{5*f2eY zvL%D^|M>jS;2RvDm^!UG>^K693^>jGs>2=|kmv$%%Aq!F@3A=%_C-QNM`v1^6@~!| z0DlUGvDmfiO2e5LW;iFq*jO)xh$)mog7)%C zh5WJ+S%rkoeOQBpy|s{0SA!}@sNa8}4QYHs&oILbGt4l<3^U9y!wfUbFvA>@aY)7? z8HZ#Xk`WPyWE_$a5phVyAsL5c9FlQJ#vvJpWE_%lNX9qx3^UB|mJeUK|3D;+Ug#kq z-qQ#XiF3tbAH4D`eECu%M05}8`C5dCh=_=Yh$uh&22a+>M5^?6$^ZZW07*qoM6N<$ Eg8dy_>;M1& literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-multi-page-rowspan.png b/tests/ref/grid-subheaders-multi-page-rowspan.png new file mode 100644 index 0000000000000000000000000000000000000000..342b05695b9a8dd07766cad52ab259497b9c76da GIT binary patch literal 1048 zcmV+z1n2vSP)&HKtN4RO-xKosHmvplp8R#uyvo8sc)ZEbC0 zVq*IG`p(YImzS4DMn;sBlxu5ih=_>8!^7Iz+9Dz%R8&;U%geH|vWJI<^z`)H+}u)9 zQY9rN?d|Ou85w3~W;r=IKR-X5oSgCT@$m5QiHV8o>gvSA#NpxLRaI3}Q&U}CU1@1) z%F4>Iv9aLb;Ip%{YHDgoNJw~icy4ZPJ3BivGBR;-aX~>rJUl#eb92DJz~|@ZH#awB zWo56gue`jxl9H14_V(}Z@1mljjg5_2Sy|7|&(YD*WMpKyxw&$3a&&ZbpP!%U>FHx* zV@pd*M@L6~etx^VyV%&+p`oGG)zzk^rb0qO{r&xrkdVE-y?c9m`}_OC!otnX&Ft*# zprD}L-QDZ!>)F}aSXfx!-{1WF{BLh>$jHb500175wWk080uf0>K~#9!?b=sck^vaT z@jEsM1SUfi_ipdK_ukufS-C13=ExYb1rzA|cGLxNb=vco+`qH?=zlSe-uD3!5&cgb z4VI74ko_M01P!@FWDXj>E#&8+!5I4dWf}EkTfW#-sLa9qVj&L+{(!G>xiYQK(#aG= zM7DRL%G1J98Y1q+v#1hDnIIy0*BR(Wh41`1h}hNT>fVVe*C3-@LbZwUs8slNuoV@r z#~>BHgW1DSP%goKNGaUB7Ye2h99BvZ$3-? SQMQf%0000NFDhq1R)ee zK7tJh1r3S_ZBY!Rfnr0Yv{l+>`ZdS3OI`4i%$Yy%WX`!a_ujd0CW$~#62lBL%rL|M z0f)NpEkoeNZ}vP0?A5ksLg4C2hXMlIY9{Wi$!b*paKtxu5paKUM2RVN`_*lNzzb=M z5ZGL>le~(6i*v-)Ed>0bgoq&UWnCWzzDc>Wpi*}|0Kw&-5%ALOCtY&ebHy=msJt@= z0_(bI8v>Vhf_glCZeHY!0{9N7?`vKfqm6Z4E*StXx2!GH%_I=?~ooBO@J1OwPXthrYyq1zg}VB zrL0JB4*@r)M8{PKn9le+A`rN>zMUX&_^1a1QyoFzsIY{AU4wj^$}qzW{}YUQxW^1L z%rL_oB;z32p$d|fDL_H88CdeA#2v%H=LgewKmine6A0G!A>jG6nOh0RQa6>62LeY_nzy{?IuN)vMI2H}09Z;58iSBhH|Ye>Wyp~&3`~`T zfyd@Ba7#7^$v8;HK{5`KagdCIWE>=uagglz^ezOZhw1M?L9()01njR9PC;N(D-}ZE z(h3VvFv$%DYIXoe30Q{|fk1YXDu z--W=|fj+amrjyNJG(q4_cVh?8wHVFJOS5Wf7{6OHIx&}1_{vus+LSWKA zBM9ucLV5^1)h4v@bYhrch8bp-+gE2>2D~)H{$2{2VR`dbKsoAc`6L53c-Q}3l{tj@UQ;U zuMv3iZRP+1FLuUvAaFF1tw-RgGvlo@>uOhj$*$z`1UwKZH8WAi_kK16eo;4xz{&8} zz%&6L-RDiu67aL*W`@AKyYDe@lkI;3McsD}f=|98;Pm0keY$h43VgQG8*X+0?1F{c z1tjW@?p{OS zOSC!w{!!&dKO#||cn~uL&TW0pz}938oF8Q1)E+;dCE)AZ%E<@;TW$VE8G+-GTZX`; zO-l@H=M902T^RvHhf&~i}ELgB$!GZ+~7Q9B_KRD@F&Bi%_rvLx|07*qoM6N<$f+}+q ARR910 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-non-repeating-header-before-multi-page-row.png b/tests/ref/grid-subheaders-non-repeating-header-before-multi-page-row.png new file mode 100644 index 0000000000000000000000000000000000000000..3db97f782ed52b337bcbaf7a0ddc7354d76feee8 GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^6+m3c0VEi%2k%EaktaqI0hTW_aCiDQNP zoxOvd9a97ZMe_7`9bKF~{KQ(I7rnmHf z?b46{E}Nb+TX-z2J8akUROD{k@aFQXmklBBN|?{{SA=)7etrGXO=3L^J_i>0FS5tlc=`#}>EAxj`_Xnh<|}veYTmTU z0~>aAYj;n5mgOYOc9?NX#0QQ1bIqM8EQcQS^BtJ#w!YKze(>{7i;C=50_(XS1}n^V k?0M~iVg=N$hw&`+%$ENz`o#vX`vnSgPgg&ebxsLQ0F=zDPyhe` literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-non-repeating-orphan-prevention.png b/tests/ref/grid-subheaders-non-repeating-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..0f37c5d1714e88a400f8e326333124a79d7daec6 GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^6+pa@14uAv+Nr!`U|>}8ba4!+xb^nhelMp&iDQMw z7f5I5axBW`SY)_j<-(TB-1T>3!D zZqky4@4VGEG^I-(DE=DvRI2a0NOkw@74yn?eoyz`Xa7-d&jrIfw`DzoO0;J`|7Eqg zF?b>G9bZ5HWab@Gb#*e&WDifBd*^&-h@G5uoAueS&AMBqFK)Z?Na^!E^G5Hhw-$5S xc-O_xIFNty;!e((il2MAB4!@I3Z!}U8CPDIc=*0T)i+S6dAjqjx4+B z7``jhEjUP8kN(688Xk0Mvb?eRzo7U2&N7yLa~FLP z&0vVQ+`2~bM9_8f8LUj~<+_OpI~64=3V4{!pJ}c4m0~D+JH4EF!cx0ud14$n_P4ql zUB!Jk-+49wqCUH4lE zeCQ6zeIv&-?Mv!pw#MDwRxC`btLu{z3^`ctt%*5uV5?=<-(77#j#zE1ZF}>vHSxlP z8UAGn2`7bkWNNw^=SEJvz{a$1zL4y$jt14U_oW&S_?qf6HEu8aZLlGPo275oT#1UE z?f;hvsrmfqPq=c}R?TOIL1J49&&hwQC%joDCo-w`(5dN0@6FikZWL#TiY$TYa1vUv6>kF9JCdsiC0YSHBesF{dgKa(3A_glz3vBBqr@d!9 z%DK(pyN9kIHq-S<(G zS}e!PA2> zEc1SDn!(jrt|gtMSa5JdBy*#0tMm5ormR)_Xa3E$7bpl{p)R@k3h(r7EKL5cXB`r1 zf0exuVmh~Dkxs7CgOfUfjR(Y6Ss4~gT2jNHTl(2qV2*KjL$CRKv(GbM2RvA0u+%(~ z^+48+)mK>~Uv+8Cb$GB>dA3BvCl==Y(#H>6?L2k7N|a%?->EMQDJ#$GUgKoBHvjFx z148eP9%zheJRqHEEwMwzP0}ZvaYNCe>&eY;3LUHWIwZW_$jYN*&ceK&H{roOi}$M- z8TWSv9G6#4=-hN=wZIPTy}k+?{{OIJo95Mcps%TvCuY`y7sa6rZ#bquTtC5fpDBx# zN|+O`M8qQ&=J$J#95`mtf4Z+>eoH00vET-mcV<-z1-YKh4hi1_rP-z>H6DnYE65zZ z>!;~dgTzDiEQ|Ht8G=I4SS8uxn}2IcxygbFIXq$eQX*?5euR{NG1G9k7}oIMlpl+Y zplO+};{p5fXICFssm!|{?$#a3kkdQCP&sAgdMStA2E!r==59BG4VRi4&vNFqrSP1T zm}Cev_Sl3TMM-BPGyyFYWA&<^Kih$>0LEXM$1z(qpBV;;AXPuyH#s-|Jzd90daCn-^RIrEa_HQWYCQ0)dr|8F<NV})yL!~XZw?`iG+8n9)Q!g+|n>~9`m`ux76ZV+4AhR$PZF{~edSb4< z>-YU{`c%1e-uwCof$#m_+f7ez)}F54@M&xQNAWzus@vf>O=5lMh_{6Wh*Y z$Nl@wwTzQCbKbt+ocr5jLzcRN)$+*;xZe4w>{H!vU-n#g*xq*?4<>G6*wfWwT0GmQ@xW`{?(3EnCj859@45M`&-cLb zl}#*mbBokfKU9QmdG)j7_o{~Z-1`t`629ftjKz(wr%o&Q_4u@nO>SiG zw5d<}8X@4nTo`XY4-oixxFNw~^)QiRvci)Dfw1s+OA0bP00Av2dkhVLKv5DI{s)Rd k_~j6DkI6ElfY*uN*$+Mp`BbOox(XDdp00i_>zopr0QZO5LI3~& literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-non-consecutive.png b/tests/ref/grid-subheaders-repeat-non-consecutive.png new file mode 100644 index 0000000000000000000000000000000000000000..2e0fe2364549f3c44f6b9758a32e010c55566fcf GIT binary patch literal 599 zcmV-d0;v6oP)c=3GC1Lxs6?;3+vjbOoo1q)sV{I~1U zTLd2YFm@P$OUd+31WsfMQ3M{nJalGaUA@)c3X7B93AiUzX=Fy7-uu-M_*G;CfwS?M z&^Q5~JmAKs2>4}_nIZ6=wr&RAZ-<^iqwc&4!ROu(aB%!)r@pyX1-?@2`Wrm}`*8Vw z85wnJTZbX=+=eLzwhP9eq85=uM@CErc)k) zd*TlaffqObVPH!d0{={WX5ecLE_>b+PaL+)M`Vvr3_*v)L@3X|7BdE(d&Iy&tWy3# zz&C2EgQp1C>VMs+6uXbJ0C*_5fV0 zvs{3T`d}Vv1cLeu16$M>*mW^*pV8Lq({txa)NE%S9;f4SJ002ovPDHLkV1j*%2t@z@ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-didnt-fit-once.png b/tests/ref/grid-subheaders-repeat-replace-didnt-fit-once.png new file mode 100644 index 0000000000000000000000000000000000000000..df984bd60952a61f1d96d34020c6b270b4d94416 GIT binary patch literal 877 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!ivHFc{^#b(azRflv+zkgid@L+rE zW`P@V!sq6?AJ{te)E^_Jc`JnlIcm1laxjN`eAT`X^!*;|LEd7H8?_G;_4gO&a@MH( zORl`cuwj|^7j*^iyVvrBIArc`>}h=3mhj-CCeNM1pvv4U3>(VV?~!iS`6?Jc-67$3 zqBfgYtQ2#)6)RJ{t@P0YXW6HAO7SviSDURBJkcXPUy7Bfy{z`gfucBr5050+N!Q68E4w#K_lS00gKs+%x<-j0@rr3%+3HyxOJ_AXPSd6}ER1`|#ezu9*r zDvlni4+e#ryTPl6JwS%f3`I%jP5-r2c89TWot(4b(^9=HosHk4`C1$qyB6}Yylxlx zP_poIPg6s^i)~a{tWbZxR-YO#L;I(rFWYp!YKpHAED*n8ut9{AE$o*;!GfA*pm|1Z zDLf}7CK;+EdmNk4qj=N4jd}Xd({hR62=kd?0Ad&eCEY+wkjjs}DNXi%ubQ|Tm7k~a zIX;;7d{WZ^UbYK8w!02Ga_DewKXtp);Ne@o^+^vR-ez#JT;unYVY)8uXj~VZu)X0C zOZ24YGeGjIB`j_xtZO;&wpOU+K=_#jlGfiUk11~W@$35L*A|t%f5SL(`0l=LJg}+2 zNnt~j%txmO>Q|R(-SdlP;y2lP&H2IisFOl0VuzIjn5unv1b(DWU#Y#jtn`AZEqkN2 z%-XX87Q%Yx91=7?w<~PenX`q7X?ari{Yq;=U?>_m8|CSN{FesyGBgZ0gw@Uf0|3Z_ o`WO@(U^!6Ig@%Oizq6mYe_r3Eap7~te^5U1boFyt=akR{0GgD2aR2}S literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-double-orphan.png b/tests/ref/grid-subheaders-repeat-replace-double-orphan.png new file mode 100644 index 0000000000000000000000000000000000000000..e340e6817a8f7fc875ed35f3cc6241b0980b44c3 GIT binary patch literal 950 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!?ODXU(>hx(ZP{ovSFKC0 zG^>r>#ze;QdE(^ze*5$59mOjjF2C{oJO7>=-z$%8POsjqz-a5(!YQogQ^CJs^~ZA- zEOLMCrZP2N*Um~-END!$lVXbIs$O@V?aI1;mhYo~v^GRH-}Rgkczs^9D2I)VC0Ao_ zX7<6$92Sok8SU+9Xy4?*!Zd$n&cOqnlkV_Kl^(Vgm}A}6aQ6B*X0m~H8LN!-B!qs`QW53aHA?ii1~Er!2{f}5<8U78K*lP zcz^gxqdP~0Qh13LOP+7G7{?pyJ+(+b?@N;8E|zrw|G&o<-mfgl}$8;i}8A51m- zH_K~=!NdJcf&ABJ7$hEQk(^|x@_V_{ra2h_9@p5l<2S8|(}g+be$~SHs%GOX+Przb0{B;h1rlS0Z8&E3DlM+CKlRb`2=z$6Rn?B)8;xqB5;CO@DUDykX#2FwZ|CDduOT6F1&vbho&)kLs zOY@KLHWqJPU^79}i`g`9EtYfdn0kLXE?iRQa$wz* z-R50PuO@t0{9w6jrKUi|QQ^&ujjH=E^Eb9nF3)#*aPqqDu>-q2BT9VV3hcP|YXcwC z=`)p$2lhqGW@WPXzoz~%X`g;Uabctx%fDsw;#v+YTFWH*G~JcE(f<02Y^Ln+^|RQH zD{NS{eGLoK@9Rl_1tK1$uNBx~_b*n7rOf)x?8dKUuXe__-n=@0kEXIf#62+%mTmIi z%N!nT3#v^_c>79rcd{^3_Ll8NiXYmvPq8+t-fd~*^(>YYsK~rva!#8E7!#bvDvzHj v`2a%(Yw%%&Csg2aCny@h!D#T%-ex~zL|E^$9WSRbf-ZS_X}6cPwu~Q`45<{pMC$x?#Hv4FP<muvFlIYr&tcs(eUE3}9l< zO8cBHQt)Q6v2c8Fanui`rfmPN2M$ZQ?p-%q(a0ZpGx%cY_c)=0rt3uuRxH{&U7qp5 zRaWzyf;R71#y4V1@|LQE-@Wt9kn>M=Yz*^l$;$`$=Vv~Mu;xF(bd<#EiHQ~r2 zwtH3ex7V!b%E%WxsN2l-qjKT@6^k3hCk5--GaayOw9cNZE*`@;=SIrE2~Sq>`c-2<2QD&a^5*F2i-QdY84iUA5s7;En)Q7d%Rb}!bLx${5otmen``TWn5tLX zJUzo%QF`#Y^eb(Zf|ShX<(xUa+kKhZ`ER*$%6$07W2pXM&H?$F!;gh_DA#NaWxBn5 z@3O}3$AO%8_WpO{1epzTCCIH%$3p#-T(+ z@z?3)6ehoOR>8&6S#!mnyB0jQ6qH#TdXU}Lk+MYK@0~J4xL*HO;i(;XKa=`Rc|}7XK%o-s<(Cckj8?m)9|rZClrJX;Ps1 z*Pp&j%gy{V60DP@EG$-E&;SS&C4unfv6$u8!USbjPAyefDayO& z^UHOtfuav{#Sh9lvi{j;Q*GF&Z79E(|dv3N^IK%R7kt%AEUq1@Y_k3{U zn<8(H=(5JSa=ddaCS5-8tY|04NmRHarX>Xe!43d1K@5n{8eydUz>W%ih z+0#@vRKB0Z;wNLYo<;5(Yt0&AQ0N+Lc<`6`$%l)I+#PcLpakvd>gTe~DWM4f(wSPh literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-gutter.png b/tests/ref/grid-subheaders-repeat-replace-gutter.png new file mode 100644 index 0000000000000000000000000000000000000000..e87e3b5bf5efeaf190d41a7a883f2ef7cd8fb0f0 GIT binary patch literal 782 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!(gIh3llhEO1 zC0An3cw{wCV>xND=hd0&a=wEX8hlyvUtDfhtGGeF3M!h@jd4Q^<#E35niDj)9aZkD=n%9r_j-Gu{z^4uxS57x0g4_>U@aCrBd z;{GSA_GRpPFKd^^O=N&?uUTf1KN%8>Zg9G zZeZ1)&C(~X!lqL2`>ccHW0pB$7E7Dt!(#O(9w^TK?NS>2bM_P#IZeMi6AnC%o%{EU fp#cggusUylxa_j{+5DBaK?&W{)z4*}Q$iB}o+4uK literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-multiple-levels.png b/tests/ref/grid-subheaders-repeat-replace-multiple-levels.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f691e43d734335c994184c2bbf699a79f74ee7 GIT binary patch literal 877 zcmV-z1CsoSP)0E zc1N>{fNy8{S2q#x`+O0Bz?V#&7&udMra(j8@(2VME+F8=o6lO*cIFCW;KM>|76dl6 z$~FYPV+GCe#F?dG2rMgY`|4Uw-7oy_%nAT@B*@$lWT-E!GzbK?o$tlK5*Nh4k$Qo^ z->!Qwurb9yqE!H}|5T{g4Vhy@hgl%7>%=k!mK%bB*M~50Us~|19RUx^$K4oM$^-%jEsGdmR~|2@@tvm?0VGm@r|& zgc*`CB*T4?tV`1kdaHF)5IC@`g}}S{^YIY4Y)}dy@V!P5JeSasl!t)D52<%PvjYHs$2Nu1$jy@3-{JgX%7XohuQ}!XS6u*u@;MpACeF(hV z-Dy+Ta&nm4dLgjYRoM)I<C$wH2S#C2!Y>* za_1nh^cw^M@0|ndYl7SAln00000NkvXXu0mjf Du*`r4 literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-orphan.png b/tests/ref/grid-subheaders-repeat-replace-orphan.png new file mode 100644 index 0000000000000000000000000000000000000000..c28e9d4ff5664d8c7384bdffc74f091d7fdd2265 GIT binary patch literal 939 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!Sh*_Ws2yNNPb`<;Hmkg`($j5!C(wmI*Q z9$3Vg@StF?!-K7}Z%gdZ36wk&#JHhI>&EN0HyigI))2T+n#9L^I``lKZDxrb`j#7V zoDaNDzIt&vN5muZ9xax4=d#2(WUMM18ka^XeCVqaWuCrVYZ{a2$8OW4e|$F|?{;3` zoTPcHtx+}7;KRRco*e&g)wUczB7G7J*crX=Yq12?Hu*M`)YparV#3jkmqEKPh|&|N2OaMa+4wL&Eb^Lx%?s*@B%@e7`Qs&5>fd-WM{( z@xh0%APFY#QG1(fY8V^{vbGA3VG|H8@u_pRl+kx!lZIX6}1VHiNY{VxH3KC;HK?e#; n4qJ-QM*xC>MIV`njxgN@xNAjUttt literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-short-lived.png b/tests/ref/grid-subheaders-repeat-replace-short-lived.png new file mode 100644 index 0000000000000000000000000000000000000000..d041888c8010e6fd37717a34b3404045ea65e90d GIT binary patch literal 795 zcmeAS@N?(olHy`uVBq!ia0vp^6+rxn14u9|R*Bodz`*p#)5S5Q;?~<+x!#i#C5{#D zcNM5@i1Tn%5fxd|5+>vrp{&|+MSSY6g2g7mhm_U|ZWg-k6uqltgGTA3d!- zcEu^dQqbJ-L5bdrz}wfO4GWZ_McCRmITXBi-G1ra&Lrk}b}TX>zrP<9+_2WsV7r}t z*V@GbKTaN;&CJOXv-j=UxmGMO`;=c6C?&kVwn<5z>EDjbd-qrx`8#)WF`j=jec@w{ z9e-a;pVno-;1}g6-m&l|}L6fU5B?WA@^@R(rW;t%&{9tvqLBWAJosEm{w%TXwFhu-uoh-}LxIMj9 zaYOjiJ6oF?cvr3WeYIs$-LEA9A9_DuT`KV7@~3wiEYohaYjXTqr4q>8sQxZ;zSe7% zhner+wH|o(V5%R7&gFJiru9$SZ3KRlb*`Mj{yDT!SG+`u1`1QCum`U1=> za#_Y(HM3XQ@4k~Pu%k5MI!9x0@lQ<-9j$l)rrDw2lm#j@#UewlU0?ZslA!U`b15&g zIdryFH6EB~d(w{MPVvnv3)ZasC)cU#^dRi;O)n0a-si^@H%!@W@FC%2I~Rw|>$2X@ z9L~THIK*?(BBq5?SnbS&9z{uKBT$G)OfpnS_Be(|V28CJIQ$NQbpC1oD*fz6vRS4n R>k3e+_jL7hS?83{1OS<_WHSH& literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace-with-footer-orphan.png b/tests/ref/grid-subheaders-repeat-replace-with-footer-orphan.png new file mode 100644 index 0000000000000000000000000000000000000000..b50fae716c252da72a6a60daa32ac8eb18113a49 GIT binary patch literal 961 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!h6T9hoLBLtq^Kg($^A(*`qx0=Sn{_68Zv5Er ztnk=jm&HrWES4;@Cai2zO(Z= zB{<({de6eNuQG7v4A%p-AC;II&x>41eBjaflSS%P7rTN*CI=&be0|Kpa=rBjB-y7t z(-37?ZRI|d+2~3oYgOZc)wk0OH!R^`tGnLY$gQujcc~VGnf$cIuoD6ErSt_kax8AP zG`eo@Y_#=fWvbu3siSdo)70;US`1-)yJo$aVClD7QGjtHjbUZEU!1j2O%ZtP9xW%l0Z@ZjkdNuD{f3neCn9?`$%`CI&i%>4{z zTNRysZag`c?jApo_*IOf#T0W zj26jBhAO|;J8in{xM0E>?y!AFZpTPiEckeXNy9<&6!XEKr5rodH@x1|)KIV6xAIn% zht2NQC26Y|ZahDlM+DF zlfeQv?;kqFC(P#O>DzwbbhV>muVYaM&wjwwNflg{mnUj17$Vmff zQ=jzL_=fGvwgcZ**QzTNeAB+!aKO`bfu-E#tJjH3ma!dthlveo$E=~kq}GVeA|99!dh1a_`5^EsNIPUimiBFfFvc^_5vcfjLd##}ac7Fn#2!dI6!wAA@C?kLLk&y*;QlIEJVKrBsj<%+~!kF#Db%~y0vf9fpzF(b`Pbgonv z|ISR^S(9$vDu|lacQ*I?{_+cs?EhMRZ#Vytc%AwB(RKP~+#TfQ54CU#tNoGx5O()( zzd*&O&oMGA+r&d#9TR-#Te2`o%YRumBV_BU{j+VkS99#Rd0n(w^3^JCZKlTK&1#Ap z^bWqV?r+F-3-Lnk}rX1mGZrk-uki&1j(}ByK=d-SsJ^z&OVC|P# z85}V&>e>Q7%6K#S*!$0ab4ZxEIzIe*SytssnbiRd8w?%Pb}2g7maU6X*znFrnoVqf zF3+1R)>VscFb(kq?;PWcNrE`ANdw;%k=J3 zxZ6CX1kR{+0SX1hPR$Mp=l|JDM0Bw*t z-^g3HQ^MlWR!*k#Z&>f{ENQ=&nBcx(1&^F{!r=qQ42|!L0Yk~>N4&wS!{qA7GB5C) zu2X`ht}sUq-}E33n}Bt?;n!EiUF`RBd@$+Y9~+K0TiYrXH@M8w5!j)&$(W02ciptdN1OfL)(}>D$$+IyQb^%L)~6-*EazS_*=D7MG(G5SIQF1|MW^gD|MpdUoVAVH zB$&#rf=UG<+9t6u<+oX|DHJ@8&0V!?on4&B9~*09J|_MA_I1t)C#xQ}9(cU5&syNd zg-c6v%pdDC*X vZL+KfbNStWT8jf+3w9~U*_^^{hw2$)R<{)Guzvj=lyyB_{an^LB{Ts5mQ$mD literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-replace.png b/tests/ref/grid-subheaders-repeat-replace.png new file mode 100644 index 0000000000000000000000000000000000000000..9fe729401af3dd9e8c9fb298a5f7b7baadd159e9 GIT binary patch literal 953 zcmeAS@N?(olHy`uVBq!ia0vp^6$}iF&Ky7n!T@gj{Qkbx3x4Y(`~;q$q6Tgc=lLzH2xN@)mCHq zqvs`C-PE9(wmGo%fbC3KrbcGU@`KB+YxnD>~EM<CQ7u1hR}(l0Ck~x2Al5e)s;X+Ztd*7G5b$Sq$8_36U&$7c-z-M~qp##6JOwHlg zW0yDU>1J1;AB599jy+WJnPHF!vh+uPO8tSxr3@@(DL!+W4~XU;;cooAb%F7`?l-TR z4m>hrcX~a^AoOLH!iNU+^9+qgXDAEYkbN%McwnyUu00oKy;9(+i;J4T*Jxh*(OMva z>pr{Uhm={*?O5j3-_7M%yM6M~jeGf-w%=GaOL2ovtSEFRFXa@=%zRy&&rIF3t548L#QxNC-Cy9x!~MUx@9W{ejX-M~!wfUbFvEWU z3&z{S5cvJLGZ6wWmDbB4u%^ou4S^kbo$3imrFzEYom%ottpy#fIkXVk^(}58=rA4h zr4~+_0ef6t+8S94*kg}#k6%*(fN!QOlJ_S7?n?(9r@mNiGX{<)RSyJCr^X@>oMlJA zgUJ<*QhRgxF|d$jxCDWC?N3l z4$6eU`6ZOrjezG*+)-xoU>+2>tVs)jpQsvl_z`esOF$$9+#cf}gTRz?CJqBDXbl28 zLI(%}k4H>nVBZmv82mJ37z2CVg48YvQ&p)E0b2^`Fd^Vk#l%$z{7DZ=wRZOvl??$W z$w|0{fa`V{A1or^&$4`F3j~g#H`x&Q9NE+e_@!*U?$ibV9_n@5d%ZluFw8K+4F48J zE#6~>8D{vOh69Blm_!i#>M{rpgo9v;0KuDkL2#R@9|Kc0DqJ5yussBpYN^3O7zi%g zj)A8Q`!MiI(}ndM696XHMQ1%h;K=Yr2t2Orj`Aa5o$^OfdO%8#JrM<0MkRGET1)g=2;An&0M#+xrV#{Q zi}slz@Z(&41`!v@xJbrDGA@#Fk&KIETqNTn85ha8NX9V33^U9y!wfUbFvAQp%<%sV Z{{rJ)W=rgkmcRf2002ovPDHLkV1g23iL3ws literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeat-with-footer.png b/tests/ref/grid-subheaders-repeat-with-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..39f8465e72743d1cd02666360d20a045f5c8518f GIT binary patch literal 584 zcmV-O0=NB%P)d4jWsoXrOhbLkk*-hhdAIjlSsu+Eecc~su6K9{~pDBl@ z{{$_iPqA+0a7|(8(Eog>{))XCXghgx23#1pNJ=&!uqI<&AlSN#fcGl<$ISN4^^SqV zR-+gK4>;u-0@oR!d%T|HTZF*k9}k*rJNZWa5P0AEUJZep7C@tJ?~n)te#mNc^T%8X z9PPH-Ah0nk9tf*Oofdn1(Cvht# z+fEL<0fFar8G*pHv!GEAmB|?dj%pd@5LgQ27y^fqLjr-<{x4(TmlQD>y!H1E10QS! zc(zEGFk!-9!KlT1OqeiX!VJk6lKrZX%#scY$vm*~MZ0;!z$sH{x1a!JuondDE(E-p z>M46gz)$zSsL9?=v-U*@yxu9@5V&C;H0oZ9+(F<-P2(yEaktaqI1^y3Z>(pqI-8r>T6bvt3=Lc=VQDc;LQr%Oa-}0WFU%I5@N}YMLUJ z#iE+A@yS=N7@3+Do9AqGPi&tH@cmI{iV{53!YQowhx>;9l2-yho?V;Ua6neqo~d!V zzEmK`8kh6W-wSM*%P{xVYnRX5%=~iv+YRD%q%8&3$SOQ|H9_{j%Yl4Zb&i|&SFV_@ zxMAu<9hNwaMz^`aOL!Z18^20D;kWgR!G`%J#}q$QInGo3Fo*G0a`pjD=Y+!Vdnd%I zr(bV9@HHi0ib;Q7qubt1TCp5|go1tT&irTL+f>EHBpsdP{Gf$XYOCbuY=H>BOWzfD z``x>$%~Cg~MymP1%is$urke=dd2#)l!}49KK)K4h4F^*a6?i47>JYD@<);T3K0RTMC B$)*4R literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-repeating-orphan-prevention.png b/tests/ref/grid-subheaders-repeating-orphan-prevention.png new file mode 100644 index 0000000000000000000000000000000000000000..0f37c5d1714e88a400f8e326333124a79d7daec6 GIT binary patch literal 347 zcmeAS@N?(olHy`uVBq!ia0vp^6+pa@14uAv+Nr!`U|>}8ba4!+xb^nhelMp&iDQMw z7f5I5axBW`SY)_j<-(TB-1T>3!D zZqky4@4VGEG^I-(DE=DvRI2a0NOkw@74yn?eoyz`Xa7-d&jrIfw`DzoO0;J`|7Eqg zF?b>G9bZ5HWab@Gb#*e&WDifBd*^&-h@G5uoAueS&AMBqFK)Z?Na^!E^G5Hhw-$5S xc-O_xIFNty;!e((il2MAB4!@I3Z!}U8CPDIc=*0T)i+S6dAjW|_HDUg$ zrM6O4E!P$-GE!QxSL$}q9G+QHSshhPF-PjVyPMDP{bE}5>9e9t-=Xrj2hGL$-^08# zL=7Z(*qRR}pi_HfZCnq2fzhV27KD_l`@#_2KuNq9#f8F)e-*VM_Eyo^-IF|>JTk8cH z=l?%d%Tkxo_Tb&a=NBWH^gf>~d1>8<=?0*o-?{EFxJu3spK@qX8p!9Ku6{1-oD!M< D=nr>z literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-auto.png b/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d632adeabf46ee397803c8d0de13f6f6204999 GIT binary patch literal 460 zcmeAS@N?(olHy`uVBq!ia0vp^6+pa@14uAv+Nr!`U|^i=>EaktaqI0Ze{GgR3HA@Q z+NUI+uDWyOT~XrG4Nnx7-z~a0#Ur>ob<_5XTu;yEXYeOi&ilk-t4 z9eZci?&18o?eXtDjsE-V?>{fEU}9Ruka(zt^FP;|;|K25FlQajZTZi!&a^2(dEeLb z_crDxJ_yp<%hC9D`P4j)IsSWE6hCa)cCDnhA@=m%4>$Tbh1ccZs6KGu`K=zt#&~Tv zeHJ~xdu@svc1_9M>waLq^|OQQOm#kUSXmkGPg#2OAd5$#lfs9ma~vL&yMD`M*(2Fi zw!vCZ&1Z%I$PL0U#&2df(eofN8I{Khl8JniIy-i}UaiSdvq|?X-?O4nXQoEkr;^O8 zr|#BgnUg)A>zcraj4d^-lE)fEs`+W}mYualr zu;z$k0pm55U(?GC3y!?BW=q{Vk>%iHR+bu*70YMFwA8ZIH$T`Y!C{u@n`Dul_~2aD ubkj^xg-EdLqYP8NN;aKudx6iuG4elF{r5}E)(Vai(o literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-relative.png b/tests/ref/grid-subheaders-too-large-non-repeating-orphan-before-relative.png new file mode 100644 index 0000000000000000000000000000000000000000..324787b2506342de1630d9dc2c013971d0d701d8 GIT binary patch literal 542 zcmV+(0^$9MP)qoX7wB>epR($dmwY;5=U_t)3gg@uKNhK7oYib+XH?Ck7-fPl!z z$cc%G+1c6c?d{gq*5u^mRaI3>OG_>;E-)}Ke0+R_goL1=po4>h*x1(SBC z>FMd{=;-$L_Pe{g#>U3N!ov0S_2uQ|Gcz-soSc`JmzbECQc_ZElL;+mS8etv#O zM@O`@w9U=UwY9aOp`o9jpGHPT!NI}m>gunrukrEmc6N5|?(SP#TS`hwFE1~Rjg8LE z&fVSJ=H}+Iva%r|A&iWS^z`(-y}kYY{r^EW{n;FJ0002#Nkldd5Jb_o zg)E#%jyU0*bI!)bWFhW<8+IIlF2R2n_*Ju+O$z|httX45+mXpM;StHq$RnI>YqG>y zSv2a`H*$y%uWsegtL&HMu#nr?Xv!eUM)L`u5=ghCC5zSEVkn1|Lbxu6(&K$u4#kJN z%L^GCpPZhZ%iySTke9>Wt~LW%l(uBCyd;aCsy<^GeAoUy_!|HK00000<6+8#%L8CY z#*mCKLo!K<-rnnSczM--AdBCcEQ0EnEQ*D-IT<`ZZRayG=noMlOqeiX!h{JECQO(x gVZs0a0058{1Ky);tIJX_@c;k-07*qoM6N<$f*kc6MgRZ+ literal 0 HcmV?d00001 diff --git a/tests/ref/grid-subheaders-too-large-repeating-orphan-before-auto.png b/tests/ref/grid-subheaders-too-large-repeating-orphan-before-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..de77beb29ac5d995e907af95d71ace020ada7054 GIT binary patch literal 525 zcmeAS@N?(olHy`uVBq!ia0vp^6+rxngBeI}ocJ*gNSz7r332@o1de83zI@rcckiW3 zm;U_u^Xu2IyLazizkXd$Pw(8hb0(}@8_D-KZ{pr)EU%!4eHa0$V=+Loa$CfW&K5N#jhK7c3-@fhMz1!2% z^Ww#ej~+d$tE+qY^5vX4b6&i7@$lip8#ivee*L<+xj85(XvT~gZ{ECl`}Xa%Yu6MN z6?=MmT3cJYy1Lrh+BR+4l#`R=?d=^J8v5+nGZht;fB*h1UcA`G#^&$ezd#RZNt}oP zQhz*M978H@y}jigD?y+a_?0%5P=PL^%sl{U+{u_5s=gkf+&`oGfWgk>{sC*=b*GkCiC KxvX zN_KX3@$vDmudh;4Qka;S>+9>{;^KaOewUY*oSdBL>FJG)jpgO#Gcz;w_4UHS!p6qN zr>Cd8yStj2nt*_S_V)JZ=;-0$;n>*NgM)+7(a}jsNiZ-le0+S_+1ZJSiR|p`?d|Q> z*4B!OiiL%R$jHdf&d%4@*WKOS=H}-5`uhC*{L<3Wy}iBk^z{EhHWgZ%)&Kwir%6OX zRCwC$)x{CQ002bM4Z+>r-Ccr3=)Wj#RKT#4VP6CP_#q)-s3I?lQOM+oC z9(Fq*n2rYh9tdX3)fx=*1vXDmSX)r+_Mj*hknuoJ%IC6Rczr0Wu)+!}tgylgE3B}> f3M;Izh + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FirstHeader
SecondHeader
Level 2Header
Level 3Header
BodyCells
YetMore
Level 2Header Inside
Level 3
EvenMore
BodyCells
One Last HeaderFor Good Measure
FooterRow
EndingTable
+ + diff --git a/tests/ref/html/multi-header-table.html b/tests/ref/html/multi-header-table.html new file mode 100644 index 000000000..8a34ac170 --- /dev/null +++ b/tests/ref/html/multi-header-table.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FirstHeader
SecondHeader
Level 2Header
Level 3Header
BodyCells
YetMore
FooterRow
EndingTable
+ + diff --git a/tests/ref/issue-5359-column-override-stays-inside-header.png b/tests/ref/issue-5359-column-override-stays-inside-header.png new file mode 100644 index 0000000000000000000000000000000000000000..8339a4090d6cc71b8eff6890a9f9d37453dd5fdc GIT binary patch literal 674 zcmV;T0$u%yP)JGgviH_kPLjn0MN)2TI4$-6e70 z>7#r6!I#YdY!&|F=x#<{5{GBKPh8?RU|WOV=CnXL)@Xvk zVZQp^pX3dWNIqccSSXSMeKih+W7cE=8*ZY*>Ye(}qEW)Owbf@^^B(}XZ~#~9&^1wfE1?2<*{-}#yy{#8}*&a7nw zgo;~69{9MzQimqm?C86sH28+;fU>o24v4 zETLyi;WXeMRr2sa6^6nTrZA8D4VwpGPtrTzGXMYp07*qo IM6N<$g0lNP@&Et; literal 0 HcmV?d00001 diff --git a/tests/suite/layout/grid/footers.typ b/tests/suite/layout/grid/footers.typ index f7f1deb0a..c0b03f50a 100644 --- a/tests/suite/layout/grid/footers.typ +++ b/tests/suite/layout/grid/footers.typ @@ -389,6 +389,29 @@ table.footer[a][b][c] ) +--- grid-footer-repeatable-unbreakable --- +#set page(height: 8em, width: auto) +#table( + [h], + table.footer( + [a], + [b], + [c], + ) +) + +--- grid-footer-non-repeatable-unbreakable --- +#set page(height: 8em, width: auto) +#table( + [h], + table.footer( + [a], + [b], + [c], + repeat: false, + ) +) + --- grid-footer-stroke-edge-cases --- // Test footer stroke priority edge case #set page(height: 10em) diff --git a/tests/suite/layout/grid/headers.typ b/tests/suite/layout/grid/headers.typ index 229bce614..ea222ee88 100644 --- a/tests/suite/layout/grid/headers.typ +++ b/tests/suite/layout/grid/headers.typ @@ -118,30 +118,81 @@ ) --- grid-header-not-at-first-row --- -// Error: 3:3-3:19 header must start at the first row -// Hint: 3:3-3:19 remove any rows before the header #grid( [a], grid.header([b]) ) --- grid-header-not-at-first-row-two-columns --- -// Error: 4:3-4:19 header must start at the first row -// Hint: 4:3-4:19 remove any rows before the header #grid( columns: 2, [a], grid.header([b]) ) ---- grow-header-multiple --- -// Error: 3:3-3:19 cannot have more than one header +--- grid-header-multiple --- #grid( grid.header([a]), grid.header([b]), [a], ) +--- grid-header-skip --- +#grid( + columns: 2, + [x], [y], + grid.header([a]), + grid.header([b]), + grid.cell(x: 1)[c], [d], + grid.header([e]), + [f], grid.cell(x: 1)[g] +) + +--- grid-header-too-large-non-repeating-orphan --- +#set page(height: 8em) +#grid( + grid.header( + [a\ ] * 5, + repeat: false, + ), + [b] +) + +--- grid-header-too-large-repeating-orphan --- +#set page(height: 8em) +#grid( + grid.header( + [a\ ] * 5, + repeat: true, + ), + [b] +) + +--- grid-header-too-large-repeating-orphan-with-footer --- +#set page(height: 8em) +#grid( + grid.header( + [a\ ] * 5, + repeat: true, + ), + [b], + grid.footer( + [c], + repeat: true, + ) +) + +--- grid-header-too-large-repeating-orphan-not-at-first-row --- +#set page(height: 8em) +#grid( + [b], + grid.header( + [a\ ] * 5, + repeat: true, + ), + [c], +) + --- table-header-in-grid --- // Error: 2:3-2:20 cannot use `table.header` as a grid header // Hint: 2:3-2:20 use `grid.header` instead @@ -228,6 +279,51 @@ table.cell(rowspan: 3, lines(15)) ) +--- grid-header-and-rowspan-contiguous-1 --- +// Block should occupy all space +#set page(height: 15em) + +#table( + rows: (auto, 2.5em, 2em, auto), + gutter: 3pt, + inset: 0pt, + table.header( + [*H*], + [*W*] + ), + table.cell(rowspan: 3, block(height: 2.5em + 2em + 20em, width: 100%, fill: red)) +) + +--- grid-header-and-rowspan-contiguous-2 --- +// Block should occupy all space +#set page(height: 15em) + +#table( + rows: (auto, 2.5em, 10em, 5em, auto), + gutter: 3pt, + inset: 0pt, + table.header( + [*H*], + [*W*] + ), + table.cell(rowspan: 3, block(height: 2.5em + 2em + 20em, width: 100%, fill: red)) +) + +--- grid-header-and-large-auto-contiguous --- +// Block should occupy all space +#set page(height: 15em) + +#table( + rows: (auto, 4.5em, auto), + gutter: 3pt, + inset: 0pt, + table.header( + [*H*], + [*W*] + ), + block(height: 2.5em + 2em + 20em, width: 100%, fill: red) +) + --- grid-header-lack-of-space --- // Test lack of space for header + text. #set page(height: 8em) @@ -255,6 +351,17 @@ ..([Test], [Test], [Test]) * 20 ) +--- grid-header-non-repeating-orphan-prevention --- +#set page(height: 5em) +#v(2em) +#grid( + grid.header(repeat: false)[*Abc*], + [a], + [b], + [c], + [d] +) + --- grid-header-empty --- // Empty header should just be a repeated blank row #set page(height: 12em) @@ -339,6 +446,56 @@ [a\ b] ) +--- grid-header-not-at-the-top --- +#set page(height: 5em) +#v(2em) +#grid( + [a], + [b], + grid.header[*Abc*], + [d], + [e], + [f], +) + +--- grid-header-replace --- +#set page(height: 5em) +#v(1.5em) +#grid( + grid.header[*Abc*], + [a], + [b], + grid.header[*Def*], + [d], + [e], + [f], +) + +--- grid-header-replace-orphan --- +#set page(height: 5em) +#grid( + grid.header[*Abc*], + [a], + [b], + grid.header[*Def*], + [d], + [e], + [f], +) + +--- grid-header-replace-doesnt-fit --- +#set page(height: 5em) +#v(0.8em) +#grid( + grid.header[*Abc*], + [a], + [b], + grid.header[*Def*], + [d], + [e], + [f], +) + --- grid-header-stroke-edge-cases --- // Test header stroke priority edge case (last header row removed) #set page(height: 8em) @@ -463,8 +620,6 @@ #table( columns: 3, [Outside], - // Error: 1:3-4:4 header must start at the first row - // Hint: 1:3-4:4 remove any rows before the header table.header( [A], table.cell(x: 1)[B], [C], table.cell(x: 1)[D], diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ index 10345cb06..cf98d4bc5 100644 --- a/tests/suite/layout/grid/html.typ +++ b/tests/suite/layout/grid/html.typ @@ -57,3 +57,78 @@ [d], [e], [f], [g], [h], [i] ) + +--- multi-header-table html --- +#table( + columns: 2, + + table.header( + [First], [Header] + ), + table.header( + [Second], [Header] + ), + table.header( + [Level 2], [Header], + level: 2, + ), + table.header( + [Level 3], [Header], + level: 3, + ), + + [Body], [Cells], + [Yet], [More], + + table.footer( + [Footer], [Row], + [Ending], [Table], + ), +) + +--- multi-header-inside-table html --- +#table( + columns: 2, + + table.header( + [First], [Header] + ), + table.header( + [Second], [Header] + ), + table.header( + [Level 2], [Header], + level: 2, + ), + table.header( + [Level 3], [Header], + level: 3, + ), + + [Body], [Cells], + [Yet], [More], + + table.header( + [Level 2], [Header Inside], + level: 2, + ), + table.header( + [Level 3], + level: 3, + ), + + [Even], [More], + [Body], [Cells], + + table.header( + [One Last Header], + [For Good Measure], + repeat: false, + level: 4, + ), + + table.footer( + [Footer], [Row], + [Ending], [Table], + ), +) diff --git a/tests/suite/layout/grid/subheaders.typ b/tests/suite/layout/grid/subheaders.typ new file mode 100644 index 000000000..56bed6a57 --- /dev/null +++ b/tests/suite/layout/grid/subheaders.typ @@ -0,0 +1,602 @@ +--- grid-subheaders-demo --- +#set page(height: 15.2em) +#table( + columns: 2, + align: center, + table.header( + table.cell(colspan: 2)[*Regional User Data*], + ), + table.header( + level: 2, + table.cell(colspan: 2)[*Germany*], + [*Username*], [*Joined*] + ), + [john123], [2024], + [rob8], [2025], + [joe1], [2025], + [joe2], [2025], + [martha], [2025], + [pear], [2025], + table.header( + level: 2, + table.cell(colspan: 2)[*United States*], + [*Username*], [*Joined*] + ), + [cool4], [2023], + [roger], [2023], + [bigfan55], [2022] +) + +--- grid-subheaders-colorful --- +#set page(width: auto, height: 12em) +#let rows(n) = { + range(n).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten() +} +#table( + columns: 5, + align: center + horizon, + table.header( + table.cell(colspan: 5)[*Cool Zone*], + ), + table.header( + level: 2, + table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*], + table.hline(start: 2, end: 3, stroke: yellow) + ), + ..rows(2), + table.header( + level: 2, + table.cell(stroke: red)[*New Name*], table.cell(stroke: aqua, colspan: 4)[*Other Data*], + table.hline(start: 2, end: 3, stroke: yellow) + ), + ..rows(3) +) + +--- grid-subheaders-basic --- +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + [c] +) + +--- grid-subheaders-basic-non-consecutive --- +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], +) + +--- grid-subheaders-basic-replace --- +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 2, [c]), + [z], +) + +--- grid-subheaders-basic-with-footer --- +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + [c], + grid.footer([d]) +) + +--- grid-subheaders-basic-non-consecutive-with-footer --- +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.footer([f]) +) + +--- grid-subheaders-repeat --- +#set page(height: 8em) +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + ..([c],) * 10, +) + +--- grid-subheaders-repeat-non-consecutive --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, +) + +--- grid-subheaders-repeat-with-footer --- +#set page(height: 8em) +#grid( + grid.header([a]), + [m], + grid.header(level: 2, [b]), + ..([c],) * 10, + grid.footer([f]) +) + +--- grid-subheaders-repeat-gutter --- +// Gutter below the header is also repeated +#set page(height: 8em) +#grid( + inset: (bottom: 0.5pt), + stroke: (bottom: 1pt), + gutter: (1pt, 6pt, 1pt), + grid.header([a]), + grid.header(level: 2, [b]), + ..([c],) * 10, +) + +--- grid-subheaders-repeat-replace --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, [c]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-replace-multiple-levels --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + ..([y],) * 10, + grid.header(level: 2, [d]), + ..([z],) * 6, +) + +--- grid-subheaders-repeat-replace-gutter --- +#set page(height: 8em) +#grid( + gutter: 3pt, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 8, + grid.header(level: 2, [c]), + ..([z],) * 4, +) + +--- grid-subheaders-repeat-replace-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 12, + grid.header(level: 2, [c]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-replace-double-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 11, + grid.header(level: 2, [c]), + grid.header(level: 3, [d]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-replace-gutter-orphan-at-child --- +#set page(height: 8em) +#grid( + gutter: 3pt, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 9, + grid.header(level: 2, [c]), + [z \ z], + ..([z],) * 3, +) + +--- grid-subheaders-repeat-replace-gutter-orphan-at-gutter --- +#set page(height: 8em) +#grid( + gutter: 3pt, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 9, + box(height: 3pt), + grid.header(level: 2, [c]), + ..([z],) * 4, +) + +--- grid-subheaders-repeat-replace-didnt-fit-once --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, [c\ c\ c]), + ..([z],) * 4, +) + +--- grid-subheaders-repeat-replace-with-footer --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + ..([y],) * 10, + grid.header(level: 2, [d]), + ..([z],) * 6, + grid.footer([f]) +) + +--- grid-subheaders-repeat-replace-with-footer-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, [c]), + ..([z],) * 10, + grid.footer([f]) +) + +--- grid-subheaders-repeat-replace-short-lived --- +// No orphan prevention for short-lived headers +// (followed by replacing headers). +#set page(height: 8em) +#grid( + grid.header([a]), + grid.header(level: 2, [b]), + grid.header(level: 2, [c]), + grid.header(level: 2, [d]), + grid.header(level: 2, [e]), + grid.header(level: 2, [f]), + grid.header(level: 2, [g]), + grid.header(level: 2, [h]), + grid.header(level: 2, [i]), + grid.header(level: 2, [j]), + grid.header(level: 3, [k]), + ..([z],) * 10, +) + +--- grid-subheaders-repeat-short-lived-also-replaces --- +// Short-lived subheaders must still replace their conflicting predecessors. +#set page(height: 8em) +#grid( + // This has to go + grid.header(level: 3, [a]), + [w], + grid.header(level: 2, [b]), + grid.header(level: 2, [c]), + grid.header(level: 2, [d]), + grid.header(level: 2, [e]), + grid.header(level: 2, [f]), + grid.header(level: 2, [g]), + grid.header(level: 2, [h]), + grid.header(level: 2, [i]), + grid.header(level: 2, [j]), + grid.header(level: 3, [k]), + ..([z],) * 10, +) + +--- grid-subheaders-multi-page-row --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [a], [b], + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [y], + ..([z],) * 10, +) + +--- grid-subheaders-non-repeat --- +#set page(height: 8em) +#grid( + grid.header(repeat: false, [a]), + [x], + grid.header(level: 2, repeat: false, [b]), + ..([y],) * 10, +) + +--- grid-subheaders-non-repeat-replace --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + ..([y],) * 9, + grid.header(level: 2, repeat: false, [d]), + ..([z],) * 6, +) + +--- grid-subheaders-non-repeating-replace-orphan --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 12, + grid.header(level: 2, repeat: false, [c]), + ..([z],) * 10, +) + +--- grid-subheaders-non-repeating-replace-didnt-fit-once --- +#set page(height: 8em) +#grid( + grid.header([a]), + [x], + grid.header(level: 2, [b]), + ..([y],) * 10, + grid.header(level: 2, repeat: false, [c\ c\ c]), + ..([z],) * 4, +) + +--- grid-subheaders-multi-page-rowspan --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [z], [z], + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell] +) + +--- grid-subheaders-multi-page-row-right-after --- +#set page(height: 8em) +#grid( + columns: 1, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [done.], + [done.] +) + +--- grid-subheaders-multi-page-rowspan-right-after --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], [y], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell], + grid.cell(x: 0)[done.], + grid.cell(x: 0)[done.] +) + +--- grid-subheaders-multi-page-row-with-footer --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [a], [b], + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [y], + ..([z],) * 10, + grid.footer([f]) +) + +--- grid-subheaders-multi-page-rowspan-with-footer --- +#set page(height: 8em) +#grid( + columns: 2, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [z], [z], + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell], + grid.footer([f]) +) + +--- grid-subheaders-multi-page-row-right-after-with-footer --- +#set page(height: 8em) +#grid( + columns: 1, + grid.header([a]), + [x], + grid.header(level: 2, [b]), + grid.header(level: 3, [c]), + grid.cell( + block(fill: red, width: 1.5em, height: 6.4em) + ), + [done.], + [done.], + grid.footer([f]) +) + +--- grid-subheaders-multi-page-rowspan-gutter --- +#set page(height: 9em) +#grid( + columns: 2, + column-gutter: 4pt, + row-gutter: (0pt, 4pt, 8pt, 4pt), + inset: (bottom: 0.5pt), + stroke: (bottom: 1pt), + grid.header([a]), + [x], + grid.header(level: 2, [b]), + [y], + grid.header(level: 3, [c]), + [z], [z], + grid.cell( + rowspan: 5, + block(fill: red, width: 1.5em, height: 6.4em) + ), + [cell], + [cell], + [a\ b], + grid.cell(x: 0)[end], +) + +--- grid-subheaders-non-repeating-header-before-multi-page-row --- +#set page(height: 6em) +#grid( + grid.header(repeat: false, [h]), + [row #colbreak() row] +) + + +--- grid-subheaders-short-lived-no-orphan-prevention --- +// No orphan prevention for short-lived headers. +#set page(height: 8em) +#v(5em) +#grid( + grid.header(level: 2, [b]), + grid.header(level: 2, [c]), + [d] +) + +--- grid-subheaders-repeating-orphan-prevention --- +#set page(height: 8em) +#v(4.5em) +#grid( + grid.header(repeat: true, level: 2, [L2]), + grid.header(repeat: true, level: 4, [L4]), + [a] +) + +--- grid-subheaders-non-repeating-orphan-prevention --- +#set page(height: 8em) +#v(4.5em) +#grid( + grid.header(repeat: false, level: 2, [L2]), + grid.header(repeat: false, level: 4, [L4]), + [a] +) + +--- grid-subheaders-alone --- +#table( + table.header([a]), + table.header(level: 2, [b]), +) + +--- grid-subheaders-alone-no-orphan-prevention --- +#set page(height: 5.3em) +#v(2em) +#grid( + grid.header([L1]), + grid.header(level: 2, [L2]), +) + +--- grid-subheaders-alone-with-gutter-no-orphan-prevention --- +#set page(height: 5.3em) +#v(2em) +#grid( + gutter: 3pt, + grid.header([L1]), + grid.header(level: 2, [L2]), +) + +--- grid-subheaders-alone-with-footer --- +#table( + table.header([a]), + table.header(level: 2, [b]), + table.footer([c]) +) + +--- grid-subheaders-alone-with-footer-no-orphan-prevention --- +#set page(height: 5.3em) +#table( + table.header([L1]), + table.header(level: 2, [L2]), + table.footer([a]) +) + +--- grid-subheaders-alone-with-gutter-and-footer-no-orphan-prevention --- +#set page(height: 5.5em) +#table( + gutter: 4pt, + table.header([L1]), + table.header(level: 2, [L2]), + table.footer([a]) +) + +--- grid-subheaders-too-large-non-repeating-orphan-before-auto --- +#set page(height: 8em) +#grid( + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: false), + grid.header([2], level: 3), + [b\ b\ b], +) + +--- grid-subheaders-too-large-repeating-orphan-before-auto --- +#set page(height: 8em) +#grid( + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: true), + grid.header([2], level: 3), + rect(width: 10pt, height: 3em, fill: red), +) + +--- grid-subheaders-too-large-repeating-orphan-before-relative --- +#set page(height: 8em) +#grid( + rows: (auto, auto, auto, 3em), + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: true), + grid.header([2], level: 3), + rect(width: 10pt, height: 3em, fill: red), +) + +--- grid-subheaders-too-large-non-repeating-orphan-before-relative --- +#set page(height: 8em) +#grid( + rows: (auto, auto, auto, 3em), + grid.header([1]), + grid.header([a\ ] * 2, level: 2, repeat: false), + grid.header([2], level: 3), + rect(width: 10pt, height: 3em, fill: red), +) From 44d410dd007569227e8eca41e39fde9a932f0d02 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 10 Jun 2025 14:44:38 +0000 Subject: [PATCH 073/162] Use the shaper in math (#6336) --- crates/typst-layout/src/inline/mod.rs | 1 + crates/typst-layout/src/inline/shaping.rs | 26 +- crates/typst-layout/src/math/accent.rs | 51 +- crates/typst-layout/src/math/attach.rs | 25 +- crates/typst-layout/src/math/frac.rs | 12 +- crates/typst-layout/src/math/fragment.rs | 796 +++++++++++++--------- crates/typst-layout/src/math/lr.rs | 33 +- crates/typst-layout/src/math/mat.rs | 32 +- crates/typst-layout/src/math/mod.rs | 56 +- crates/typst-layout/src/math/root.rs | 6 +- crates/typst-layout/src/math/shared.rs | 23 +- crates/typst-layout/src/math/stretch.rs | 300 +------- crates/typst-layout/src/math/text.rs | 73 +- crates/typst-layout/src/math/underover.rs | 8 +- crates/typst-library/src/text/font/mod.rs | 15 +- crates/typst-library/src/text/item.rs | 18 + crates/typst-library/src/text/mod.rs | 18 + crates/typst-pdf/src/text.rs | 10 +- crates/typst-render/src/text.rs | 9 +- crates/typst-svg/src/text.rs | 23 +- tests/ref/math-accent-dotless-greedy.png | Bin 0 -> 710 bytes tests/suite/math/accent.typ | 6 + 22 files changed, 731 insertions(+), 810 deletions(-) create mode 100644 tests/ref/math-accent-dotless-greedy.png diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 5ef820d07..6cafb9b00 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -9,6 +9,7 @@ mod prepare; mod shaping; pub use self::box_::layout_box; +pub use self::shaping::create_shape_plan; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index ca723c0a5..935a86b38 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -1,18 +1,16 @@ use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; -use std::str::FromStr; use std::sync::Arc; use az::SaturatingAs; -use ecow::EcoString; use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer}; use ttf_parser::Tag; use typst_library::engine::Engine; use typst_library::foundations::{Smart, StyleChain}; use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; use typst_library::text::{ - families, features, is_default_ignorable, variant, Font, FontFamily, FontVariant, - Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem, + families, features, is_default_ignorable, language, variant, Font, FontFamily, + FontVariant, Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem, }; use typst_library::World; use typst_utils::SliceExt; @@ -295,6 +293,8 @@ impl<'a> ShapedText<'a> { + justification_left + justification_right, x_offset: shaped.x_offset + justification_left, + y_advance: Em::zero(), + y_offset: Em::zero(), range: (shaped.range.start - range.start).saturating_as() ..(shaped.range.end - range.start).saturating_as(), span, @@ -934,7 +934,7 @@ fn shape_segment<'a>( /// Create a shape plan. #[comemo::memoize] -fn create_shape_plan( +pub fn create_shape_plan( font: &Font, direction: rustybuzz::Direction, script: rustybuzz::Script, @@ -952,7 +952,7 @@ fn create_shape_plan( /// Shape the text with tofus from the given font. fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { - let x_advance = font.advance(0).unwrap_or_default(); + let x_advance = font.x_advance(0).unwrap_or_default(); let add_glyph = |(cluster, c): (usize, char)| { let start = base + cluster; let end = start + c.len_utf8(); @@ -1044,20 +1044,8 @@ fn calculate_adjustability(ctx: &mut ShapingContext, lang: Lang, region: Option< /// Difference between non-breaking and normal space. fn nbsp_delta(font: &Font) -> Option { - let space = font.ttf().glyph_index(' ')?.0; let nbsp = font.ttf().glyph_index('\u{00A0}')?.0; - Some(font.advance(nbsp)? - font.advance(space)?) -} - -/// Process the language and region of a style chain into a -/// rustybuzz-compatible BCP 47 language. -fn language(styles: StyleChain) -> rustybuzz::Language { - let mut bcp: EcoString = TextElem::lang_in(styles).as_str().into(); - if let Some(region) = TextElem::region_in(styles) { - bcp.push('-'); - bcp.push_str(region.as_str()); - } - rustybuzz::Language::from_str(&bcp).unwrap() + Some(font.x_advance(nbsp)? - font.space_width()?) } /// Returns true if all glyphs in `glyphs` have ranges within the range `range`. diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 301606466..159703b8e 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -3,7 +3,10 @@ use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::math::AccentElem; -use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; +use super::{ + style_cramped, style_dtls, style_flac, FrameFragment, GlyphFragment, MathContext, + MathFragment, +}; /// How much the accent can be shorter than the base. const ACCENT_SHORT_FALL: Em = Em::new(0.5); @@ -15,40 +18,40 @@ pub fn layout_accent( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let cramped = style_cramped(); - let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; - let accent = elem.accent; let top_accent = !accent.is_bottom(); - // Try to replace base glyph with its dotless variant. - if top_accent && elem.dotless(styles) { - if let MathFragment::Glyph(glyph) = &mut base { - glyph.make_dotless_form(ctx); - } - } + // Try to replace the base glyph with its dotless variant. + let dtls = style_dtls(); + let base_styles = + if top_accent && elem.dotless(styles) { styles.chain(&dtls) } else { styles }; + + let cramped = style_cramped(); + let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?; // Preserve class to preserve automatic spacing. let base_class = base.class(); let base_attach = base.accent_attach(); - let mut glyph = GlyphFragment::new(ctx, styles, accent.0, elem.span()); + // Try to replace the accent glyph with its flattened variant. + let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); + let flac = style_flac(); + let accent_styles = if top_accent && base.ascent() > flattened_base_height { + styles.chain(&flac) + } else { + styles + }; - // Try to replace accent glyph with its flattened variant. - if top_accent { - let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.ascent() > flattened_base_height { - glyph.make_flattened_accent_form(ctx); - } - } + let mut glyph = + GlyphFragment::new_char(ctx.font, accent_styles, accent.0, elem.span())?; - // Forcing the accent to be at least as large as the base makes it too - // wide in many case. + // Forcing the accent to be at least as large as the base makes it too wide + // in many cases. let width = elem.size(styles).relative_to(base.width()); - let short_fall = ACCENT_SHORT_FALL.at(glyph.font_size); - let variant = glyph.stretch_horizontal(ctx, width - short_fall); - let accent = variant.frame; - let accent_attach = variant.accent_attach.0; + let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size); + glyph.stretch_horizontal(ctx, width - short_fall); + let accent_attach = glyph.accent_attach.0; + let accent = glyph.into_frame(); let (gap, accent_pos, base_pos) = if top_accent { // Descent is negative because the accent's ink bottom is above the diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 90aad941e..a7f3cad5f 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -66,7 +66,6 @@ pub fn layout_attach( let relative_to_width = measure!(t, width).max(measure!(b, width)); stretch_fragment( ctx, - styles, &mut base, Some(Axis::X), Some(relative_to_width), @@ -220,7 +219,6 @@ fn layout_attachments( // Calculate the distance each pre-script extends to the left of the base's // width. let (tl_pre_width, bl_pre_width) = compute_pre_script_widths( - ctx, &base, [tl.as_ref(), bl.as_ref()], (tx_shift, bx_shift), @@ -231,7 +229,6 @@ fn layout_attachments( // base's width. Also calculate each post-script's kerning (we need this for // its position later). let ((tr_post_width, tr_kern), (br_post_width, br_kern)) = compute_post_script_widths( - ctx, &base, [tr.as_ref(), br.as_ref()], (tx_shift, bx_shift), @@ -287,14 +284,13 @@ fn layout_attachments( /// post-script's kerning value. The first tuple is for the post-superscript, /// and the second is for the post-subscript. fn compute_post_script_widths( - ctx: &MathContext, base: &MathFragment, [tr, br]: [Option<&MathFragment>; 2], (tr_shift, br_shift): (Abs, Abs), space_after_post_script: Abs, ) -> ((Abs, Abs), (Abs, Abs)) { let tr_values = tr.map_or_default(|tr| { - let kern = math_kern(ctx, base, tr, tr_shift, Corner::TopRight); + let kern = math_kern(base, tr, tr_shift, Corner::TopRight); (space_after_post_script + tr.width() + kern, kern) }); @@ -302,7 +298,7 @@ fn compute_post_script_widths( // need to shift the post-subscript left by the base's italic correction // (see the kerning algorithm as described in the OpenType MATH spec). let br_values = br.map_or_default(|br| { - let kern = math_kern(ctx, base, br, br_shift, Corner::BottomRight) + let kern = math_kern(base, br, br_shift, Corner::BottomRight) - base.italics_correction(); (space_after_post_script + br.width() + kern, kern) }); @@ -317,19 +313,18 @@ fn compute_post_script_widths( /// extends left of the base's width and the second being the distance the /// pre-subscript extends left of the base's width. fn compute_pre_script_widths( - ctx: &MathContext, base: &MathFragment, [tl, bl]: [Option<&MathFragment>; 2], (tl_shift, bl_shift): (Abs, Abs), space_before_pre_script: Abs, ) -> (Abs, Abs) { let tl_pre_width = tl.map_or_default(|tl| { - let kern = math_kern(ctx, base, tl, tl_shift, Corner::TopLeft); + let kern = math_kern(base, tl, tl_shift, Corner::TopLeft); space_before_pre_script + tl.width() + kern }); let bl_pre_width = bl.map_or_default(|bl| { - let kern = math_kern(ctx, base, bl, bl_shift, Corner::BottomLeft); + let kern = math_kern(base, bl, bl_shift, Corner::BottomLeft); space_before_pre_script + bl.width() + kern }); @@ -471,13 +466,7 @@ fn compute_script_shifts( /// a negative value means shifting the script closer to the base. Requires the /// distance from the base's baseline to the script's baseline, as well as the /// script's corner (tl, tr, bl, br). -fn math_kern( - ctx: &MathContext, - base: &MathFragment, - script: &MathFragment, - shift: Abs, - pos: Corner, -) -> Abs { +fn math_kern(base: &MathFragment, script: &MathFragment, shift: Abs, pos: Corner) -> Abs { // This process is described under the MathKernInfo table in the OpenType // MATH spec. @@ -502,8 +491,8 @@ fn math_kern( // Calculate the sum of kerning values for each correction height. let summed_kern = |height| { - let base_kern = base.kern_at_height(ctx, pos, height); - let attach_kern = script.kern_at_height(ctx, pos.inv(), height); + let base_kern = base.kern_at_height(pos, height); + let attach_kern = script.kern_at_height(pos.inv(), height); base_kern + attach_kern }; diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 2567349d0..091f328f6 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -109,14 +109,14 @@ fn layout_frac_like( frame.push_frame(denom_pos, denom); if binom { - let mut left = GlyphFragment::new(ctx, styles, '(', span) - .stretch_vertical(ctx, height - short_fall); - left.center_on_axis(ctx); + let mut left = GlyphFragment::new_char(ctx.font, styles, '(', span)?; + left.stretch_vertical(ctx, height - short_fall); + left.center_on_axis(); ctx.push(left); ctx.push(FrameFragment::new(styles, frame)); - let mut right = GlyphFragment::new(ctx, styles, ')', span) - .stretch_vertical(ctx, height - short_fall); - right.center_on_axis(ctx); + let mut right = GlyphFragment::new_char(ctx.font, styles, ')', span)?; + right.stretch_vertical(ctx, height - short_fall); + right.center_on_axis(); ctx.push(right); } else { frame.push( diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 01fa6be4b..eb85eeb5d 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,28 +1,32 @@ use std::fmt::{self, Debug, Formatter}; -use rustybuzz::Feature; -use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; -use ttf_parser::opentype_layout::LayoutTable; -use ttf_parser::{GlyphId, Rect}; +use az::SaturatingAs; +use rustybuzz::{BufferFlags, UnicodeBuffer}; +use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; +use ttf_parser::GlyphId; +use typst_library::diag::{bail, warning, SourceResult}; use typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ - Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, + Abs, Axes, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, }; use typst_library::math::{EquationElem, MathSize}; -use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; -use typst_library::visualize::{FixedStroke, Paint}; +use typst_library::text::{features, language, Font, Glyph, TextElem, TextItem}; use typst_syntax::Span; -use typst_utils::default_math_class; +use typst_utils::{default_math_class, Get}; use unicode_math_class::MathClass; -use super::{stretch_glyph, MathContext, Scaled}; +use super::MathContext; +use crate::inline::create_shape_plan; use crate::modifiers::{FrameModifiers, FrameModify}; +/// Maximum number of times extenders can be repeated. +const MAX_REPEATS: usize = 1024; + +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum MathFragment { Glyph(GlyphFragment), - Variant(VariantFragment), Frame(FrameFragment), Spacing(Abs, bool), Space(Abs), @@ -33,13 +37,18 @@ pub enum MathFragment { impl MathFragment { pub fn size(&self) -> Size { - Size::new(self.width(), self.height()) + match self { + Self::Glyph(glyph) => glyph.size, + Self::Frame(fragment) => fragment.frame.size(), + Self::Spacing(amount, _) => Size::with_x(*amount), + Self::Space(amount) => Size::with_x(*amount), + _ => Size::zero(), + } } pub fn width(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.width, - Self::Variant(variant) => variant.frame.width(), + Self::Glyph(glyph) => glyph.size.x, Self::Frame(fragment) => fragment.frame.width(), Self::Spacing(amount, _) => *amount, Self::Space(amount) => *amount, @@ -49,8 +58,7 @@ impl MathFragment { pub fn height(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.height(), - Self::Variant(variant) => variant.frame.height(), + Self::Glyph(glyph) => glyph.size.y, Self::Frame(fragment) => fragment.frame.height(), _ => Abs::zero(), } @@ -58,17 +66,15 @@ impl MathFragment { pub fn ascent(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.ascent, - Self::Variant(variant) => variant.frame.ascent(), - Self::Frame(fragment) => fragment.frame.baseline(), + Self::Glyph(glyph) => glyph.ascent(), + Self::Frame(fragment) => fragment.frame.ascent(), _ => Abs::zero(), } } pub fn descent(&self) -> Abs { match self { - Self::Glyph(glyph) => glyph.descent, - Self::Variant(variant) => variant.frame.descent(), + Self::Glyph(glyph) => glyph.descent(), Self::Frame(fragment) => fragment.frame.descent(), _ => Abs::zero(), } @@ -85,7 +91,6 @@ impl MathFragment { pub fn class(&self) -> MathClass { match self { Self::Glyph(glyph) => glyph.class, - Self::Variant(variant) => variant.class, Self::Frame(fragment) => fragment.class, Self::Spacing(_, _) => MathClass::Space, Self::Space(_) => MathClass::Space, @@ -98,7 +103,6 @@ impl MathFragment { pub fn math_size(&self) -> Option { match self { Self::Glyph(glyph) => Some(glyph.math_size), - Self::Variant(variant) => Some(variant.math_size), Self::Frame(fragment) => Some(fragment.math_size), _ => None, } @@ -106,8 +110,7 @@ impl MathFragment { pub fn font_size(&self) -> Option { match self { - Self::Glyph(glyph) => Some(glyph.font_size), - Self::Variant(variant) => Some(variant.font_size), + Self::Glyph(glyph) => Some(glyph.item.size), Self::Frame(fragment) => Some(fragment.font_size), _ => None, } @@ -116,7 +119,6 @@ impl MathFragment { pub fn set_class(&mut self, class: MathClass) { match self { Self::Glyph(glyph) => glyph.class = class, - Self::Variant(variant) => variant.class = class, Self::Frame(fragment) => fragment.class = class, _ => {} } @@ -125,7 +127,6 @@ impl MathFragment { pub fn set_limits(&mut self, limits: Limits) { match self { Self::Glyph(glyph) => glyph.limits = limits, - Self::Variant(variant) => variant.limits = limits, Self::Frame(fragment) => fragment.limits = limits, _ => {} } @@ -149,7 +150,6 @@ impl MathFragment { pub fn is_text_like(&self) -> bool { match self { Self::Glyph(glyph) => !glyph.extended_shape, - Self::Variant(variant) => !variant.extended_shape, MathFragment::Frame(frame) => frame.text_like, _ => false, } @@ -158,7 +158,6 @@ impl MathFragment { pub fn italics_correction(&self) -> Abs { match self { Self::Glyph(glyph) => glyph.italics_correction, - Self::Variant(variant) => variant.italics_correction, Self::Frame(fragment) => fragment.italics_correction, _ => Abs::zero(), } @@ -167,7 +166,6 @@ impl MathFragment { pub fn accent_attach(&self) -> (Abs, Abs) { match self { Self::Glyph(glyph) => glyph.accent_attach, - Self::Variant(variant) => variant.accent_attach, Self::Frame(fragment) => fragment.accent_attach, _ => (self.width() / 2.0, self.width() / 2.0), } @@ -176,7 +174,6 @@ impl MathFragment { pub fn into_frame(self) -> Frame { match self { Self::Glyph(glyph) => glyph.into_frame(), - Self::Variant(variant) => variant.frame, Self::Frame(fragment) => fragment.frame, Self::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); @@ -190,7 +187,6 @@ impl MathFragment { pub fn limits(&self) -> Limits { match self { MathFragment::Glyph(glyph) => glyph.limits, - MathFragment::Variant(variant) => variant.limits, MathFragment::Frame(fragment) => fragment.limits, _ => Limits::Never, } @@ -198,11 +194,31 @@ impl MathFragment { /// If no kern table is provided for a corner, a kerning amount of zero is /// assumed. - pub fn kern_at_height(&self, ctx: &MathContext, corner: Corner, height: Abs) -> Abs { + pub fn kern_at_height(&self, corner: Corner, height: Abs) -> Abs { match self { Self::Glyph(glyph) => { - kern_at_height(ctx, glyph.font_size, glyph.id, corner, height) - .unwrap_or_default() + // For glyph assemblies we pick either the start or end glyph + // depending on the corner. + let is_vertical = + glyph.item.glyphs.iter().all(|glyph| glyph.y_advance != Em::zero()); + let glyph_index = match (is_vertical, corner) { + (true, Corner::TopLeft | Corner::TopRight) => { + glyph.item.glyphs.len() - 1 + } + (false, Corner::TopRight | Corner::BottomRight) => { + glyph.item.glyphs.len() - 1 + } + _ => 0, + }; + + kern_at_height( + &glyph.item.font, + GlyphId(glyph.item.glyphs[glyph_index].id), + corner, + Em::from_length(height, glyph.item.size), + ) + .unwrap_or_default() + .at(glyph.item.size) } _ => Abs::zero(), } @@ -215,12 +231,6 @@ impl From for MathFragment { } } -impl From for MathFragment { - fn from(variant: VariantFragment) -> Self { - Self::Variant(variant) - } -} - impl From for MathFragment { fn from(fragment: FrameFragment) -> Self { Self::Frame(fragment) @@ -229,266 +239,282 @@ impl From for MathFragment { #[derive(Clone)] pub struct GlyphFragment { - pub id: GlyphId, - pub c: char, - pub font: Font, - pub lang: Lang, - pub region: Option, - pub fill: Paint, - pub stroke: Option, - pub shift: Abs, - pub width: Abs, - pub ascent: Abs, - pub descent: Abs, + // Text stuff. + pub item: TextItem, + pub base_glyph: Glyph, + // Math stuff. + pub size: Size, + pub baseline: Option, pub italics_correction: Abs, pub accent_attach: (Abs, Abs), - pub font_size: Abs, - pub class: MathClass, pub math_size: MathSize, - pub span: Span, - pub modifiers: FrameModifiers, + pub class: MathClass, pub limits: Limits, pub extended_shape: bool, + pub mid_stretched: Option, + // External frame stuff. + pub modifiers: FrameModifiers, + pub shift: Abs, + pub align: Abs, } impl GlyphFragment { - pub fn new(ctx: &MathContext, styles: StyleChain, c: char, span: Span) -> Self { - let id = ctx.ttf.glyph_index(c).unwrap_or_default(); - let id = Self::adjust_glyph_index(ctx, id); - Self::with_id(ctx, styles, c, id, span) - } - - pub fn try_new( - ctx: &MathContext, + /// Calls `new` with the given character. + pub fn new_char( + font: &Font, styles: StyleChain, c: char, span: Span, - ) -> Option { - let id = ctx.ttf.glyph_index(c)?; - let id = Self::adjust_glyph_index(ctx, id); - Some(Self::with_id(ctx, styles, c, id, span)) + ) -> SourceResult { + Self::new(font, styles, c.encode_utf8(&mut [0; 4]), span) } - pub fn with_id( - ctx: &MathContext, + /// Try to create a new glyph out of the given string. Will bail if the + /// result from shaping the string is not a single glyph or is a tofu. + #[comemo::memoize] + pub fn new( + font: &Font, styles: StyleChain, - c: char, - id: GlyphId, + text: &str, span: Span, - ) -> Self { + ) -> SourceResult { + let mut buffer = UnicodeBuffer::new(); + buffer.push_str(text); + buffer.set_language(language(styles)); + // TODO: Use `rustybuzz::script::MATH` once + // https://github.com/harfbuzz/rustybuzz/pull/165 is released. + buffer.set_script( + rustybuzz::Script::from_iso15924_tag(ttf_parser::Tag::from_bytes(b"math")) + .unwrap(), + ); + buffer.set_direction(rustybuzz::Direction::LeftToRight); + buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES); + + let features = features(styles); + let plan = create_shape_plan( + font, + buffer.direction(), + buffer.script(), + buffer.language().as_ref(), + &features, + ); + + let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); + if buffer.len() != 1 { + bail!(span, "did not get a single glyph after shaping {}", text); + } + + let info = buffer.glyph_infos()[0]; + let pos = buffer.glyph_positions()[0]; + + // TODO: add support for coverage and fallback, like in normal text shaping. + if info.glyph_id == 0 { + bail!(span, "current font is missing a glyph for {}", text); + } + + let cluster = info.cluster as usize; + let c = text[cluster..].chars().next().unwrap(); + let limits = Limits::for_char(c); let class = EquationElem::class_in(styles) .or_else(|| default_math_class(c)) .unwrap_or(MathClass::Normal); - let mut fragment = Self { - id, - c, - font: ctx.font.clone(), - lang: TextElem::lang_in(styles), - region: TextElem::region_in(styles), + let glyph = Glyph { + id: info.glyph_id as u16, + x_advance: font.to_em(pos.x_advance), + x_offset: font.to_em(pos.x_offset), + y_advance: font.to_em(pos.y_advance), + y_offset: font.to_em(pos.y_offset), + range: 0..text.len().saturating_as(), + span: (span, 0), + }; + + let item = TextItem { + font: font.clone(), + size: TextElem::size_in(styles), fill: TextElem::fill_in(styles).as_decoration(), stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()), - shift: TextElem::baseline_in(styles), - font_size: TextElem::size_in(styles), + lang: TextElem::lang_in(styles), + region: TextElem::region_in(styles), + text: text.into(), + glyphs: vec![glyph.clone()], + }; + + let mut fragment = Self { + item, + base_glyph: glyph, + // Math math_size: EquationElem::size_in(styles), - width: Abs::zero(), - ascent: Abs::zero(), - descent: Abs::zero(), - limits: Limits::for_char(c), + class, + limits, + mid_stretched: None, + // Math in need of updating. + extended_shape: false, italics_correction: Abs::zero(), accent_attach: (Abs::zero(), Abs::zero()), - class, - span, + size: Size::zero(), + baseline: None, + // Misc + align: Abs::zero(), + shift: TextElem::baseline_in(styles), modifiers: FrameModifiers::get_in(styles), - extended_shape: false, }; - fragment.set_id(ctx, id); - fragment - } - - /// Apply GSUB substitutions. - fn adjust_glyph_index(ctx: &MathContext, id: GlyphId) -> GlyphId { - if let Some(glyphwise_tables) = &ctx.glyphwise_tables { - glyphwise_tables.iter().fold(id, |id, table| table.apply(id)) - } else { - id - } + fragment.update_glyph(); + Ok(fragment) } /// Sets element id and boxes in appropriate way without changing other /// styles. This is used to replace the glyph with a stretch variant. - pub fn set_id(&mut self, ctx: &MathContext, id: GlyphId) { - let advance = ctx.ttf.glyph_hor_advance(id).unwrap_or_default(); - let italics = italics_correction(ctx, id, self.font_size).unwrap_or_default(); - let bbox = ctx.ttf.glyph_bounding_box(id).unwrap_or(Rect { - x_min: 0, - y_min: 0, - x_max: 0, - y_max: 0, - }); + pub fn update_glyph(&mut self) { + let id = GlyphId(self.item.glyphs[0].id); - let mut width = advance.scaled(ctx, self.font_size); + let extended_shape = is_extended_shape(&self.item.font, id); + let italics = italics_correction(&self.item.font, id).unwrap_or_default(); + let width = self.item.width(); + if !extended_shape { + self.item.glyphs[0].x_advance += italics; + } + let italics = italics.at(self.item.size); + + let (ascent, descent) = + ascent_descent(&self.item.font, id).unwrap_or((Em::zero(), Em::zero())); // The fallback for accents is half the width plus or minus the italics // correction. This is similar to how top and bottom attachments are // shifted. For bottom accents we do not use the accent attach of the // base as it is meant for top acccents. - let top_accent_attach = - accent_attach(ctx, id, self.font_size).unwrap_or((width + italics) / 2.0); + let top_accent_attach = accent_attach(&self.item.font, id) + .map(|x| x.at(self.item.size)) + .unwrap_or((width + italics) / 2.0); let bottom_accent_attach = (width - italics) / 2.0; - let extended_shape = is_extended_shape(ctx, id); - if !extended_shape { - width += italics; - } - - self.id = id; - self.width = width; - self.ascent = bbox.y_max.scaled(ctx, self.font_size); - self.descent = -bbox.y_min.scaled(ctx, self.font_size); + self.baseline = Some(ascent.at(self.item.size)); + self.size = Size::new( + self.item.width(), + ascent.at(self.item.size) + descent.at(self.item.size), + ); self.italics_correction = italics; self.accent_attach = (top_accent_attach, bottom_accent_attach); self.extended_shape = extended_shape; } - pub fn height(&self) -> Abs { - self.ascent + self.descent + // Reset a GlyphFragment's text field and math properties back to its + // base_id's. This is used to return a glyph to its unstretched state. + pub fn reset_glyph(&mut self) { + self.align = Abs::zero(); + self.item.glyphs = vec![self.base_glyph.clone()]; + self.update_glyph(); } - pub fn into_variant(self) -> VariantFragment { - VariantFragment { - c: self.c, - font_size: self.font_size, - italics_correction: self.italics_correction, - accent_attach: self.accent_attach, - class: self.class, - math_size: self.math_size, - span: self.span, - limits: self.limits, - extended_shape: self.extended_shape, - frame: self.into_frame(), - mid_stretched: None, - } + pub fn baseline(&self) -> Abs { + self.ascent() + } + + /// The distance from the baseline to the top of the frame. + pub fn ascent(&self) -> Abs { + self.baseline.unwrap_or(self.size.y) + } + + /// The distance from the baseline to the bottom of the frame. + pub fn descent(&self) -> Abs { + self.size.y - self.ascent() } pub fn into_frame(self) -> Frame { - let item = TextItem { - font: self.font.clone(), - size: self.font_size, - fill: self.fill, - stroke: self.stroke, - lang: self.lang, - region: self.region, - text: self.c.into(), - glyphs: vec![Glyph { - id: self.id.0, - x_advance: Em::from_length(self.width, self.font_size), - x_offset: Em::zero(), - range: 0..self.c.len_utf8() as u16, - span: (self.span, 0), - }], - }; - let size = Size::new(self.width, self.ascent + self.descent); - let mut frame = Frame::soft(size); - frame.set_baseline(self.ascent); - frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); + let mut frame = Frame::soft(self.size); + frame.set_baseline(self.baseline()); + frame.push( + Point::with_y(self.ascent() + self.shift + self.align), + FrameItem::Text(self.item), + ); frame.modify(&self.modifiers); frame } - pub fn make_script_size(&mut self, ctx: &MathContext) { - let alt_id = - ctx.ssty_table.as_ref().and_then(|ssty| ssty.try_apply(self.id, None)); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_script_script_size(&mut self, ctx: &MathContext) { - let alt_id = ctx.ssty_table.as_ref().and_then(|ssty| { - // We explicitly request to apply the alternate set with value 1, - // as opposed to the default value in ssty, as the former - // corresponds to second level scripts and the latter corresponds - // to first level scripts. - ssty.try_apply(self.id, Some(1)) - .or_else(|| ssty.try_apply(self.id, None)) - }); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_dotless_form(&mut self, ctx: &MathContext) { - let alt_id = - ctx.dtls_table.as_ref().and_then(|dtls| dtls.try_apply(self.id, None)); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - - pub fn make_flattened_accent_form(&mut self, ctx: &MathContext) { - let alt_id = - ctx.flac_table.as_ref().and_then(|flac| flac.try_apply(self.id, None)); - if let Some(alt_id) = alt_id { - self.set_id(ctx, alt_id); - } - } - /// Try to stretch a glyph to a desired height. - pub fn stretch_vertical(self, ctx: &mut MathContext, height: Abs) -> VariantFragment { - stretch_glyph(ctx, self, height, Axis::Y) + pub fn stretch_vertical(&mut self, ctx: &mut MathContext, height: Abs) { + self.stretch(ctx, height, Axis::Y) } /// Try to stretch a glyph to a desired width. - pub fn stretch_horizontal( - self, - ctx: &mut MathContext, - width: Abs, - ) -> VariantFragment { - stretch_glyph(ctx, self, width, Axis::X) + pub fn stretch_horizontal(&mut self, ctx: &mut MathContext, width: Abs) { + self.stretch(ctx, width, Axis::X) + } + + /// Try to stretch a glyph to a desired width or height. + /// + /// The resulting frame may not have the exact desired width or height. + pub fn stretch(&mut self, ctx: &mut MathContext, target: Abs, axis: Axis) { + self.reset_glyph(); + + // If the base glyph is good enough, use it. + let mut advance = self.size.get(axis); + if axis == Axis::X && !self.extended_shape { + // For consistency, we subtract the italics correction from the + // glyph's width if it was added in `update_glyph`. + advance -= self.italics_correction; + } + if target <= advance { + return; + } + + let id = GlyphId(self.item.glyphs[0].id); + let font = self.item.font.clone(); + let Some(construction) = glyph_construction(&font, id, axis) else { return }; + + // Search for a pre-made variant with a good advance. + let mut best_id = id; + let mut best_advance = advance; + for variant in construction.variants { + best_id = variant.variant_glyph; + best_advance = + self.item.font.to_em(variant.advance_measurement).at(self.item.size); + if target <= best_advance { + break; + } + } + + // This is either good or the best we've got. + if target <= best_advance || construction.assembly.is_none() { + self.item.glyphs[0].id = best_id.0; + self.item.glyphs[0].x_advance = + self.item.font.x_advance(best_id.0).unwrap_or_default(); + self.item.glyphs[0].x_offset = Em::zero(); + self.item.glyphs[0].y_advance = + self.item.font.y_advance(best_id.0).unwrap_or_default(); + self.item.glyphs[0].y_offset = Em::zero(); + self.update_glyph(); + return; + } + + // Assemble from parts. + let assembly = construction.assembly.unwrap(); + let min_overlap = min_connector_overlap(&self.item.font) + .unwrap_or_default() + .at(self.item.size); + assemble(ctx, self, assembly, min_overlap, target, axis); + } + + /// Vertically adjust the fragment's frame so that it is centered + /// on the axis. + pub fn center_on_axis(&mut self) { + self.align_on_axis(VAlignment::Horizon); + } + + /// Vertically adjust the fragment's frame so that it is aligned + /// to the given alignment on the axis. + pub fn align_on_axis(&mut self, align: VAlignment) { + let h = self.size.y; + let axis = axis_height(&self.item.font).unwrap().at(self.item.size); + self.align += self.baseline(); + self.baseline = Some(align.inv().position(h + axis * 2.0)); + self.align -= self.baseline(); } } impl Debug for GlyphFragment { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "GlyphFragment({:?})", self.c) - } -} - -#[derive(Clone)] -pub struct VariantFragment { - pub c: char, - pub italics_correction: Abs, - pub accent_attach: (Abs, Abs), - pub frame: Frame, - pub font_size: Abs, - pub class: MathClass, - pub math_size: MathSize, - pub span: Span, - pub limits: Limits, - pub mid_stretched: Option, - pub extended_shape: bool, -} - -impl VariantFragment { - /// Vertically adjust the fragment's frame so that it is centered - /// on the axis. - pub fn center_on_axis(&mut self, ctx: &MathContext) { - self.align_on_axis(ctx, VAlignment::Horizon) - } - - /// Vertically adjust the fragment's frame so that it is aligned - /// to the given alignment on the axis. - pub fn align_on_axis(&mut self, ctx: &MathContext, align: VAlignment) { - let h = self.frame.height(); - let axis = ctx.constants.axis_height().scaled(ctx, self.font_size); - self.frame.set_baseline(align.inv().position(h + axis * 2.0)); - } -} - -impl Debug for VariantFragment { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "VariantFragment({:?})", self.c) + write!(f, "GlyphFragment({:?})", self.item.text) } } @@ -566,46 +592,47 @@ impl FrameFragment { } } +fn ascent_descent(font: &Font, id: GlyphId) -> Option<(Em, Em)> { + let bbox = font.ttf().glyph_bounding_box(id)?; + Some((font.to_em(bbox.y_max), -font.to_em(bbox.y_min))) +} + /// Look up the italics correction for a glyph. -fn italics_correction(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option { - Some( - ctx.table - .glyph_info? - .italic_corrections? - .get(id)? - .scaled(ctx, font_size), - ) +fn italics_correction(font: &Font, id: GlyphId) -> Option { + font.ttf() + .tables() + .math? + .glyph_info? + .italic_corrections? + .get(id) + .map(|value| font.to_em(value.value)) } /// Loop up the top accent attachment position for a glyph. -fn accent_attach(ctx: &MathContext, id: GlyphId, font_size: Abs) -> Option { - Some( - ctx.table - .glyph_info? - .top_accent_attachments? - .get(id)? - .scaled(ctx, font_size), - ) +fn accent_attach(font: &Font, id: GlyphId) -> Option { + font.ttf() + .tables() + .math? + .glyph_info? + .top_accent_attachments? + .get(id) + .map(|value| font.to_em(value.value)) } /// Look up whether a glyph is an extended shape. -fn is_extended_shape(ctx: &MathContext, id: GlyphId) -> bool { - ctx.table - .glyph_info - .and_then(|info| info.extended_shapes) - .and_then(|info| info.get(id)) +fn is_extended_shape(font: &Font, id: GlyphId) -> bool { + font.ttf() + .tables() + .math + .and_then(|math| math.glyph_info) + .and_then(|glyph_info| glyph_info.extended_shapes) + .and_then(|coverage| coverage.get(id)) .is_some() } /// Look up a kerning value at a specific corner and height. -fn kern_at_height( - ctx: &MathContext, - font_size: Abs, - id: GlyphId, - corner: Corner, - height: Abs, -) -> Option { - let kerns = ctx.table.glyph_info?.kern_infos?.get(id)?; +fn kern_at_height(font: &Font, id: GlyphId, corner: Corner, height: Em) -> Option { + let kerns = font.ttf().tables().math?.glyph_info?.kern_infos?.get(id)?; let kern = match corner { Corner::TopLeft => kerns.top_left, Corner::TopRight => kerns.top_right, @@ -614,11 +641,187 @@ fn kern_at_height( }?; let mut i = 0; - while i < kern.count() && height > kern.height(i)?.scaled(ctx, font_size) { + while i < kern.count() && height > font.to_em(kern.height(i)?.value) { i += 1; } - Some(kern.kern(i)?.scaled(ctx, font_size)) + Some(font.to_em(kern.kern(i)?.value)) +} + +fn axis_height(font: &Font) -> Option { + Some(font.to_em(font.ttf().tables().math?.constants?.axis_height().value)) +} + +pub fn stretch_axes(font: &Font, id: u16) -> Axes { + let id = GlyphId(id); + let horizontal = font + .ttf() + .tables() + .math + .and_then(|math| math.variants) + .and_then(|variants| variants.horizontal_constructions.get(id)) + .is_some(); + let vertical = font + .ttf() + .tables() + .math + .and_then(|math| math.variants) + .and_then(|variants| variants.vertical_constructions.get(id)) + .is_some(); + + Axes::new(horizontal, vertical) +} + +fn min_connector_overlap(font: &Font) -> Option { + font.ttf() + .tables() + .math? + .variants + .map(|variants| font.to_em(variants.min_connector_overlap)) +} + +fn glyph_construction(font: &Font, id: GlyphId, axis: Axis) -> Option { + font.ttf() + .tables() + .math? + .variants + .map(|variants| match axis { + Axis::X => variants.horizontal_constructions, + Axis::Y => variants.vertical_constructions, + })? + .get(id) +} + +/// Assemble a glyph from parts. +fn assemble( + ctx: &mut MathContext, + base: &mut GlyphFragment, + assembly: GlyphAssembly, + min_overlap: Abs, + target: Abs, + axis: Axis, +) { + // Determine the number of times the extenders need to be repeated as well + // as a ratio specifying how much to spread the parts apart + // (0 = maximal overlap, 1 = minimal overlap). + let mut full; + let mut ratio; + let mut repeat = 0; + loop { + full = Abs::zero(); + ratio = 0.0; + + let mut parts = parts(assembly, repeat).peekable(); + let mut growable = Abs::zero(); + + while let Some(part) = parts.next() { + let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size); + if let Some(next) = parts.peek() { + let max_overlap = base + .item + .font + .to_em(part.end_connector_length.min(next.start_connector_length)) + .at(base.item.size); + if max_overlap < min_overlap { + // This condition happening is indicative of a bug in the + // font. + ctx.engine.sink.warn(warning!( + base.item.glyphs[0].span.0, + "glyph has assembly parts with overlap less than minConnectorOverlap"; + hint: "its rendering may appear broken - this is probably a font bug"; + hint: "please file an issue at https://github.com/typst/typst/issues" + )); + } + + advance -= max_overlap; + growable += max_overlap - min_overlap; + } + + full += advance; + } + + if full < target { + let delta = target - full; + ratio = (delta / growable).min(1.0); + full += ratio * growable; + } + + if target <= full || repeat >= MAX_REPEATS { + break; + } + + repeat += 1; + } + + let mut glyphs = vec![]; + let mut parts = parts(assembly, repeat).peekable(); + while let Some(part) = parts.next() { + let mut advance = base.item.font.to_em(part.full_advance).at(base.item.size); + if let Some(next) = parts.peek() { + let max_overlap = base + .item + .font + .to_em(part.end_connector_length.min(next.start_connector_length)) + .at(base.item.size); + advance -= max_overlap; + advance += ratio * (max_overlap - min_overlap); + } + let (x, y) = match axis { + Axis::X => (Em::from_length(advance, base.item.size), Em::zero()), + Axis::Y => (Em::zero(), Em::from_length(advance, base.item.size)), + }; + glyphs.push(Glyph { + id: part.glyph_id.0, + x_advance: x, + x_offset: Em::zero(), + y_advance: y, + y_offset: Em::zero(), + ..base.item.glyphs[0].clone() + }); + } + + match axis { + Axis::X => base.size.x = full, + Axis::Y => { + base.baseline = None; + base.size.y = full; + base.size.x = glyphs + .iter() + .map(|glyph| base.item.font.x_advance(glyph.id).unwrap_or_default()) + .max() + .unwrap_or_default() + .at(base.item.size); + } + } + + base.item.glyphs = glyphs; + base.italics_correction = base + .item + .font + .to_em(assembly.italics_correction.value) + .at(base.item.size); + if axis == Axis::X { + base.accent_attach = (full / 2.0, full / 2.0); + } + base.mid_stretched = None; + base.extended_shape = true; +} + +/// Return an iterator over the assembly's parts with extenders repeated the +/// specified number of times. +fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator + '_ { + assembly.parts.into_iter().flat_map(move |part| { + let count = if part.part_flags.extender() { repeat } else { 1 }; + std::iter::repeat_n(part, count) + }) +} + +pub fn has_dtls_feat(font: &Font) -> bool { + font.ttf() + .tables() + .gsub + .and_then(|gsub| gsub.features.index(ttf_parser::Tag::from_bytes(b"dtls"))) + .is_some() } /// Describes in which situation a frame should use limits for attachments. @@ -671,56 +874,3 @@ impl Limits { fn is_integral_char(c: char) -> bool { ('∫'..='∳').contains(&c) || ('⨋'..='⨜').contains(&c) } - -/// An OpenType substitution table that is applicable to glyph-wise substitutions. -pub enum GlyphwiseSubsts<'a> { - Single(SingleSubstitution<'a>), - Alternate(AlternateSubstitution<'a>, u32), -} - -impl<'a> GlyphwiseSubsts<'a> { - pub fn new(gsub: Option>, feature: Feature) -> Option { - let gsub = gsub?; - let table = gsub - .features - .find(feature.tag) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index))?; - let table = table.subtables.get::(0)?; - match table { - SubstitutionSubtable::Single(single_glyphs) => { - Some(Self::Single(single_glyphs)) - } - SubstitutionSubtable::Alternate(alt_glyphs) => { - Some(Self::Alternate(alt_glyphs, feature.value)) - } - _ => None, - } - } - - pub fn try_apply( - &self, - glyph_id: GlyphId, - alt_value: Option, - ) -> Option { - match self { - Self::Single(single) => match single { - SingleSubstitution::Format1 { coverage, delta } => coverage - .get(glyph_id) - .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))), - SingleSubstitution::Format2 { coverage, substitutes } => { - coverage.get(glyph_id).and_then(|idx| substitutes.get(idx)) - } - }, - Self::Alternate(alternate, value) => alternate - .coverage - .get(glyph_id) - .and_then(|idx| alternate.alternate_sets.get(idx)) - .and_then(|set| set.alternates.get(alt_value.unwrap_or(*value) as u16)), - } - } - - pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { - self.try_apply(glyph_id, None).unwrap_or(glyph_id) - } -} diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index bf8235411..e0caf4179 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -45,20 +45,20 @@ pub fn layout_lr( // Scale up fragments at both ends. match inner_fragments { - [one] => scale(ctx, styles, one, relative_to, height, None), + [one] => scale(ctx, one, relative_to, height, None), [first, .., last] => { - scale(ctx, styles, first, relative_to, height, Some(MathClass::Opening)); - scale(ctx, styles, last, relative_to, height, Some(MathClass::Closing)); + scale(ctx, first, relative_to, height, Some(MathClass::Opening)); + scale(ctx, last, relative_to, height, Some(MathClass::Closing)); } _ => {} } - // Handle MathFragment::Variant fragments that should be scaled up. + // Handle MathFragment::Glyph fragments that should be scaled up. for fragment in inner_fragments.iter_mut() { - if let MathFragment::Variant(ref mut variant) = fragment { - if variant.mid_stretched == Some(false) { - variant.mid_stretched = Some(true); - scale(ctx, styles, fragment, relative_to, height, Some(MathClass::Large)); + if let MathFragment::Glyph(ref mut glyph) = fragment { + if glyph.mid_stretched == Some(false) { + glyph.mid_stretched = Some(true); + scale(ctx, fragment, relative_to, height, Some(MathClass::Large)); } } } @@ -95,18 +95,9 @@ pub fn layout_mid( let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?; for fragment in &mut fragments { - match fragment { - MathFragment::Glyph(glyph) => { - let mut new = glyph.clone().into_variant(); - new.mid_stretched = Some(false); - new.class = MathClass::Fence; - *fragment = MathFragment::Variant(new); - } - MathFragment::Variant(variant) => { - variant.mid_stretched = Some(false); - variant.class = MathClass::Fence; - } - _ => {} + if let MathFragment::Glyph(ref mut glyph) = fragment { + glyph.mid_stretched = Some(false); + glyph.class = MathClass::Fence; } } @@ -117,7 +108,6 @@ pub fn layout_mid( /// Scale a math fragment to a height. fn scale( ctx: &mut MathContext, - styles: StyleChain, fragment: &mut MathFragment, relative_to: Abs, height: Rel, @@ -132,7 +122,6 @@ fn scale( let short_fall = DELIM_SHORT_FALL.at(fragment.font_size().unwrap_or_default()); stretch_fragment( ctx, - styles, fragment, Some(Axis::Y), Some(relative_to), diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index e509cecc7..278b1343e 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -9,8 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; use typst_syntax::Span; use super::{ - alignments, delimiter_alignment, style_for_denominator, AlignmentResult, - FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, + alignments, style_for_denominator, AlignmentResult, FrameFragment, GlyphFragment, + LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; const VERTICAL_PADDING: Ratio = Ratio::new(0.1); @@ -183,8 +183,12 @@ fn layout_body( // We pad ascent and descent with the ascent and descent of the paren // to ensure that normal matrices are aligned with others unless they are // way too big. - let paren = - GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); + let paren = GlyphFragment::new_char( + ctx.font, + styles.chain(&denom_style), + '(', + Span::detached(), + )?; for (column, col) in columns.iter().zip(&mut cols) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) { @@ -202,8 +206,8 @@ fn layout_body( )); } - ascent.set_max(cell.ascent().max(paren.ascent)); - descent.set_max(cell.descent().max(paren.descent)); + ascent.set_max(cell.ascent().max(paren.ascent())); + descent.set_max(cell.descent().max(paren.descent())); col.push(cell); } @@ -312,19 +316,19 @@ fn layout_delimiters( let target = height + VERTICAL_PADDING.of(height); frame.set_baseline(height / 2.0 + axis); - if let Some(left) = left { - let mut left = GlyphFragment::new(ctx, styles, left, span) - .stretch_vertical(ctx, target - short_fall); - left.align_on_axis(ctx, delimiter_alignment(left.c)); + if let Some(left_c) = left { + let mut left = GlyphFragment::new_char(ctx.font, styles, left_c, span)?; + left.stretch_vertical(ctx, target - short_fall); + left.center_on_axis(); ctx.push(left); } ctx.push(FrameFragment::new(styles, frame)); - if let Some(right) = right { - let mut right = GlyphFragment::new(ctx, styles, right, span) - .stretch_vertical(ctx, target - short_fall); - right.align_on_axis(ctx, delimiter_alignment(right.c)); + if let Some(right_c) = right { + let mut right = GlyphFragment::new_char(ctx.font, styles, right_c, span)?; + right.stretch_vertical(ctx, target - short_fall); + right.center_on_axis(); ctx.push(right); } diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 708a4443d..5fd22e578 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -13,8 +13,6 @@ mod stretch; mod text; mod underover; -use rustybuzz::Feature; -use ttf_parser::Tag; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{ @@ -30,7 +28,7 @@ use typst_library::math::*; use typst_library::model::ParElem; use typst_library::routines::{Arenas, RealizationKind}; use typst_library::text::{ - families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, + families, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, }; use typst_library::World; use typst_syntax::Span; @@ -38,11 +36,11 @@ use typst_utils::Numeric; use unicode_math_class::MathClass; use self::fragment::{ - FrameFragment, GlyphFragment, GlyphwiseSubsts, Limits, MathFragment, VariantFragment, + has_dtls_feat, stretch_axes, FrameFragment, GlyphFragment, Limits, MathFragment, }; use self::run::{LeftRightAlternator, MathRun, MathRunFrameBuilder}; use self::shared::*; -use self::stretch::{stretch_fragment, stretch_glyph}; +use self::stretch::stretch_fragment; /// Layout an inline equation (in a paragraph). #[typst_macros::time(span = elem.span())] @@ -58,7 +56,7 @@ pub fn layout_equation_inline( let font = find_math_font(engine, styles, elem.span())?; let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font); + let mut ctx = MathContext::new(engine, &mut locator, region, &font); let scale_style = style_for_script_scale(&ctx); let styles = styles.chain(&scale_style); @@ -113,7 +111,7 @@ pub fn layout_equation_block( let font = find_math_font(engine, styles, span)?; let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font); + let mut ctx = MathContext::new(engine, &mut locator, regions.base(), &font); let scale_style = style_for_script_scale(&ctx); let styles = styles.chain(&scale_style); @@ -374,14 +372,7 @@ struct MathContext<'a, 'v, 'e> { region: Region, // Font-related. font: &'a Font, - ttf: &'a ttf_parser::Face<'a>, - table: ttf_parser::math::Table<'a>, constants: ttf_parser::math::Constants<'a>, - dtls_table: Option>, - flac_table: Option>, - ssty_table: Option>, - glyphwise_tables: Option>>, - space_width: Em, // Mutable. fragments: Vec, } @@ -391,46 +382,20 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { fn new( engine: &'v mut Engine<'e>, locator: &'v mut SplitLocator<'a>, - styles: StyleChain<'a>, base: Size, font: &'a Font, ) -> Self { - let math_table = font.ttf().tables().math.unwrap(); - let gsub_table = font.ttf().tables().gsub; - let constants = math_table.constants.unwrap(); - - let feat = |tag: &[u8; 4]| { - GlyphwiseSubsts::new(gsub_table, Feature::new(Tag::from_bytes(tag), 0, ..)) - }; - - let features = features(styles); - let glyphwise_tables = Some( - features - .into_iter() - .filter_map(|feature| GlyphwiseSubsts::new(gsub_table, feature)) - .collect(), - ); - - let ttf = font.ttf(); - let space_width = ttf - .glyph_index(' ') - .and_then(|id| ttf.glyph_hor_advance(id)) - .map(|advance| font.to_em(advance)) - .unwrap_or(THICK); + // These unwraps are safe as the font given is one returned by the + // find_math_font function, which only returns fonts that have a math + // constants table. + let constants = font.ttf().tables().math.unwrap().constants.unwrap(); Self { engine, locator, region: Region::new(base, Axes::splat(false)), font, - ttf, - table: math_table, constants, - dtls_table: feat(b"dtls"), - flac_table: feat(b"flac"), - ssty_table: feat(b"ssty"), - glyphwise_tables, - space_width, fragments: vec![], } } @@ -529,7 +494,8 @@ fn layout_realized( if let Some(elem) = elem.to_packed::() { ctx.push(MathFragment::Tag(elem.tag.clone())); } else if elem.is::() { - ctx.push(MathFragment::Space(ctx.space_width.resolve(styles))); + let space_width = ctx.font.space_width().unwrap_or(THICK); + ctx.push(MathFragment::Space(space_width.resolve(styles))); } else if elem.is::() { ctx.push(MathFragment::Linebreak); } else if let Some(elem) = elem.to_packed::() { diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 32f527198..91b9b16af 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -49,9 +49,9 @@ pub fn layout_root( // Layout root symbol. let target = radicand.height() + thickness + gap; - let sqrt = GlyphFragment::new(ctx, styles, '√', span) - .stretch_vertical(ctx, target) - .frame; + let mut sqrt = GlyphFragment::new_char(ctx.font, styles, '√', span)?; + sqrt.stretch_vertical(ctx, target); + let sqrt = sqrt.into_frame(); // Layout the index. let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 600c130d4..1f88d2dd7 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -1,7 +1,9 @@ use ttf_parser::math::MathValue; +use ttf_parser::Tag; use typst_library::foundations::{Style, StyleChain}; -use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; +use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size}; use typst_library::math::{EquationElem, MathSize}; +use typst_library::text::{FontFeatures, TextElem}; use typst_utils::LazyHash; use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; @@ -59,6 +61,16 @@ pub fn style_cramped() -> LazyHash + + diff --git a/tests/suite/html/syntax.typ b/tests/suite/html/syntax.typ index fb5caf3bd..eb1c86994 100644 --- a/tests/suite/html/syntax.typ +++ b/tests/suite/html/syntax.typ @@ -10,3 +10,54 @@ #html.pre("hello") #html.pre("\nhello") #html.pre("\n\nhello") + +--- html-script html --- +// This should be pretty and indented. +#html.script( + ```js + const x = 1 + const y = 2 + console.log(x < y, Math.max(1, 2)) + ```.text, +) + +// This should have extra newlines, but no indent because of the multiline +// string literal. +#html.script("console.log(`Hello\nWorld`)") + +// This should be untouched. +#html.script( + type: "text/python", + ```py + x = 1 + y = 2 + print(x < y, max(x, y)) + ```.text, +) + +--- html-style html --- +// This should be pretty and indented. +#html.style( + ```css + body { + text: red; + } + ```.text, +) + +--- html-raw-text-contains-elem html --- +// Error: 14-32 HTML raw text element cannot have non-text children +#html.script(html.strong[Hello]) + +--- html-raw-text-contains-frame html --- +// Error: 2-29 HTML raw text element cannot have non-text children +#html.script(html.frame[Ok]) + +--- html-raw-text-contains-closing-tag html --- +// Error: 2-32 HTML raw text element cannot contain its own closing tag +// Hint: 2-32 the sequence `") From 38dd6da237b8d1ea86f82069338d9ceae479d180 Mon Sep 17 00:00:00 2001 From: Wannes Malfait <46323945+WannesMalfait@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:58:04 +0200 Subject: [PATCH 108/162] Fix stroke cap of shapes with partial stroke (#5688) --- crates/typst-layout/src/shapes.rs | 113 +++++++++++++++++++++++++++--- tests/ref/rect-stroke-caps.png | Bin 0 -> 252 bytes tests/suite/visualize/rect.typ | 16 +++++ 3 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 tests/ref/rect-stroke-caps.png diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 7ab41e9d4..0616b4ce4 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -11,8 +11,8 @@ use typst_library::layout::{ }; use typst_library::visualize::{ CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule, - FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem, - Shape, SquareElem, Stroke, + FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem, + RectElem, Shape, SquareElem, Stroke, }; use typst_syntax::Span; use typst_utils::{Get, Numeric}; @@ -889,7 +889,13 @@ fn segmented_rect( let end = current; last = current; let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue }; - let (shape, ontop) = segment(start, end, &corners, stroke); + let start_cap = stroke.cap; + let end_cap = match strokes.get_ref(end.side_ccw()) { + Some(stroke) => stroke.cap, + None => start_cap, + }; + let (shape, ontop) = + segment(start, end, start_cap, end_cap, &corners, stroke); if ontop { res.push(shape); } else { @@ -899,7 +905,14 @@ fn segmented_rect( } } else if let Some(stroke) = &strokes.top { // single segment - let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke); + let (shape, _) = segment( + Corner::TopLeft, + Corner::TopLeft, + stroke.cap, + stroke.cap, + &corners, + stroke, + ); res.push(shape); } res @@ -946,6 +959,8 @@ fn curve_segment( fn segment( start: Corner, end: Corner, + start_cap: LineCap, + end_cap: LineCap, corners: &Corners, stroke: &FixedStroke, ) -> (Shape, bool) { @@ -979,7 +994,7 @@ fn segment( let use_fill = solid && fill_corners(start, end, corners); let shape = if use_fill { - fill_segment(start, end, corners, stroke) + fill_segment(start, end, start_cap, end_cap, corners, stroke) } else { stroke_segment(start, end, corners, stroke.clone()) }; @@ -1010,6 +1025,8 @@ fn stroke_segment( fn fill_segment( start: Corner, end: Corner, + start_cap: LineCap, + end_cap: LineCap, corners: &Corners, stroke: &FixedStroke, ) -> Shape { @@ -1035,8 +1052,7 @@ fn fill_segment( if c.arc_outer() { curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); } else { - curve.line(c.outer()); - curve.line(c.end_outer()); + c.start_cap(&mut curve, start_cap); } } @@ -1079,7 +1095,7 @@ fn fill_segment( if c.arc_inner() { curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); } else { - curve.line(c.center_inner()); + c.end_cap(&mut curve, end_cap); } } @@ -1134,6 +1150,16 @@ struct ControlPoints { } impl ControlPoints { + /// Rotate point around the origin, relative to the top-left. + fn rotate_centered(&self, point: Point) -> Point { + match self.corner { + Corner::TopLeft => point, + Corner::TopRight => Point { x: -point.y, y: point.x }, + Corner::BottomRight => Point { x: -point.x, y: -point.y }, + Corner::BottomLeft => Point { x: point.y, y: -point.x }, + } + } + /// Move and rotate the point from top-left to the required corner. fn rotate(&self, point: Point) -> Point { match self.corner { @@ -1280,6 +1306,77 @@ impl ControlPoints { y: self.stroke_after, }) } + + /// Draw the cap at the beginning of the segment. + /// + /// If this corner has a stroke before it, + /// a default "butt" cap is used. + /// + /// NOTE: doesn't support the case where the corner has a radius. + pub fn start_cap(&self, curve: &mut Curve, cap_type: LineCap) { + if self.stroke_before != Abs::zero() + || self.radius != Abs::zero() + || cap_type == LineCap::Butt + { + // Just the default cap. + curve.line(self.outer()); + } else if cap_type == LineCap::Square { + // Extend by the stroke width. + let offset = + self.rotate_centered(Point { x: -self.stroke_after, y: Abs::zero() }); + curve.line(self.end_inner() + offset); + curve.line(self.outer() + offset); + } else if cap_type == LineCap::Round { + // We push the center by a little bit to ensure the correct + // half of the circle gets drawn. If it is perfectly centered + // the `arc` function just degenerates into a line, which we + // do not want in this case. + curve.arc( + self.end_inner(), + (self.end_inner() + + self.rotate_centered(Point { x: Abs::raw(1.0), y: Abs::zero() }) + + self.outer()) + / 2., + self.outer(), + ); + } + curve.line(self.end_outer()); + } + + /// Draw the cap at the end of the segment. + /// + /// If this corner has a stroke before it, + /// a default "butt" cap is used. + /// + /// NOTE: doesn't support the case where the corner has a radius. + pub fn end_cap(&self, curve: &mut Curve, cap_type: LineCap) { + if self.stroke_after != Abs::zero() + || self.radius != Abs::zero() + || cap_type == LineCap::Butt + { + // Just the default cap. + curve.line(self.center_inner()); + } else if cap_type == LineCap::Square { + // Extend by the stroke width. + let offset = + self.rotate_centered(Point { x: Abs::zero(), y: -self.stroke_before }); + curve.line(self.outer() + offset); + curve.line(self.center_inner() + offset); + } else if cap_type == LineCap::Round { + // We push the center by a little bit to ensure the correct + // half of the circle gets drawn. If it is perfectly centered + // the `arc` function just degenerates into a line, which we + // do not want in this case. + curve.arc( + self.outer(), + (self.outer() + + self.rotate_centered(Point { x: Abs::zero(), y: Abs::raw(1.0) }) + + self.center_inner()) + / 2., + self.center_inner(), + ); + } + } } /// Helper to draw arcs with Bézier curves. diff --git a/tests/ref/rect-stroke-caps.png b/tests/ref/rect-stroke-caps.png new file mode 100644 index 0000000000000000000000000000000000000000..13a34ad9aaf255c1a8f758c93842dd88cdb850ff GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^6+oQ90VEjYZ)Q&gQinZV978H@y}j-zc*sG(>iSw3$>=}Dp&?udk6iOuDG73+bd*Hz3W{yHpf3r%RKw) z&_(ZBlLmFqUC$1=8YV!&gW$fsFP7*@9q{%E{ Date: Mon, 23 Jun 2025 17:09:03 +0200 Subject: [PATCH 109/162] Adding Croatian translations entries (#6413) --- crates/typst-library/src/text/lang.rs | 1 + crates/typst-library/translations/hr.txt | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 crates/typst-library/translations/hr.txt diff --git a/crates/typst-library/src/text/lang.rs b/crates/typst-library/src/text/lang.rs index e06156c43..a170714b5 100644 --- a/crates/typst-library/src/text/lang.rs +++ b/crates/typst-library/src/text/lang.rs @@ -30,6 +30,7 @@ const TRANSLATIONS: &[(&str, &str)] = &[ translation!("fr"), translation!("gl"), translation!("he"), + translation!("hr"), translation!("hu"), translation!("id"), translation!("is"), diff --git a/crates/typst-library/translations/hr.txt b/crates/typst-library/translations/hr.txt new file mode 100644 index 000000000..ea0754592 --- /dev/null +++ b/crates/typst-library/translations/hr.txt @@ -0,0 +1,8 @@ +figure = Slika +table = Tablica +equation = Jednadžba +bibliography = Literatura +heading = Odjeljak +outline = Sadržaj +raw = Kôd +page = str. From 24293a6c121a4b4e02c32901fec44e0093aa5d8c Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:56:58 +0300 Subject: [PATCH 110/162] Rewrite `outline.indent` example (#6383) Co-authored-by: Laurenz --- crates/typst-library/src/model/outline.rs | 35 +++++++++++------------ 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 489c375e6..16a116146 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -225,25 +225,21 @@ pub struct OutlineElem { /// to just specifying `{2em}`. /// /// ```example - /// #set heading(numbering: "1.a.") + /// >>> #show heading: none + /// #set heading(numbering: "I-I.") + /// #set outline(title: none) /// - /// #outline( - /// title: [Contents (Automatic)], - /// indent: auto, - /// ) + /// #outline() + /// #line(length: 100%) + /// #outline(indent: 3em) /// - /// #outline( - /// title: [Contents (Length)], - /// indent: 2em, - /// ) - /// - /// = About ACME Corp. - /// == History - /// === Origins - /// #lorem(10) - /// - /// == Products - /// #lorem(10) + /// = Software engineering technologies + /// == Requirements + /// == Tools and technologies + /// === Code editors + /// == Analyzing alternatives + /// = Designing software components + /// = Testing and integration /// ``` pub indent: Smart, } @@ -450,8 +446,9 @@ impl OutlineEntry { /// at the same level are aligned. /// /// If the outline's indent is a fixed value or a function, the prefixes are - /// indented, but the inner contents are simply inset from the prefix by the - /// specified `gap`, rather than aligning outline-wide. + /// indented, but the inner contents are simply offset from the prefix by + /// the specified `gap`, rather than aligning outline-wide. For a visual + /// explanation, see [`outline.indent`]($outline.indent). #[func(contextual)] pub fn indented( &self, From 899de6d5d501c6aed04897d425dd3615e745743e Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 24 Jun 2025 10:03:10 +0000 Subject: [PATCH 111/162] Use ICU data to check if accent is bottom (#6393) Co-authored-by: Laurenz --- crates/typst-library/src/math/accent.rs | 30 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index f2c9168c2..c8569ea23 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -1,3 +1,10 @@ +use std::sync::LazyLock; + +use icu_properties::maps::CodePointMapData; +use icu_properties::CanonicalCombiningClass; +use icu_provider::AsDeserializingBufferProvider; +use icu_provider_blob::BlobDataProvider; + use crate::diag::bail; use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem}; use crate::layout::{Length, Rel}; @@ -81,17 +88,22 @@ impl Accent { Self(Self::combine(c).unwrap_or(c)) } - /// List of bottom accents. Currently just a list of ones included in the - /// Unicode math class document. - const BOTTOM: &[char] = &[ - '\u{0323}', '\u{032C}', '\u{032D}', '\u{032E}', '\u{032F}', '\u{0330}', - '\u{0331}', '\u{0332}', '\u{0333}', '\u{033A}', '\u{20E8}', '\u{20EC}', - '\u{20ED}', '\u{20EE}', '\u{20EF}', - ]; - /// Whether this accent is a bottom accent or not. pub fn is_bottom(&self) -> bool { - Self::BOTTOM.contains(&self.0) + static COMBINING_CLASS_DATA: LazyLock> = + LazyLock::new(|| { + icu_properties::maps::load_canonical_combining_class( + &BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU) + .unwrap() + .as_deserializing(), + ) + .unwrap() + }); + + matches!( + COMBINING_CLASS_DATA.as_borrowed().get(self.0), + CanonicalCombiningClass::Below + ) } } From 87c56865606e027f552a4dbc800c6851b0d0b821 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:22:55 +0300 Subject: [PATCH 112/162] Add docs for `std` module (#6407) Co-authored-by: Laurenz --- crates/typst-library/src/lib.rs | 2 +- docs/reference/groups.yml | 53 +++++++++++++++++++++++++++++++++ docs/src/lib.rs | 2 +- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index c39024f71..fa7977888 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -148,7 +148,7 @@ pub struct Library { /// The default style properties (for page size, font selection, and /// everything else configurable via set and show rules). pub styles: Styles, - /// The standard library as a value. Used to provide the `std` variable. + /// The standard library as a value. Used to provide the `std` module. pub std: Binding, /// In-development features that were enabled. pub features: Features, diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index e5aa7e999..c7e3d9964 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -137,6 +137,59 @@ In addition to the functions listed below, the `calc` module also defines the constants `pi`, `tau`, `e`, and `inf`. +- name: std + title: Standard library + category: foundations + path: ["std"] + details: | + A module that contains all globally accessible items. + + # Using "shadowed" definitions + The `std` module is useful whenever you overrode a name from the global + scope (this is called _shadowing_). For instance, you might have used the + name `text` for a parameter. To still access the `text` element, write + `std.text`. + + ```example + >>> #set page(margin: (left: 3em)) + #let par = [My special paragraph.] + #let special(text) = { + set std.text(style: "italic") + set std.par.line(numbering: "1") + text + } + + #special(par) + + #lorem(10) + ``` + + # Conditional access + You can also use this in combination with the [dictionary + constructor]($dictionary) to conditionally access global definitions. This + can, for instance, be useful to use new or experimental functionality when + it is available, while falling back to an alternative implementation if + used on an older Typst version. In particular, this allows us to create + [polyfills](https://en.wikipedia.org/wiki/Polyfill_(programming)). + + This can be as simple as creating an alias to prevent warning messages, for + example, conditionally using `pattern` in Typst version 0.12, but using + [`tiling`] in newer versions. Since the parameters accepted by the `tiling` + function match those of the older `pattern` function, using the `tiling` + function when available and falling back to `pattern` otherwise will unify + the usage across all versions. Note that, when creating a polyfill, + [`sys.version`]($category/foundations/sys) can also be very useful. + + ```typ + #let tiling = if "tiling" in dictionary(std) { + tiling + } else { + pattern + } + + ... + ``` + - name: sys title: System category: foundations diff --git a/docs/src/lib.rs b/docs/src/lib.rs index b81f0dc66..9bd21c2e8 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -37,7 +37,7 @@ static GROUPS: LazyLock> = LazyLock::new(|| { let mut groups: Vec = yaml::from_str(load!("reference/groups.yml")).unwrap(); for group in &mut groups { - if group.filter.is_empty() { + if group.filter.is_empty() && group.name != "std" { group.filter = group .module() .scope() From f162c371017f0d503cfae8738cbbf505b9f11173 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:49:28 +0300 Subject: [PATCH 113/162] Improve equation reference example (#6481) --- crates/typst-library/src/model/reference.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index f22d70b32..6fddc56ca 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -91,16 +91,13 @@ use crate::text::TextElem; /// #show ref: it => { /// let eq = math.equation /// let el = it.element -/// if el != none and el.func() == eq { -/// // Override equation references. -/// link(el.location(),numbering( -/// el.numbering, -/// ..counter(eq).at(el.location()) -/// )) -/// } else { -/// // Other references as usual. -/// it -/// } +/// // Skip all other references. +/// if el == none or el.func() != eq { return it } +/// // Override equation references. +/// link(el.location(), numbering( +/// el.numbering, +/// ..counter(eq).at(el.location()) +/// )) /// } /// /// = Beginnings From d4be7c4ca54ce1907ce5f7af8a603cf3f4c5a42f Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:00:51 +0300 Subject: [PATCH 114/162] Add page reference customization example (#6480) Co-authored-by: Laurenz --- crates/typst-library/src/model/reference.rs | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 6fddc56ca..17f93b7c4 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -79,6 +79,36 @@ use crate::text::TextElem; /// reference: `[@intro[Chapter]]`. /// /// # Customization +/// When you only ever need to reference pages of a figure/table/heading/etc. in +/// a document, the default `form` field value can be changed to `{"page"}` with +/// a set rule. If you prefer a short "p." supplement over "page", the +/// [`page.supplement`]($page.supplement) field can be used for changing this: +/// +/// ```example +/// #set page( +/// numbering: "1", +/// supplement: "p.", +/// >>> margin: (bottom: 3em), +/// >>> footer-descent: 1.25em, +/// ) +/// #set ref(form: "page") +/// +/// #figure( +/// stack( +/// dir: ltr, +/// spacing: 1em, +/// circle(), +/// square(), +/// ), +/// caption: [Shapes], +/// ) +/// +/// #pagebreak() +/// +/// See @shapes for examples +/// of different shapes. +/// ``` +/// /// If you write a show rule for references, you can access the referenced /// element through the `element` field of the reference. The `element` may /// be `{none}` even if it exists if Typst hasn't discovered it yet, so you From 70399a94fd58cc5e3e953c10670c396de8f7f6f7 Mon Sep 17 00:00:00 2001 From: Tobias Schmitz Date: Tue, 24 Jun 2025 15:23:37 +0200 Subject: [PATCH 115/162] Bump `krilla` to current Git version (#6488) Co-authored-by: Laurenz --- Cargo.lock | 18 ++++++++---------- Cargo.toml | 4 ++-- crates/typst-pdf/src/embed.rs | 3 +-- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58cac3c58..3ea423f5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,9 +786,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "font-types" -version = "0.8.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf" +checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7" dependencies = [ "bytemuck", ] @@ -1367,8 +1367,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9" +source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" dependencies = [ "base64", "bumpalo", @@ -1396,8 +1395,7 @@ dependencies = [ [[package]] name = "krilla-svg" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e" +source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7" dependencies = [ "flate2", "fontdb", @@ -2106,9 +2104,9 @@ dependencies = [ [[package]] name = "read-fonts" -version = "0.28.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288" +checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91" dependencies = [ "bytemuck", "font-types", @@ -2434,9 +2432,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "skrifa" -version = "0.30.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8" +checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97" dependencies = [ "bytemuck", "read-fonts", diff --git a/Cargo.toml b/Cargo.toml index 72ab9094d..3cfb72008 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,8 +73,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg indexmap = { version = "2", features = ["serde"] } infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" -krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] } -krilla-svg = "0.1.0" +krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] } +krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" } kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs index f0cd9060a..36330c445 100644 --- a/crates/typst-pdf/src/embed.rs +++ b/crates/typst-pdf/src/embed.rs @@ -34,8 +34,7 @@ pub(crate) fn embed_files( }, }; let data: Arc + Send + Sync> = Arc::new(embed.data.clone()); - // TODO: update when new krilla version lands (https://github.com/LaurenzV/krilla/pull/203) - let compress = should_compress(&embed.data).unwrap_or(true); + let compress = should_compress(&embed.data); let file = EmbeddedFile { path, From 9e3c1199edddc0422d34a266681d2efe1babd0c1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 24 Jun 2025 17:05:02 +0200 Subject: [PATCH 116/162] Check that git tree is clean after build (#6495) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5c81537b..2354de582 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,7 @@ jobs: - run: cargo clippy --workspace --all-targets --no-default-features - run: cargo fmt --check --all - run: cargo doc --workspace --no-deps + - run: git diff --exit-code min-version: name: Check minimum Rust version From f2f527c451b1b05b393af99b89c528aadb203ce6 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 24 Jun 2025 17:52:15 +0200 Subject: [PATCH 117/162] Also fix encoding of ` + diff --git a/tests/suite/html/syntax.typ b/tests/suite/html/syntax.typ index eb1c86994..4bda0c686 100644 --- a/tests/suite/html/syntax.typ +++ b/tests/suite/html/syntax.typ @@ -11,6 +11,9 @@ #html.pre("\nhello") #html.pre("\n\nhello") +--- html-textarea-starting-with-newline html --- +#html.textarea("\nenter") + --- html-script html --- // This should be pretty and indented. #html.script( From d54544297beba0a762bee9bc731baab96e4d7250 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 25 Jun 2025 12:58:40 -0400 Subject: [PATCH 118/162] Minor fixes to doc comments (#6500) --- crates/typst-layout/src/inline/line.rs | 6 +++++- crates/typst-library/src/model/bibliography.rs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 659d33f4a..f05189275 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -640,7 +640,7 @@ impl<'a> Items<'a> { self.0.push(entry.into()); } - /// Iterate over the items + /// Iterate over the items. pub fn iter(&self) -> impl Iterator> { self.0.iter().map(|item| &**item) } @@ -698,6 +698,10 @@ impl Debug for Items<'_> { } /// A reference to or a boxed item. +/// +/// This is conceptually similar to a [`Cow<'a, Item<'a>>`][std::borrow::Cow], +/// but we box owned items since an [`Item`] is much bigger than +/// a box. pub enum ItemEntry<'a> { Ref(&'a Item<'a>), Box(Box>), diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 8056d4ab3..e1a073594 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -592,7 +592,7 @@ impl Works { /// Context for generating the bibliography. struct Generator<'a> { - /// The routines that is used to evaluate mathematical material in citations. + /// The routines that are used to evaluate mathematical material in citations. routines: &'a Routines, /// The world that is used to evaluate mathematical material in citations. world: Tracked<'a, dyn World + 'a>, @@ -609,7 +609,7 @@ struct Generator<'a> { /// Details about a group of merged citations. All citations are put into groups /// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). -/// Even single citations will be put into groups of length ones. +/// Even single citations will be put into groups of length one. struct GroupInfo { /// The group's location. location: Location, From d3caedd813b1ca4379a71eb1b4aa636096d53a04 Mon Sep 17 00:00:00 2001 From: Connor K Date: Wed, 25 Jun 2025 12:59:19 -0400 Subject: [PATCH 119/162] Fix typos in page-setup.md (#6499) --- docs/guides/page-setup.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md index 36ed0fa23..1682c1220 100644 --- a/docs/guides/page-setup.md +++ b/docs/guides/page-setup.md @@ -206,7 +206,6 @@ label exists on the current page: ```typ >>> #set page("a5", margin: (x: 2.5cm, y: 3cm)) #set page(header: context { - let page-counter = let matches = query() let current = counter(page).get() let has-table = matches.any(m => @@ -218,7 +217,7 @@ label exists on the current page: #h(1fr) National Academy of Sciences ] -})) +}) #lorem(100) #pagebreak() From 35809387f88483bfa3d0978cfc3303eba0de632b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 Jun 2025 10:06:22 +0200 Subject: [PATCH 120/162] Support `in` operator on strings and modules (#6498) --- .../typst-library/src/foundations/module.rs | 19 +++++++++++++++---- crates/typst-library/src/foundations/ops.rs | 1 + docs/reference/groups.yml | 6 +----- tests/suite/scripting/ops.typ | 2 ++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 55d8bab63..14eefca39 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -19,11 +19,8 @@ use crate::foundations::{repr, ty, Content, Scope, Value}; /// /// You can access definitions from the module using [field access /// notation]($scripting/#fields) and interact with it using the [import and -/// include syntaxes]($scripting/#modules). Alternatively, it is possible to -/// convert a module to a dictionary, and therefore access its contents -/// dynamically, using the [dictionary constructor]($dictionary/#constructor). +/// include syntaxes]($scripting/#modules). /// -/// # Example /// ```example /// <<< #import "utils.typ" /// <<< #utils.add(2, 5) @@ -34,6 +31,20 @@ use crate::foundations::{repr, ty, Content, Scope, Value}; /// >>> /// >>> #(-3) /// ``` +/// +/// You can check whether a definition is present in a module using the `{in}` +/// operator, with a string on the left-hand side. This can be useful to +/// [conditionally access]($category/foundations/std/#conditional-access) +/// definitions in a module. +/// +/// ```example +/// #("table" in std) \ +/// #("nope" in std) +/// ``` +/// +/// Alternatively, it is possible to convert a module to a dictionary, and +/// therefore access its contents dynamically, using the [dictionary +/// constructor]($dictionary/#constructor). #[ty(cast)] #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 6c2408446..3c6a5e6cf 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -558,6 +558,7 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option { (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Dyn(a), Str(b)) => a.downcast::().map(|regex| regex.is_match(b)), (Str(a), Dict(b)) => Some(b.contains(a)), + (Str(a), Module(b)) => Some(b.scope().get(a).is_some()), (a, Array(b)) => Some(b.contains(a.clone())), _ => Option::None, diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index c7e3d9964..e01d99dc4 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -181,11 +181,7 @@ [`sys.version`]($category/foundations/sys) can also be very useful. ```typ - #let tiling = if "tiling" in dictionary(std) { - tiling - } else { - pattern - } + #let tiling = if "tiling" in std { tiling } else { pattern } ... ``` diff --git a/tests/suite/scripting/ops.typ b/tests/suite/scripting/ops.typ index d17c0117f..561682f05 100644 --- a/tests/suite/scripting/ops.typ +++ b/tests/suite/scripting/ops.typ @@ -264,6 +264,8 @@ #test("Hey" not in "abheyCd", true) #test("a" not /* fun comment? */ in "abc", false) +#test("sys" in std, true) +#test("system" in std, false) --- ops-not-trailing --- // Error: 10 expected keyword `in` From 6a1d6c08e2d6e4c184c6d177e67796b23ccbe4c7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 26 Jun 2025 10:07:41 +0200 Subject: [PATCH 121/162] Consistent sizing for `html.frame` (#6505) --- crates/typst-html/src/encode.rs | 15 ++++++++++----- crates/typst-html/src/lib.rs | 7 +++++-- crates/typst-library/src/html/dom.rs | 17 ++++++++++++++--- .../src/introspection/introspector.rs | 2 +- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 9c7938360..84860dbe9 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -3,9 +3,8 @@ use std::fmt::Write; use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::foundations::Repr; use typst_library::html::{ - attr, charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag, + attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag, }; -use typst_library::layout::Frame; use typst_syntax::Span; /// Encodes an HTML document into a string. @@ -304,9 +303,15 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> { } /// Encode a laid out frame into the writer. -fn write_frame(w: &mut Writer, frame: &Frame) { +fn write_frame(w: &mut Writer, frame: &HtmlFrame) { // FIXME: This string replacement is obviously a hack. - let svg = typst_svg::svg_frame(frame) - .replace(" Self::intern(&v)?, } +/// Layouted content that will be embedded into HTML as an SVG. +#[derive(Debug, Clone, Hash)] +pub struct HtmlFrame { + /// The frame that will be displayed as an SVG. + pub inner: Frame, + /// The text size where the frame was defined. This is used to size the + /// frame with em units to make text in and outside of the frame sized + /// consistently. + pub text_size: Abs, +} + /// Defines syntactical properties of HTML tags, attributes, and text. pub mod charsets { /// Check whether a character is in a tag name. diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index 9751dfcb8..d2ad0525b 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -446,7 +446,7 @@ impl IntrospectorBuilder { HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), HtmlNode::Frame(frame) => self.discover_in_frame( sink, - frame, + &frame.inner, NonZeroUsize::ONE, Transform::identity(), ), From 04fd0acacab8cf2e82268da9c18ef4bcf37507dc Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 26 Jun 2025 09:24:21 +0100 Subject: [PATCH 122/162] Allow deprecating symbol variants (#6441) --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/typst-ide/src/complete.rs | 2 +- .../typst-library/src/foundations/symbol.rs | 59 ++++++++++++------- crates/typst-library/src/foundations/value.rs | 4 +- docs/src/lib.rs | 14 ++--- tests/suite/math/attach.typ | 6 +- 7 files changed, 51 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ea423f5f..91ff48432 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "codex" version = "0.1.1" -source = "git+https://github.com/typst/codex?rev=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe" +source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928" [[package]] name = "color-print" diff --git a/Cargo.toml b/Cargo.toml index 3cfb72008..76d83995f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "56eb217" } +codex = { git = "https://github.com/typst/codex", rev = "a5428cb" } color-print = "0.3.6" comemo = "0.4" csv = "1" diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 536423318..bc5b3e10e 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -448,7 +448,7 @@ fn field_access_completions( match value { Value::Symbol(symbol) => { for modifier in symbol.modifiers() { - if let Ok(modified) = symbol.clone().modified(modifier) { + if let Ok(modified) = symbol.clone().modified((), modifier) { ctx.completions.push(Completion { kind: CompletionKind::Symbol(modified.get()), label: modifier.into(), diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 0f503edd0..f57bb0c2a 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -8,7 +8,7 @@ use serde::{Serialize, Serializer}; use typst_syntax::{is_ident, Span, Spanned}; use typst_utils::hash128; -use crate::diag::{bail, SourceResult, StrResult}; +use crate::diag::{bail, DeprecationSink, SourceResult, StrResult}; use crate::foundations::{ cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed, PlainText, Repr as _, @@ -54,18 +54,22 @@ enum Repr { /// A native symbol that has no named variant. Single(char), /// A native symbol with multiple named variants. - Complex(&'static [(ModifierSet<&'static str>, char)]), + Complex(&'static [Variant<&'static str>]), /// A symbol with multiple named variants, where some modifiers may have /// been applied. Also used for symbols defined at runtime by the user with /// no modifier applied. Modified(Arc<(List, ModifierSet)>), } +/// A symbol variant, consisting of a set of modifiers, a character, and an +/// optional deprecation message. +type Variant = (ModifierSet, char, Option); + /// A collection of symbols. #[derive(Clone, Eq, PartialEq, Hash)] enum List { - Static(&'static [(ModifierSet<&'static str>, char)]), - Runtime(Box<[(ModifierSet, char)]>), + Static(&'static [Variant<&'static str>]), + Runtime(Box<[Variant]>), } impl Symbol { @@ -76,14 +80,14 @@ impl Symbol { /// Create a symbol with a static variant list. #[track_caller] - pub const fn list(list: &'static [(ModifierSet<&'static str>, char)]) -> Self { + pub const fn list(list: &'static [Variant<&'static str>]) -> Self { debug_assert!(!list.is_empty()); Self(Repr::Complex(list)) } /// Create a symbol with a runtime variant list. #[track_caller] - pub fn runtime(list: Box<[(ModifierSet, char)]>) -> Self { + pub fn runtime(list: Box<[Variant]>) -> Self { debug_assert!(!list.is_empty()); Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default())))) } @@ -93,9 +97,11 @@ impl Symbol { match &self.0 { Repr::Single(c) => *c, Repr::Complex(_) => ModifierSet::<&'static str>::default() - .best_match_in(self.variants()) + .best_match_in(self.variants().map(|(m, c, _)| (m, c))) .unwrap(), - Repr::Modified(arc) => arc.1.best_match_in(self.variants()).unwrap(), + Repr::Modified(arc) => { + arc.1.best_match_in(self.variants().map(|(m, c, _)| (m, c))).unwrap() + } } } @@ -128,7 +134,11 @@ impl Symbol { } /// Apply a modifier to the symbol. - pub fn modified(mut self, modifier: &str) -> StrResult { + pub fn modified( + mut self, + sink: impl DeprecationSink, + modifier: &str, + ) -> StrResult { if let Repr::Complex(list) = self.0 { self.0 = Repr::Modified(Arc::new((List::Static(list), ModifierSet::default()))); @@ -137,7 +147,12 @@ impl Symbol { if let Repr::Modified(arc) = &mut self.0 { let (list, modifiers) = Arc::make_mut(arc); modifiers.insert_raw(modifier); - if modifiers.best_match_in(list.variants()).is_some() { + if let Some(deprecation) = + modifiers.best_match_in(list.variants().map(|(m, _, d)| (m, d))) + { + if let Some(message) = deprecation { + sink.emit(message) + } return Ok(self); } } @@ -146,7 +161,7 @@ impl Symbol { } /// The characters that are covered by this symbol. - pub fn variants(&self) -> impl Iterator, char)> { + pub fn variants(&self) -> impl Iterator> { match &self.0 { Repr::Single(c) => Variants::Single(Some(*c).into_iter()), Repr::Complex(list) => Variants::Static(list.iter()), @@ -161,7 +176,7 @@ impl Symbol { _ => ModifierSet::default(), }; self.variants() - .flat_map(|(m, _)| m) + .flat_map(|(m, _, _)| m) .filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier)) .collect::>() .into_iter() @@ -256,7 +271,7 @@ impl Symbol { let list = variants .into_iter() - .map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1)) + .map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1, None)) .collect(); Ok(Symbol::runtime(list)) } @@ -316,17 +331,17 @@ impl crate::foundations::Repr for Symbol { } fn repr_variants<'a>( - variants: impl Iterator, char)>, + variants: impl Iterator>, applied_modifiers: ModifierSet<&str>, ) -> String { crate::foundations::repr::pretty_array_like( &variants - .filter(|(modifiers, _)| { + .filter(|(modifiers, _, _)| { // Only keep variants that can still be accessed, i.e., variants // that contain all applied modifiers. applied_modifiers.iter().all(|am| modifiers.contains(am)) }) - .map(|(modifiers, c)| { + .map(|(modifiers, c, _)| { let trimmed_modifiers = modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m)); if trimmed_modifiers.clone().all(|m| m.is_empty()) { @@ -379,18 +394,20 @@ cast! { /// Iterator over variants. enum Variants<'a> { Single(std::option::IntoIter), - Static(std::slice::Iter<'static, (ModifierSet<&'static str>, char)>), - Runtime(std::slice::Iter<'a, (ModifierSet, char)>), + Static(std::slice::Iter<'static, Variant<&'static str>>), + Runtime(std::slice::Iter<'a, Variant>), } impl<'a> Iterator for Variants<'a> { - type Item = (ModifierSet<&'a str>, char); + type Item = Variant<&'a str>; fn next(&mut self) -> Option { match self { - Self::Single(iter) => Some((ModifierSet::default(), iter.next()?)), + Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)), Self::Static(list) => list.next().copied(), - Self::Runtime(list) => list.next().map(|(m, c)| (m.as_deref(), *c)), + Self::Runtime(list) => { + list.next().map(|(m, c, d)| (m.as_deref(), *c, d.as_deref())) + } } } } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 854c2486e..4bcf2d4e3 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -157,7 +157,9 @@ impl Value { /// Try to access a field on the value. pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult { match self { - Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), + Self::Symbol(symbol) => { + symbol.clone().modified(sink, field).map(Self::Symbol) + } Self::Version(version) => version.component(field).map(Self::Int), Self::Dict(dict) => dict.get(field).cloned(), Self::Content(content) => content.field_by_name(field), diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 9bd21c2e8..dc6b62c72 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -720,18 +720,12 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { } }; - for (variant, c) in symbol.variants() { + for (variant, c, deprecation) in symbol.variants() { let shorthand = |list: &[(&'static str, char)]| { list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) }; let name = complete(variant); - let deprecation = match name.as_str() { - "integral.sect" => { - Some("`integral.sect` is deprecated, use `integral.inter` instead") - } - _ => binding.deprecation(), - }; list.push(SymbolModel { name, @@ -742,10 +736,10 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { accent: typst::math::Accent::combine(c).is_some(), alternates: symbol .variants() - .filter(|(other, _)| other != &variant) - .map(|(other, _)| complete(other)) + .filter(|(other, _, _)| other != &variant) + .map(|(other, _, _)| complete(other)) .collect(), - deprecation, + deprecation: deprecation.or_else(|| binding.deprecation()), }); } } diff --git a/tests/suite/math/attach.typ b/tests/suite/math/attach.typ index cedc3a4ab..979018478 100644 --- a/tests/suite/math/attach.typ +++ b/tests/suite/math/attach.typ @@ -121,8 +121,8 @@ $a scripts(=)^"def" b quad a scripts(lt.eq)_"really" b quad a scripts(arrow.r.lo --- math-attach-integral --- // Test default of scripts attachments on integrals at display size. -$ integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $ -$integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$ +$ integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $ +$integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$ --- math-attach-large-operator --- // Test default of limit attachments on large operators at display size only. @@ -179,7 +179,7 @@ $ a0 + a1 + a0_2 \ #{ let var = $x^1$ for i in range(24) { - var = $var$ + var = $var$ } $var_2$ } From 5dd5771df03a666fe17930b0b071b06266e5937f Mon Sep 17 00:00:00 2001 From: "Said A." <47973576+Daaiid@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:18:51 +0200 Subject: [PATCH 123/162] Disallow empty labels and references (#5776) (#6332) Co-authored-by: Laurenz --- crates/typst-eval/src/markup.rs | 7 ++++-- crates/typst-ide/src/definition.rs | 3 ++- crates/typst-library/src/foundations/label.rs | 23 ++++++++++++------ .../typst-library/src/model/bibliography.rs | 6 ++++- crates/typst-syntax/src/ast.rs | 2 ++ crates/typst-syntax/src/lexer.rs | 2 +- tests/ref/ref-to-empty-label-not-possible.png | Bin 0 -> 182 bytes tests/suite/foundations/label.typ | 4 +++ tests/suite/model/bibliography.typ | 8 ++++++ tests/suite/model/ref.typ | 11 +++++++++ 10 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 tests/ref/ref-to-empty-label-not-possible.png diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs index 5beefa912..9118ded56 100644 --- a/crates/typst-eval/src/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -205,7 +205,9 @@ impl Eval for ast::Label<'_> { type Output = Value; fn eval(self, _: &mut Vm) -> SourceResult { - Ok(Value::Label(Label::new(PicoStr::intern(self.get())))) + Ok(Value::Label( + Label::new(PicoStr::intern(self.get())).expect("unexpected empty label"), + )) } } @@ -213,7 +215,8 @@ impl Eval for ast::Ref<'_> { type Output = Content; fn eval(self, vm: &mut Vm) -> SourceResult { - let target = Label::new(PicoStr::intern(self.target())); + let target = Label::new(PicoStr::intern(self.target())) + .expect("unexpected empty reference"); let mut elem = RefElem::new(target); if let Some(supplement) = self.supplement() { elem.push_supplement(Smart::Custom(Some(Supplement::Content( diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs index 69d702b3b..ae1ba287b 100644 --- a/crates/typst-ide/src/definition.rs +++ b/crates/typst-ide/src/definition.rs @@ -72,7 +72,8 @@ pub fn definition( // Try to jump to the referenced content. DerefTarget::Ref(node) => { - let label = Label::new(PicoStr::intern(node.cast::()?.target())); + let label = Label::new(PicoStr::intern(node.cast::()?.target())) + .expect("unexpected empty reference"); let selector = Selector::Label(label); let elem = document?.introspector.query_first(&selector)?; return Some(Definition::Span(elem.span())); diff --git a/crates/typst-library/src/foundations/label.rs b/crates/typst-library/src/foundations/label.rs index 3b9b010c5..b1ac58bf2 100644 --- a/crates/typst-library/src/foundations/label.rs +++ b/crates/typst-library/src/foundations/label.rs @@ -1,7 +1,8 @@ use ecow::{eco_format, EcoString}; use typst_utils::{PicoStr, ResolvedPicoStr}; -use crate::foundations::{func, scope, ty, Repr, Str}; +use crate::diag::StrResult; +use crate::foundations::{bail, func, scope, ty, Repr, Str}; /// A label for an element. /// @@ -27,7 +28,8 @@ use crate::foundations::{func, scope, ty, Repr, Str}; /// # Syntax /// This function also has dedicated syntax: You can create a label by enclosing /// its name in angle brackets. This works both in markup and code. A label's -/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. +/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. A label cannot +/// be empty. /// /// Note that there is a syntactical difference when using the dedicated syntax /// for this function. In the code below, the `[
]` terminates the heading and @@ -50,8 +52,11 @@ pub struct Label(PicoStr); impl Label { /// Creates a label from an interned string. - pub fn new(name: PicoStr) -> Self { - Self(name) + /// + /// Returns `None` if the given string is empty. + pub fn new(name: PicoStr) -> Option { + const EMPTY: PicoStr = PicoStr::constant(""); + (name != EMPTY).then_some(Self(name)) } /// Resolves the label to a string. @@ -70,10 +75,14 @@ impl Label { /// Creates a label from a string. #[func(constructor)] pub fn construct( - /// The name of the label. + /// The name of the label. Must not be empty. name: Str, - ) -> Label { - Self(PicoStr::intern(name.as_str())) + ) -> StrResult

${title}