From a2f685483a5d0ee23c299ce7631d738658d12d89 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:43:41 +0100 Subject: [PATCH 01/44] Improve `repr` for `arguments` (#5652) --- crates/typst-library/src/foundations/args.rs | 2 +- tests/suite/math/call.typ | 34 ++++++++++---------- tests/suite/scripting/call.typ | 4 +-- tests/suite/scripting/params.typ | 10 +++--- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index a60e6d7f2..a4cbcb28b 100644 --- a/crates/typst-library/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -366,7 +366,7 @@ impl Debug for Args { impl Repr for Args { fn repr(&self) -> EcoString { let pieces = self.items.iter().map(Arg::repr).collect::>(); - repr::pretty_array_like(&pieces, false).into() + eco_format!("arguments{}", repr::pretty_array_like(&pieces, false)) } } diff --git a/tests/suite/math/call.typ b/tests/suite/math/call.typ index 2477d9b6d..136be8a74 100644 --- a/tests/suite/math/call.typ +++ b/tests/suite/math/call.typ @@ -11,11 +11,11 @@ $ pi(a,b,) $ --- math-call-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(a)$, "([a])") -#check($args(a,)$, "([a])") -#check($args(a,b)$, "([a], [b])") -#check($args(a,b,)$, "([a], [b])") -#check($args(,a,b,,,)$, "([], [a], [b], [], [])") +#check($args(a)$, "arguments([a])") +#check($args(a,)$, "arguments([a])") +#check($args(a,b)$, "arguments([a], [b])") +#check($args(a,b,)$, "arguments([a], [b])") +#check($args(,a,b,,,)$, "arguments([], [a], [b], [], [])") --- math-call-2d-non-func --- // Error: 6-7 expected content, found array @@ -31,21 +31,21 @@ $ mat(#"code"; "wins") $ --- math-call-2d-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(a;b)$, "(([a],), ([b],))") -#check($args(a,b;c)$, "(([a], [b]), ([c],))") -#check($args(a,b;c,d;e,f)$, "(([a], [b]), ([c], [d]), ([e], [f]))") +#check($args(a;b)$, "arguments(([a],), ([b],))") +#check($args(a,b;c)$, "arguments(([a], [b]), ([c],))") +#check($args(a,b;c,d;e,f)$, "arguments(([a], [b]), ([c], [d]), ([e], [f]))") --- math-call-2d-repr-structure --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args( a; b; )$, "(([a],), ([b],))") -#check($args(a; ; c)$, "(([a],), ([],), ([c],))") -#check($args(a b,/**/; b)$, "((sequence([a], [ ], [b]), []), ([b],))") -#check($args(a/**/b, ; b)$, "((sequence([a], [b]), []), ([b],))") -#check($args( ;/**/a/**/b/**/; )$, "(([],), (sequence([a], [b]),))") -#check($args( ; , ; )$, "(([],), ([], []))") +#check($args( a; b; )$, "arguments(([a],), ([b],))") +#check($args(a; ; c)$, "arguments(([a],), ([],), ([c],))") +#check($args(a b,/**/; b)$, "arguments((sequence([a], [ ], [b]), []), ([b],))") +#check($args(a/**/b, ; b)$, "arguments((sequence([a], [b]), []), ([b],))") +#check($args( ;/**/a/**/b/**/; )$, "arguments(([],), (sequence([a], [b]),))") +#check($args( ; , ; )$, "arguments(([],), ([], []))") #check($args(/**/; // funky whitespace/trivia - , /**/ ;/**/)$, "(([],), ([], []))") + , /**/ ;/**/)$, "arguments(([],), ([], []))") --- math-call-empty-args-non-func --- // Trailing commas and empty args introduce blank content in math @@ -56,9 +56,9 @@ $ sin( ,/**/x/**/, , /**/y, ,/**/, ) $ --- math-call-empty-args-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(,x,,y,,)$, "([], [x], [], [y], [])") +#check($args(,x,,y,,)$, "arguments([], [x], [], [y], [])") // with whitespace/trivia: -#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "([], [x], [], [y], [], [])") +#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "arguments([], [x], [], [y], [], [])") --- math-call-value-non-func --- $ sin(1) $ diff --git a/tests/suite/scripting/call.typ b/tests/suite/scripting/call.typ index 5a5fb326d..af5f5eaab 100644 --- a/tests/suite/scripting/call.typ +++ b/tests/suite/scripting/call.typ @@ -141,7 +141,7 @@ #{ let save(..args) = { test(type(args), arguments) - test(repr(args), "(three: true, 1, 2)") + test(repr(args), "arguments(three: true, 1, 2)") } save(1, 2, three: true) @@ -159,7 +159,7 @@ #{ let more = (c: 3, d: 4) let tostr(..args) = repr(args) - test(tostr(a: 1, ..more, b: 2), "(a: 1, c: 3, d: 4, b: 2)") + test(tostr(a: 1, ..more, b: 2), "arguments(a: 1, c: 3, d: 4, b: 2)") } --- call-args-spread-none --- diff --git a/tests/suite/scripting/params.typ b/tests/suite/scripting/params.typ index 688124f20..0f14fc3ee 100644 --- a/tests/suite/scripting/params.typ +++ b/tests/suite/scripting/params.typ @@ -29,17 +29,17 @@ // Spread at beginning. #{ let f(..a, b) = (a, b) - test(repr(f(1)), "((), 1)") - test(repr(f(1, 2, 3)), "((1, 2), 3)") - test(repr(f(1, 2, 3, 4, 5)), "((1, 2, 3, 4), 5)") + test(repr(f(1)), "(arguments(), 1)") + test(repr(f(1, 2, 3)), "(arguments(1, 2), 3)") + test(repr(f(1, 2, 3, 4, 5)), "(arguments(1, 2, 3, 4), 5)") } --- params-sink-in-middle --- // Spread in the middle. #{ let f(a, ..b, c) = (a, b, c) - test(repr(f(1, 2)), "(1, (), 2)") - test(repr(f(1, 2, 3, 4, 5)), "(1, (2, 3, 4), 5)") + test(repr(f(1, 2)), "(1, arguments(), 2)") + test(repr(f(1, 2, 3, 4, 5)), "(1, arguments(2, 3, 4), 5)") } --- params-sink-unnamed-empty --- From 39eeb116a47993e7c3ec756a975f41762dbb008b Mon Sep 17 00:00:00 2001 From: Jie Wang <124119483+escwxyz@users.noreply.github.com> Date: Mon, 6 Jan 2025 20:58:36 +0800 Subject: [PATCH 02/44] Fix typos in scripting reference (#5660) --- docs/reference/scripting.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference/scripting.md b/docs/reference/scripting.md index 89508eee0..6c7a7b338 100644 --- a/docs/reference/scripting.md +++ b/docs/reference/scripting.md @@ -120,7 +120,7 @@ You can use the underscore to discard elements in a destructuring pattern: The y coordinate is #y. ``` -Destructuring also work in argument lists of functions ... +Destructuring also works in argument lists of functions ... ```example #let left = (2, 4, 5) @@ -145,7 +145,7 @@ swap variables among other things. ## Conditionals With a conditional, you can display or compute different things depending on whether some condition is fulfilled. Typst supports `{if}`, `{else if}` and -`{else}` expression. When the condition evaluates to `{true}`, the conditional +`{else}` expressions. When the condition evaluates to `{true}`, the conditional yields the value resulting from the if's body, otherwise yields the value resulting from the else's body. @@ -269,7 +269,7 @@ the following two equivalent ways: The structure of a method call is `{value.method(..args)}` and its equivalent full function call is `{type(value).method(value, ..args)}`. The documentation -of each type lists it's scoped functions. You cannot currently define your own +of each type lists its scoped functions. You cannot currently define your own methods. ```example From 36508c66db184af3c4e620155810d1d768687481 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:00:44 +0100 Subject: [PATCH 03/44] Fix French text in example (#5635) --- docs/guides/tables.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guides/tables.md b/docs/guides/tables.md index 5c9cf11da..5b7efdc48 100644 --- a/docs/guides/tables.md +++ b/docs/guides/tables.md @@ -886,8 +886,8 @@ everything else by providing an array in the `align` argument: stroke: none, table.header[Day][Location][Hotel or Apartment][Activities], - [1], [Paris, France], [Hotel de L'Europe], [Arrival, Evening River Cruise], - [2], [Paris, France], [Hotel de L'Europe], [Louvre Museum, Eiffel Tower], + [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise], + [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower], [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting], [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum], [5], [Zermatt, Switzerland], [Alpine Lodge], [Visit Matterhorn, Skiing], @@ -911,8 +911,8 @@ bottom-aligned. Let's use a function instead to do so: stroke: none, table.header[Day][Location][Hotel or Apartment][Activities], - [1], [Paris, France], [Hotel de L'Europe], [Arrival, Evening River Cruise], - [2], [Paris, France], [Hotel de L'Europe], [Louvre Museum, Eiffel Tower], + [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise], + [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower], <<< // ... remaining days omitted >>> [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting], >>> [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum], From 3a1503154f1162b594d2b016fb19506d9eeaf51e Mon Sep 17 00:00:00 2001 From: Johann Birnick <6528009+jbirnick@users.noreply.github.com> Date: Mon, 6 Jan 2025 05:13:53 -0800 Subject: [PATCH 04/44] Basic HTML `heading` test (#5619) --- tests/ref/html/heading-html-basic.html | 30 ++++++++++++++++++++++++++ tests/suite/model/heading.typ | 21 ++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/ref/html/heading-html-basic.html diff --git a/tests/ref/html/heading-html-basic.html b/tests/ref/html/heading-html-basic.html new file mode 100644 index 000000000..69a1d0172 --- /dev/null +++ b/tests/ref/html/heading-html-basic.html @@ -0,0 +1,30 @@ + + + + + + + +

+ Level 1 +

+

+ Level 2 +

+

+ Level 3 +

+
+ Level 4 +
+
+ Level 5 +
+
+ Level 6 +
+
+ Level 7 +
+ + \ No newline at end of file diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ index d182724c8..72dc4aa37 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -115,3 +115,24 @@ Not in heading // Error: 1:19-1:25 cannot reference heading without numbering // Hint: 1:19-1:25 you can enable heading numbering with `#set heading(numbering: "1.")` Cannot be used as @intro + +--- heading-html-basic html --- +// level 1 => h2 +// ... +// level 5 => h6 +// level 6 => div with role=heading and aria-level=7 +// ... + += Level 1 +== Level 2 +=== Level 3 +==== Level 4 +===== Level 5 +// Warning: 1-15 heading of level 6 was transformed to
, which is not supported by all assistive technology +// Hint: 1-15 HTML only supports

to

, not +// Hint: 1-15 you may want to restructure your document so that it doesn't contain deep headings +====== Level 6 +// Warning: 1-16 heading of level 7 was transformed to
, which is not supported by all assistive technology +// Hint: 1-16 HTML only supports

to

, not +// Hint: 1-16 you may want to restructure your document so that it doesn't contain deep headings +======= Level 7 From ec1e8f9e8dce05d982782eeedeb497184658a8db Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:15:11 +0300 Subject: [PATCH 05/44] Added precise definition for "character" in the docs for `str.split` (#5616) --- crates/typst-library/src/foundations/str.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 8ac99ac63..4025d1ab3 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -577,9 +577,10 @@ impl Str { /// of the resulting parts. /// /// When the empty string is used as a separator, it separates every - /// character in the string, along with the beginning and end of the - /// string. In practice, this means that the resulting list of parts - /// will contain the empty string at the start and end of the list. + /// character (i.e., Unicode code point) in the string, along with the + /// beginning and end of the string. In practice, this means that the + /// resulting list of parts will contain the empty string at the start + /// and end of the list. #[func] pub fn split( &self, From e8bbf3794fb077cdd35095467eb763b476ee7e99 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Mon, 6 Jan 2025 21:31:42 +0800 Subject: [PATCH 06/44] Select items by import paths (#5518) --- crates/typst-ide/src/matchers.rs | 46 +++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index d02eb2a95..b92cbf557 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -89,15 +89,21 @@ pub fn named_items( // ``` Some(ast::Imports::Items(items)) => { for item in items.iter() { - let original = item.original_name(); let bound = item.bound_name(); - let scope = source.and_then(|(value, _)| value.scope()); - let span = scope - .and_then(|s| s.get_span(&original)) - .unwrap_or(Span::detached()) - .or(bound.span()); - let value = scope.and_then(|s| s.get(&original)); + let (span, value) = item.path().iter().fold( + (bound.span(), source.map(|(value, _)| value)), + |(span, value), path_ident| { + let scope = value.and_then(|v| v.scope()); + let span = scope + .and_then(|s| s.get_span(&path_ident)) + .unwrap_or(Span::detached()) + .or(span); + let value = scope.and_then(|s| s.get(&path_ident)); + (span, value) + }, + ); + if let Some(res) = recv(NamedItem::Import(bound.get(), span, value)) { @@ -269,16 +275,18 @@ mod tests { use std::borrow::Borrow; use ecow::EcoString; + use typst::foundations::Value; use typst::syntax::{LinkedNode, Side}; use super::named_items; - use crate::tests::{FilePos, WorldLike}; + use crate::tests::{FilePos, TestWorld, WorldLike}; - type Response = Vec; + type Response = Vec<(EcoString, Option)>; trait ResponseExt { fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self; fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self; + fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self; } impl ResponseExt for Response { @@ -286,7 +294,7 @@ mod tests { fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self { for item in includes { assert!( - self.iter().any(|v| v == item), + self.iter().any(|v| v.0 == item), "{item:?} was not contained in {self:?}", ); } @@ -297,12 +305,21 @@ mod tests { fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self { for item in excludes { assert!( - !self.iter().any(|v| v == item), + !self.iter().any(|v| v.0 == item), "{item:?} was wrongly contained in {self:?}", ); } self } + + #[track_caller] + fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self { + assert!( + self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value), + "{name_value:?} was not contained in {self:?}", + ); + self + } } #[track_caller] @@ -314,7 +331,7 @@ mod tests { let leaf = node.leaf_at(cursor, Side::After).unwrap(); let mut items = vec![]; named_items(world, leaf, |s| { - items.push(s.name().clone()); + items.push((s.name().clone(), s.value().clone())); None::<()> }); items @@ -340,5 +357,10 @@ mod tests { #[test] fn test_named_items_import() { test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]); + + let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);") + .with_source("foo.typ", "#import \"a.typ\"") + .with_source("a.typ", "#let b = 1;"); + test(&world, 2).must_include_value(("b", Some(&Value::Int(1)))); } } From cb8d862a55601127a1e3ff02feaefdabd37b583f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 6 Jan 2025 14:55:34 +0100 Subject: [PATCH 07/44] Terminate pretty-printed HTML with trailing newline (#5661) --- crates/typst-html/src/encode.rs | 3 +++ tests/ref/html/heading-html-basic.html | 2 +- tests/ref/html/link-basic.html | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index b87b0e1d6..62146f867 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -12,6 +12,9 @@ pub fn html(document: &HtmlDocument) -> SourceResult { w.buf.push_str(""); write_indent(&mut w); write_element(&mut w, &document.root)?; + if w.pretty { + w.buf.push('\n'); + } Ok(w.buf) } diff --git a/tests/ref/html/heading-html-basic.html b/tests/ref/html/heading-html-basic.html index 69a1d0172..56b1e32b7 100644 --- a/tests/ref/html/heading-html-basic.html +++ b/tests/ref/html/heading-html-basic.html @@ -27,4 +27,4 @@ Level 7
- \ No newline at end of file + diff --git a/tests/ref/html/link-basic.html b/tests/ref/html/link-basic.html index 1f4e02e12..5d998667e 100644 --- a/tests/ref/html/link-basic.html +++ b/tests/ref/html/link-basic.html @@ -18,4 +18,4 @@ Contact hi@typst.app or call 123 for more information.

- \ No newline at end of file + From ce7f680fd5f21f79548280c844e1abbabc0d4e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?bbb651=20=F0=9F=87=AE=F0=9F=87=B1?= <53972231+bbb651@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:13:17 +0200 Subject: [PATCH 08/44] Avoid stripping url prefixes multiple times or multiple prefixes (#5659) --- crates/typst-library/src/model/link.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 8ab129fdd..bbc47da05 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -135,10 +135,10 @@ impl Show for Packed { } fn body_from_url(url: &Url) -> Content { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); - } + let text = ["mailto:", "tel:"] + .into_iter() + .find_map(|prefix| url.strip_prefix(prefix)) + .unwrap_or(url); let shorter = text.len() < url.len(); TextElem::packed(if shorter { text.into() } else { (**url).clone() }) } From 5c876535cc89912b32bc29a17c753ae9b1f03938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Mon, 6 Jan 2025 16:20:28 +0100 Subject: [PATCH 09/44] Move `CellGrid` from `typst-layout` to `typst-library` (#5585) --- crates/typst-layout/src/grid/layouter.rs | 18 +- crates/typst-layout/src/grid/lines.rs | 38 +- crates/typst-layout/src/grid/mod.rs | 410 ++------------- crates/typst-layout/src/grid/repeated.rs | 41 +- crates/typst-layout/src/grid/rowspans.rs | 6 +- crates/typst-layout/src/lists.rs | 3 +- .../src/layout/{grid.rs => grid/mod.rs} | 2 + .../src/layout/grid/resolve.rs} | 474 +++++++++++++++++- crates/typst-library/src/layout/mod.rs | 2 +- 9 files changed, 502 insertions(+), 492 deletions(-) rename crates/typst-library/src/layout/{grid.rs => grid/mod.rs} (99%) rename crates/{typst-layout/src/grid/cells.rs => typst-library/src/layout/grid/resolve.rs} (77%) diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 7c94617dc..1f9cf6796 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,6 +3,7 @@ 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::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -13,8 +14,8 @@ use typst_syntax::Span; use typst_utils::{MaybeReverseIter, Numeric}; use super::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, - LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup, + generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, + LineSegment, Rowspan, UnbreakableRowGroup, }; /// Performs grid layout. @@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> { let size = Size::new(available, height); let pod = Region::new(size, Axes::splat(false)); - let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame(); + let frame = + layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame(); resolved.set_max(frame.width() - already_covered_width); } @@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> { }; let frames = - cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); + layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames(); // Skip the first region if one cell in it is empty. Then, // remeasure. @@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> { // rows. pod.full = self.regions.full; } - let frame = cell - .layout(engine, disambiguator, self.styles, pod)? - .into_frame(); + let frame = + layout_cell(cell, engine, disambiguator, self.styles, pod)? + .into_frame(); let mut pos = pos; if self.is_rtl { // In the grid, cell colspans expand to the right, @@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> { // Push the layouted frames into the individual output frames. let fragment = - cell.layout(engine, disambiguator, self.styles, pod)?; + layout_cell(cell, engine, disambiguator, self.styles, pod)?; for (output, frame) in outputs.iter_mut().zip(fragment) { let mut pos = pos; if self.is_rtl { diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 3e89612a1..1227953d1 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -1,41 +1,11 @@ -use std::num::NonZeroUsize; use std::sync::Arc; use typst_library::foundations::{AlternativeFold, Fold}; +use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable}; use typst_library::layout::Abs; use typst_library::visualize::Stroke; -use super::{CellGrid, LinePosition, Repeatable, RowPiece}; - -/// Represents an explicit grid line (horizontal or vertical) specified by the -/// user. -pub struct Line { - /// The index of the track after this line. This will be the index of the - /// row a horizontal line is above of, or of the column right after a - /// vertical line. - /// - /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols` - /// or `grid.rows`, ignoring gutter tracks, as appropriate). - pub index: usize, - /// The index of the track at which this line starts being drawn. - /// This is the first column a horizontal line appears in, or the first row - /// a vertical line appears in. - /// - /// Must be within `0..tracks.len()` minus gutter tracks. - pub start: usize, - /// The index after the last track through which the line is drawn. - /// Thus, the line is drawn through tracks `start..end` (note that `end` is - /// exclusive). - /// - /// Must be within `1..=tracks.len()` minus gutter tracks. - /// `None` indicates the line should go all the way to the end. - pub end: Option, - /// The line's stroke. This is `None` when the line is explicitly used to - /// override a previously specified line. - pub stroke: Option>>, - /// The line's position in relation to the track with its index. - pub position: LinePosition, -} +use super::RowPiece; /// Indicates which priority a particular grid line segment should have, based /// on the highest priority configuration that defined the segment's stroke. @@ -588,13 +558,13 @@ pub fn hline_stroke_at_column( #[cfg(test)] mod test { + use std::num::NonZeroUsize; use typst_library::foundations::Content; use typst_library::introspection::Locator; + use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition}; use typst_library::layout::{Axes, Sides, Sizing}; use typst_utils::NonZeroExt; - use super::super::cells::Entry; - use super::super::Cell; use super::*; fn sample_cell() -> Cell<'static> { diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs index 769bef8c5..1b4380f0a 100644 --- a/crates/typst-layout/src/grid/mod.rs +++ b/crates/typst-layout/src/grid/mod.rs @@ -1,40 +1,44 @@ -mod cells; mod layouter; mod lines; mod repeated; mod rowspans; -pub use self::cells::{Cell, CellGrid}; pub use self::layouter::GridLayouter; -use std::num::NonZeroUsize; -use std::sync::Arc; - -use ecow::eco_format; -use typst_library::diag::{SourceResult, Trace, Tracepoint}; +use typst_library::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::foundations::{Fold, Packed, Smart, StyleChain}; +use typst_library::foundations::{Packed, StyleChain}; use typst_library::introspection::Locator; -use typst_library::layout::{ - Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length, - OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, -}; -use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; -use typst_library::text::TextElem; -use typst_library::visualize::{Paint, Stroke}; -use typst_syntax::Span; +use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell}; +use typst_library::layout::{Fragment, GridElem, Regions}; +use typst_library::model::TableElem; -use self::cells::{ - LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem, -}; use self::layouter::RowPiece; use self::lines::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line, - LineSegment, + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment, }; -use self::repeated::{Footer, Header, Repeatable}; use self::rowspans::{Rowspan, UnbreakableRowGroup}; +/// Layout the cell into the given regions. +/// +/// The `disambiguator` indicates which instance of this cell this should be +/// layouted as. For normal cells, it is always `0`, but for headers and +/// footers, it indicates the index of the header/footer among all. See the +/// [`Locator`] docs for more details on the concepts behind this. +pub fn layout_cell( + cell: &Cell, + engine: &mut Engine, + disambiguator: usize, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + let mut locator = cell.locator.relayout(); + if disambiguator > 0 { + locator = locator.split().next_inner(disambiguator as u128); + } + crate::layout_fragment(engine, &cell.body, locator, styles, regions) +} + /// Layout the grid. #[typst_macros::time(span = elem.span())] pub fn layout_grid( @@ -44,54 +48,8 @@ pub fn layout_grid( styles: StyleChain, regions: Regions, ) -> SourceResult { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the grid when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); - let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { - GridChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - GridChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - GridChild::Item(item) => { - ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) - } - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - - // Measure the columns and layout the grid row-by-row. - layouter.layout(engine) + let grid = grid_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } /// Layout the table. @@ -103,314 +61,6 @@ pub fn layout_table( styles: StyleChain, regions: Regions, ) -> SourceResult { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the table when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); - let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { - TableChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - TableChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - TableChild::Item(item) => { - ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) - } - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - layouter.layout(engine) -} - -fn grid_item_to_resolvable( - item: &GridItem, - styles: StyleChain, -) -> ResolvableGridItem> { - match item { - GridItem::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - GridItem::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } -} - -fn table_item_to_resolvable( - item: &TableItem, - styles: StyleChain, -) -> ResolvableGridItem> { - match item { - TableItem::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - TableItem::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } -} - -impl ResolvableCell for Packed { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the table is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on table cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } -} - -impl ResolvableCell for Packed { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the grid is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on grid cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } + let grid = table_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 8d08d56db..22d2a09ef 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,50 +1,11 @@ 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::rowspans::UnbreakableRowGroup; -/// A repeatable grid header. Starts at the first row. -pub struct Header { - /// The index after the last row included in this header. - pub end: usize, -} - -/// A repeatable grid footer. Stops at the last row. -pub struct Footer { - /// The first row included in this footer. - pub start: usize, -} - -/// A possibly repeatable grid object. -/// 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), -} - -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. - pub fn as_repeated(&self) -> Option<&T> { - match self { - Self::Repeated(repeated) => Some(repeated), - Self::NotRepeated(_) => None, - } - } -} - impl GridLayouter<'_> { /// Layouts the header's rows. /// Skips regions as necessary. diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 93d4c960d..5039695d8 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -1,12 +1,12 @@ use typst_library::diag::SourceResult; 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::repeated::Repeatable; -use super::{Cell, GridLayouter}; +use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. pub struct Rowspan { @@ -141,7 +141,7 @@ impl GridLayouter<'_> { } // Push the layouted frames directly into the finished frames. - let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; + 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 .finished diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 0d51a1e4e..9479959b2 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -4,11 +4,12 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::introspection::Locator; +use typst_library::layout::grid::resolve::{Cell, CellGrid}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; use typst_library::text::TextElem; -use crate::grid::{Cell, CellGrid, GridLayouter}; +use crate::grid::GridLayouter; /// Layout the list. #[typst_macros::time(span = elem.span())] diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid/mod.rs similarity index 99% rename from crates/typst-library/src/layout/grid.rs rename to crates/typst-library/src/layout/grid/mod.rs index 2e1e9abc4..e46440fb4 100644 --- a/crates/typst-library/src/layout/grid.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -1,3 +1,5 @@ +pub mod resolve; + use std::num::NonZeroUsize; use std::sync::Arc; diff --git a/crates/typst-layout/src/grid/cells.rs b/crates/typst-library/src/layout/grid/resolve.rs similarity index 77% rename from crates/typst-layout/src/grid/cells.rs rename to crates/typst-library/src/layout/grid/resolve.rs index 175e21833..adaff1c18 100644 --- a/crates/typst-layout/src/grid/cells.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -2,19 +2,463 @@ use std::num::NonZeroUsize; use std::sync::Arc; use ecow::eco_format; -use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; +use typst_library::diag::{ + bail, At, Hint, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint, +}; use typst_library::engine::Engine; -use typst_library::foundations::{Content, Smart, StyleChain}; +use typst_library::foundations::{Content, Fold, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ - Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides, - Sizing, + Abs, Alignment, Axes, Celled, GridCell, GridChild, GridElem, GridItem, Length, + OuterHAlignment, OuterVAlignment, Rel, ResolvedCelled, Sides, Sizing, }; +use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; +use typst_library::text::TextElem; use typst_library::visualize::{Paint, Stroke}; +use typst_library::Dir; + use typst_syntax::Span; use typst_utils::NonZeroExt; -use super::{Footer, Header, Line, Repeatable}; +/// Convert a grid to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn grid_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> SourceResult> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the grid when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); + let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); + let children = elem.children().iter().map(|child| match child { + GridChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(resolve_item), + }, + GridChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children().iter().map(resolve_item), + }, + GridChild::Item(item) => { + ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +/// Convert a table to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn table_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> SourceResult> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the table when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); + let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); + let children = elem.children().iter().map(|child| match child { + TableChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children().iter().map(resolve_item), + }, + TableChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children().iter().map(resolve_item), + }, + TableChild::Item(item) => { + ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +fn grid_item_to_resolvable( + item: &GridItem, + styles: StyleChain, +) -> ResolvableGridItem> { + match item { + GridItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + GridItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +fn table_item_to_resolvable( + item: &TableItem, + styles: StyleChain, +) -> ResolvableGridItem> { + match item { + TableItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + TableItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +impl ResolvableCell for Packed { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the table is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on table cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} + +impl ResolvableCell for Packed { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the grid is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on grid cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} + +/// Represents an explicit grid line (horizontal or vertical) specified by the +/// user. +pub struct Line { + /// The index of the track after this line. This will be the index of the + /// row a horizontal line is above of, or of the column right after a + /// vertical line. + /// + /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols` + /// or `grid.rows`, ignoring gutter tracks, as appropriate). + pub index: usize, + /// The index of the track at which this line starts being drawn. + /// This is the first column a horizontal line appears in, or the first row + /// a vertical line appears in. + /// + /// Must be within `0..tracks.len()` minus gutter tracks. + pub start: usize, + /// The index after the last track through which the line is drawn. + /// Thus, the line is drawn through tracks `start..end` (note that `end` is + /// exclusive). + /// + /// Must be within `1..=tracks.len()` minus gutter tracks. + /// `None` indicates the line should go all the way to the end. + pub end: Option, + /// The line's stroke. This is `None` when the line is explicitly used to + /// override a previously specified line. + pub stroke: Option>>, + /// The line's position in relation to the track with its index. + pub position: LinePosition, +} + +/// A repeatable grid header. Starts at the first row. +pub struct Header { + /// The index after the last row included in this header. + pub end: usize, +} + +/// A repeatable grid footer. Stops at the last row. +pub struct Footer { + /// The first row included in this footer. + pub start: usize, +} + +/// A possibly repeatable grid object. +/// 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), +} + +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. + pub fn as_repeated(&self) -> Option<&T> { + match self { + Self::Repeated(repeated) => Some(repeated), + Self::NotRepeated(_) => None, + } + } +} /// Used for cell-like elements which are aware of their final properties in /// the table, and may have property overrides. @@ -131,26 +575,6 @@ impl<'a> Cell<'a> { breakable: true, } } - - /// Layout the cell into the given regions. - /// - /// The `disambiguator` indicates which instance of this cell this should be - /// layouted as. For normal cells, it is always `0`, but for headers and - /// footers, it indicates the index of the header/footer among all. See the - /// [`Locator`] docs for more details on the concepts behind this. - pub fn layout( - &self, - engine: &mut Engine, - disambiguator: usize, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - let mut locator = self.locator.relayout(); - if disambiguator > 0 { - locator = locator.split().next_inner(disambiguator as u128); - } - crate::layout_fragment(engine, &self.body, locator, styles, regions) - } } /// Indicates whether the line should be drawn before or after the track with diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index b54d6906e..574a2830a 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -12,7 +12,7 @@ mod em; mod fr; mod fragment; mod frame; -mod grid; +pub mod grid; mod hide; #[path = "layout.rs"] mod layout_; From e09b55f00f4213a76285e90825dfab570a051359 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:41:58 +0100 Subject: [PATCH 10/44] Allow adding and joining `arguments` (#5651) --- crates/typst-library/src/foundations/args.rs | 16 ++++++++++++++++ crates/typst-library/src/foundations/ops.rs | 2 ++ .../{scripting => foundations}/arguments.typ | 9 +++++++++ 3 files changed, 27 insertions(+) rename tests/suite/{scripting => foundations}/arguments.typ (60%) diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index a4cbcb28b..44aa9dd6d 100644 --- a/crates/typst-library/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -1,4 +1,5 @@ use std::fmt::{self, Debug, Formatter}; +use std::ops::Add; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use typst_syntax::{Span, Spanned}; @@ -376,6 +377,21 @@ impl PartialEq for Args { } } +impl Add for Args { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self.items.retain(|item| { + !item.name.as_ref().is_some_and(|name| { + rhs.items.iter().any(|a| a.name.as_ref() == Some(name)) + }) + }); + self.items.extend(rhs.items); + self.span = Span::detached(); + self + } +} + /// An argument to a function call: `12` or `draw: false`. #[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 8d12966bf..85a041b6c 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -36,6 +36,7 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), + (Args(a), Args(b)) => Args(a + b), (a, b) => mismatch!("cannot join {} with {}", a, b), }) } @@ -136,6 +137,7 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), + (Args(a), Args(b)) => Args(a + b), (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { Stroke::from_pair(color, thickness).into_value() diff --git a/tests/suite/scripting/arguments.typ b/tests/suite/foundations/arguments.typ similarity index 60% rename from tests/suite/scripting/arguments.typ rename to tests/suite/foundations/arguments.typ index e82f49624..1439b6be5 100644 --- a/tests/suite/scripting/arguments.typ +++ b/tests/suite/foundations/arguments.typ @@ -16,3 +16,12 @@ #let args = arguments(0, 1, a: 2, 3) // Error: 2-14 arguments do not contain key "b" and no default value was specified #args.at("b") + +--- arguments-plus-sum-join --- +#let lhs = arguments(0, "1", key: "value", 3) +#let rhs = arguments(other-key: 4, key: "other value", 3) +#let result = arguments(0, "1", 3, other-key: 4, key: "other value", 3) +#test(lhs + rhs, result) +#test({lhs; rhs}, result) +#test((lhs, rhs).sum(), result) +#test((lhs, rhs).join(), result) From 265df6c29f4d142a372917dd708bfba780f7cfbc Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 7 Jan 2025 14:13:15 +0100 Subject: [PATCH 11/44] Remove closing slashes from img tags (#5665) --- README.md | 12 ++++++------ docs/src/main.rs | 4 ++-- tools/test-helper/src/extension.ts | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5d5c4798a..a5d20d2e6 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,19 @@

Documentation + > Typst App + > Discord Server + > Apache-2 License + > Jobs at Typst + >

Typst is a new markup-based typesetting system that is designed to be as powerful @@ -39,7 +39,7 @@ A [gentle introduction][tutorial] to Typst is available in our documentation. However, if you want to see the power of Typst encapsulated in one image, here it is:

- Example + Example

diff --git a/docs/src/main.rs b/docs/src/main.rs index 064bf9a8f..d14c1347a 100644 --- a/docs/src/main.rs +++ b/docs/src/main.rs @@ -46,11 +46,11 @@ impl Resolver for CliResolver<'_> { if let Some(code) = source { let code_safe = code.as_str(); Html::new(format!( - r#"
{code_safe}
Preview
"# + r#"
{code_safe}
Preview
"# )) } else { Html::new(format!( - r#"
Preview
"# + r#"
Preview
"# )) } } diff --git a/tools/test-helper/src/extension.ts b/tools/test-helper/src/extension.ts index 2e2b7d218..b98b4bad4 100644 --- a/tools/test-helper/src/extension.ts +++ b/tools/test-helper/src/extension.ts @@ -474,7 +474,7 @@ function getWebviewContent( data-vscode-context='{"webviewSection":"png"}' src="${webViewSrcs.png}" alt="Placeholder" - /> + >
@@ -484,7 +484,7 @@ function getWebviewContent( data-vscode-context='{"webviewSection":"ref"}' src="${webViewSrcs.ref}" alt="Placeholder" - /> + >
${stdoutHtml} From 0a374d238016c0101d11cbc3f4bc621f3895ad36 Mon Sep 17 00:00:00 2001 From: Niklas Eicker Date: Wed, 8 Jan 2025 10:38:34 +0100 Subject: [PATCH 12/44] Embed files associated with the document as a whole (#5221) Co-authored-by: Laurenz --- crates/typst-cli/src/args.rs | 3 + crates/typst-cli/src/compile.rs | 1 + crates/typst-library/src/lib.rs | 2 + crates/typst-library/src/pdf/embed.rs | 131 ++++++++++++++++++++++++++ crates/typst-library/src/pdf/mod.rs | 24 +++++ crates/typst-pdf/src/catalog.rs | 84 +++++++++++------ crates/typst-pdf/src/embed.rs | 122 ++++++++++++++++++++++++ crates/typst-pdf/src/lib.rs | 46 +++++++-- docs/src/lib.rs | 2 + tests/suite/pdf/embed.typ | 30 ++++++ 10 files changed, 411 insertions(+), 34 deletions(-) create mode 100644 crates/typst-library/src/pdf/embed.rs create mode 100644 crates/typst-library/src/pdf/mod.rs create mode 100644 crates/typst-pdf/src/embed.rs create mode 100644 tests/suite/pdf/embed.typ diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 83c4c8f9e..d6855d100 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -473,6 +473,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[value(name = "a-2b")] A_2b, + /// PDF/A-3b. + #[value(name = "a-3b")] + A_3b, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index adeef0f2d..515a777a2 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -136,6 +136,7 @@ impl CompileConfig { .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)? diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 87b2fcb44..2ea77eaa5 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -21,6 +21,7 @@ pub mod layout; pub mod loading; pub mod math; pub mod model; +pub mod pdf; pub mod routines; pub mod symbols; pub mod text; @@ -249,6 +250,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { self::introspection::define(&mut global); self::loading::define(&mut global); self::symbols::define(&mut global); + self::pdf::define(&mut global); global.reset_category(); if features.is_enabled(Feature::Html) { global.define_module(self::html::module()); diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs new file mode 100644 index 000000000..db4986225 --- /dev/null +++ b/crates/typst-library/src/pdf/embed.rs @@ -0,0 +1,131 @@ +use ecow::EcoString; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, func, scope, Cast, Content, NativeElement, Packed, Show, StyleChain, +}; +use crate::introspection::Locatable; +use crate::loading::Readable; +use crate::World; + +/// A file that will be embedded into the output PDF. +/// +/// This can be used to distribute additional files that are related to the PDF +/// within it. PDF readers will display the files in a file listing. +/// +/// Some international standards use this mechanism to embed machine-readable +/// data (e.g., ZUGFeRD/Factur-X for invoices) that mirrors the visual content +/// of the PDF. +/// +/// # Example +/// ```typ +/// #pdf.embed( +/// "experiment.csv", +/// relationship: "supplement", +/// mime-type: "text/csv", +/// description: "Raw Oxygen readings from the Arctic experiment", +/// ) +/// ``` +/// +/// # Notes +/// - This element is ignored if exporting to a format other than PDF. +/// - File embeddings are not currently supported for PDF/A-2, even if the +/// embedded file conforms to PDF/A-1 or PDF/A-2. +#[elem(scope, Show, Locatable)] +pub struct EmbedElem { + /// Path to a file to be embedded. + /// + /// For more details, see the [Paths section]($syntax/#paths). + #[required] + #[parse( + let Spanned { v: path, span } = + args.expect::>("path to the file to be embedded")?; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + path + )] + #[borrowed] + pub path: EcoString, + + /// The resolved project-relative path. + #[internal] + #[required] + #[parse(id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into())] + pub resolved_path: EcoString, + + /// The raw file data. + #[internal] + #[required] + #[parse(Readable::Bytes(data))] + pub data: Readable, + + /// The relationship of the embedded file to the document. + /// + /// Ignored if export doesn't target PDF/A-3. + pub relationship: Option, + + /// The MIME type of the embedded file. + #[borrowed] + pub mime_type: Option, + + /// A description for the embedded file. + #[borrowed] + pub description: Option, +} + +#[scope] +impl EmbedElem { + /// Decode a file embedding from bytes or a string. + #[func(title = "Embed Data")] + fn decode( + /// The call span of this function. + span: Span, + /// The path that will be written into the PDF. Typst will not read from + /// this path since the data is provided in the following argument. + path: EcoString, + /// The data to embed as a file. + data: Readable, + /// The relationship of the embedded file to the document. + #[named] + relationship: Option>, + /// The MIME type of the embedded file. + #[named] + mime_type: Option>, + /// A description for the embedded file. + #[named] + description: Option>, + ) -> StrResult { + let mut elem = EmbedElem::new(path.clone(), path, data); + if let Some(description) = description { + elem.push_description(description); + } + if let Some(mime_type) = mime_type { + elem.push_mime_type(mime_type); + } + if let Some(relationship) = relationship { + elem.push_relationship(relationship); + } + Ok(elem.pack().spanned(span)) + } +} + +impl Show for Packed { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(Content::empty()) + } +} + +/// The relationship of an embedded file with the document. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum EmbeddedFileRelationship { + /// The PDF document was created from the source file. + Source, + /// The file was used to derive a visual presentation in the PDF. + Data, + /// An alternative representation of the document. + Alternative, + /// Additional resources for the document. + Supplement, +} diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs new file mode 100644 index 000000000..669835d4c --- /dev/null +++ b/crates/typst-library/src/pdf/mod.rs @@ -0,0 +1,24 @@ +//! PDF-specific functionality. + +mod embed; + +pub use self::embed::*; + +use crate::foundations::{category, Category, Module, Scope}; + +/// PDF-specific functionality. +#[category] +pub static PDF: Category; + +/// Hook up the `pdf` module. +pub(super) fn define(global: &mut Scope) { + global.category(PDF); + global.define_module(module()); +} + +/// Hook up all `pdf` definitions. +pub fn module() -> Module { + let mut scope = Scope::deduplicating(); + scope.define_elem::(); + Module::new("pdf", scope) +} diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs index c4b0e2e83..709b01553 100644 --- a/crates/typst-pdf/src/catalog.rs +++ b/crates/typst-pdf/src/catalog.rs @@ -12,7 +12,7 @@ use typst_syntax::Span; use xmp_writer::{DateTime, LangId, RenditionClass, XmpWriter}; use crate::page::PdfPageLabel; -use crate::{hash_base64, outline, TextStrExt, Timezone, WithEverything}; +use crate::{hash_base64, outline, TextStrExt, Timestamp, Timezone, WithEverything}; /// Write the document catalog. pub fn write_catalog( @@ -86,23 +86,10 @@ pub fn write_catalog( info.keywords(TextStr::trimmed(&joined)); xmp.pdf_keywords(&joined); } - - // (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 (ctx.document.info.date, ctx.options.timestamp) { - (Smart::Custom(date), _) => (date, None), - (Smart::Auto, Some(timestamp)) => { - (Some(timestamp.datetime), Some(timestamp.timezone)) - } - _ => (None, None), - }; - if let Some(date) = date { - if let Some(pdf_date) = pdf_date(date, tz) { - info.creation_date(pdf_date); - info.modified_date(pdf_date); - } + 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(); @@ -154,7 +141,7 @@ pub fn write_catalog( } // Assert dominance. - if ctx.options.standards.pdfa { + if let Some((part, conformance)) = ctx.options.standards.pdfa_part { let mut extension_schemas = xmp.extension_schemas(); extension_schemas .xmp_media_management() @@ -162,8 +149,8 @@ pub fn write_catalog( .describe_instance_id(); extension_schemas.pdf().properties().describe_all(); extension_schemas.finish(); - xmp.pdfa_part(2); - xmp.pdfa_conformance("B"); + xmp.pdfa_part(part); + xmp.pdfa_conformance(conformance); } let xmp_buf = xmp.finish(None); @@ -182,13 +169,35 @@ pub fn write_catalog( catalog.viewer_preferences().direction(dir); catalog.metadata(meta_ref); - // Write the named destination tree if there are any entries. - if !ctx.references.named_destinations.dests.is_empty() { + 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(); - 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_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(); } } @@ -289,8 +298,27 @@ pub(crate) fn write_page_labels( 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. -fn pdf_date(datetime: Datetime, tz: Option) -> Option { +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); diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs new file mode 100644 index 000000000..b32f6e45d --- /dev/null +++ b/crates/typst-pdf/src/embed.rs @@ -0,0 +1,122 @@ +use std::collections::BTreeMap; + +use ecow::EcoString; +use pdf_writer::types::AssociationKind; +use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::foundations::{NativeElement, Packed, StyleChain}; +use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; + +use crate::catalog::{document_date, pdf_date}; +use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs}; + +/// 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.resolved_path.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.resolved_path.clone(), id).is_some() { + bail!( + elem.span(), + "duplicate embedded file for path `{}`", embed.resolved_path; + 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.resolved_path.as_bytes())); + file_spec.unic_file(TextStr(&embed.resolved_path)); + 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, + None => AssociationKind::Unspecified, + }); + } + + if let Some(description) = embed.description(StyleChain::default()) { + if description.len() > Str::PDFA_LIMIT { + bail!(embed.span(), "embedded file description is too long"); + } + file_spec.description(TextStr(description)); + } + + Ok(file_spec_dict_ref) +} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index f45c62bb5..88e62389c 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -4,6 +4,7 @@ mod catalog; mod color; mod color_font; mod content; +mod embed; mod extg; mod font; mod gradient; @@ -14,12 +15,13 @@ mod page; mod resources; mod tiling; -use std::collections::HashMap; +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 serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; @@ -33,6 +35,7 @@ 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}; @@ -67,6 +70,7 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult, } impl PdfStandards { /// Validates a list of PDF standards for compatibility and returns their /// encapsulated representation. pub fn new(list: &[PdfStandard]) -> StrResult { - Ok(Self { pdfa: list.contains(&PdfStandard::A_2b) }) + let a2b = list.contains(&PdfStandard::A_2b); + let a3b = list.contains(&PdfStandard::A_3b); + + if a2b && a3b { + bail!("PDF cannot conform to A-2B and A-3B at the same time") + } + + let pdfa = a2b || a3b; + Ok(Self { + pdfa, + embedded_files: !a2b, + pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")), + }) } } @@ -166,10 +188,9 @@ impl Debug for PdfStandards { } } -#[allow(clippy::derivable_impls)] impl Default for PdfStandards { fn default() -> Self { - Self { pdfa: false } + Self { pdfa: false, embedded_files: true, pdfa_part: None } } } @@ -186,6 +207,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[serde(rename = "a-2b")] A_2b, + /// PDF/A-3b. + #[serde(rename = "a-3b")] + A_3b, } /// A struct to build a PDF following a fixed succession of phases. @@ -316,6 +340,8 @@ struct References { 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 @@ -481,6 +507,14 @@ impl Renumber for HashMap { } } +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 { diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 5ca3724ab..e92799718 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -25,6 +25,7 @@ use typst::layout::{Abs, Margin, PageElem, PagedDocument, LAYOUT}; use typst::loading::DATA_LOADING; use typst::math::MATH; use typst::model::MODEL; +use typst::pdf::PDF; use typst::symbols::SYMBOLS; use typst::text::{Font, FontBook, TEXT}; use typst::utils::LazyHash; @@ -163,6 +164,7 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { category_page(resolver, VISUALIZE), category_page(resolver, INTROSPECTION), category_page(resolver, DATA_LOADING), + category_page(resolver, PDF), ]; page } diff --git a/tests/suite/pdf/embed.typ b/tests/suite/pdf/embed.typ new file mode 100644 index 000000000..bb5c9316c --- /dev/null +++ b/tests/suite/pdf/embed.typ @@ -0,0 +1,30 @@ +// Test file embeddings. The tests here so far are unsatisfactory because we +// have no PDF testing infrastructure. That should be improved in the future. + +--- pdf-embed --- +#pdf.embed("/assets/text/hello.txt") +#pdf.embed( + "/assets/data/details.toml", + relationship: "supplement", + mime-type: "application/toml", + description: "Information about a secret project", +) + +--- pdf-embed-invalid-relationship --- +#pdf.embed( + "/assets/text/hello.txt", + // Error: 17-23 expected "source", "data", "alternative", "supplement", or none + relationship: "test", + mime-type: "text/plain", + description: "A test file", +) + +--- pdf-embed-decode --- +#pdf.embed.decode("hello.txt", read("/assets/text/hello.txt")) +#pdf.embed.decode( + "a_file_name.txt", + read("/assets/text/hello.txt"), + relationship: "supplement", + mime-type: "text/plain", + description: "A description", +) From dacd6acd5e73d35c6e7a7a3b144f16ae70d03daa Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 8 Jan 2025 11:57:56 +0100 Subject: [PATCH 13/44] More flexible and efficient `Bytes` representation (#5670) --- crates/typst-cli/src/world.rs | 2 +- crates/typst-ide/src/tests.rs | 4 +- crates/typst-kit/src/fonts.rs | 8 +- crates/typst-layout/src/image.rs | 2 +- crates/typst-library/src/foundations/bytes.rs | 146 +++++++++++++----- crates/typst-library/src/foundations/float.rs | 12 +- crates/typst-library/src/foundations/int.rs | 5 +- .../typst-library/src/foundations/plugin.rs | 2 +- crates/typst-library/src/foundations/value.rs | 6 +- crates/typst-library/src/loading/cbor.rs | 2 +- crates/typst-library/src/loading/mod.rs | 24 ++- crates/typst-library/src/text/font/color.rs | 13 +- .../src/visualize/image/raster.rs | 2 +- crates/typst-svg/src/text.rs | 5 +- docs/src/html.rs | 2 +- docs/src/lib.rs | 2 +- tests/fuzz/src/compile.rs | 2 +- tests/src/world.rs | 6 +- 18 files changed, 160 insertions(+), 85 deletions(-) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index af6cf228f..12e80d273 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -305,7 +305,7 @@ impl FileSlot { ) -> FileResult { self.file.get_or_init( || read(self.id, project_root, package_storage), - |data, _| Ok(data.into()), + |data, _| Ok(Bytes::new(data)), ) } } diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index f41808dac..6678ab841 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -55,7 +55,7 @@ impl TestWorld { pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self { let id = FileId::new(None, VirtualPath::new(path)); let data = typst_dev_assets::get_by_name(filename).unwrap(); - let bytes = Bytes::from_static(data); + let bytes = Bytes::new(data); Arc::make_mut(&mut self.files).assets.insert(id, bytes); self } @@ -152,7 +152,7 @@ impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); Self { diff --git a/crates/typst-kit/src/fonts.rs b/crates/typst-kit/src/fonts.rs index 83e13fd8f..c15d739ec 100644 --- a/crates/typst-kit/src/fonts.rs +++ b/crates/typst-kit/src/fonts.rs @@ -13,6 +13,7 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; use fontdb::{Database, Source}; +use typst_library::foundations::Bytes; use typst_library::text::{Font, FontBook, FontInfo}; use typst_timing::TimingScope; @@ -52,9 +53,8 @@ impl FontSlot { .as_ref() .expect("`path` is not `None` if `font` is uninitialized"), ) - .ok()? - .into(); - Font::new(data, self.index) + .ok()?; + Font::new(Bytes::new(data), self.index) }) .clone() } @@ -196,7 +196,7 @@ impl FontSearcher { #[cfg(feature = "embed-fonts")] fn add_embedded(&mut self) { for data in typst_assets::fonts() { - let buffer = typst_library::foundations::Bytes::from_static(data); + let buffer = Bytes::new(data); for (i, font) in Font::iter(buffer).enumerate() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 77e1d0838..59e2c0210 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -50,7 +50,7 @@ pub fn layout_image( // Construct the image itself. let image = Image::with_fonts( - data.clone().into(), + data.clone().into_bytes(), format, elem.alt(styles), engine.world, diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index 05fe4763a..20034d074 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -1,5 +1,6 @@ -use std::borrow::Cow; +use std::any::Any; use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref}; use std::sync::Arc; @@ -39,18 +40,44 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value /// #str(data.slice(1, 4)) /// ``` #[ty(scope, cast)] -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Bytes(Arc>>); +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Bytes(Arc>); impl Bytes { - /// Create a buffer from a static byte slice. - pub fn from_static(slice: &'static [u8]) -> Self { - Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) + /// Create `Bytes` from anything byte-like. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be + /// allocated. + /// + /// If the type is `Vec` and the `Bytes` are unique (i.e. not cloned), + /// the vector will be reused when mutating to the `Bytes`. + /// + /// If your source type is a string, prefer [`Bytes::from_string`] to + /// directly use the UTF-8 encoded string data without any copying. + pub fn new(data: T) -> Self + where + T: AsRef<[u8]> + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(data))) + } + + /// Create `Bytes` from anything string-like, implicitly viewing the UTF-8 + /// representation. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `String` or `EcoString` without any copying. + pub fn from_string(data: T) -> Self + where + T: AsRef + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(StrWrapper(data)))) } /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.as_slice().is_empty() } /// Return a view into the buffer. @@ -60,7 +87,7 @@ impl Bytes { /// Return a copy of the buffer as a vector. pub fn to_vec(&self) -> Vec { - self.0.to_vec() + self.as_slice().to_vec() } /// Resolve an index or throw an out of bounds error. @@ -72,12 +99,10 @@ impl Bytes { /// /// `index == len` is considered in bounds. fn locate_opt(&self, index: i64) -> Option { + let len = self.as_slice().len(); let wrapped = - if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; - - wrapped - .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v <= self.0.len()) + if index >= 0 { Some(index) } else { (len as i64).checked_add(index) }; + wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len) } } @@ -106,7 +131,7 @@ impl Bytes { /// The length in bytes. #[func(title = "Length")] pub fn len(&self) -> usize { - self.0.len() + self.as_slice().len() } /// Returns the byte at the specified index. Returns the default value if @@ -122,13 +147,13 @@ impl Bytes { default: Option, ) -> StrResult { self.locate_opt(index) - .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into()))) .or(default) .ok_or_else(|| out_of_bounds_no_default(index, self.len())) } - /// Extracts a subslice of the bytes. Fails with an error if the start or end - /// index is out of bounds. + /// Extracts a subslice of the bytes. Fails with an error if the start or + /// end index is out of bounds. #[func] pub fn slice( &self, @@ -148,9 +173,17 @@ impl Bytes { if end.is_none() { end = count.map(|c: i64| start + c); } + let start = self.locate(start)?; let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); - Ok(self.0[start..end].into()) + let slice = &self.as_slice()[start..end]; + + // We could hold a view into the original bytes here instead of + // making a copy, but it's unclear when that's worth it. Java + // originally did that for strings, but went back on it because a + // very small view into a very large buffer would be a sort of + // memory leak. + Ok(Bytes::new(slice.to_vec())) } } @@ -170,7 +203,15 @@ impl Deref for Bytes { type Target = [u8]; fn deref(&self) -> &Self::Target { - &self.0 + self.0.as_bytes() + } +} + +impl Eq for Bytes {} + +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) } } @@ -180,18 +221,6 @@ impl AsRef<[u8]> for Bytes { } } -impl From<&[u8]> for Bytes { - fn from(slice: &[u8]) -> Self { - Self(Arc::new(LazyHash::new(slice.to_vec().into()))) - } -} - -impl From> for Bytes { - fn from(vec: Vec) -> Self { - Self(Arc::new(LazyHash::new(vec.into()))) - } -} - impl Add for Bytes { type Output = Self; @@ -207,10 +236,12 @@ impl AddAssign for Bytes { // Nothing to do } else if self.is_empty() { *self = rhs; - } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { - Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); + } else if let Some(vec) = Arc::get_mut(&mut self.0) + .and_then(|unique| unique.as_any_mut().downcast_mut::>()) + { + vec.extend_from_slice(&rhs); } else { - *self = Self::from([self.as_slice(), rhs.as_slice()].concat()); + *self = Self::new([self.as_slice(), rhs.as_slice()].concat()); } } } @@ -228,20 +259,61 @@ impl Serialize for Bytes { } } +/// Any type that can back a byte buffer. +trait Bytelike: Send + Sync { + fn as_bytes(&self) -> &[u8]; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl Bytelike for T +where + T: AsRef<[u8]> + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.as_ref() + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Hash for dyn Bytelike { + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +/// Makes string-like objects usable with `Bytes`. +struct StrWrapper(T); + +impl Bytelike for StrWrapper +where + T: AsRef + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.0.as_ref().as_bytes() + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + /// A value that can be cast to bytes. pub struct ToBytes(Bytes); cast! { ToBytes, - v: Str => Self(v.as_bytes().into()), + v: Str => Self(Bytes::from_string(v)), v: Array => Self(v.iter() .map(|item| match item { Value::Int(byte @ 0..=255) => Ok(*byte as u8), Value::Int(_) => bail!("number must be between 0 and 255"), value => Err(::error(value)), }) - .collect::, _>>()? - .into() + .collect::, _>>() + .map(Bytes::new)? ), v: Bytes => Self(v), } diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index c3d4e0e73..fcc46b034 100644 --- a/crates/typst-library/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -163,18 +163,14 @@ impl f64 { size: u32, ) -> StrResult { Ok(match size { - 8 => match endian { + 8 => Bytes::new(match endian { Endianness::Little => self.to_le_bytes(), Endianness::Big => self.to_be_bytes(), - } - .as_slice() - .into(), - 4 => match endian { + }), + 4 => Bytes::new(match endian { Endianness::Little => (self as f32).to_le_bytes(), Endianness::Big => (self as f32).to_be_bytes(), - } - .as_slice() - .into(), + }), _ => bail!("size must be either 4 or 8"), }) } diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index bddffada3..83a89bf8a 100644 --- a/crates/typst-library/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -1,6 +1,7 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use ecow::{eco_format, EcoString}; +use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{ @@ -322,7 +323,7 @@ impl i64 { Endianness::Little => self.to_le_bytes(), }; - let mut buf = vec![0u8; size]; + let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size); match endian { Endianness::Big => { // Copy the bytes from the array to the buffer, starting from @@ -339,7 +340,7 @@ impl i64 { } } - Bytes::from(buf) + Bytes::new(buf) } } diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index f57257a45..a7c341d8c 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -293,7 +293,7 @@ impl Plugin { _ => bail!("plugin did not respect the protocol"), }; - Ok(output.into()) + Ok(Bytes::new(output)) } /// An iterator over all the function names defined by the plugin. diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index eb0d6eedc..efc480d3f 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -459,15 +459,15 @@ impl<'de> Visitor<'de> for ValueVisitor { } fn visit_bytes(self, v: &[u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_byte_buf(self, v: Vec) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v).into_value()) } fn visit_none(self) -> Result { diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 977059c3d..a03e5c998 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -55,7 +55,7 @@ impl cbor { let Spanned { v: value, span } = value; let mut res = Vec::new(); ciborium::into_writer(&value, &mut res) - .map(|_| res.into()) + .map(|_| Bytes::new(res)) .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) .at(span) } diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index ae74df864..120b3e3af 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -56,15 +56,22 @@ pub enum Readable { impl Readable { pub fn as_slice(&self) -> &[u8] { match self { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes(), + Self::Bytes(v) => v, + Self::Str(v) => v.as_bytes(), } } pub fn as_str(&self) -> Option<&str> { match self { - Readable::Str(v) => Some(v.as_str()), - Readable::Bytes(v) => std::str::from_utf8(v).ok(), + Self::Str(v) => Some(v.as_str()), + Self::Bytes(v) => std::str::from_utf8(v).ok(), + } + } + + pub fn into_bytes(self) -> Bytes { + match self { + Self::Bytes(v) => v, + Self::Str(v) => Bytes::from_string(v), } } } @@ -78,12 +85,3 @@ cast! { v: Str => Self::Str(v), v: Bytes => Self::Bytes(v), } - -impl From for Bytes { - fn from(value: Readable) -> Self { - match value { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes().into(), - } - } -} diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index 08f6fe0a3..e3183e885 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -7,6 +7,7 @@ use typst_syntax::Span; use usvg::tiny_skia_path; use xmlwriter::XmlWriter; +use crate::foundations::Bytes; use crate::layout::{Abs, Frame, FrameItem, Point, Size}; use crate::text::{Font, Glyph}; use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat}; @@ -101,8 +102,12 @@ fn draw_raster_glyph( upem: Abs, raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { - let image = - Image::new(raster_image.data.into(), RasterFormat::Png.into(), None).ok()?; + let image = Image::new( + Bytes::new(raster_image.data.to_vec()), + RasterFormat::Png.into(), + None, + ) + .ok()?; // Apple Color emoji doesn't provide offset information (or at least // not in a way ttf-parser understands), so we artificially shift their @@ -175,7 +180,7 @@ fn draw_colr_glyph( let data = svg.end_document().into_bytes(); - let image = Image::new(data.into(), VectorFormat::Svg.into(), None).ok()?; + let image = Image::new(Bytes::new(data), VectorFormat::Svg.into(), None).ok()?; let y_shift = Abs::pt(upem.to_pt() - y_max); let position = Point::new(Abs::pt(x_min), y_shift); @@ -251,7 +256,7 @@ fn draw_svg_glyph( ); let image = - Image::new(wrapper_svg.into_bytes().into(), VectorFormat::Svg.into(), None) + Image::new(Bytes::new(wrapper_svg.into_bytes()), VectorFormat::Svg.into(), None) .ok()?; let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 829826c75..098843a25 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -274,7 +274,7 @@ mod tests { #[track_caller] fn test(path: &str, format: RasterFormat, dpi: f64) { let data = typst_dev_assets::get(path).unwrap(); - let bytes = Bytes::from_static(data); + let bytes = Bytes::new(data); let image = RasterImage::new(bytes, format).unwrap(); assert_eq!(image.dpi().map(f64::round), Some(dpi)); } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 80de32089..fa471b2ae 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -3,6 +3,7 @@ use std::io::Read; use base64::Engine; use ecow::EcoString; use ttf_parser::GlyphId; +use typst_library::foundations::Bytes; use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; use typst_library::text::{Font, TextItem}; use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; @@ -243,7 +244,9 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64 if raster.format != ttf_parser::RasterImageFormat::PNG { return None; } - let image = Image::new(raster.data.into(), RasterFormat::Png.into(), None).ok()?; + let image = + Image::new(Bytes::new(raster.data.to_vec()), RasterFormat::Png.into(), None) + .ok()?; Some((image, raster.x as f64, raster.y as f64)) } diff --git a/docs/src/html.rs b/docs/src/html.rs index a1206032d..4eb3954c3 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -486,7 +486,7 @@ impl World for DocWorld { fn file(&self, id: FileId) -> FileResult { assert!(id.package().is_none()); - Ok(Bytes::from_static( + Ok(Bytes::new( typst_dev_assets::get_by_name( &id.vpath().as_rootless_path().to_string_lossy(), ) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index e92799718..2751500e3 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -78,7 +78,7 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { static FONTS: LazyLock<(LazyHash, Vec)> = LazyLock::new(|| { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); let book = FontBook::from_fonts(&fonts); (LazyHash::new(book), fonts) diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index 37e21deb9..3dedfb737 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -19,7 +19,7 @@ struct FuzzWorld { impl FuzzWorld { fn new(text: &str) -> Self { let data = typst_assets::fonts().next().unwrap(); - let font = Font::new(Bytes::from_static(data), 0).unwrap(); + let font = Font::new(Bytes::new(data), 0).unwrap(); let book = FontBook::from_fonts([&font]); Self { library: LazyHash::new(Library::default()), diff --git a/tests/src/world.rs b/tests/src/world.rs index a08f1efa8..5c2678328 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -98,7 +98,7 @@ impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); Self { @@ -140,8 +140,8 @@ impl FileSlot { self.file .get_or_init(|| { read(&system_path(self.id)?).map(|cow| match cow { - Cow::Owned(buf) => buf.into(), - Cow::Borrowed(buf) => Bytes::from_static(buf), + Cow::Owned(buf) => Bytes::new(buf), + Cow::Borrowed(buf) => Bytes::new(buf), }) }) .clone() From e2b37fef33a92a7086790e04fb133472413c0c0a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 9 Jan 2025 10:34:16 +0100 Subject: [PATCH 14/44] Revamp data loading and deprecate `decode` functions (#5671) --- crates/typst-eval/src/import.rs | 2 +- crates/typst-ide/src/complete.rs | 37 +- crates/typst-layout/src/image.rs | 52 ++- crates/typst-library/src/foundations/array.rs | 47 ++ crates/typst-library/src/foundations/bytes.rs | 51 ++- crates/typst-library/src/foundations/cast.rs | 59 ++- .../typst-library/src/foundations/plugin.rs | 14 +- crates/typst-library/src/foundations/str.rs | 6 +- .../typst-library/src/foundations/styles.rs | 10 +- crates/typst-library/src/loading/cbor.rs | 30 +- crates/typst-library/src/loading/csv.rs | 109 ++--- crates/typst-library/src/loading/json.rs | 29 +- crates/typst-library/src/loading/mod.rs | 95 +++- crates/typst-library/src/loading/read.rs | 11 +- crates/typst-library/src/loading/toml.rs | 33 +- crates/typst-library/src/loading/xml.rs | 39 +- crates/typst-library/src/loading/yaml.rs | 29 +- .../typst-library/src/model/bibliography.rs | 407 +++++++++--------- crates/typst-library/src/model/cite.rs | 34 +- crates/typst-library/src/model/document.rs | 10 +- crates/typst-library/src/pdf/embed.rs | 86 ++-- crates/typst-library/src/text/raw.rs | 376 ++++++++-------- .../typst-library/src/visualize/image/mod.rs | 52 ++- .../typst-library/src/visualize/image/svg.rs | 1 + crates/typst-pdf/src/embed.rs | 10 +- crates/typst-utils/src/hash.rs | 71 +++ crates/typst-utils/src/lib.rs | 2 +- tests/suite/pdf/embed.typ | 20 +- 28 files changed, 1000 insertions(+), 722 deletions(-) diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 5b67c0608..2060d25f1 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -211,7 +211,7 @@ fn resolve_package( // Evaluate the manifest. let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); let bytes = engine.world.file(manifest_id).at(span)?; - let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; + let string = bytes.as_str().map_err(FileError::from).at(span)?; let manifest: PackageManifest = toml::from_str(string) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) .at(span)?; diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index c22ea7e40..0f8abddb7 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -817,19 +817,8 @@ fn param_value_completions<'a>( ) { if param.name == "font" { ctx.font_completions(); - } else if param.name == "path" { - ctx.file_completions_with_extensions(match func.name() { - Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], - Some("csv") => &["csv"], - Some("plugin") => &["wasm"], - Some("cbor") => &["cbor"], - Some("json") => &["json"], - Some("toml") => &["toml"], - Some("xml") => &["xml"], - Some("yaml") => &["yml", "yaml"], - Some("bibliography") => &["bib", "yml", "yaml"], - _ => &[], - }); + } else if let Some(extensions) = path_completion(func, param) { + ctx.file_completions_with_extensions(extensions); } else if func.name() == Some("figure") && param.name == "body" { ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure."); ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure."); @@ -838,6 +827,28 @@ fn param_value_completions<'a>( ctx.cast_completions(¶m.input); } +/// 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("csv"), "source") => &["csv"], + (Some("plugin"), "source") => &["wasm"], + (Some("cbor"), "source") => &["cbor"], + (Some("json"), "source") => &["json"], + (Some("toml"), "source") => &["toml"], + (Some("xml"), "source") => &["xml"], + (Some("yaml"), "source") => &["yml", "yaml"], + (Some("bibliography"), "sources") => &["bib", "yml", "yaml"], + (Some("bibliography"), "style") => &["csl"], + (Some("cite"), "style") => &["csl"], + (Some("raw"), "syntaxes") => &["sublime-syntax"], + (Some("raw"), "theme") => &["tmtheme"], + (Some("embed"), "path") => &[], + (None, "path") => &[], + _ => return None, + }) +} + /// Resolve a callee expression to a global function. fn resolve_global_callee<'a>( ctx: &CompletionContext<'a>, diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 59e2c0210..e521b993f 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -1,13 +1,13 @@ use std::ffi::OsStr; -use typst_library::diag::{bail, warning, At, SourceResult, StrResult}; +use typst_library::diag::{warning, At, SourceResult, StrResult}; use typst_library::engine::Engine; -use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, }; -use typst_library::loading::Readable; +use typst_library::loading::DataSource; use typst_library::text::families; use typst_library::visualize::{ Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat, @@ -26,17 +26,17 @@ pub fn layout_image( // Take the format that was explicitly defined, or parse the extension, // or try to detect the format. - let data = elem.data(); + let Derived { source, derived: data } = &elem.source; let format = match elem.format(styles) { Smart::Custom(v) => v, - Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?, + Smart::Auto => determine_format(source, 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_some_and(|s| s.contains(" StrResult { - let ext = std::path::Path::new(path) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); +/// Try to determine the image format based on the data. +fn determine_format(source: &DataSource, data: &Bytes) -> StrResult { + if let DataSource::Path(path) = source { + let ext = std::path::Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); - Ok(match ext.as_str() { - "png" => ImageFormat::Raster(RasterFormat::Png), - "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), - "gif" => ImageFormat::Raster(RasterFormat::Gif), - "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), - _ => match &data { - Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), - Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { - Some(f) => ImageFormat::Raster(f), - None => bail!("unknown image format"), - }, - }, - }) + match ext.as_str() { + "png" => return Ok(ImageFormat::Raster(RasterFormat::Png)), + "jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)), + "gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)), + "svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)), + _ => {} + } + } + + Ok(ImageFormat::detect(data).ok_or("unknown image format")?) } diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index 4667ee765..e79a4e930 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -1124,6 +1124,53 @@ impl FromValue for SmallVec<[T; N]> { } } +/// One element, or multiple provided as an array. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct OneOrMultiple(pub Vec); + +impl Reflect for OneOrMultiple { + fn input() -> CastInfo { + T::input() + Array::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || T::castable(value) + } +} + +impl IntoValue for OneOrMultiple { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for OneOrMultiple { + fn from_value(value: Value) -> HintedStrResult { + if T::castable(&value) { + return Ok(Self(vec![T::from_value(value)?])); + } + if Array::castable(&value) { + return Ok(Self( + Array::from_value(value)? + .into_iter() + .map(|value| T::from_value(value)) + .collect::>()?, + )); + } + Err(Self::error(&value)) + } +} + +impl Default for OneOrMultiple { + fn default() -> Self { + Self(vec![]) + } +} + /// The error message when the array is empty. #[cold] fn array_is_empty() -> EcoString { diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index 20034d074..d633c99ad 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -2,6 +2,7 @@ use std::any::Any; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref}; +use std::str::Utf8Error; use std::sync::Arc; use ecow::{eco_format, EcoString}; @@ -80,16 +81,37 @@ impl Bytes { self.as_slice().is_empty() } - /// Return a view into the buffer. + /// Return a view into the bytes. pub fn as_slice(&self) -> &[u8] { self } - /// Return a copy of the buffer as a vector. + /// Try to view the bytes as an UTF-8 string. + /// + /// If these bytes were created via `Bytes::from_string`, UTF-8 validation + /// is skipped. + pub fn as_str(&self) -> Result<&str, Utf8Error> { + self.inner().as_str() + } + + /// Return a copy of the bytes as a vector. pub fn to_vec(&self) -> Vec { self.as_slice().to_vec() } + /// Try to turn the bytes into a `Str`. + /// + /// - If these bytes were created via `Bytes::from_string::`, the + /// string is cloned directly. + /// - If these bytes were created via `Bytes::from_string`, but from a + /// different type of string, UTF-8 validation is still skipped. + pub fn to_str(&self) -> Result { + match self.inner().as_any().downcast_ref::() { + Some(string) => Ok(string.clone()), + None => self.as_str().map(Into::into), + } + } + /// Resolve an index or throw an out of bounds error. fn locate(&self, index: i64) -> StrResult { self.locate_opt(index).ok_or_else(|| out_of_bounds(index, self.len())) @@ -104,6 +126,11 @@ impl Bytes { if index >= 0 { Some(index) } else { (len as i64).checked_add(index) }; wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len) } + + /// Access the inner `dyn Bytelike`. + fn inner(&self) -> &dyn Bytelike { + &**self.0 + } } #[scope] @@ -203,7 +230,7 @@ impl Deref for Bytes { type Target = [u8]; fn deref(&self) -> &Self::Target { - self.0.as_bytes() + self.inner().as_bytes() } } @@ -262,6 +289,8 @@ impl Serialize for Bytes { /// Any type that can back a byte buffer. trait Bytelike: Send + Sync { fn as_bytes(&self) -> &[u8]; + fn as_str(&self) -> Result<&str, Utf8Error>; + fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; } @@ -273,6 +302,14 @@ where self.as_ref() } + fn as_str(&self) -> Result<&str, Utf8Error> { + std::str::from_utf8(self.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { self } @@ -295,6 +332,14 @@ where self.0.as_ref().as_bytes() } + fn as_str(&self) -> Result<&str, Utf8Error> { + Ok(self.0.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { self } diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs index 84f38f36e..38f409c67 100644 --- a/crates/typst-library/src/foundations/cast.rs +++ b/crates/typst-library/src/foundations/cast.rs @@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned}; use unicode_math_class::MathClass; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; -use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; +use crate::foundations::{ + array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value, +}; /// Determine details of a type. /// @@ -497,3 +499,58 @@ cast! { /// An operator that can be both unary or binary like `+`. "vary" => MathClass::Vary, } + +/// A type that contains a user-visible source portion and something that is +/// derived from it, but not user-visible. +/// +/// An example usage would be `source` being a `DataSource` and `derived` a +/// TextMate theme parsed from it. With `Derived`, we can store both parts in +/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue` +/// impls. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Derived { + /// The source portion. + pub source: S, + /// The derived portion. + pub derived: D, +} + +impl Derived { + /// Create a new instance from the `source` and the `derived` data. + pub fn new(source: S, derived: D) -> Self { + Self { source, derived } + } +} + +impl Reflect for Derived { + fn input() -> CastInfo { + S::input() + } + + fn output() -> CastInfo { + S::output() + } + + fn castable(value: &Value) -> bool { + S::castable(value) + } + + fn error(found: &Value) -> HintedString { + S::error(found) + } +} + +impl IntoValue for Derived { + fn into_value(self) -> Value { + self.source.into_value() + } +} + +impl Fold for Derived { + fn fold(self, outer: Self) -> Self { + Self { + source: self.source.fold(outer.source), + derived: self.derived.fold(outer.derived), + } + } +} diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index a7c341d8c..adf23a47c 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -9,7 +9,7 @@ use wasmi::{AsContext, AsContextMut}; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{func, repr, scope, ty, Bytes}; -use crate::World; +use crate::loading::{DataSource, Load}; /// A WebAssembly plugin. /// @@ -154,15 +154,13 @@ impl Plugin { pub fn construct( /// The engine. engine: &mut Engine, - /// Path to a WebAssembly file. + /// A path to a WebAssembly file or raw WebAssembly bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - Plugin::new(data).at(span) + let data = source.load(engine.world)?; + Plugin::new(data).at(source.span) } } diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 4025d1ab3..2e90b3071 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -784,11 +784,7 @@ cast! { v: f64 => Self::Str(repr::display_float(v).into()), v: Decimal => Self::Str(format_str!("{}", v)), v: Version => Self::Str(format_str!("{}", v)), - v: Bytes => Self::Str( - std::str::from_utf8(&v) - .map_err(|_| "bytes are not valid utf-8")? - .into() - ), + v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?), v: Label => Self::Str(v.resolve().as_str().into()), v: Type => Self::Str(v.long_name().into()), v: Str => Self::Str(v), diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 7354719e9..37094dcd8 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -12,7 +12,8 @@ use typst_utils::LazyHash; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ - cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector, + cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr, + Selector, }; use crate::text::{FontFamily, FontList, TextElem}; @@ -939,6 +940,13 @@ impl Fold for SmallVec<[T; N]> { } } +impl Fold for OneOrMultiple { + fn fold(self, mut outer: Self) -> Self { + outer.0.extend(self.0); + outer + } +} + /// A variant of fold for foldable optional (`Option`) values where an inner /// `None` value isn't respected (contrary to `Option`'s usual `Fold` /// implementation, with which folding with an inner `None` always returns diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index a03e5c998..13d551201 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -1,10 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Bytes, Value}; -use crate::World; +use crate::loading::{DataSource, Load}; /// Reads structured data from a CBOR file. /// @@ -21,29 +21,31 @@ use crate::World; pub fn cbor( /// The engine. engine: &mut Engine, - /// Path to a CBOR file. + /// A path to a CBOR file or raw CBOR bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - cbor::decode(Spanned::new(data, span)) + let data = source.load(engine.world)?; + ciborium::from_reader(data.as_slice()) + .map_err(|err| eco_format!("failed to parse CBOR ({err})")) + .at(source.span) } #[scope] impl cbor { /// Reads structured data from CBOR bytes. + /// + /// This function is deprecated. The [`cbor`] function now accepts bytes + /// directly. #[func(title = "Decode CBOR")] pub fn decode( - /// cbor data. + /// The engine. + engine: &mut Engine, + /// CBOR data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - ciborium::from_reader(data.as_slice()) - .map_err(|err| eco_format!("failed to parse CBOR ({err})")) - .at(span) + cbor(engine, data.map(DataSource::Bytes)) } /// Encode structured data into CBOR bytes. diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6822505d3..8171c4832 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -4,8 +4,7 @@ use typst_syntax::Spanned; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a CSV file. /// @@ -28,10 +27,10 @@ use crate::World; pub fn csv( /// The engine. engine: &mut Engine, - /// Path to a CSV file. + /// Path to a CSV file or raw CSV bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, /// The delimiter that separates columns in the CSV file. /// Must be a single ASCII character. #[named] @@ -48,17 +47,63 @@ pub fn csv( #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type) + let data = source.load(engine.world)?; + + let mut builder = ::csv::ReaderBuilder::new(); + let has_headers = row_type == RowType::Dict; + builder.has_headers(has_headers); + builder.delimiter(delimiter.0 as u8); + + // Counting lines from 1 by default. + let mut line_offset: usize = 1; + let mut reader = builder.from_reader(data.as_slice()); + let mut headers: Option<::csv::StringRecord> = None; + + if has_headers { + // Counting lines from 2 because we have a header. + line_offset += 1; + headers = Some( + reader + .headers() + .map_err(|err| format_csv_error(err, 1)) + .at(source.span)? + .clone(), + ); + } + + let mut array = Array::new(); + for (line, result) in reader.records().enumerate() { + // Original solution was to use line from error, but that is + // 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 item = if let Some(headers) = &headers { + let mut dict = Dict::new(); + for (field, value) in headers.iter().zip(&row) { + dict.insert(field.into(), value.into_value()); + } + dict.into_value() + } else { + let sub = row.into_iter().map(|field| field.into_value()).collect(); + Value::Array(sub) + }; + array.push(item); + } + + Ok(array) } #[scope] impl csv { /// Reads structured data from a CSV string/bytes. + /// + /// This function is deprecated. The [`csv`] function now accepts bytes + /// directly. #[func(title = "Decode CSV")] pub fn decode( + /// The engine. + engine: &mut Engine, /// CSV data. data: Spanned, /// The delimiter that separates columns in the CSV file. @@ -77,51 +122,7 @@ impl csv { #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: data, span } = data; - let has_headers = row_type == RowType::Dict; - - let mut builder = ::csv::ReaderBuilder::new(); - builder.has_headers(has_headers); - builder.delimiter(delimiter.0 as u8); - - // Counting lines from 1 by default. - let mut line_offset: usize = 1; - let mut reader = builder.from_reader(data.as_slice()); - let mut headers: Option<::csv::StringRecord> = None; - - if has_headers { - // Counting lines from 2 because we have a header. - line_offset += 1; - headers = Some( - reader - .headers() - .map_err(|err| format_csv_error(err, 1)) - .at(span)? - .clone(), - ); - } - - let mut array = Array::new(); - for (line, result) in reader.records().enumerate() { - // Original solution was to use line from error, but that is - // 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(span)?; - let item = if let Some(headers) = &headers { - let mut dict = Dict::new(); - for (field, value) in headers.iter().zip(&row) { - dict.insert(field.into(), value.into_value()); - } - dict.into_value() - } else { - let sub = row.into_iter().map(|field| field.into_value()).collect(); - Value::Array(sub) - }; - array.push(item); - } - - Ok(array) + csv(engine, data.map(Readable::into_source), delimiter, row_type) } } diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 597cf4cc6..3128d77da 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a JSON file. /// @@ -53,29 +52,31 @@ use crate::World; pub fn json( /// The engine. engine: &mut Engine, - /// Path to a JSON file. + /// Path to a JSON file or raw JSON bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - json::decode(Spanned::new(Readable::Bytes(data), span)) + 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) } #[scope] impl json { /// Reads structured data from a JSON string/bytes. + /// + /// This function is deprecated. The [`json`] function now accepts bytes + /// directly. #[func(title = "Decode JSON")] pub fn decode( + /// The engine. + engine: &mut Engine, /// JSON data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(span) + json(engine, data.map(Readable::into_source)) } /// Encodes structured data into a JSON string. diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index 120b3e3af..171ae651a 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -15,6 +15,10 @@ mod xml_; #[path = "yaml.rs"] mod yaml_; +use comemo::Tracked; +use ecow::EcoString; +use typst_syntax::Spanned; + pub use self::cbor_::*; pub use self::csv_::*; pub use self::json_::*; @@ -23,7 +27,10 @@ pub use self::toml_::*; pub use self::xml_::*; pub use self::yaml_::*; +use crate::diag::{At, SourceResult}; +use crate::foundations::OneOrMultiple; use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; +use crate::World; /// Data loading from external files. /// @@ -44,6 +51,76 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); } +/// Something we can retrieve byte data from. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum DataSource { + /// A path to a file. + Path(EcoString), + /// Raw bytes. + Bytes(Bytes), +} + +cast! { + DataSource, + self => match self { + Self::Path(v) => v.into_value(), + Self::Bytes(v) => v.into_value(), + }, + v: EcoString => Self::Path(v), + v: Bytes => Self::Bytes(v), +} + +/// Loads data from a path or provided bytes. +pub trait Load { + /// Bytes or a list of bytes (if there are multiple sources). + type Output; + + /// Load the bytes. + fn load(&self, world: Tracked) -> SourceResult; +} + +impl Load for Spanned { + type Output = Bytes; + + fn load(&self, world: Tracked) -> SourceResult { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&DataSource> { + type Output = Bytes; + + 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) + } + DataSource::Bytes(bytes) => Ok(bytes.clone()), + } + } +} + +impl Load for Spanned> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&OneOrMultiple> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.v + .0 + .iter() + .map(|source| Spanned::new(source, self.span).load(world)) + .collect() + } +} + /// A value that can be read from a file. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Readable { @@ -54,26 +131,16 @@ pub enum Readable { } impl Readable { - pub fn as_slice(&self) -> &[u8] { - match self { - Self::Bytes(v) => v, - Self::Str(v) => v.as_bytes(), - } - } - - pub fn as_str(&self) -> Option<&str> { - match self { - Self::Str(v) => Some(v.as_str()), - Self::Bytes(v) => std::str::from_utf8(v).ok(), - } - } - pub fn into_bytes(self) -> Bytes { match self { Self::Bytes(v) => v, Self::Str(v) => Bytes::from_string(v), } } + + pub fn into_source(self) -> DataSource { + DataSource::Bytes(self.into_bytes()) + } } cast! { diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index 23e6e27e7..bf363f846 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, Cast}; use crate::loading::Readable; @@ -42,12 +42,9 @@ pub fn read( let data = engine.world.file(id).at(span)?; Ok(match encoding { None => Readable::Bytes(data), - Some(Encoding::Utf8) => Readable::Str( - std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)? - .into(), - ), + Some(Encoding::Utf8) => { + Readable::Str(data.to_str().map_err(FileError::from).at(span)?) + } }) } diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 5167703ef..e3a01cdd5 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -1,11 +1,10 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{is_newline, Spanned}; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a TOML file. /// @@ -31,32 +30,32 @@ use crate::World; pub fn toml( /// The engine. engine: &mut Engine, - /// Path to a TOML file. + /// A path to a TOML file or raw TOML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - toml::decode(Spanned::new(Readable::Bytes(data), span)) + 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) } #[scope] impl toml { /// Reads structured data from a TOML string/bytes. + /// + /// This function is deprecated. The [`toml`] function now accepts bytes + /// directly. #[func(title = "Decode TOML")] pub fn decode( + /// The engine. + engine: &mut Engine, /// TOML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let raw = std::str::from_utf8(data.as_slice()) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(span) + toml(engine, data.map(Readable::into_source)) } /// Encodes structured data into a TOML string. diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 3b1a9674b..53ec3d93b 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -5,8 +5,7 @@ use typst_syntax::Spanned; use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from an XML file. /// @@ -60,36 +59,36 @@ use crate::World; pub fn xml( /// The engine. engine: &mut Engine, - /// Path to an XML file. + /// A path to an XML file or raw XML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - xml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + let text = data.as_str().map_err(FileError::from).at(source.span)?; + let document = roxmltree::Document::parse_with_options( + text, + ParsingOptions { allow_dtd: true, ..Default::default() }, + ) + .map_err(format_xml_error) + .at(source.span)?; + Ok(convert_xml(document.root())) } #[scope] impl xml { /// Reads structured data from an XML string/bytes. + /// + /// This function is deprecated. The [`xml`] function now accepts bytes + /// directly. #[func(title = "Decode XML")] pub fn decode( + /// The engine. + engine: &mut Engine, /// XML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let text = std::str::from_utf8(data.as_slice()) - .map_err(FileError::from) - .at(span)?; - let document = roxmltree::Document::parse_with_options( - text, - ParsingOptions { allow_dtd: true, ..Default::default() }, - ) - .map_err(format_xml_error) - .at(span)?; - Ok(convert_xml(document.root())) + xml(engine, data.map(Readable::into_source)) } } diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 0e8ca3fb0..2eb26be8f 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a YAML file. /// @@ -43,29 +42,31 @@ use crate::World; pub fn yaml( /// The engine. engine: &mut Engine, - /// Path to a YAML file. + /// A path to a YAML file or raw YAML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - yaml::decode(Spanned::new(Readable::Bytes(data), span)) + 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) } #[scope] impl yaml { /// Reads structured data from a YAML string/bytes. + /// + /// This function is deprecated. The [`yaml`] function now accepts bytes + /// directly. #[func(title = "Decode YAML")] pub fn decode( + /// The engine. + engine: &mut Engine, /// YAML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(span) + yaml(engine, data.map(Readable::into_source)) } /// Encode structured data into a YAML string. diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 280ac4a42..4ab4ff22c 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -1,7 +1,7 @@ +use std::any::TypeId; use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; @@ -12,26 +12,26 @@ use hayagriva::archive::ArchivedStyle; use hayagriva::io::BibLaTeXError; use hayagriva::{ citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, - SpecificLocator, + Library, SpecificLocator, }; use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; -use typed_arena::Arena; use typst_syntax::{Span, Spanned}; -use typst_utils::{LazyHash, NonZeroExt, PicoStr}; +use typst_utils::{ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, - NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, - Styles, Synthesize, Type, Value, + elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, + OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, + Synthesize, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing, TrackSizings, VElem, }; +use crate::loading::{DataSource, Load}; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, Url, @@ -86,13 +86,20 @@ use crate::World; /// ``` #[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] pub struct BibliographyElem { - /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. + /// One or multiple paths to or raw bytes for Hayagriva `.yml` and/or + /// BibLaTeX `.bib` files. + /// + /// This can be a: + /// - A path string to load a bibliography file from the given path. For + /// more details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the bibliography should be decoded. + /// - An array where each item is one the above. #[required] #[parse( - let (paths, bibliography) = Bibliography::parse(engine, args)?; - paths + let sources = args.expect("sources")?; + Bibliography::load(engine.world, sources)? )] - pub path: BibliographyPaths, + pub sources: Derived, Bibliography>, /// The title of the bibliography. /// @@ -116,19 +123,22 @@ pub struct BibliographyElem { /// The bibliography style. /// - /// Should be either one of the built-in styles (see below) or a path to - /// a [CSL file](https://citationstyles.org/). Some of the styles listed - /// below appear twice, once with their full name and once with a short - /// alias. - #[parse(CslStyle::parse(engine, args)?)] - #[default(CslStyle::from_name("ieee").unwrap())] - pub style: CslStyle, - - /// The loaded bibliography. - #[internal] - #[required] - #[parse(bibliography)] - pub bibliography: Bibliography, + /// This can be: + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>("style")? { + Some(source) => Some(CslStyle::load(engine.world, source)?), + None => None, + })] + #[default({ + let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers; + Derived::new(CslSource::Named(default), CslStyle::from_archived(default)) + })] + pub style: Derived, /// The language setting where the bibliography is. #[internal] @@ -141,17 +151,6 @@ pub struct BibliographyElem { pub region: Option, } -/// A list of bibliography file paths. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct BibliographyPaths(Vec); - -cast! { - BibliographyPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked) -> StrResult> { @@ -169,13 +168,12 @@ impl BibliographyElem { } /// Whether the bibliography contains the given key. - pub fn has(engine: &Engine, key: impl Into) -> bool { - let key = key.into(); + pub fn has(engine: &Engine, key: Label) -> bool { engine .introspector .query(&Self::elem().select()) .iter() - .any(|elem| elem.to_packed::().unwrap().bibliography().has(key)) + .any(|elem| elem.to_packed::().unwrap().sources.derived.has(key)) } /// Find all bibliography keys. @@ -183,9 +181,9 @@ impl BibliographyElem { let mut vec = vec![]; for elem in introspector.query(&Self::elem().select()).iter() { let this = elem.to_packed::().unwrap(); - for (key, entry) in this.bibliography().iter() { + for (key, entry) in this.sources.derived.iter() { let detail = entry.title().map(|title| title.value.to_str().into()); - vec.push((Label::new(key), detail)) + vec.push((key, detail)) } } vec @@ -282,63 +280,35 @@ impl LocalName for Packed { } /// A loaded bibliography. -#[derive(Clone, PartialEq)] -pub struct Bibliography { - map: Arc>, - hash: u128, -} +#[derive(Clone, PartialEq, Hash)] +pub struct Bibliography(Arc>>); impl Bibliography { - /// Parse the bibliography argument. - fn parse( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult<(BibliographyPaths, Bibliography)> { - let Spanned { v: paths, span } = - args.expect::>("path to bibliography file")?; - - // Load bibliography files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Parse. - let bibliography = Self::load(&paths, &data).at(span)?; - - Ok((paths, bibliography)) + /// Load a bibliography from data sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Self>> { + let data = sources.load(world)?; + let bibliography = Self::decode(&sources.v, &data).at(sources.span)?; + Ok(Derived::new(sources.v, bibliography)) } - /// Load bibliography entries from paths. + /// Decode a bibliography from loaded data sources. #[comemo::memoize] #[typst_macros::time(name = "load bibliography")] - fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult { + fn decode( + sources: &OneOrMultiple, + data: &[Bytes], + ) -> StrResult { let mut map = IndexMap::new(); let mut duplicates = Vec::::new(); // We might have multiple bib/yaml files - for (path, bytes) in paths.0.iter().zip(data) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - - let ext = Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - let library = 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(path, src, errors))?, - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), - }; - + for (source, data) in sources.0.iter().zip(data) { + let library = decode_library(source, data)?; for entry in library { - match map.entry(PicoStr::intern(entry.key())) { + match map.entry(Label::new(PicoStr::intern(entry.key()))) { indexmap::map::Entry::Vacant(vacant) => { vacant.insert(entry); } @@ -353,182 +323,210 @@ impl Bibliography { bail!("duplicate bibliography keys: {}", duplicates.join(", ")); } - Ok(Bibliography { - map: Arc::new(map), - hash: typst_utils::hash128(data), - }) + Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data))))) } - fn has(&self, key: impl Into) -> bool { - self.map.contains_key(&key.into()) + fn has(&self, key: Label) -> bool { + self.0.contains_key(&key) } - fn iter(&self) -> impl Iterator { - self.map.iter().map(|(&k, v)| (k, v)) + fn get(&self, key: Label) -> Option<&hayagriva::Entry> { + self.0.get(&key) + } + + fn iter(&self) -> impl Iterator { + self.0.iter().map(|(&k, v)| (k, v)) } } impl Debug for Bibliography { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_set().entries(self.map.keys()).finish() + f.debug_set().entries(self.0.keys()).finish() } } -impl Hash for Bibliography { - fn hash(&self, state: &mut H) { - self.hash.hash(state); +/// Decode on library from one data source. +fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { + let src = data.as_str().map_err(FileError::from)?; + + if let DataSource::Path(path) = source { + // If we got a path, use the extension to determine whether it is + // YAML or BibLaTeX. + let ext = Path::new(path.as_str()) + .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)"), + } + } 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) { + 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, + }; + + // If neither decoded correctly, check whether `:` or `{` appears + // more often to guess whether it's more likely to be YAML or BibLaTeX + // and emit the more appropriate error. + let mut yaml = 0; + let mut biblatex = 0; + for c in src.chars() { + match c { + ':' => yaml += 1, + '{' => biblatex += 1, + _ => {} + } + } + + if yaml > biblatex { + bail!("failed to parse YAML ({haya_err})") + } else { + Err(format_biblatex_error(src, None, bib_errs)) + } } } /// Format a BibLaTeX loading error. -fn format_biblatex_error(path: &str, src: &str, errors: Vec) -> EcoString { +fn format_biblatex_error( + src: &str, + path: Option<&str>, + errors: Vec, +) -> EcoString { let Some(error) = errors.first() else { - return eco_format!("failed to parse BibLaTeX file ({path})"); + return match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path})"), + None => eco_format!("failed to parse BibLaTeX"), + }; }; let (span, 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(); - eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") + match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})"), + None => eco_format!("failed to parse BibLaTeX ({line}: {msg})"), + } } /// A loaded CSL style. -#[ty(cast)] #[derive(Debug, Clone, PartialEq, Hash)] -pub struct CslStyle { - name: Option, - style: Arc>, -} +pub struct CslStyle(Arc>); impl CslStyle { - /// Parse the style argument. - pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult> { - let Some(Spanned { v: string, span }) = - args.named::>("style")? - else { - return Ok(None); - }; - - Ok(Some(Self::parse_impl(engine, &string, span).at(span)?)) - } - - /// Parse the style argument with `Smart`. - pub fn parse_smart( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult>> { - let Some(Spanned { v: smart, span }) = - args.named::>>("style")? - else { - return Ok(None); - }; - - Ok(Some(match smart { - Smart::Auto => Smart::Auto, - Smart::Custom(string) => { - Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?) + /// Load a CSL style from a data source. + pub fn load( + world: Tracked, + Spanned { v: source, span }: Spanned, + ) -> SourceResult> { + 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)? } - })) - } - - /// Parse internally. - fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult { - let ext = Path::new(string) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - if ext == "csl" { - let id = span.resolve_path(string)?; - let data = engine.world.file(id)?; - CslStyle::from_data(&data) - } else { - CslStyle::from_name(string) - } + }; + Ok(Derived::new(source, style)) } /// Load a built-in CSL style. #[comemo::memoize] - pub fn from_name(name: &str) -> StrResult { - match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) { - Some(citationberg::Style::Independent(style)) => Ok(Self { - name: Some(name.into()), - style: Arc::new(LazyHash::new(style)), - }), - _ => bail!("unknown style: `{name}`"), + pub fn from_archived(archived: ArchivedStyle) -> CslStyle { + match archived.get() { + citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), archived)), + ))), + // Ensured by `test_bibliography_load_builtin_styles`. + _ => unreachable!("archive should not contain dependant styles"), } } /// Load a CSL style from file contents. #[comemo::memoize] - pub fn from_data(data: &Bytes) -> StrResult { - let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; + pub fn from_data(data: Bytes) -> StrResult { + let text = data.as_str().map_err(FileError::from)?; citationberg::IndependentStyle::from_xml(text) - .map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) }) + .map(|style| { + Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), data)), + ))) + }) .map_err(|err| eco_format!("failed to load CSL style ({err})")) } /// Get the underlying independent style. pub fn get(&self) -> &citationberg::IndependentStyle { - self.style.as_ref() + self.0.as_ref() } } -// This Reflect impl is technically a bit wrong because it doesn't say what -// FromValue and IntoValue really do. Instead, it says what the `style` argument -// on `bibliography` and `cite` expect (through manual parsing). -impl Reflect for CslStyle { +/// Source for a CSL style. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CslSource { + /// A predefined named style. + Named(ArchivedStyle), + /// A normal data source. + Normal(DataSource), +} + +impl Reflect for CslSource { #[comemo::memoize] fn input() -> CastInfo { - let ty = std::iter::once(CastInfo::Type(Type::of::())); - let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| { + let source = std::iter::once(DataSource::input()); + let names = ArchivedStyle::all().iter().map(|name| { CastInfo::Value(name.names()[0].into_value(), name.display_name()) }); - CastInfo::Union(ty.chain(options).collect()) + CastInfo::Union(source.into_iter().chain(names).collect()) } fn output() -> CastInfo { - EcoString::output() + DataSource::output() } fn castable(value: &Value) -> bool { - if let Value::Dyn(dynamic) = &value { - if dynamic.is::() { - return true; - } - } - - false + DataSource::castable(value) } } -impl FromValue for CslStyle { +impl FromValue for CslSource { fn from_value(value: Value) -> HintedStrResult { - if let Value::Dyn(dynamic) = &value { - if let Some(concrete) = dynamic.downcast::() { - return Ok(concrete.clone()); + if EcoString::castable(&value) { + let string = EcoString::from_value(value.clone())?; + if Path::new(string.as_str()).extension().is_none() { + let style = ArchivedStyle::by_name(&string) + .ok_or_else(|| eco_format!("unknown style: {}", string))?; + return Ok(CslSource::Named(style)); } } - Err(::error(&value)) + DataSource::from_value(value).map(CslSource::Normal) } } -impl IntoValue for CslStyle { +impl IntoValue for CslSource { fn into_value(self) -> Value { - Value::dynamic(self) - } -} - -impl Repr for CslStyle { - fn repr(&self) -> EcoString { - self.name - .as_ref() - .map(|name| name.repr()) - .unwrap_or_else(|| "..".into()) + match self { + // We prefer the shorter names which are at the back of the array. + Self::Named(v) => v.names().last().unwrap().into_value(), + Self::Normal(v) => v.into_value(), + } } } @@ -632,9 +630,8 @@ impl<'a> Generator<'a> { static LOCALES: LazyLock> = LazyLock::new(hayagriva::archive::locales); - let database = self.bibliography.bibliography(); - let bibliography_style = self.bibliography.style(StyleChain::default()); - let styles = Arena::new(); + let database = &self.bibliography.sources.derived; + let bibliography_style = &self.bibliography.style(StyleChain::default()).derived; // Process all citation groups. let mut driver = BibliographyDriver::new(); @@ -654,7 +651,7 @@ impl<'a> Generator<'a> { // Create infos and items for each child in the group. for child in children { let key = *child.key(); - let Some(entry) = database.map.get(&key.into_inner()) else { + let Some(entry) = database.get(key) else { errors.push(error!( child.span(), "key `{}` does not exist in the bibliography", @@ -695,8 +692,8 @@ impl<'a> Generator<'a> { } let style = match first.style(StyleChain::default()) { - Smart::Auto => &bibliography_style.style, - Smart::Custom(style) => styles.alloc(style.style), + Smart::Auto => bibliography_style.get(), + Smart::Custom(style) => style.derived.get(), }; self.infos.push(GroupInfo { @@ -727,7 +724,7 @@ impl<'a> Generator<'a> { // Add hidden items for everything if we should print the whole // bibliography. if self.bibliography.full(StyleChain::default()) { - for entry in database.map.values() { + for (_, entry) in database.iter() { driver.citation(CitationRequest::new( vec![CitationItem::new(entry, None, None, true, None)], bibliography_style.get(), @@ -1097,3 +1094,15 @@ fn locale(lang: Lang, region: Option) -> citationberg::LocaleCode { } citationberg::LocaleCode(value) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bibliography_load_builtin_styles() { + for &archived in ArchivedStyle::all() { + let _ = CslStyle::from_archived(archived); + } + } +} diff --git a/crates/typst-library/src/model/cite.rs b/crates/typst-library/src/model/cite.rs index ac0cfa790..29497993d 100644 --- a/crates/typst-library/src/model/cite.rs +++ b/crates/typst-library/src/model/cite.rs @@ -1,11 +1,14 @@ +use typst_syntax::Spanned; + use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Cast, Content, Label, Packed, Show, Smart, StyleChain, Synthesize, + cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain, + Synthesize, }; use crate::introspection::Locatable; use crate::model::bibliography::Works; -use crate::model::CslStyle; +use crate::model::{CslSource, CslStyle}; use crate::text::{Lang, Region, TextElem}; /// Cite a work from the bibliography. @@ -87,15 +90,24 @@ pub struct CiteElem { /// The citation style. /// - /// Should be either `{auto}`, one of the built-in styles (see below) or a - /// path to a [CSL file](https://citationstyles.org/). Some of the styles - /// listed below appear twice, once with their full name and once with a - /// short alias. - /// - /// When set to `{auto}`, automatically use the - /// [bibliography's style]($bibliography.style) for the citations. - #[parse(CslStyle::parse_smart(engine, args)?)] - pub style: Smart, + /// This can be: + /// - `{auto}` to automatically use the + /// [bibliography's style]($bibliography.style) for citations. + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>>("style")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom( + CslStyle::load(engine.world, Spanned::new(source, span))? + )), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub style: Smart>, /// The text language setting where the citation is. #[internal] diff --git a/crates/typst-library/src/model/document.rs b/crates/typst-library/src/model/document.rs index 5124b2487..1bce6b357 100644 --- a/crates/typst-library/src/model/document.rs +++ b/crates/typst-library/src/model/document.rs @@ -3,8 +3,8 @@ use ecow::EcoString; use crate::diag::{bail, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain, - Styles, Value, + cast, elem, Args, Array, Construct, Content, Datetime, Fields, OneOrMultiple, Smart, + StyleChain, Styles, Value, }; /// The root element of a document and its metadata. @@ -35,7 +35,7 @@ pub struct DocumentElem { /// The document's authors. #[ghost] - pub author: Author, + pub author: OneOrMultiple, /// The document's description. #[ghost] @@ -43,7 +43,7 @@ pub struct DocumentElem { /// The document's keywords. #[ghost] - pub keywords: Keywords, + pub keywords: OneOrMultiple, /// The document's creation date. /// @@ -93,7 +93,7 @@ cast! { pub struct DocumentInfo { /// The document's title. pub title: Option, - /// The document's author. + /// The document's author(s). pub author: Vec, /// The document's description. pub description: Option, diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index db4986225..f9ca3ca09 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -1,13 +1,10 @@ use ecow::EcoString; -use typst_syntax::{Span, Spanned}; +use typst_syntax::Spanned; -use crate::diag::{At, SourceResult, StrResult}; +use crate::diag::{At, SourceResult}; use crate::engine::Engine; -use crate::foundations::{ - elem, func, scope, Cast, Content, NativeElement, Packed, Show, StyleChain, -}; +use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; use crate::introspection::Locatable; -use crate::loading::Readable; use crate::World; /// A file that will be embedded into the output PDF. @@ -33,33 +30,40 @@ use crate::World; /// - This element is ignored if exporting to a format other than PDF. /// - File embeddings are not currently supported for PDF/A-2, even if the /// embedded file conforms to PDF/A-1 or PDF/A-2. -#[elem(scope, Show, Locatable)] +#[elem(Show, Locatable)] pub struct EmbedElem { - /// Path to a file to be embedded. + /// Path of the file to be embedded. /// - /// For more details, see the [Paths section]($syntax/#paths). + /// Must always be specified, but is only read from if no data is provided + /// in the following argument. + /// + /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] #[parse( let Spanned { v: path, span } = - args.expect::>("path to the file to be embedded")?; + args.expect::>("path")?; let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - path + // The derived part is the project-relative resolved path. + let resolved = id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into(); + Derived::new(path.clone(), resolved) )] #[borrowed] - pub path: EcoString, + pub path: Derived, - /// The resolved project-relative path. - #[internal] + /// Raw file data, optionally. + /// + /// If omitted, the data is read from the specified path. + #[positional] + // Not actually required as an argument, but always present as a field. + // We can't distinguish between the two at the moment. #[required] - #[parse(id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into())] - pub resolved_path: EcoString, - - /// The raw file data. - #[internal] - #[required] - #[parse(Readable::Bytes(data))] - pub data: Readable, + #[parse( + match args.find::()? { + Some(data) => data, + None => engine.world.file(id).at(span)?, + } + )] + pub data: Bytes, /// The relationship of the embedded file to the document. /// @@ -75,42 +79,6 @@ pub struct EmbedElem { pub description: Option, } -#[scope] -impl EmbedElem { - /// Decode a file embedding from bytes or a string. - #[func(title = "Embed Data")] - fn decode( - /// The call span of this function. - span: Span, - /// The path that will be written into the PDF. Typst will not read from - /// this path since the data is provided in the following argument. - path: EcoString, - /// The data to embed as a file. - data: Readable, - /// The relationship of the embedded file to the document. - #[named] - relationship: Option>, - /// The MIME type of the embedded file. - #[named] - mime_type: Option>, - /// A description for the embedded file. - #[named] - description: Option>, - ) -> StrResult { - let mut elem = EmbedElem::new(path.clone(), path, data); - if let Some(description) = description { - elem.push_description(description); - } - if let Some(mime_type) = mime_type { - elem.push_mime_type(mime_type); - } - if let Some(relationship) = relationship { - elem.push_relationship(relationship); - } - Ok(elem.pack().spanned(span)) - } -} - impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { Ok(Content::empty()) diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 10a7cfee1..cd718d2a1 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -1,23 +1,25 @@ use std::cell::LazyCell; -use std::hash::Hash; use std::ops::Range; use std::sync::{Arc, LazyLock}; +use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; -use syntect::highlighting::{self as synt, Theme}; +use syntect::highlighting as synt; use syntect::parsing::{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, HintedStrResult, SourceResult, StrResult}; +use crate::diag::{At, FileError, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, - PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, Value, + cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, + PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; +use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; use crate::text::{ FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, @@ -25,12 +27,6 @@ use crate::text::{ use crate::visualize::Color; use crate::World; -// Shorthand for highlighter closures. -type StyleFn<'a> = - &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; -type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); -type ThemeArgType = Smart>; - /// Raw text with optional syntax highlighting. /// /// Displays the text verbatim and in a monospace font. This is typically used @@ -186,9 +182,15 @@ pub struct RawElem { #[default(HAlignment::Start)] pub align: HAlignment, - /// One or multiple additional syntax definitions to load. The syntax - /// definitions should be in the - /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// Additional syntax definitions to load. The syntax definitions should be + /// in the [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// + /// You can pass any of the following values: + /// + /// - A path string to load a syntax file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the syntax should be decoded. + /// - An array where each item is one the above. /// /// ````example /// #set raw(syntaxes: "SExpressions.sublime-syntax") @@ -201,22 +203,24 @@ pub struct RawElem { /// (* x (factorial (- x 1))))) /// ``` /// ```` - #[parse( - let (syntaxes, syntaxes_data) = parse_syntaxes(engine, args)?; - syntaxes - )] + #[parse(match args.named("syntaxes")? { + Some(sources) => Some(RawSyntax::load(engine.world, sources)?), + None => None, + })] #[fold] - pub syntaxes: SyntaxPaths, + pub syntaxes: Derived, Vec>, - /// The raw file buffers of syntax definition files. - #[internal] - #[parse(syntaxes_data)] - #[fold] - pub syntaxes_data: Vec, - - /// The theme to use for syntax highlighting. Theme files should be in the + /// The theme to use for syntax highlighting. Themes should be in the /// [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). /// + /// You can pass any of the following values: + /// + /// - `{none}`: Disables syntax highlighting. + /// - `{auto}`: Highlights with Typst's default theme. + /// - A path string to load a theme file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the theme should be decoded. + /// /// Applying a theme only affects the color of specifically highlighted /// text. It does not consider the theme's foreground and background /// properties, so that you retain control over the color of raw text. You @@ -224,8 +228,6 @@ pub struct RawElem { /// the background with a [filled block]($block.fill). You could also use /// the [`xml`] function to extract these properties from the theme. /// - /// Additionally, you can set the theme to `{none}` to disable highlighting. - /// /// ````example /// #set raw(theme: "halcyon.tmTheme") /// #show raw: it => block( @@ -240,18 +242,16 @@ pub struct RawElem { /// #let hi = "Hello World" /// ``` /// ```` - #[parse( - let (theme_path, theme_data) = parse_theme(engine, args)?; - theme_path - )] + #[parse(match args.named::>>>("theme")? { + Some(Spanned { v: Smart::Custom(Some(source)), span }) => Some(Smart::Custom( + Some(RawTheme::load(engine.world, Spanned::new(source, span))?) + )), + Some(Spanned { v: Smart::Custom(None), .. }) => Some(Smart::Custom(None)), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] #[borrowed] - pub theme: ThemeArgType, - - /// The raw file buffer of syntax theme file. - #[internal] - #[parse(theme_data.map(Some))] - #[borrowed] - pub theme_data: Option, + pub theme: Smart>>, /// The size for a tab stop in spaces. A tab is replaced with enough spaces to /// align with the next multiple of the size. @@ -325,9 +325,6 @@ impl Packed { .map(|s| s.to_lowercase()) .or(Some("txt".into())); - let extra_syntaxes = LazyCell::new(|| { - load_syntaxes(&elem.syntaxes(styles), &elem.syntaxes_data(styles)).unwrap() - }); let non_highlighted_result = |lines: EcoVec<(EcoString, Span)>| { lines.into_iter().enumerate().map(|(i, (line, line_span))| { Packed::new(RawLine::new( @@ -340,17 +337,13 @@ impl Packed { }) }; - let theme = elem.theme(styles).as_ref().as_ref().map(|theme_path| { - theme_path.as_ref().map(|path| { - load_theme(path, elem.theme_data(styles).as_ref().as_ref().unwrap()) - .unwrap() - }) - }); - let theme: &Theme = match theme { + let syntaxes = LazyCell::new(|| elem.syntaxes(styles)); + let theme: &synt::Theme = match elem.theme(styles) { Smart::Auto => &RAW_THEME, - Smart::Custom(Some(ref theme)) => theme, + Smart::Custom(Some(theme)) => theme.derived.get(), Smart::Custom(None) => return non_highlighted_result(lines).collect(), }; + let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK); let mut seq = vec![]; @@ -391,13 +384,14 @@ impl Packed { ) .highlight(); } else if let Some((syntax_set, syntax)) = lang.and_then(|token| { - RAW_SYNTAXES - .find_syntax_by_token(&token) - .map(|syntax| (&*RAW_SYNTAXES, syntax)) - .or_else(|| { - extra_syntaxes - .find_syntax_by_token(&token) - .map(|syntax| (&**extra_syntaxes, syntax)) + // Prefer user-provided syntaxes over built-in ones. + syntaxes + .derived + .iter() + .map(|syntax| syntax.get()) + .chain(std::iter::once(&*RAW_SYNTAXES)) + .find_map(|set| { + set.find_syntax_by_token(&token).map(|syntax| (set, syntax)) }) }) { let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme); @@ -532,6 +526,89 @@ cast! { v: EcoString => Self::Text(v), } +/// A loaded syntax. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawSyntax(Arc>); + +impl RawSyntax { + /// Load syntaxes from sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Vec>> { + let data = sources.load(world)?; + let list = sources + .v + .0 + .iter() + .zip(&data) + .map(|(source, data)| Self::decode(source, data)) + .collect::>() + .at(sources.span)?; + 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})") + } + }, + )?; + + let mut builder = SyntaxSetBuilder::new(); + builder.add(syntax); + + Ok(RawSyntax(Arc::new(ManuallyHash::new( + builder.build(), + typst_utils::hash128(data), + )))) + } + + /// Return the underlying syntax set. + fn get(&self) -> &SyntaxSet { + self.0.as_ref() + } +} + +/// A loaded syntect theme. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawTheme(Arc>); + +impl RawTheme { + /// Load a theme from a data source. + fn load( + world: Tracked, + source: Spanned, + ) -> SourceResult> { + let data = source.load(world)?; + let theme = Self::decode(&data).at(source.span)?; + 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))))) + } + + /// Get the underlying syntect theme. + pub fn get(&self) -> &synt::Theme { + self.0.as_ref() + } +} + /// A highlighted line of raw text. /// /// This is a helper element that is synthesized by [`raw`] elements. @@ -593,6 +670,11 @@ struct ThemedHighlighter<'a> { line_fn: LineFn<'a>, } +// Shorthands for highlighter closures. +type StyleFn<'a> = + &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; +type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); + impl<'a> ThemedHighlighter<'a> { pub fn new( code: &'a str, @@ -738,108 +820,50 @@ fn to_syn(color: Color) -> synt::Color { synt::Color { r, g, b, a } } -/// A list of raw syntax file paths. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct SyntaxPaths(Vec); - -cast! { - SyntaxPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - -impl Fold for SyntaxPaths { - fn fold(self, outer: Self) -> Self { - Self(self.0.fold(outer.0)) +/// Create a syntect theme item. +fn item( + scope: &str, + color: Option<&str>, + font_style: Option, +) -> synt::ThemeItem { + synt::ThemeItem { + scope: scope.parse().unwrap(), + style: synt::StyleModifier { + foreground: color.map(|s| to_syn(s.parse::().unwrap())), + background: None, + font_style, + }, } } -/// Load a syntax set from a list of syntax file paths. -#[comemo::memoize] -#[typst_macros::time(name = "load syntaxes")] -fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult> { - let mut out = SyntaxSetBuilder::new(); +/// Replace tabs with spaces to align with multiples of `tab_size`. +fn align_tabs(text: &str, tab_size: usize) -> EcoString { + let replacement = " ".repeat(tab_size); + let divisor = tab_size.max(1); + let amount = text.chars().filter(|&c| c == '\t').count(); - // We might have multiple sublime-syntax/yaml files - for (path, bytes) in paths.0.iter().zip(bytes.iter()) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| { - eco_format!("failed to parse syntax file `{path}` ({err})") - })?); + let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); + let mut column = 0; + + for grapheme in text.graphemes(true) { + match grapheme { + "\t" => { + let required = tab_size - column % divisor; + res.push_str(&replacement[..required]); + column += required; + } + "\n" => { + res.push_str(grapheme); + column = 0; + } + _ => { + res.push_str(grapheme); + column += 1; + } + } } - Ok(Arc::new(out.build())) -} - -/// Function to parse the syntaxes argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_syntaxes( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option>)> { - let Some(Spanned { v: paths, span }) = - args.named::>("syntaxes")? - else { - return Ok((None, None)); - }; - - // Load syntax files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Check that parsing works. - let _ = load_syntaxes(&paths, &data).at(span)?; - - Ok((Some(paths), Some(data))) -} - -#[comemo::memoize] -#[typst_macros::time(name = "load theme")] -fn load_theme(path: &str, bytes: &Bytes) -> StrResult> { - let mut cursor = std::io::Cursor::new(bytes.as_slice()); - - synt::ThemeSet::load_from_reader(&mut cursor) - .map(Arc::new) - .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})")) -} - -/// Function to parse the theme argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_theme( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option)> { - let Some(Spanned { v: path, span }) = args.named::>("theme")? - else { - // Argument `theme` not found. - return Ok((None, None)); - }; - - let Smart::Custom(path) = path else { - // Argument `theme` is `auto`. - return Ok((Some(Smart::Auto), None)); - }; - - let Some(path) = path else { - // Argument `theme` is `none`. - return Ok((Some(Smart::Custom(None)), None)); - }; - - // Load theme file. - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - - // Check that parsing works. - let _ = load_theme(&path, &data).at(span)?; - - Ok((Some(Smart::Custom(Some(path))), Some(data))) + res } /// The syntect syntax definitions. @@ -886,49 +910,3 @@ pub static RAW_THEME: LazyLock = LazyLock::new(|| synt::Theme { item("markup.deleted, meta.diff.header.from-file", Some("#d73a49"), None), ], }); - -/// Create a syntect theme item. -fn item( - scope: &str, - color: Option<&str>, - font_style: Option, -) -> synt::ThemeItem { - synt::ThemeItem { - scope: scope.parse().unwrap(), - style: synt::StyleModifier { - foreground: color.map(|s| to_syn(s.parse::().unwrap())), - background: None, - font_style, - }, - } -} - -/// Replace tabs with spaces to align with multiples of `tab_size`. -fn align_tabs(text: &str, tab_size: usize) -> EcoString { - let replacement = " ".repeat(tab_size); - let divisor = tab_size.max(1); - let amount = text.chars().filter(|&c| c == '\t').count(); - - let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); - let mut column = 0; - - for grapheme in text.graphemes(true) { - match grapheme { - "\t" => { - let required = tab_size - column % divisor; - res.push_str(&replacement[..required]); - column += required; - } - "\n" => { - res.push_str(grapheme); - column = 0; - } - _ => { - res.push_str(grapheme); - column += 1; - } - } - } - - res -} diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 452bb65c1..0f0602011 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -14,14 +14,14 @@ use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::{At, SourceResult, StrResult}; +use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart, - StyleChain, + cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show, + Smart, StyleChain, }; use crate::layout::{BlockElem, Length, Rel, Sizing}; -use crate::loading::Readable; +use crate::loading::{DataSource, Load, Readable}; use crate::model::Figurable; use crate::text::LocalName; use crate::World; @@ -46,25 +46,16 @@ use crate::World; /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { - /// Path to an image file. + /// A path to an image file or raw bytes making up an encoded image. /// - /// For more details, see the [Paths section]($syntax/#paths). + /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] #[parse( - let Spanned { v: path, span } = - args.expect::>("path to image file")?; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - path + let source = args.expect::>("source")?; + let data = source.load(engine.world)?; + Derived::new(source.v, data) )] - #[borrowed] - pub path: EcoString, - - /// The raw file data. - #[internal] - #[required] - #[parse(Readable::Bytes(data))] - pub data: Readable, + pub source: Derived, /// The image's format. Detected automatically by default. /// @@ -106,6 +97,9 @@ pub struct ImageElem { impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. /// + /// This function is deprecated. The [`image`] function now accepts bytes + /// directly. + /// /// ```example /// #let original = read("diagram.svg") /// #let changed = original.replace( @@ -138,7 +132,9 @@ impl ImageElem { #[named] fit: Option, ) -> StrResult { - let mut elem = ImageElem::new(EcoString::new(), data); + let bytes = data.into_bytes(); + let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); + let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); } @@ -337,6 +333,22 @@ pub enum ImageFormat { Vector(VectorFormat), } +impl ImageFormat { + /// Try to detect the format of an image from data. + pub fn detect(data: &[u8]) -> Option { + if let Some(format) = RasterFormat::detect(data) { + return Some(Self::Raster(format)); + } + + // SVG or compressed SVG. + if data.starts_with(b"().unwrap(); - if embed.resolved_path.len() > Str::PDFA_LIMIT { + 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.resolved_path.clone(), id).is_some() { + if embedded_files.insert(embed.path.derived.clone(), id).is_some() { bail!( elem.span(), - "duplicate embedded file for path `{}`", embed.resolved_path; + "duplicate embedded file for path `{}`", embed.path.derived; hint: "embedded file paths must be unique", ); } @@ -92,8 +92,8 @@ fn embed_file( embedded_file.finish(); let mut file_spec = chunk.file_spec(file_spec_dict_ref); - file_spec.path(Str(embed.resolved_path.as_bytes())); - file_spec.unic_file(TextStr(&embed.resolved_path)); + file_spec.path(Str(embed.path.derived.as_bytes())); + file_spec.unic_file(TextStr(&embed.path.derived)); file_spec .insert(Name(b"EF")) .dict() diff --git a/crates/typst-utils/src/hash.rs b/crates/typst-utils/src/hash.rs index 3dbadbe20..9687da20b 100644 --- a/crates/typst-utils/src/hash.rs +++ b/crates/typst-utils/src/hash.rs @@ -162,3 +162,74 @@ impl Debug for LazyHash { self.value.fmt(f) } } + +/// A wrapper type with a manually computed hash. +/// +/// This can be used to turn an unhashable type into a hashable one where the +/// hash is provided manually. Typically, the hash is derived from the data +/// which was used to construct to the unhashable type. +/// +/// For instance, you could hash the bytes that were parsed into an unhashable +/// data structure. +/// +/// # Equality +/// Because Typst uses high-quality 128 bit hashes in all places, the risk of a +/// hash collision is reduced to an absolute minimum. Therefore, this type +/// additionally provides `PartialEq` and `Eq` implementations that compare by +/// hash instead of by value. For this to be correct, your hash implementation +/// **must feed all information relevant to the `PartialEq` impl to the +/// hasher.** +#[derive(Clone)] +pub struct ManuallyHash { + /// A manually computed hash. + hash: u128, + /// The underlying value. + value: T, +} + +impl ManuallyHash { + /// Wraps an item with a pre-computed hash. + /// + /// The hash should be computed with `typst_utils::hash128`. + #[inline] + pub fn new(value: T, hash: u128) -> Self { + Self { hash, value } + } + + /// Returns the wrapped value. + #[inline] + pub fn into_inner(self) -> T { + self.value + } +} + +impl Hash for ManuallyHash { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u128(self.hash); + } +} + +impl Eq for ManuallyHash {} + +impl PartialEq for ManuallyHash { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Deref for ManuallyHash { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Debug for ManuallyHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 61703250a..d392e4093 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -15,7 +15,7 @@ mod scalar; pub use self::bitset::{BitSet, SmallBitSet}; pub use self::deferred::Deferred; pub use self::duration::format_duration; -pub use self::hash::LazyHash; +pub use self::hash::{LazyHash, ManuallyHash}; pub use self::pico::{PicoStr, ResolvedPicoStr}; pub use self::round::{round_int_with_precision, round_with_precision}; pub use self::scalar::Scalar; diff --git a/tests/suite/pdf/embed.typ b/tests/suite/pdf/embed.typ index bb5c9316c..83f006d63 100644 --- a/tests/suite/pdf/embed.typ +++ b/tests/suite/pdf/embed.typ @@ -10,6 +10,16 @@ description: "Information about a secret project", ) +--- pdf-embed-bytes --- +#pdf.embed("hello.txt", read("/assets/text/hello.txt", encoding: none)) +#pdf.embed( + "a_file_name.txt", + read("/assets/text/hello.txt", encoding: none), + relationship: "supplement", + mime-type: "text/plain", + description: "A description", +) + --- pdf-embed-invalid-relationship --- #pdf.embed( "/assets/text/hello.txt", @@ -18,13 +28,3 @@ mime-type: "text/plain", description: "A test file", ) - ---- pdf-embed-decode --- -#pdf.embed.decode("hello.txt", read("/assets/text/hello.txt")) -#pdf.embed.decode( - "a_file_name.txt", - read("/assets/text/hello.txt"), - relationship: "supplement", - mime-type: "text/plain", - description: "A description", -) From be6629c7cbd00b06beab2b1477c4270859906cb2 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 9 Jan 2025 10:49:06 +0000 Subject: [PATCH 15/44] Better math argument parsing (#5008) --- crates/typst-eval/src/call.rs | 3 +- crates/typst-library/src/math/mod.rs | 6 +- crates/typst-syntax/src/lexer.rs | 44 +++++ crates/typst-syntax/src/parser.rs | 171 ++++++++++-------- crates/typst-syntax/src/set.rs | 4 + tests/ref/math-call-named-args.png | Bin 0 -> 526 bytes .../ref/math-call-spread-shorthand-clash.png | Bin 0 -> 119 bytes tests/ref/math-mat-gaps.png | Bin 489 -> 1309 bytes tests/ref/math-mat-spread-1d.png | Bin 0 -> 1017 bytes tests/ref/math-mat-spread-2d.png | Bin 0 -> 3391 bytes tests/ref/math-mat-spread.png | Bin 0 -> 1814 bytes tests/suite/math/call.typ | 134 ++++++++++++++ tests/suite/math/mat.typ | 26 +++ 13 files changed, 308 insertions(+), 80 deletions(-) create mode 100644 tests/ref/math-call-named-args.png create mode 100644 tests/ref/math-call-spread-shorthand-clash.png create mode 100644 tests/ref/math-mat-spread-1d.png create mode 100644 tests/ref/math-mat-spread-2d.png create mode 100644 tests/ref/math-mat-spread.png diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index fc934cef5..0a9e1c486 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -685,8 +685,7 @@ mod tests { // Named-params. test(s, "$ foo(bar: y) $", &["foo"]); - // This should be updated when we improve named-param parsing: - test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]); + test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]); // Field access in math. test(s, "$ foo.bar $", &["foo"]); diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 5a83c854f..3b4b133d9 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -82,8 +82,9 @@ use crate::text::TextElem; /// - Within them, Typst is still in "math mode". Thus, you can write math /// directly into them, but need to use hash syntax to pass code expressions /// (except for strings, which are available in the math syntax). -/// - They support positional and named arguments, but don't support trailing -/// content blocks and argument spreading. +/// - They support positional and named arguments, as well as argument +/// spreading. +/// - They don't support trailing content blocks. /// - They provide additional syntax for 2-dimensional argument lists. The /// semicolon (`;`) merges preceding arguments separated by commas into an /// array argument. @@ -92,6 +93,7 @@ use crate::text::TextElem; /// $ frac(a^2, 2) $ /// $ vec(1, 2, delim: "[") $ /// $ mat(1, 2; 3, 4) $ +/// $ mat(..#range(1, 5).chunks(2)) $ /// $ lim_x = /// op("lim", limits: #true)_x $ /// ``` diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index b0cb5c464..6b5d28162 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -616,6 +616,11 @@ impl Lexer<'_> { '~' if self.s.eat_if('>') => SyntaxKind::MathShorthand, '*' | '-' | '~' => SyntaxKind::MathShorthand, + '.' => SyntaxKind::Dot, + ',' => SyntaxKind::Comma, + ';' => SyntaxKind::Semicolon, + ')' => SyntaxKind::RightParen, + '#' => SyntaxKind::Hash, '_' => SyntaxKind::Underscore, '$' => SyntaxKind::Dollar, @@ -685,6 +690,45 @@ impl Lexer<'_> { } SyntaxKind::Text } + + /// Handle named arguments in math function call. + pub fn maybe_math_named_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if(is_id_start) { + self.s.eat_while(is_id_continue); + // Check that a colon directly follows the identifier, and not the + // `:=` or `::=` math shorthands. + if self.s.at(':') && !self.s.at(":=") && !self.s.at("::=") { + // Check that the identifier is not just `_`. + let node = if self.s.from(start) != "_" { + SyntaxNode::leaf(SyntaxKind::Ident, self.s.from(start)) + } else { + let msg = SyntaxError::new("expected identifier, found underscore"); + SyntaxNode::error(msg, self.s.from(start)) + }; + return Some(node); + } + } + self.s.jump(cursor); + None + } + + /// Handle spread arguments in math function call. + pub fn maybe_math_spread_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if("..") { + // Check that neither a space nor a dot follows the spread syntax. + // A dot would clash with the `...` math shorthand. + if !self.space_or_end() && !self.s.at('.') { + let node = SyntaxNode::leaf(SyntaxKind::Dots, self.s.from(start)); + return Some(node); + } + } + self.s.jump(cursor); + None + } } /// Code. diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 6c1778c4a..335b8f1a2 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -217,16 +217,20 @@ fn math(p: &mut Parser, stop_set: SyntaxSet) { p.wrap(m, SyntaxKind::Math); } -/// Parses a sequence of math expressions. -fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) { +/// Parses a sequence of math expressions. Returns the number of expressions +/// parsed. +fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) -> usize { debug_assert!(stop_set.contains(SyntaxKind::End)); + let mut count = 0; while !p.at_set(stop_set) { if p.at_set(set::MATH_EXPR) { math_expr(p); + count += 1; } else { p.unexpected(); } } + count } /// Parses a single math expression: This includes math elements like @@ -254,6 +258,13 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } } + SyntaxKind::Dot + | SyntaxKind::Comma + | SyntaxKind::Semicolon + | SyntaxKind::RightParen => { + p.convert_and_eat(SyntaxKind::Text); + } + SyntaxKind::Text | SyntaxKind::MathShorthand => { continuable = matches!( math_class(p.current_text()), @@ -398,7 +409,13 @@ fn math_delimited(p: &mut Parser) { while !p.at_set(syntax_set!(Dollar, End)) { if math_class(p.current_text()) == Some(MathClass::Closing) { p.wrap(m2, SyntaxKind::Math); - p.eat(); + // We could be at the shorthand `|]`, which shouldn't be converted + // to a `Text` kind. + if p.at(SyntaxKind::RightParen) { + p.convert_and_eat(SyntaxKind::Text); + } else { + p.eat(); + } p.wrap(m, SyntaxKind::MathDelimited); return; } @@ -455,94 +472,90 @@ fn math_args(p: &mut Parser) { let m = p.marker(); p.convert_and_eat(SyntaxKind::LeftParen); - let mut namable = true; - let mut named = None; + let mut positional = true; let mut has_arrays = false; - let mut array = p.marker(); - let mut arg = p.marker(); - // The number of math expressions per argument. - let mut count = 0; - while !p.at_set(syntax_set!(Dollar, End)) { - if namable - && (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text)) - && p.text[p.current_end()..].starts_with(':') - { - p.convert_and_eat(SyntaxKind::Ident); - p.convert_and_eat(SyntaxKind::Colon); - named = Some(arg); - arg = p.marker(); - array = p.marker(); - } + let mut maybe_array_start = p.marker(); + let mut seen = HashSet::new(); + while !p.at_set(syntax_set!(End, Dollar, RightParen)) { + positional = math_arg(p, &mut seen); - match p.current_text() { - ")" => break, - ";" => { - maybe_wrap_in_math(p, arg, count, named); - p.wrap(array, SyntaxKind::Array); - p.convert_and_eat(SyntaxKind::Semicolon); - array = p.marker(); - arg = p.marker(); - count = 0; - namable = true; - named = None; - has_arrays = true; - continue; - } - "," => { - maybe_wrap_in_math(p, arg, count, named); - p.convert_and_eat(SyntaxKind::Comma); - arg = p.marker(); - count = 0; - namable = true; - if named.is_some() { - array = p.marker(); - named = None; + match p.current() { + SyntaxKind::Comma => { + p.eat(); + if !positional { + maybe_array_start = p.marker(); } - continue; } - _ => {} - } + SyntaxKind::Semicolon => { + if !positional { + maybe_array_start = p.marker(); + } - if p.at_set(set::MATH_EXPR) { - math_expr(p); - count += 1; - } else { - p.unexpected(); - } - - namable = false; - } - - if arg != p.marker() { - maybe_wrap_in_math(p, arg, count, named); - if named.is_some() { - array = p.marker(); + // Parses an array: `a, b, c;`. + // The semicolon merges preceding arguments separated by commas + // into an array argument. + p.wrap(maybe_array_start, SyntaxKind::Array); + p.eat(); + maybe_array_start = p.marker(); + has_arrays = true; + } + SyntaxKind::End | SyntaxKind::Dollar | SyntaxKind::RightParen => {} + _ => p.expected("comma or semicolon"), } } - if has_arrays && array != p.marker() { - p.wrap(array, SyntaxKind::Array); - } - - if p.at(SyntaxKind::Text) && p.current_text() == ")" { - p.convert_and_eat(SyntaxKind::RightParen); - } else { - p.expected("closing paren"); - p.balanced = false; + // Check if we need to wrap the preceding arguments in an array. + if maybe_array_start != p.marker() && has_arrays && positional { + p.wrap(maybe_array_start, SyntaxKind::Array); } + p.expect_closing_delimiter(m, SyntaxKind::RightParen); p.wrap(m, SyntaxKind::Args); } -/// Wrap math function arguments to join adjacent math content or create an -/// empty 'Math' node for when we have 0 args. +/// Parses a single argument in a math argument list. /// -/// We don't wrap when `count == 1`, since wrapping would change the type of the -/// expression from potentially non-content to content. Ex: `$ func(#12pt) $` -/// would change the type from size to content if wrapped. -fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: Option) { +/// Returns whether the parsed argument was positional or not. +fn math_arg<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) -> bool { + let m = p.marker(); + let start = p.current_start(); + + if p.at(SyntaxKind::Dot) { + // Parses a spread argument: `..args`. + if let Some(spread) = p.lexer.maybe_math_spread_arg(start) { + p.token.node = spread; + p.eat(); + math_expr(p); + p.wrap(m, SyntaxKind::Spread); + return true; + } + } + + let mut positional = true; + if p.at_set(syntax_set!(Text, MathIdent, Underscore)) { + // Parses a named argument: `thickness: #12pt`. + if let Some(named) = p.lexer.maybe_math_named_arg(start) { + p.token.node = named; + let text = p.current_text(); + p.eat(); + p.convert_and_eat(SyntaxKind::Colon); + if !seen.insert(text) { + p[m].convert_to_error(eco_format!("duplicate argument: {text}")); + } + positional = false; + } + } + + // Parses a normal positional argument. + let arg = p.marker(); + let count = math_exprs(p, syntax_set!(End, Dollar, Comma, Semicolon, RightParen)); if count == 0 { + // Named argument requires a value. + if !positional { + p.expected("expression"); + } + // Flush trivia so that the new empty Math node will be wrapped _inside_ // any `SyntaxKind::Array` elements created in `math_args`. // (And if we don't follow by wrapping in an array, it has no effect.) @@ -553,13 +566,19 @@ fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: OptionlYDU@OZcq8+xeR|OVW@dc;k#Z6&Sg_!P z!wbQ-37jv^by&cSeN6USz>8@v&%QN)TNnMK1Cap?JN!_`f&~i}oNRcbe|W?MPSuw? zE#TEdjEq^p>zOz;Q#F8@^rni+Nw8qSf_DpCsoB8Kvt2gu`6_PUPq}ou3{)ooSlDyV z4O~C@rh6J-Z4LlZxPfzJ27LbQ^;t*orH`#+I{+U-03HrHf@@=U-vRKf1VB&4q~pf@ zZcN~BSLXHAf&~i}oIto11fPK@0{9*T!KxFumFeAk^Cd7k3M^;OT+RAU;Gq(Ivj6~W zk}s~~1U}K=_!s^z@OoF=zz_Uk4}d}e;PlquxEq+L$$x9|J66Yn1q*h$KT)ZH31x;{ Q3jhEB07*qoM6N<$f;J`WvH$=8 literal 0 HcmV?d00001 diff --git a/tests/ref/math-call-spread-shorthand-clash.png b/tests/ref/math-call-spread-shorthand-clash.png new file mode 100644 index 0000000000000000000000000000000000000000..4129ef5d2200ad7666e413e0845a80cd8798cc16 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS_0VEhE<%|3RQg)s$jv*Ddl7HAcG$dYm6xi*q zE4Q@*#9-fgl>h(#s(=6Qitmd4WBcO&%U}PE&3lf0Tyb=FVdQ&MBb@06@$x0ssI2 literal 0 HcmV?d00001 diff --git a/tests/ref/math-mat-gaps.png b/tests/ref/math-mat-gaps.png index 5c954766c7bd01ca2b3a9d3a5959873a35dcbbae..405358776c3c42f9e20a7ac48ae152506cf3dfa1 100644 GIT binary patch delta 1303 zcmV+y1?c+e1Dy(x7k_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%_ N002ovPDHLkV1nF(Wyt^l delta 476 zcmV<20VDpM3h4uo7k@Pf00000ugP${00057NklR@q(inw=kf(WD*#3Iv zQQx8Td;bHkzI}He8()-7!y4AGhCc)DoyWAL!O+02DuUGK41WcDnGGD(p>Cws3FKZO z59jT{)9N3Txu&r%z)6lge3l77EXq8?6mIDGOCQ&qce3EnrJjNgmSBwKyHRM;KXw}{QcU28=mx;svRA8&iW%r@#wK~=V zr2EBT3Jw5#0ZiH`L>|7&CRhQ-E%Gp)c7jwkM<`$>mN9L~t)~bLYgohI8h!&^m0`w3 S3eG%zR|d%uk&H|0z=f;($0H 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 V44U_U8)N_g002ovPDHLkV1kQrigN$} literal 0 HcmV?d00001 diff --git a/tests/ref/math-mat-spread.png b/tests/ref/math-mat-spread.png new file mode 100644 index 0000000000000000000000000000000000000000..dc8b2bf7e6b1ead308d6f3d127edb61bc7cb0c16 GIT binary patch literal 1814 zcmV+x2kH2UP)(>&(DjCi~IZg#>U2deSN;ZzOS#Z{QUgj;NY5?nlm#qDk>_vy1Hg&X4~7_ z$jHcqgoJu}diM7A@$vCPLqla{Wwf-k!otFFadFht)LmU&#l^*wlauc5?&am>Yinyp zMn->sf6dL!pP!$Sl9K1==Z%exVq#+6i?Z;{-TCS8%~6c_=kGEyGW_-UZEbCkkdQ(` zLQqgp;g`CcoSdPdp;lH_-QC^T*x1+C*TXJ#KtMo;hllOI)y-0k>b1^nY;4Fuf%4Mf zg@uKtrlv(jMJy~VT3TATxVWdMr+j>TfPjGT$=l(WyZP(#%Sne*Q&UJtNYGuB+=Z_8 z;Og3cs`S|9>$=hV@b&fE=ilGoxw*OIqQS{Ugzm-J^3&q?$uS4pTGCw?8{4t;+wtSj)sc~r>bB3Rsi~KjmmeP=qN1Xno}OuG zY0%KnS65f_)#Kse;p5}uwY9aCm6hh^=IZL|etv#TOiWQxQJ9#RR8&-CWMm;BA#`+f zv$M0nz`%lnf?;7{$H&Lk*4AojY91aQHa0ddFE3tRUZ|+3s;a8SIebk`O{1fu+S=MW zIy%hE%y4jUq@<)dIXRDykA{YZot>S%y}ilF$s;2pK|w)FOH0|=+3V};DJdy+b#3vQn9hIva+&ZU|?@=Z?3Mc;^N{oG&IZ0%jxOq z?d|RG@bL2T^6&5OI5;@-^YcDFKK1qWSXfwS@00Y`dL_t(|+U?kPP?Ki>$MG+a zkU;{13MxaHDj@DHSUc@*)!M!H-h1!8_uhNjJ+)Qax++efs8E$k!2yzltp3q^?s@J= za^!MHeZqP7e*Svz_r90!-J82N?*)UwVA$)WOd^~qlXw_=s2P$_%(*sr=UvdR)CJZ9 zD?rG~%pPxt#3RtIUm`%CG!TkLQe6P1QnV|`4Gr7+GPM}^%!_93;i$0E*qQ{as6n&z zZ(TrybX|)K+nbafMv}9=`~+9Abee0TU!M1mKVrN z2I}CoXzsC)mkun-p?m9J0n8&RzHdhIFGcun4P-fiiATfv z7t8_AzDP4FD=VRN3`*G{Fz>NPq2U~)ODZd8!buijAW^*eDqz{g<}FA~K7^%yz%dj^ zM=C5QggF~f+K@Wzgb1DtEdG=zY8?Q!{Ga?Fq^w$va3VlXHvdmj8PT*&Kv=b?US4TM zgmIsMX2J15djgI(5UfAc&2IuGO-8r<8=!dv6JAG|5AdTO-73BX%Bv4ZMtc8)CirFn z`mG%Xk;dhsUtI zYs4@7>u;?cAk-~>`6WPY+QUJww-c$o5I}93!_gVxJt`3Ev@RQf&K+$*5Nt)Gkkqz8 z>#g49cW(xvzBmy;ZQ8%%0Bj9=Y^Y6l*dw0M{;;sxG>2RMf!)brVYO)vvmOG3h1I4z zyzZBuw}4>pv$7dboAxlSzDUWIWDB4+t>M37oz}${B~ zpP7g>Zm4H1Ah&qF>;)ju8}HR#A1vwYg>Xu-WZt#{XglgCQ>d1AEkC6|_)yo&W#<07*qoM6N<$ Ef(eM%`v3p{ literal 0 HcmV?d00001 diff --git a/tests/suite/math/call.typ b/tests/suite/math/call.typ index 136be8a74..5caacfac6 100644 --- a/tests/suite/math/call.typ +++ b/tests/suite/math/call.typ @@ -8,6 +8,112 @@ $ pi(a,) $ $ pi(a,b) $ $ pi(a,b,) $ +--- math-call-unclosed-func --- +#let func(x) = x +// Error: 6-7 unclosed delimiter +$func(a$ + +--- math-call-unclosed-non-func --- +// Error: 5-6 unclosed delimiter +$sin(x$ + +--- math-call-named-args --- +#let func1(my: none) = my +#let func2(_my: none) = _my +#let func3(my-body: none) = my-body +#let func4(_my-body: none) = _my-body +#let func5(m: none) = m +$ func1(my: a) $ +$ func2(_my: a) $ +$ func3(my-body: a) $ +$ func4(_my-body: a) $ +$ func5(m: a) $ +$ func5(m: sigma : f) $ +$ func5(m: sigma:pi) $ + +--- math-call-named-args-no-expr --- +#let func(m: none) = m +// Error: 10 expected expression +$ func(m: ) $ + +--- math-call-named-args-duplicate --- +#let func(my: none) = my +// Error: 15-17 duplicate argument: my +$ func(my: a, my: b) $ + +--- math-call-named-args-shorthand-clash-1 --- +#let func(m: none) = m +// Error: 18-21 unexpected argument +$func(m: =) func(m:=)$ + +--- math-call-named-args-shorthand-clash-2 --- +#let func(m: none) = m +// Error: 41-45 unexpected argument +$func(m::) func(m: :=) func(m:: =) func(m::=)$ + +--- math-call-named-single-underscore --- +#let func(x) = x +// Error: 8-9 expected identifier, found underscore +$ func(_: a) $ + +--- math-call-named-single-char-error --- +#let func(m: none) = m +// Error: 8-13 unexpected argument +$ func(m : a) $ + +--- math-call-named-args-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(_a: a)$, "arguments(_a: [a])") +#check($args(_a-b: a)$, "arguments(_a-b: [a])") +#check($args(a-b: a)$, "arguments(a-b: [a])") +#check($args(a-b-c: a)$, "arguments(a-b-c: [a])") +#check($args(a--c: a)$, "arguments(a--c: [a])") +#check($args(a: a-b)$, "arguments(a: sequence([a], [−], [b]))") +#check($args(a-b: a-b)$, "arguments(a-b: sequence([a], [−], [b]))") +#check($args(a-b)$, "arguments(sequence([a], [−], [b]))") + +--- math-call-spread-content-error --- +#let args(..body) = body +// Error: 7-16 cannot spread content +$args(..(a + b))$ + +--- math-call-spread-multiple-exprs --- +#let args(..body) = body +// Error: 10 expected comma or semicolon +$args(..a + b)$ + +--- math-call-spread-unexpected-dots --- +#let args(..body) = body +// Error: 8-10 unexpected dots +$args(#..range(1, 5).chunks(2))$ + +--- math-call-spread-shorthand-clash --- +#let func(body) = body +$func(...)$ + +--- math-call-spread-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(..#range(0, 4).chunks(2))$, "arguments((0, 1), (2, 3))") +#check($#args(range(1, 5).chunks(2))$, "arguments(((1, 2), (3, 4)))") +#check($#args(..range(1, 5).chunks(2))$, "arguments((1, 2), (3, 4))") +#check($args(#(..range(2, 6).chunks(2)))$, "arguments(((2, 3), (4, 5)))") +#let nums = range(0, 4).chunks(2) +#check($args(..nums)$, "arguments((0, 1), (2, 3))") +#check($args(..nums;)$, "arguments(((0, 1), (2, 3)))") +#check($args(..nums, ..nums)$, "arguments((0, 1), (2, 3), (0, 1), (2, 3))") +#check($args(..nums, 4, 5)$, "arguments((0, 1), (2, 3), [4], [5])") +#check($args(..nums, ..#range(4, 6))$, "arguments((0, 1), (2, 3), 4, 5)") +#check($args(..nums, #range(4, 6))$, "arguments((0, 1), (2, 3), (4, 5))") +#check($args(..nums, 1, 2; 3, 4)$, "arguments(((0, 1), (2, 3), [1], [2]), ([3], [4]))") +#check($args(1, 2; ..nums)$, "arguments(([1], [2]), ((0, 1), (2, 3)))") +#check($args(1, 2; 3, 4)$, "arguments(([1], [2]), ([3], [4]))") +#check($args(1, 2; 3, 4; ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4]), (5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7);)$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7),)$, "arguments(([1], [2]), ([3], [4], 5, 6))") + --- math-call-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) @@ -35,6 +141,34 @@ $ mat(#"code"; "wins") $ #check($args(a,b;c)$, "arguments(([a], [b]), ([c],))") #check($args(a,b;c,d;e,f)$, "arguments(([a], [b]), ([c], [d]), ([e], [f]))") +--- math-call-2d-named-repr --- +#let args(..body) = (body.pos(), body.named()) +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a: b)$, "((), (a: [b]))") +#check($args(1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (:))") +#check($args(a: b, 1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, a: b, 2; 3, 4)$, "(([1], ([2],), ([3], [4])), (a: [b]))") +#check($args(1, 2, a: b; 3, 4)$, "(([1], [2], (), ([3], [4])), (a: [b]))") +#check($args(1, 2; a: b, 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, 2; 3, a: b, 4)$, "((([1], [2]), [3], ([4],)), (a: [b]))") +#check($args(1, 2; 3, 4, a: b)$, "((([1], [2]), [3], [4]), (a: [b]))") +#check($args(a: b, 1, 2, 3, c: d)$, "(([1], [2], [3]), (a: [b], c: [d]))") +#check($args(1, 2, 3; a: b)$, "((([1], [2], [3]),), (a: [b]))") +#check($args(a-b: a,, e:f;; d)$, "(([], (), ([],), ([d],)), (a-b: [a], e: [f]))") +#check($args(a: b, ..#range(0, 4))$, "((0, 1, 2, 3), (a: [b]))") + +--- math-call-2d-escape-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a\;b)$, "arguments(sequence([a], [;], [b]))") +#check($args(a\,b;c)$, "arguments((sequence([a], [,], [b]),), ([c],))") +#check($args(b\;c\,d;e)$, "arguments((sequence([b], [;], [c], [,], [d]),), ([e],))") +#check($args(a\: b)$, "arguments(sequence([a], [:], [ ], [b]))") +#check($args(a : b)$, "arguments(sequence([a], [ ], [:], [ ], [b]))") +#check($args(\..a)$, "arguments(sequence([.], [.], [a]))") +#check($args(.. a)$, "arguments(sequence([.], [.], [ ], [a]))") +#check($args(a..b)$, "arguments(sequence([a], [.], [.], [b]))") + --- math-call-2d-repr-structure --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) diff --git a/tests/suite/math/mat.typ b/tests/suite/math/mat.typ index 391ff1677..b7d6a6871 100644 --- a/tests/suite/math/mat.typ +++ b/tests/suite/math/mat.typ @@ -54,6 +54,30 @@ $ a + mat(delim: #none, 1, 2; 3, 4) + b $ $ mat(1, 2; 3, 4; delim: "[") $, ) +--- math-mat-spread --- +// Test argument spreading in matrix. +$ mat(..#range(1, 5).chunks(2)) + mat(#(..range(2).map(_ => range(2)))) $ + +#let nums = ((1,) * 5).intersperse(0).chunks(3) +$ mat(..nums, delim: "[") $ + +--- math-mat-spread-1d --- +$ mat(..#range(1, 5) ; 1, ..#range(2, 5)) + mat(..#range(1, 3), ..#range(3, 5) ; ..#range(1, 4), 4) $ + +--- math-mat-spread-2d --- +#let nums = range(0, 2).map(i => (i, i+1)) +$ mat(..nums, delim: "|",) + mat(..nums; delim: "|",) $ +$ mat(..nums) mat(..nums;) \ + mat(..nums;,) mat(..nums,) $ + +--- math-mat-spread-expected-array-error --- +#let nums = range(0, 2).map(i => (i, i+1)) +// Error: 15-16 expected array, found content +$ mat(..nums, 0, 1) $ + --- math-mat-gap --- #set math.mat(gap: 1em) $ mat(1, 2; 3, 4) $ @@ -61,6 +85,8 @@ $ mat(1, 2; 3, 4) $ --- math-mat-gaps --- #set math.mat(row-gap: 1em, column-gap: 2em) $ mat(1, 2; 3, 4) $ +$ mat(column-gap: #1em, 1, 2; 3, 4) + mat(row-gap: #2em, 1, 2; 3, 4) $ --- math-mat-augment --- // Test matrix line drawing (augmentation). From 9473aface183feaf48601c5264c3604f5798169e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 9 Jan 2025 14:00:18 +0100 Subject: [PATCH 16/44] Fix memory size of `TextElem` (#5674) --- crates/typst-library/src/text/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 25ed009e9..d372c399f 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -555,6 +555,7 @@ pub struct TextElem { /// #lorem(10) /// ``` #[fold] + #[ghost] pub costs: Costs, /// Whether to apply kerning. @@ -1431,3 +1432,13 @@ fn check_font_list(engine: &mut Engine, list: &Spanned) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_text_elem_size() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + } +} From 6b9b78596a6103dfbcadafaeb03eda624da5306a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 10 Jan 2025 17:54:11 +0100 Subject: [PATCH 17/44] Don't generate accessors for required fields (#5680) --- crates/typst-layout/src/inline/collect.rs | 11 +++++----- crates/typst-layout/src/lists.rs | 4 ++-- crates/typst-layout/src/math/accent.rs | 6 +++--- crates/typst-layout/src/math/attach.rs | 12 +++++------ crates/typst-layout/src/math/cancel.rs | 2 +- crates/typst-layout/src/math/frac.rs | 6 +++--- crates/typst-layout/src/math/lr.rs | 9 ++++----- crates/typst-layout/src/math/mat.rs | 8 ++++---- crates/typst-layout/src/math/mod.rs | 13 ++++++------ crates/typst-layout/src/math/root.rs | 3 +-- crates/typst-layout/src/math/stretch.rs | 2 +- crates/typst-layout/src/math/text.rs | 2 +- crates/typst-layout/src/math/underover.rs | 20 +++++++++---------- crates/typst-layout/src/shapes.rs | 6 +++--- crates/typst-layout/src/stack.rs | 6 +++--- crates/typst-layout/src/transforms.rs | 6 +++--- .../src/introspection/counter.rs | 2 +- .../typst-library/src/introspection/state.rs | 2 +- crates/typst-library/src/layout/align.rs | 2 +- crates/typst-library/src/layout/container.rs | 2 +- crates/typst-library/src/layout/grid/mod.rs | 2 +- .../typst-library/src/layout/grid/resolve.rs | 12 +++++------ crates/typst-library/src/layout/hide.rs | 2 +- crates/typst-library/src/layout/layout.rs | 2 +- crates/typst-library/src/math/accent.rs | 2 +- crates/typst-library/src/math/attach.rs | 4 ++-- .../typst-library/src/model/bibliography.rs | 9 ++++----- crates/typst-library/src/model/figure.rs | 17 ++++++++-------- crates/typst-library/src/model/footnote.rs | 15 +++++++------- crates/typst-library/src/model/heading.rs | 4 ++-- crates/typst-library/src/model/link.rs | 7 +++---- crates/typst-library/src/model/outline.rs | 13 ++++++------ crates/typst-library/src/model/quote.rs | 2 +- crates/typst-library/src/model/reference.rs | 14 ++++++------- crates/typst-library/src/model/table.rs | 2 +- crates/typst-library/src/model/terms.rs | 6 +++--- crates/typst-library/src/text/deco.rs | 8 ++++---- crates/typst-library/src/text/mod.rs | 2 +- crates/typst-library/src/text/raw.rs | 8 ++++---- crates/typst-library/src/text/shift.rs | 8 ++++---- crates/typst-library/src/text/smallcaps.rs | 2 +- crates/typst-macros/src/elem.rs | 11 +++++++--- crates/typst-pdf/src/embed.rs | 2 +- crates/typst-pdf/src/outline.rs | 3 +-- 44 files changed, 137 insertions(+), 144 deletions(-) diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 23e82c417..fcf7508e9 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -161,9 +161,9 @@ pub fn collect<'a>( } if let Some(case) = TextElem::case_in(styles) { - full.push_str(&case.apply(elem.text())); + full.push_str(&case.apply(&elem.text)); } else { - full.push_str(elem.text()); + full.push_str(&elem.text); } if dir != outer_dir { @@ -172,13 +172,12 @@ pub fn collect<'a>( } }); } else if let Some(elem) = child.to_packed::() { - let amount = elem.amount(); - if amount.is_zero() { + if elem.amount.is_zero() { continue; } - collector.push_item(match amount { - Spacing::Fr(fr) => Item::Fractional(*fr, None), + collector.push_item(match elem.amount { + Spacing::Fr(fr) => Item::Fractional(fr, None), Spacing::Rel(rel) => Item::Absolute( rel.resolve(styles).relative_to(region.x), elem.weak(styles), diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 9479959b2..63127474b 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -40,7 +40,7 @@ pub fn layout_list( let mut cells = vec![]; let mut locator = locator.split(); - for item in elem.children() { + for item in &elem.children { cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); @@ -101,7 +101,7 @@ pub fn layout_enum( // relation to the item it refers to. let number_align = elem.number_align(styles); - for item in elem.children() { + for item in &elem.children { number = item.number(styles).unwrap_or(number); let context = Context::new(None, Some(styles)); diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 0ebe785f1..951870d68 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -16,7 +16,7 @@ pub fn layout_accent( styles: StyleChain, ) -> SourceResult<()> { let cramped = style_cramped(); - let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; + 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 { @@ -29,8 +29,8 @@ pub fn layout_accent( let width = elem.size(styles).relative_to(base.width()); - let Accent(c) = elem.accent(); - let mut glyph = GlyphFragment::new(ctx, styles, *c, 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); diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 263fc5c6d..8a67d53b3 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -29,7 +29,7 @@ pub fn layout_attach( let elem = merged.as_ref().unwrap_or(elem); let stretch = stretch_size(styles, elem); - let mut base = ctx.layout_into_fragment(elem.base(), styles)?; + let mut base = ctx.layout_into_fragment(&elem.base, styles)?; let sup_style = style_for_superscript(styles); let sup_style_chain = styles.chain(&sup_style); let tl = elem.tl(sup_style_chain); @@ -95,7 +95,7 @@ pub fn layout_primes( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - match *elem.count() { + match elem.count { count @ 1..=4 => { let c = match count { 1 => '′', @@ -134,7 +134,7 @@ pub fn layout_scripts( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(Limits::Never); ctx.push(fragment); Ok(()) @@ -148,7 +148,7 @@ pub fn layout_limits( styles: StyleChain, ) -> SourceResult<()> { let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(limits); ctx.push(fragment); Ok(()) @@ -157,9 +157,9 @@ pub fn layout_limits( /// Get the size to stretch the base to. fn stretch_size(styles: StyleChain, elem: &Packed) -> Option> { // Extract from an EquationElem. - let mut base = elem.base(); + let mut base = &elem.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } base.to_packed::().map(|stretch| stretch.size(styles)) diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs index 716832fbf..9826397fa 100644 --- a/crates/typst-layout/src/math/cancel.rs +++ b/crates/typst-layout/src/math/cancel.rs @@ -16,7 +16,7 @@ pub fn layout_cancel( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let body = ctx.layout_into_fragment(elem.body(), styles)?; + let body = ctx.layout_into_fragment(&elem.body, styles)?; // Preserve properties of body. let body_class = body.class(); diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index fdc3be172..63463d761 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -23,8 +23,8 @@ pub fn layout_frac( layout_frac_like( ctx, styles, - elem.num(), - std::slice::from_ref(elem.denom()), + &elem.num, + std::slice::from_ref(&elem.denom), false, elem.span(), ) @@ -37,7 +37,7 @@ pub fn layout_binom( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) + layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span()) } /// Layout a fraction or binomial. diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index 2f4556fe5..19176ee88 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -13,17 +13,16 @@ pub fn layout_lr( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut body = elem.body(); - // Extract from an EquationElem. + let mut body = &elem.body; if let Some(equation) = body.to_packed::() { - body = equation.body(); + body = &equation.body; } // Extract implicit LrElem. if let Some(lr) = body.to_packed::() { if lr.size(styles).is_one() { - body = lr.body(); + body = &lr.body; } } @@ -100,7 +99,7 @@ pub fn layout_mid( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?; + let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?; for fragment in &mut fragments { match fragment { diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index d28bb037d..bf4929026 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -27,7 +27,7 @@ pub fn layout_vec( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, elem.align(styles), elem.gap(styles), LeftRightAlternator::Right, @@ -44,7 +44,7 @@ pub fn layout_mat( styles: StyleChain, ) -> SourceResult<()> { let augment = elem.augment(styles); - let rows = elem.rows(); + let rows = &elem.rows; if let Some(aug) = &augment { for &offset in &aug.hline.0 { @@ -58,7 +58,7 @@ pub fn layout_mat( } } - let ncols = elem.rows().first().map_or(0, |row| row.len()); + let ncols = rows.first().map_or(0, |row| row.len()); for &offset in &aug.vline.0 { if offset == 0 || offset.unsigned_abs() >= ncols { @@ -97,7 +97,7 @@ pub fn layout_cases( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, FixedAlignment::Start, elem.gap(styles), LeftRightAlternator::None, diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 62ecd1725..06dc6653b 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -632,7 +632,7 @@ fn layout_h( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - if let Spacing::Rel(rel) = elem.amount() { + if let Spacing::Rel(rel) = elem.amount { if rel.rel.is_zero() { ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles))); } @@ -647,11 +647,10 @@ fn layout_class( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let class = *elem.class(); - let style = EquationElem::set_class(Some(class)).wrap(); - let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?; - fragment.set_class(class); - fragment.set_limits(Limits::for_class(class)); + let style = EquationElem::set_class(Some(elem.class)).wrap(); + let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?; + fragment.set_class(elem.class); + fragment.set_limits(Limits::for_class(elem.class)); ctx.push(fragment); Ok(()) } @@ -663,7 +662,7 @@ fn layout_op( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let fragment = ctx.layout_into_fragment(elem.text(), styles)?; + let fragment = ctx.layout_into_fragment(&elem.text, styles)?; let italics = fragment.italics_correction(); let accent_attach = fragment.accent_attach(); let text_like = fragment.is_text_like(); diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 4e5d844f2..a6b5c03d0 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -18,7 +18,6 @@ pub fn layout_root( styles: StyleChain, ) -> SourceResult<()> { let index = elem.index(styles); - let radicand = elem.radicand(); let span = elem.span(); let gap = scaled!( @@ -36,7 +35,7 @@ pub fn layout_root( let radicand = { let cramped = style_cramped(); let styles = styles.chain(&cramped); - let run = ctx.layout_into_run(radicand, styles)?; + let run = ctx.layout_into_run(&elem.radicand, styles)?; let multiline = run.is_multiline(); let mut radicand = run.into_fragment(styles).into_frame(); if multiline { diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 4bc5a9262..6379bdb2e 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -21,7 +21,7 @@ pub fn layout_stretch( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; stretch_fragment( ctx, styles, diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index eb30373dd..7e849c46c 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -20,7 +20,7 @@ pub fn layout_text( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let text = elem.text(); + let text = &elem.text; let span = elem.span(); let mut chars = text.chars(); let math_size = EquationElem::size_in(styles); diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index e55996389..7b3617c3e 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -32,7 +32,7 @@ pub fn layout_underline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under) } /// Lays out an [`OverlineElem`]. @@ -42,7 +42,7 @@ pub fn layout_overline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over) } /// Lays out an [`UnderbraceElem`]. @@ -55,7 +55,7 @@ pub fn layout_underbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏟', BRACE_GAP, @@ -74,7 +74,7 @@ pub fn layout_overbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏞', BRACE_GAP, @@ -93,7 +93,7 @@ pub fn layout_underbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎵', BRACKET_GAP, @@ -112,7 +112,7 @@ pub fn layout_overbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎴', BRACKET_GAP, @@ -131,7 +131,7 @@ pub fn layout_underparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏝', PAREN_GAP, @@ -150,7 +150,7 @@ pub fn layout_overparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏜', PAREN_GAP, @@ -169,7 +169,7 @@ pub fn layout_undershell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏡', SHELL_GAP, @@ -188,7 +188,7 @@ pub fn layout_overshell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏠', SHELL_GAP, diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 7c56bf763..eb665f06a 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -62,7 +62,7 @@ pub fn layout_path( axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() }; - let vertices = elem.vertices(); + let vertices = &elem.vertices; let points: Vec = vertices.iter().map(|c| resolve(c.vertex())).collect(); let mut size = Size::zero(); @@ -150,7 +150,7 @@ pub fn layout_curve( ) -> SourceResult { let mut builder = CurveBuilder::new(region, styles); - for item in elem.components() { + for item in &elem.components { match item { CurveComponent::Move(element) => { let relative = element.relative(styles); @@ -399,7 +399,7 @@ pub fn layout_polygon( region: Region, ) -> SourceResult { let points: Vec = elem - .vertices() + .vertices .iter() .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) .collect(); diff --git a/crates/typst-layout/src/stack.rs b/crates/typst-layout/src/stack.rs index a3ebc9f36..c468945eb 100644 --- a/crates/typst-layout/src/stack.rs +++ b/crates/typst-layout/src/stack.rs @@ -27,7 +27,7 @@ pub fn layout_stack( let spacing = elem.spacing(styles); let mut deferred = None; - for child in elem.children() { + for child in &elem.children { match child { StackChild::Spacing(kind) => { layouter.layout_spacing(*kind); @@ -36,14 +36,14 @@ pub fn layout_stack( StackChild::Block(block) => { // Transparently handle `h`. if let (Axis::X, Some(h)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*h.amount()); + layouter.layout_spacing(h.amount); deferred = None; continue; } // Transparently handle `v`. if let (Axis::Y, Some(v)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*v.amount()); + layouter.layout_spacing(v.amount); deferred = None; continue; } diff --git a/crates/typst-layout/src/transforms.rs b/crates/typst-layout/src/transforms.rs index e0f29c4c2..f4526dd09 100644 --- a/crates/typst-layout/src/transforms.rs +++ b/crates/typst-layout/src/transforms.rs @@ -52,7 +52,7 @@ pub fn layout_rotate( region, size, styles, - elem.body(), + &elem.body, Transform::rotate(angle), align, elem.reflow(styles), @@ -81,7 +81,7 @@ pub fn layout_scale( region, size, styles, - elem.body(), + &elem.body, Transform::scale(scale.x, scale.y), elem.origin(styles).resolve(styles), elem.reflow(styles), @@ -169,7 +169,7 @@ pub fn layout_skew( region, size, styles, - elem.body(), + &elem.body, Transform::skew(ax, ay), align, elem.reflow(styles), diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index e189103d9..d26a9f9f5 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -800,7 +800,7 @@ impl ManualPageCounter { let Some(elem) = elem.to_packed::() else { continue; }; - if *elem.key() == CounterKey::Page { + if elem.key == CounterKey::Page { let mut state = CounterState(smallvec![self.logical]); state.update(engine, elem.update.clone())?; self.logical = state.first(); diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index 7e019e6c7..e6ab926bf 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -245,7 +245,7 @@ impl State { for elem in introspector.query(&self.selector()) { let elem = elem.to_packed::().unwrap(); - match elem.update() { + match &elem.update { StateUpdate::Set(value) => state = value.clone(), StateUpdate::Func(func) => { state = func.call(&mut engine, Context::none().track(), [state])? diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index e8ba4d7c3..5604d6831 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -100,7 +100,7 @@ pub struct AlignElem { impl Show for Packed { #[typst_macros::time(name = "align", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().aligned(self.alignment(styles))) + Ok(self.body.clone().aligned(self.alignment(styles))) } } diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index 266d1d88f..c8c74269b 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst-library/src/layout/container.rs @@ -166,7 +166,7 @@ impl Packed { styles: StyleChain, region: Size, ) -> SourceResult> { - self.body().call(engine, locator, styles, region) + self.body.call(engine, locator, styles, region) } } diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index e46440fb4..6616c3311 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -749,7 +749,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index adaff1c18..504159e83 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -42,16 +42,16 @@ pub fn grid_to_cellgrid<'a>( // Use trace to link back to the grid when a specific cell errors let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { + let children = elem.children.iter().map(|child| match child { GridChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), span: header.span(), - items: header.children().iter().map(resolve_item), + items: header.children.iter().map(resolve_item), }, GridChild::Footer(footer) => ResolvableGridChild::Footer { repeat: footer.repeat(styles), span: footer.span(), - items: footer.children().iter().map(resolve_item), + items: footer.children.iter().map(resolve_item), }, GridChild::Item(item) => { ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) @@ -95,16 +95,16 @@ pub fn table_to_cellgrid<'a>( // Use trace to link back to the table when a specific cell errors let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { + let children = elem.children.iter().map(|child| match child { TableChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), span: header.span(), - items: header.children().iter().map(resolve_item), + items: header.children.iter().map(resolve_item), }, TableChild::Footer(footer) => ResolvableGridChild::Footer { repeat: footer.repeat(styles), span: footer.span(), - items: footer.children().iter().map(resolve_item), + items: footer.children.iter().map(resolve_item), }, TableChild::Item(item) => { ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index 1b8b9bd57..eca33471a 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs @@ -29,6 +29,6 @@ pub struct HideElem { impl Show for Packed { #[typst_macros::time(name = "hide", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(HideElem::set_hidden(true))) + Ok(self.body.clone().styled(HideElem::set_hidden(true))) } } diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index c3d112e16..05e4f6d9b 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -89,7 +89,7 @@ impl Show for Packed { let loc = elem.location().unwrap(); let context = Context::new(Some(loc), Some(styles)); let result = elem - .func() + .func .call( engine, context.track(), diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index fee705ee4..b87e527f2 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -143,7 +143,7 @@ cast! { self => self.0.into_value(), v: char => Self::new(v), v: Content => match v.to_packed::() { - Some(elem) => Value::Str(elem.text().clone().into()).cast()?, + Some(elem) => Value::Str(elem.text.clone().into()).cast()?, None => bail!("expected text"), }, } diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs index e1f577272..d526aba57 100644 --- a/crates/typst-library/src/math/attach.rs +++ b/crates/typst-library/src/math/attach.rs @@ -47,9 +47,9 @@ impl Packed { /// base AttachElem where possible. pub fn merge_base(&self) -> Option { // Extract from an EquationElem. - let mut base = self.base(); + let mut base = &self.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } // Move attachments from elem into base where possible. diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 4ab4ff22c..95db8a222 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -638,7 +638,7 @@ impl<'a> Generator<'a> { for elem in &self.groups { let group = elem.to_packed::().unwrap(); let location = elem.location().unwrap(); - let children = group.children(); + let children = &group.children; // Groups should never be empty. let Some(first) = children.first() else { continue }; @@ -650,12 +650,11 @@ impl<'a> Generator<'a> { // Create infos and items for each child in the group. for child in children { - let key = *child.key(); - let Some(entry) = database.get(key) else { + let Some(entry) = database.get(child.key) else { errors.push(error!( child.span(), "key `{}` does not exist in the bibliography", - key.resolve() + child.key.resolve() )); continue; }; @@ -682,7 +681,7 @@ impl<'a> Generator<'a> { }; normal &= special_form.is_none(); - subinfos.push(CiteInfo { key, supplement, hidden }); + subinfos.push(CiteInfo { key: child.key, supplement, hidden }); items.push(CitationItem::new(entry, locator, None, hidden, special_form)); } diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index fd843ee53..52dca966d 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -257,7 +257,7 @@ impl Synthesize for Packed { // Determine the figure's kind. let kind = elem.kind(styles).unwrap_or_else(|| { - elem.body() + elem.body .query_first(&Selector::can::()) .map(|elem| FigureKind::Elem(elem.func())) .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) @@ -288,14 +288,13 @@ impl Synthesize for Packed { // Resolve the supplement with the first descendant of the kind or // just the body, if none was found. let descendant = match kind { - FigureKind::Elem(func) => elem - .body() - .query_first(&Selector::Elem(func, None)) - .map(Cow::Owned), + FigureKind::Elem(func) => { + elem.body.query_first(&Selector::Elem(func, None)).map(Cow::Owned) + } FigureKind::Name(_) => None, }; - let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body())); + let target = descendant.unwrap_or_else(|| Cow::Borrowed(&elem.body)); Some(supplement.resolve(engine, styles, [target])?) } }; @@ -437,7 +436,7 @@ impl Outlinable for Packed { return Ok(None); }; - let mut realized = caption.body().clone(); + let mut realized = caption.body.clone(); if let ( Smart::Custom(Some(Supplement::Content(mut supplement))), Some(Some(counter)), @@ -460,7 +459,7 @@ impl Outlinable for Packed { let separator = caption.get_separator(StyleChain::default()); - realized = supplement + numbers + separator + caption.body(); + realized = supplement + numbers + separator + caption.body.clone(); } Ok(Some(realized)) @@ -604,7 +603,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "figure.caption", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); if let ( Some(Some(mut supplement)), diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index ffc78ea05..f3b2a19eb 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -105,12 +105,12 @@ impl FootnoteElem { /// Tests if this footnote is a reference to another footnote. pub fn is_ref(&self) -> bool { - matches!(self.body(), FootnoteBody::Reference(_)) + matches!(self.body, FootnoteBody::Reference(_)) } /// Returns the content of the body of this footnote if it is not a ref. pub fn body_content(&self) -> Option<&Content> { - match self.body() { + match &self.body { FootnoteBody::Content(content) => Some(content), _ => None, } @@ -120,9 +120,9 @@ impl FootnoteElem { impl Packed { /// Returns the location of the definition of this footnote. pub fn declaration_location(&self, engine: &Engine) -> StrResult { - match self.body() { + match self.body { FootnoteBody::Reference(label) => { - let element = engine.introspector.query_label(*label)?; + let element = engine.introspector.query_label(label)?; let footnote = element .to_packed::() .ok_or("referenced element should be a footnote")?; @@ -281,12 +281,11 @@ impl Show for Packed { #[typst_macros::time(name = "footnote.entry", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let span = self.span(); - let note = self.note(); let number_gap = Em::new(0.05); let default = StyleChain::default(); - let numbering = note.numbering(default); + let numbering = self.note.numbering(default); let counter = Counter::of(FootnoteElem::elem()); - let Some(loc) = note.location() else { + let Some(loc) = self.note.location() else { bail!( span, "footnote entry must have a location"; hint: "try using a query or a show rule to customize the footnote instead" @@ -304,7 +303,7 @@ impl Show for Packed { HElem::new(self.indent(styles).into()).pack(), sup, HElem::new(number_gap.into()).with_weak(true).pack(), - note.body_content().unwrap().clone(), + self.note.body_content().unwrap().clone(), ])) } } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index ec9cf4e99..db131afec 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -223,7 +223,7 @@ impl Show for Packed { const SPACING_TO_NUMBERING: Em = Em::new(0.3); let span = self.span(); - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); let hanging_indent = self.hanging_indent(styles); let mut indent = match hanging_indent { @@ -360,7 +360,7 @@ impl Outlinable for Packed { return Ok(None); } - let mut content = self.body().clone(); + let mut content = self.body.clone(); if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { let numbers = Counter::of(HeadingElem::elem()).display_at_loc( engine, diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index bbc47da05..4558cb394 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -102,11 +102,10 @@ impl LinkElem { impl Show for Packed { #[typst_macros::time(name = "link", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); - let dest = self.dest(); + let body = self.body.clone(); Ok(if TargetElem::target_in(styles).is_html() { - if let LinkTarget::Dest(Destination::Url(url)) = dest { + if let LinkTarget::Dest(Destination::Url(url)) = &self.dest { HtmlElem::new(tag::a) .with_attr(attr::href, url.clone().into_inner()) .with_body(Some(body)) @@ -120,7 +119,7 @@ impl Show for Packed { body } } else { - let linked = match self.dest() { + let linked = match &self.dest { LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Label(label) => { let elem = engine.introspector.query_label(*label).at(self.span())?; diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index e8d32a540..84661c1c2 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -219,8 +219,7 @@ impl Show for Packed { continue; }; - let level = entry.level(); - if depth < *level { + if depth < entry.level { continue; } @@ -229,7 +228,7 @@ impl Show for Packed { while ancestors .last() .and_then(|ancestor| ancestor.with::()) - .is_some_and(|last| last.level() >= *level) + .is_some_and(|last| last.level() >= entry.level) { ancestors.pop(); } @@ -483,7 +482,7 @@ impl Show for Packed { #[typst_macros::time(name = "outline.entry", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let mut seq = vec![]; - let elem = self.element(); + let elem = &self.element; // In case a user constructs an outline entry with an arbitrary element. let Some(location) = elem.location() else { @@ -512,7 +511,7 @@ impl Show for Packed { seq.push(TextElem::packed("\u{202B}")); } - seq.push(self.body().clone().linked(Destination::Location(location))); + seq.push(self.body.clone().linked(Destination::Location(location))); if rtl { // "Pop Directional Formatting" @@ -520,7 +519,7 @@ impl Show for Packed { } // Add filler symbols between the section name and page number. - if let Some(filler) = self.fill() { + if let Some(filler) = &self.fill { seq.push(SpaceElem::shared().clone()); seq.push( BoxElem::new() @@ -535,7 +534,7 @@ impl Show for Packed { } // Add the page number. - let page = self.page().clone().linked(Destination::Location(location)); + let page = self.page.clone().linked(Destination::Location(location)); seq.push(page); Ok(Content::sequence(seq)) diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 110825f13..2eaa32d4c 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -156,7 +156,7 @@ cast! { impl Show for Packed { #[typst_macros::time(name = "quote", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); let block = self.block(styles); if self.quotes(styles) == Smart::Custom(true) || !block { diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 96aa2117d..316617688 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -182,9 +182,8 @@ impl Synthesize for Packed { elem.push_citation(Some(citation)); elem.push_element(None); - let target = *elem.target(); - if !BibliographyElem::has(engine, target) { - if let Ok(found) = engine.introspector.query_label(target).cloned() { + if !BibliographyElem::has(engine, elem.target) { + if let Ok(found) = engine.introspector.query_label(elem.target).cloned() { elem.push_element(Some(found)); return Ok(()); } @@ -197,8 +196,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "ref", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let target = *self.target(); - let elem = engine.introspector.query_label(target); + let elem = engine.introspector.query_label(self.target); let span = self.span(); let form = self.form(styles); @@ -229,7 +227,7 @@ impl Show for Packed { } // RefForm::Normal - if BibliographyElem::has(engine, target) { + if BibliographyElem::has(engine, self.target) { if elem.is_ok() { bail!(span, "label occurs in the document and its bibliography"); } @@ -240,7 +238,7 @@ impl Show for Packed { let elem = elem.at(span)?; if let Some(footnote) = elem.to_packed::() { - return Ok(footnote.into_ref(target).pack().spanned(span)); + return Ok(footnote.into_ref(self.target).pack().spanned(span)); } let elem = elem.clone(); @@ -319,7 +317,7 @@ fn to_citation( engine: &mut Engine, styles: StyleChain, ) -> SourceResult> { - let mut elem = Packed::new(CiteElem::new(*reference.target()).with_supplement( + let mut elem = Packed::new(CiteElem::new(reference.target).with_supplement( match reference.supplement(styles).clone() { Smart::Custom(Some(Supplement::Content(content))) => Some(content), _ => None, diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 7dfaf45d7..fa44cb58a 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -706,7 +706,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 13aa8c6d5..1261ea4f4 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -151,12 +151,12 @@ impl Show for Packed { .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); let mut children = vec![]; - for child in self.children().iter() { + for child in self.children.iter() { let mut seq = vec![]; seq.extend(unpad.clone()); - seq.push(child.term().clone().strong()); + seq.push(child.term.clone().strong()); seq.push((*separator).clone()); - seq.push(child.description().clone()); + seq.push(child.description.clone()); children.push(StackChild::Block(Content::sequence(seq))); } diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 5da7ecec4..485d0edcf 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -81,7 +81,7 @@ pub struct UnderlineElem { impl Show for Packed { #[typst_macros::time(name = "underline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Underline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -173,7 +173,7 @@ pub struct OverlineElem { impl Show for Packed { #[typst_macros::time(name = "overline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Overline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -250,7 +250,7 @@ pub struct StrikeElem { impl Show for Packed { #[typst_macros::time(name = "strike", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { // Note that we do not support evade option for strikethrough. line: DecoLine::Strikethrough { stroke: self.stroke(styles).unwrap_or_default(), @@ -345,7 +345,7 @@ pub struct HighlightElem { impl Show for Packed { #[typst_macros::time(name = "highlight", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Highlight { fill: self.fill(styles), stroke: self diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index d372c399f..6cca24587 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -794,7 +794,7 @@ impl Construct for TextElem { impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index cd718d2a1..01d6d8f01 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -315,7 +315,7 @@ impl Packed { #[comemo::memoize] fn highlight(&self, styles: StyleChain) -> Vec> { let elem = self.as_ref(); - let lines = preprocess(elem.text(), styles, self.span()); + let lines = preprocess(&elem.text, styles, self.span()); let count = lines.len() as i64; let lang = elem @@ -490,7 +490,7 @@ impl Figurable for Packed {} impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(&self.text().get()); + text.push_str(&self.text.get()); } } @@ -638,13 +638,13 @@ pub struct RawLine { impl Show for Packed { #[typst_macros::time(name = "raw.line", span = self.span())] fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult { - Ok(self.body().clone()) + Ok(self.body.clone()) } } impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 9723bbf0c..3eec0758b 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -50,7 +50,7 @@ pub struct SubElem { impl Show for Packed { #[typst_macros::time(name = "sub", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, true) { @@ -109,7 +109,7 @@ pub struct SuperElem { impl Show for Packed { #[typst_macros::time(name = "super", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, false) { @@ -132,9 +132,9 @@ fn convert_script(content: &Content, sub: bool) -> Option { Some(' '.into()) } else if let Some(elem) = content.to_packed::() { if sub { - elem.text().chars().map(to_subscript_codepoint).collect() + elem.text.chars().map(to_subscript_codepoint).collect() } else { - elem.text().chars().map(to_superscript_codepoint).collect() + elem.text.chars().map(to_superscript_codepoint).collect() } } else if let Some(sequence) = content.to_packed::() { sequence diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index bf003bd1c..1e88974f5 100644 --- a/crates/typst-library/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs @@ -53,6 +53,6 @@ pub struct SmallcapsElem { impl Show for Packed { #[typst_macros::time(name = "smallcaps", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_smallcaps(true))) + Ok(self.body.clone().styled(TextElem::set_smallcaps(true))) } } diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index 78a3c1800..67fe7ed6a 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -63,6 +63,11 @@ impl Elem { self.real_fields().filter(|field| !field.ghost) } + /// Fields that get accessor, with, and push methods. + fn accessor_fields(&self) -> impl Iterator + Clone { + self.struct_fields().filter(|field| !field.required) + } + /// Fields that are relevant for equality. /// /// Synthesized fields are excluded to ensure equality before and after @@ -442,9 +447,9 @@ fn create_inherent_impl(element: &Elem) -> TokenStream { let Elem { ident, .. } = element; let new_func = create_new_func(element); - let with_field_methods = element.struct_fields().map(create_with_field_method); - let push_field_methods = element.struct_fields().map(create_push_field_method); - let field_methods = element.struct_fields().map(create_field_method); + let with_field_methods = element.accessor_fields().map(create_with_field_method); + let push_field_methods = element.accessor_fields().map(create_push_field_method); + let field_methods = element.accessor_fields().map(create_field_method); let field_in_methods = element.style_fields().map(create_field_in_method); let set_field_methods = element.style_fields().map(create_set_field_method); diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs index 3ba2ac076..597638f4b 100644 --- a/crates/typst-pdf/src/embed.rs +++ b/crates/typst-pdf/src/embed.rs @@ -59,7 +59,7 @@ fn embed_file( let embedded_file_stream_ref = chunk.alloc.bump(); let file_spec_dict_ref = chunk.alloc.bump(); - let data = embed.data().as_slice(); + let data = embed.data.as_slice(); let compressed = deflate(data); let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index b9e71319f..ff72eb86a 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -184,8 +184,7 @@ fn write_outline_item( outline.count(-(node.children.len() as i32)); } - let body = node.element.body(); - outline.title(TextStr::trimmed(body.plain_text().trim())); + outline.title(TextStr::trimmed(node.element.body.plain_text().trim())); let loc = node.element.location().unwrap(); let pos = ctx.document.introspector.position(loc); From a4ac4e656267e718a5cf60d1e959f74b2b7346f3 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 13 Jan 2025 20:19:37 +0100 Subject: [PATCH 18/44] Make `typst-timing` WASM-compatible (#5689) --- Cargo.lock | 11 + Cargo.toml | 1 + crates/typst-timing/Cargo.toml | 6 + crates/typst-timing/src/lib.rs | 372 +++++++++++++++++++-------------- 4 files changed, 235 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c0bfe138..8aa7c0ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3093,6 +3093,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "web-sys", ] [[package]] @@ -3418,6 +3419,16 @@ dependencies = [ "indexmap-nostd", ] +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index b4f704f80..1be7816a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ ureq = { version = "2", default-features = false, features = ["native-tls", "gzi usvg = { version = "0.43", default-features = false, features = ["text"] } walkdir = "2" wasmi = "0.39.0" +web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" xmp-writer = "0.3" diff --git a/crates/typst-timing/Cargo.toml b/crates/typst-timing/Cargo.toml index 2d42269fc..dbc2813c7 100644 --- a/crates/typst-timing/Cargo.toml +++ b/crates/typst-timing/Cargo.toml @@ -17,5 +17,11 @@ parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { workspace = true, features = ["Window", "WorkerGlobalScope", "Performance"], optional = true } + +[features] +wasm = ["dep:web-sys"] + [lints] workspace = true diff --git a/crates/typst-timing/src/lib.rs b/crates/typst-timing/src/lib.rs index b4653170b..6da2cdf02 100644 --- a/crates/typst-timing/src/lib.rs +++ b/crates/typst-timing/src/lib.rs @@ -1,149 +1,13 @@ //! Performance timing for Typst. -#![cfg_attr(target_arch = "wasm32", allow(dead_code, unused_variables))] - -use std::hash::Hash; use std::io::Write; use std::num::NonZeroU64; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Relaxed; -use std::thread::ThreadId; -use std::time::{Duration, SystemTime}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use parking_lot::Mutex; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -/// Whether the timer is enabled. Defaults to `false`. -static ENABLED: AtomicBool = AtomicBool::new(false); - -/// The global event recorder. -static RECORDER: Mutex = Mutex::new(Recorder::new()); - -/// The recorder of events. -struct Recorder { - /// The events that have been recorded. - events: Vec, - /// The discriminator of the next event. - discriminator: u64, -} - -impl Recorder { - /// Create a new recorder. - const fn new() -> Self { - Self { events: Vec::new(), discriminator: 0 } - } -} - -/// An event that has been recorded. -#[derive(Clone, Copy, Eq, PartialEq, Hash)] -struct Event { - /// Whether this is a start or end event. - kind: EventKind, - /// The start time of this event. - timestamp: SystemTime, - /// The discriminator of this event. - id: u64, - /// The name of this event. - name: &'static str, - /// The raw value of the span of code that this event was recorded in. - span: Option, - /// The thread ID of this event. - thread_id: ThreadId, -} - -/// Whether an event marks the start or end of a scope. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -enum EventKind { - Start, - End, -} - -/// Enable the timer. -#[inline] -pub fn enable() { - // We only need atomicity and no synchronization of other - // operations, so `Relaxed` is fine. - ENABLED.store(true, Relaxed); -} - -/// Whether the timer is enabled. -#[inline] -pub fn is_enabled() -> bool { - ENABLED.load(Relaxed) -} - -/// Clears the recorded events. -#[inline] -pub fn clear() { - RECORDER.lock().events.clear(); -} - -/// A scope that records an event when it is dropped. -pub struct TimingScope { - name: &'static str, - span: Option, - id: u64, - thread_id: ThreadId, -} - -impl TimingScope { - /// Create a new scope if timing is enabled. - #[inline] - pub fn new(name: &'static str) -> Option { - Self::with_span(name, None) - } - - /// Create a new scope with a span if timing is enabled. - /// - /// The span is a raw number because `typst-timing` can't depend on - /// `typst-syntax` (or else `typst-syntax` couldn't depend on - /// `typst-timing`). - #[inline] - pub fn with_span(name: &'static str, span: Option) -> Option { - #[cfg(not(target_arch = "wasm32"))] - if is_enabled() { - return Some(Self::new_impl(name, span)); - } - None - } - - /// Create a new scope without checking if timing is enabled. - fn new_impl(name: &'static str, span: Option) -> Self { - let timestamp = SystemTime::now(); - let thread_id = std::thread::current().id(); - - let mut recorder = RECORDER.lock(); - let id = recorder.discriminator; - recorder.discriminator += 1; - recorder.events.push(Event { - kind: EventKind::Start, - timestamp, - id, - name, - span, - thread_id, - }); - - Self { name, span, id, thread_id } - } -} - -impl Drop for TimingScope { - fn drop(&mut self) { - let event = Event { - kind: EventKind::End, - timestamp: SystemTime::now(), - id: self.id, - name: self.name, - span: self.span, - thread_id: self.thread_id, - }; - - RECORDER.lock().events.push(event); - } -} - /// Creates a timing scope around an expression. /// /// The output of the expression is returned. @@ -179,6 +43,46 @@ macro_rules! timed { }}; } +thread_local! { + /// Data that is initialized once per thread. + static THREAD_DATA: ThreadData = ThreadData { + id: { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + static COUNTER: AtomicU64 = AtomicU64::new(1); + COUNTER.fetch_add(1, Ordering::Relaxed) + }, + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer::new(), + }; +} + +/// Whether the timer is enabled. Defaults to `false`. +static ENABLED: AtomicBool = AtomicBool::new(false); + +/// The list of collected events. +static EVENTS: Mutex> = Mutex::new(Vec::new()); + +/// Enable the timer. +#[inline] +pub fn enable() { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + ENABLED.store(true, Ordering::Relaxed); +} + +/// Whether the timer is enabled. +#[inline] +pub fn is_enabled() -> bool { + ENABLED.load(Ordering::Relaxed) +} + +/// Clears the recorded events. +#[inline] +pub fn clear() { + EVENTS.lock().clear(); +} + /// Export data as JSON for Chrome's tracing tool. /// /// The `source` function is called for each span to get the source code @@ -205,19 +109,15 @@ pub fn export_json( line: u32, } - let recorder = RECORDER.lock(); - let run_start = recorder - .events - .first() - .map(|event| event.timestamp) - .unwrap_or_else(SystemTime::now); + let lock = EVENTS.lock(); + let events = lock.as_slice(); let mut serializer = serde_json::Serializer::new(writer); let mut seq = serializer - .serialize_seq(Some(recorder.events.len())) + .serialize_seq(Some(events.len())) .map_err(|e| format!("failed to serialize events: {e}"))?; - for event in recorder.events.iter() { + for event in events.iter() { seq.serialize_element(&Entry { name: event.name, cat: "typst", @@ -225,17 +125,9 @@ pub fn export_json( EventKind::Start => "B", EventKind::End => "E", }, - ts: event - .timestamp - .duration_since(run_start) - .unwrap_or(Duration::ZERO) - .as_nanos() as f64 - / 1_000.0, + ts: event.timestamp.micros_since(events[0].timestamp), pid: 1, - tid: unsafe { - // Safety: `thread_id` is a `ThreadId` which is a `u64`. - std::mem::transmute_copy(&event.thread_id) - }, + tid: event.thread_id, args: event.span.map(&mut source).map(|(file, line)| Args { file, line }), }) .map_err(|e| format!("failed to serialize event: {e}"))?; @@ -245,3 +137,173 @@ pub fn export_json( Ok(()) } + +/// A scope that records an event when it is dropped. +pub struct TimingScope { + name: &'static str, + span: Option, + thread_id: u64, +} + +impl TimingScope { + /// Create a new scope if timing is enabled. + #[inline] + pub fn new(name: &'static str) -> Option { + Self::with_span(name, None) + } + + /// Create a new scope with a span if timing is enabled. + /// + /// The span is a raw number because `typst-timing` can't depend on + /// `typst-syntax` (or else `typst-syntax` couldn't depend on + /// `typst-timing`). + #[inline] + pub fn with_span(name: &'static str, span: Option) -> Option { + if is_enabled() { + return Some(Self::new_impl(name, span)); + } + None + } + + /// Create a new scope without checking if timing is enabled. + fn new_impl(name: &'static str, span: Option) -> Self { + let (thread_id, timestamp) = + THREAD_DATA.with(|data| (data.id, Timestamp::now_with(data))); + EVENTS.lock().push(Event { + kind: EventKind::Start, + timestamp, + name, + span, + thread_id, + }); + Self { name, span, thread_id } + } +} + +impl Drop for TimingScope { + fn drop(&mut self) { + let timestamp = Timestamp::now(); + EVENTS.lock().push(Event { + kind: EventKind::End, + timestamp, + name: self.name, + span: self.span, + thread_id: self.thread_id, + }); + } +} + +/// An event that has been recorded. +struct Event { + /// Whether this is a start or end event. + kind: EventKind, + /// The time at which this event occurred. + timestamp: Timestamp, + /// The name of this event. + name: &'static str, + /// The raw value of the span of code that this event was recorded in. + span: Option, + /// The thread ID of this event. + thread_id: u64, +} + +/// Whether an event marks the start or end of a scope. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum EventKind { + Start, + End, +} + +/// A cross-platform way to get the current time. +#[derive(Copy, Clone)] +struct Timestamp { + #[cfg(not(target_arch = "wasm32"))] + inner: std::time::SystemTime, + #[cfg(target_arch = "wasm32")] + inner: f64, +} + +impl Timestamp { + fn now() -> Self { + #[cfg(target_arch = "wasm32")] + return THREAD_DATA.with(Self::now_with); + + #[cfg(not(target_arch = "wasm32"))] + Self { inner: std::time::SystemTime::now() } + } + + #[allow(unused_variables)] + fn now_with(data: &ThreadData) -> Self { + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + return Self { inner: data.timer.now() }; + + #[cfg(all(target_arch = "wasm32", not(feature = "wasm")))] + return Self { inner: 0.0 }; + + #[cfg(not(target_arch = "wasm32"))] + Self::now() + } + + fn micros_since(self, start: Self) -> f64 { + #[cfg(target_arch = "wasm32")] + return (self.inner - start.inner) * 1000.0; + + #[cfg(not(target_arch = "wasm32"))] + (self + .inner + .duration_since(start.inner) + .unwrap_or(std::time::Duration::ZERO) + .as_nanos() as f64 + / 1_000.0) + } +} + +/// Per-thread data. +struct ThreadData { + /// The thread's ID. + /// + /// In contrast to `std::thread::current().id()`, this is wasm-compatible + /// and also a bit cheaper to access because the std version does a bit more + /// stuff (including cloning an `Arc`). + id: u64, + /// A way to get the time from WebAssembly. + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer, +} + +/// A way to get the time from WebAssembly. +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +struct WasmTimer { + /// The cached JS performance handle for the thread. + perf: web_sys::Performance, + /// The cached JS time origin. + time_origin: f64, +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +impl WasmTimer { + fn new() -> Self { + // Retrieve `performance` from global object, either the window or + // globalThis. + let perf = web_sys::window() + .and_then(|window| window.performance()) + .or_else(|| { + use web_sys::wasm_bindgen::JsCast; + web_sys::js_sys::global() + .dyn_into::() + .ok() + .and_then(|scope| scope.performance()) + }) + .expect("failed to get JS performance handle"); + + // Every thread gets its own time origin. To make the results consistent + // across threads, we need to add this to each `now()` call. + let time_origin = perf.time_origin(); + + Self { perf, time_origin } + } + + fn now(&self) -> f64 { + self.time_origin + self.perf.now() + } +} From 63c4720ed2b9e034fda6810a9c0e521355a24c44 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:40:29 -0500 Subject: [PATCH 19/44] Fix list indent when starting at an open bracket (#5677) --- crates/typst-syntax/src/parser.rs | 19 +++++++++----- tests/suite/model/list.typ | 43 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 335b8f1a2..a65e5ff6b 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -1605,10 +1605,12 @@ impl AtNewline { _ => true, }, AtNewline::StopParBreak => parbreak, - AtNewline::RequireColumn(min_col) => match column { - Some(column) => column <= min_col, - None => false, // Don't stop if we had no column. - }, + 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). + column.is_some_and(|column| column <= min_col) + } } } } @@ -1703,10 +1705,13 @@ impl<'s> Parser<'s> { self.token.newline.is_some() } - /// The number of characters until the most recent newline from the current - /// token, or 0 if it did not follow a newline. + /// The number of characters until the most recent newline from the start of + /// the current token. Uses a cached value from the newline mode if present. fn current_column(&self) -> usize { - self.token.newline.and_then(|newline| newline.column).unwrap_or(0) + self.token + .newline + .and_then(|newline| newline.column) + .unwrap_or_else(|| self.lexer.column(self.token.start)) } /// The current token's text. diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 138abf70e..b3d9a830b 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -77,6 +77,49 @@ _Shopping list_ #test(indented, manual) +--- list-indent-bracket-nesting --- +// Test list indent nesting behavior when directly at a starting bracket. + +#let indented = { + [- indented + - less + ] + [- indented + - same + - then less + - then same + ] + [- indented + - more + - then same + - then less + ] +} + +#let item = list.item +#let manual = { + { + item[indented]; [ ] + item[less]; [ ] + } + { + item[indented]; [ ] + item[same]; [ ] + item[then less #{ + item[then same] + }]; [ ] + } + { + item[indented #{ + item[more] + }]; [ ] + item[then same]; [ ] + item[then less]; [ ] + } +} + +#test(indented, manual) + --- list-tabs --- // This works because tabs are used consistently. - A with 1 tab From c22c47b9c97062309ed841679bb49fc15bb6a398 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Thu, 16 Jan 2025 14:40:57 +0100 Subject: [PATCH 20/44] Add font exception for NewCM Sans Math (#5682) --- crates/typst-library/src/text/font/exceptions.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/typst-library/src/text/font/exceptions.rs b/crates/typst-library/src/text/font/exceptions.rs index 465ec510c..00038c50c 100644 --- a/crates/typst-library/src/text/font/exceptions.rs +++ b/crates/typst-library/src/text/font/exceptions.rs @@ -228,6 +228,8 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { .style(FontStyle::Oblique), "NewCMSans10-Regular" => Exception::new() .family("New Computer Modern Sans"), + "NewCMSansMath-Regular" => Exception::new() + .family("New Computer Modern Sans Math"), "NewCMUncial08-Bold" => Exception::new() .family("New Computer Modern Uncial 08"), "NewCMUncial08-Book" => Exception::new() From b90ad470d60f4a90e3ba2e78aa4746fbe08783ab Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 21 Jan 2025 12:10:06 +0100 Subject: [PATCH 21/44] Remove redundant doc comments on standard arguments (#5725) --- crates/typst-library/src/foundations/args.rs | 2 -- crates/typst-library/src/foundations/array.rs | 25 ------------------- crates/typst-library/src/foundations/calc.rs | 11 -------- .../typst-library/src/foundations/datetime.rs | 1 - crates/typst-library/src/foundations/func.rs | 4 --- crates/typst-library/src/foundations/mod.rs | 1 - .../typst-library/src/foundations/plugin.rs | 1 - crates/typst-library/src/foundations/str.rs | 2 -- .../typst-library/src/foundations/symbol.rs | 1 - .../typst-library/src/foundations/target.rs | 5 +--- .../src/introspection/counter.rs | 14 ----------- .../typst-library/src/introspection/here.rs | 5 +--- .../typst-library/src/introspection/locate.rs | 2 -- .../typst-library/src/introspection/query.rs | 2 -- .../typst-library/src/introspection/state.rs | 10 -------- crates/typst-library/src/layout/layout.rs | 1 - crates/typst-library/src/layout/measure.rs | 3 --- crates/typst-library/src/loading/cbor.rs | 2 -- crates/typst-library/src/loading/csv.rs | 2 -- crates/typst-library/src/loading/json.rs | 2 -- crates/typst-library/src/loading/read.rs | 1 - crates/typst-library/src/loading/toml.rs | 2 -- crates/typst-library/src/loading/xml.rs | 2 -- crates/typst-library/src/loading/yaml.rs | 2 -- crates/typst-library/src/math/root.rs | 1 - crates/typst-library/src/model/numbering.rs | 2 -- crates/typst-library/src/visualize/color.rs | 19 -------------- .../typst-library/src/visualize/gradient.rs | 4 --- .../typst-library/src/visualize/image/mod.rs | 1 - crates/typst-library/src/visualize/polygon.rs | 2 +- crates/typst-library/src/visualize/stroke.rs | 2 -- crates/typst-library/src/visualize/tiling.rs | 1 - 32 files changed, 3 insertions(+), 132 deletions(-) diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index 44aa9dd6d..430c4e9ad 100644 --- a/crates/typst-library/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -305,8 +305,6 @@ impl Args { /// ``` #[func(constructor)] pub fn construct( - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to construct. #[external] diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index e79a4e930..aad7266bc 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -301,9 +301,7 @@ impl Array { #[func] pub fn find( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -325,9 +323,7 @@ impl Array { #[func] pub fn position( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -363,8 +359,6 @@ impl Array { /// ``` #[func] pub fn range( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The start of the range (inclusive). #[external] @@ -402,9 +396,7 @@ impl Array { #[func] pub fn filter( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -427,9 +419,7 @@ impl Array { #[func] pub fn map( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. mapper: Func, @@ -481,8 +471,6 @@ impl Array { #[func] pub fn zip( self, - /// The real arguments (the `others` arguments are just for the docs, this - /// function is a bit involved, so we parse the positional arguments manually). args: &mut Args, /// Whether all arrays have to have the same length. /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an @@ -569,9 +557,7 @@ impl Array { #[func] pub fn fold( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The initial value to start with. init: Value, @@ -631,9 +617,7 @@ impl Array { #[func] pub fn any( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -651,9 +635,7 @@ impl Array { #[func] pub fn all( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -831,11 +813,8 @@ impl Array { #[func] pub fn sorted( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// If given, applies this function to the elements in the array to /// determine the keys to sort by. @@ -881,9 +860,7 @@ impl Array { #[func(title = "Deduplicate")] pub fn dedup( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// If given, applies this function to the elements in the array to /// determine the keys to deduplicate by. @@ -967,9 +944,7 @@ impl Array { #[func] pub fn reduce( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The reducing function. Must have two parameters: One for the /// accumulated value and one for an item. diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index fd4498e07..a8e0eaeb3 100644 --- a/crates/typst-library/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -97,7 +97,6 @@ cast! { /// ``` #[func(title = "Power")] pub fn pow( - /// The callsite span. span: Span, /// The base of the power. /// @@ -159,7 +158,6 @@ pub fn pow( /// ``` #[func(title = "Exponential")] pub fn exp( - /// The callsite span. span: Span, /// The exponent of the power. exponent: Spanned, @@ -412,7 +410,6 @@ pub fn tanh( /// ``` #[func(title = "Logarithm")] pub fn log( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -454,7 +451,6 @@ pub fn log( /// ``` #[func(title = "Natural Logarithm")] pub fn ln( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -782,7 +778,6 @@ pub fn round( /// ``` #[func] pub fn clamp( - /// The callsite span. span: Span, /// The number to clamp. value: DecNum, @@ -815,7 +810,6 @@ pub fn clamp( /// ``` #[func(title = "Minimum")] pub fn min( - /// The callsite span. span: Span, /// The sequence of values from which to extract the minimum. /// Must not be empty. @@ -833,7 +827,6 @@ pub fn min( /// ``` #[func(title = "Maximum")] pub fn max( - /// The callsite span. span: Span, /// The sequence of values from which to extract the maximum. /// Must not be empty. @@ -911,7 +904,6 @@ pub fn odd( /// ``` #[func(title = "Remainder")] pub fn rem( - /// The span of the function call. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -950,7 +942,6 @@ pub fn rem( /// ``` #[func(title = "Euclidean Division")] pub fn div_euclid( - /// The callsite span. span: Span, /// The dividend of the division. dividend: DecNum, @@ -994,7 +985,6 @@ pub fn div_euclid( /// ``` #[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])] pub fn rem_euclid( - /// The callsite span. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -1031,7 +1021,6 @@ pub fn rem_euclid( /// ``` #[func(title = "Quotient")] pub fn quo( - /// The span of the function call. span: Span, /// The dividend of the quotient. dividend: DecNum, diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs index d15cd417a..2fc48a521 100644 --- a/crates/typst-library/src/foundations/datetime.rs +++ b/crates/typst-library/src/foundations/datetime.rs @@ -318,7 +318,6 @@ impl Datetime { /// ``` #[func] pub fn today( - /// The engine. engine: &mut Engine, /// An offset to apply to the current UTC date. If set to `{auto}`, the /// offset will be the local offset. diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 40c826df9..cb3eba161 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -334,8 +334,6 @@ impl Func { #[func] pub fn with( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to apply to the function. #[external] @@ -361,8 +359,6 @@ impl Func { #[func] pub fn where_( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The fields to filter for. #[variadic] diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index d960a666c..2c3730d53 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -266,7 +266,6 @@ impl assert { /// ``` #[func(title = "Evaluate")] pub fn eval( - /// The engine. engine: &mut Engine, /// A string of Typst code to evaluate. source: Spanned, diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index adf23a47c..d41261edc 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -152,7 +152,6 @@ impl Plugin { /// Creates a new plugin from a WebAssembly file. #[func(constructor)] pub fn construct( - /// The engine. engine: &mut Engine, /// A path to a WebAssembly file or raw WebAssembly bytes. /// diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 2e90b3071..551ac04f5 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -425,9 +425,7 @@ impl Str { #[func] pub fn replace( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The pattern to search for. pattern: StrPattern, diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 72800f311..3045970de 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -187,7 +187,6 @@ impl Symbol { /// ``` #[func(constructor)] pub fn construct( - /// The callsite span. span: Span, /// The variants of the symbol. /// diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index b743ea1ab..5841552e4 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -30,9 +30,6 @@ pub struct TargetElem { /// Returns the current compilation target. #[func(contextual)] -pub fn target( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn target(context: Tracked) -> HintedStrResult { Ok(TargetElem::target_in(context.styles()?)) } diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index d26a9f9f5..5432df238 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -428,11 +428,8 @@ impl Counter { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -444,11 +441,8 @@ impl Counter { #[func(contextual)] pub fn display( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The call span of the display. span: Span, /// A [numbering pattern or a function]($numbering), which specifies how /// to display the counter. If given a function, that function receives @@ -482,11 +476,8 @@ impl Counter { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the counter's value should be retrieved. selector: LocatableSelector, @@ -500,11 +491,8 @@ impl Counter { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -528,7 +516,6 @@ impl Counter { #[func] pub fn step( self, - /// The call span of the update. span: Span, /// The depth at which to step the counter. Defaults to `{1}`. #[named] @@ -545,7 +532,6 @@ impl Counter { #[func] pub fn update( self, - /// The call span of the update. span: Span, /// If given an integer or array of integers, sets the counter to that /// value. If given a function, that function receives the previous diff --git a/crates/typst-library/src/introspection/here.rs b/crates/typst-library/src/introspection/here.rs index 9d6133816..510093247 100644 --- a/crates/typst-library/src/introspection/here.rs +++ b/crates/typst-library/src/introspection/here.rs @@ -44,9 +44,6 @@ use crate::introspection::Location; /// ``` /// Refer to the [`selector`] type for more details on before/after selectors. #[func(contextual)] -pub fn here( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn here(context: Tracked) -> HintedStrResult { context.location() } diff --git a/crates/typst-library/src/introspection/locate.rs b/crates/typst-library/src/introspection/locate.rs index f6631b021..50f217851 100644 --- a/crates/typst-library/src/introspection/locate.rs +++ b/crates/typst-library/src/introspection/locate.rs @@ -24,9 +24,7 @@ use crate::introspection::Location; /// ``` #[func(contextual)] pub fn locate( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// A selector that should match exactly one element. This element will be /// located. diff --git a/crates/typst-library/src/introspection/query.rs b/crates/typst-library/src/introspection/query.rs index f616208c5..b742ac010 100644 --- a/crates/typst-library/src/introspection/query.rs +++ b/crates/typst-library/src/introspection/query.rs @@ -136,9 +136,7 @@ use crate::foundations::{func, Array, Context, LocatableSelector, Value}; /// ``` #[func(contextual)] pub fn query( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Can be /// - an element function like a `heading` or `figure`, diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index e6ab926bf..cc3f566b5 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -289,11 +289,8 @@ impl State { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -309,11 +306,8 @@ impl State { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the state's value should be retrieved. selector: LocatableSelector, @@ -326,11 +320,8 @@ impl State { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -349,7 +340,6 @@ impl State { #[func] pub fn update( self, - /// The span of the `update` call. span: Span, /// If given a non function-value, sets the state to that value. If /// given a function, that function receives the previous state and has diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 05e4f6d9b..cde3187d3 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -54,7 +54,6 @@ use crate::layout::{BlockElem, Size}; /// corresponding page dimension is set to `{auto}`. #[func] pub fn layout( - /// The call span of this function. span: Span, /// A function to call with the outer container's size. Its return value is /// displayed in the document. diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs index 0c6071eb0..93c48ad40 100644 --- a/crates/typst-library/src/layout/measure.rs +++ b/crates/typst-library/src/layout/measure.rs @@ -43,11 +43,8 @@ use crate::layout::{Abs, Axes, Length, Region, Size}; /// `height`, both of type [`length`]. #[func(contextual)] pub fn measure( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The width available to layout the content. /// diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 13d551201..2bdeb80ef 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -19,7 +19,6 @@ use crate::loading::{DataSource, Load}; /// floating point numbers, which may result in an approximative value. #[func(scope, title = "CBOR")] pub fn cbor( - /// The engine. engine: &mut Engine, /// A path to a CBOR file or raw CBOR bytes. /// @@ -40,7 +39,6 @@ impl cbor { /// directly. #[func(title = "Decode CBOR")] pub fn decode( - /// The engine. engine: &mut Engine, /// CBOR data. data: Spanned, diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 8171c4832..e5dabfaa6 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -25,7 +25,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "CSV")] pub fn csv( - /// The engine. engine: &mut Engine, /// Path to a CSV file or raw CSV bytes. /// @@ -102,7 +101,6 @@ impl csv { /// directly. #[func(title = "Decode CSV")] pub fn decode( - /// The engine. engine: &mut Engine, /// CSV data. data: Spanned, diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 3128d77da..035c5e4a7 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -50,7 +50,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "JSON")] pub fn json( - /// The engine. engine: &mut Engine, /// Path to a JSON file or raw JSON bytes. /// @@ -71,7 +70,6 @@ impl json { /// directly. #[func(title = "Decode JSON")] pub fn decode( - /// The engine. engine: &mut Engine, /// JSON data. data: Spanned, diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index bf363f846..32dadc799 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -24,7 +24,6 @@ use crate::World; /// ``` #[func] pub fn read( - /// The engine. engine: &mut Engine, /// Path to a file. /// diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index e3a01cdd5..402207b02 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -28,7 +28,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "TOML")] pub fn toml( - /// The engine. engine: &mut Engine, /// A path to a TOML file or raw TOML bytes. /// @@ -50,7 +49,6 @@ impl toml { /// directly. #[func(title = "Decode TOML")] pub fn decode( - /// The engine. engine: &mut Engine, /// TOML data. data: Spanned, diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 53ec3d93b..ca467c238 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -57,7 +57,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "XML")] pub fn xml( - /// The engine. engine: &mut Engine, /// A path to an XML file or raw XML bytes. /// @@ -83,7 +82,6 @@ impl xml { /// directly. #[func(title = "Decode XML")] pub fn decode( - /// The engine. engine: &mut Engine, /// XML data. data: Spanned, diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 2eb26be8f..5767cb640 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -40,7 +40,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "YAML")] pub fn yaml( - /// The engine. engine: &mut Engine, /// A path to a YAML file or raw YAML bytes. /// @@ -61,7 +60,6 @@ impl yaml { /// directly. #[func(title = "Decode YAML")] pub fn decode( - /// The engine. engine: &mut Engine, /// YAML data. data: Spanned, diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs index e25c6d423..ad111700b 100644 --- a/crates/typst-library/src/math/root.rs +++ b/crates/typst-library/src/math/root.rs @@ -10,7 +10,6 @@ use crate::math::Mathy; /// ``` #[func(title = "Square Root")] pub fn sqrt( - /// The call span of this function. span: Span, /// The expression to take the square root of. radicand: Content, diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 4e2fe4579..150506758 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -53,9 +53,7 @@ use crate::text::Case; /// ``` #[func] pub fn numbering( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Defines how the numbering works. /// diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index 8ff8dbdbc..b14312513 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -248,8 +248,6 @@ impl Color { /// ``` #[func] pub fn luma( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -300,8 +298,6 @@ impl Color { /// ``` #[func] pub fn oklab( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -358,8 +354,6 @@ impl Color { /// ``` #[func] pub fn oklch( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -420,8 +414,6 @@ impl Color { /// ``` #[func(title = "Linear RGB")] pub fn linear_rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -477,8 +469,6 @@ impl Color { /// ``` #[func(title = "RGB")] pub fn rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -555,8 +545,6 @@ impl Color { /// ``` #[func(title = "CMYK")] pub fn cmyk( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The cyan component. #[external] @@ -614,8 +602,6 @@ impl Color { /// ``` #[func(title = "HSL")] pub fn hsl( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -673,8 +659,6 @@ impl Color { /// ``` #[func(title = "HSV")] pub fn hsv( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -898,7 +882,6 @@ impl Color { #[func] pub fn saturate( self, - /// The call span span: Span, /// The factor to saturate the color by. factor: Ratio, @@ -924,7 +907,6 @@ impl Color { #[func] pub fn desaturate( self, - /// The call span span: Span, /// The factor to desaturate the color by. factor: Ratio, @@ -1001,7 +983,6 @@ impl Color { #[func] pub fn rotate( self, - /// The call span span: Span, /// The angle to rotate the hue by. angle: Angle, diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index e16e5d88a..431f07dd4 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -200,9 +200,7 @@ impl Gradient { /// ``` #[func(title = "Linear Gradient")] pub fn linear( - /// The args of this function. args: &mut Args, - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -292,7 +290,6 @@ impl Gradient { /// ``` #[func] fn radial( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -407,7 +404,6 @@ impl Gradient { /// ``` #[func] pub fn conic( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 0f0602011..77f8426e4 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -112,7 +112,6 @@ impl ImageElem { /// ``` #[func(title = "Decode Image")] pub fn decode( - /// The call span of this function. span: Span, /// The data to decode as an image. Can be a string for SVGs. data: Readable, diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index 465f2c1a7..42b083431 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -67,8 +67,8 @@ impl PolygonElem { /// ``` #[func(title = "Regular Polygon")] pub fn regular( - /// The call span of this function. span: Span, + /// How to fill the polygon. See the general /// [polygon's documentation]($polygon.fill) for more details. #[named] diff --git a/crates/typst-library/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs index 97a1535db..a0830cf19 100644 --- a/crates/typst-library/src/visualize/stroke.rs +++ b/crates/typst-library/src/visualize/stroke.rs @@ -97,8 +97,6 @@ impl Stroke { /// ``` #[func(constructor)] pub fn construct( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The color or gradient to use for the stroke. diff --git a/crates/typst-library/src/visualize/tiling.rs b/crates/typst-library/src/visualize/tiling.rs index d699d3b6d..98a71f927 100644 --- a/crates/typst-library/src/visualize/tiling.rs +++ b/crates/typst-library/src/visualize/tiling.rs @@ -138,7 +138,6 @@ impl Tiling { #[func(constructor)] pub fn construct( engine: &mut Engine, - /// The callsite span. span: Span, /// The bounding box of each cell of the tiling. #[named] From b45f574703f674c962e8678b4af0aabe081216a1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 22 Jan 2025 13:58:57 +0100 Subject: [PATCH 22/44] Move no-hyphenation style in link from show to show-set rule (#5731) --- crates/typst-library/src/model/link.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 4558cb394..5df6bead4 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -6,8 +6,8 @@ use smallvec::SmallVec; use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Label, NativeElement, Packed, Repr, Show, Smart, StyleChain, - TargetElem, + cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart, + StyleChain, Styles, TargetElem, }; use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; @@ -16,7 +16,7 @@ use crate::text::{Hyphenate, TextElem}; /// Links to a URL or a location in the document. /// -/// By default, links are not styled any different from normal text. However, +/// By default, links do not look any different from normal text. However, /// you can easily apply a style of your choice with a show rule. /// /// # Example @@ -31,6 +31,11 @@ use crate::text::{Hyphenate, TextElem}; /// ] /// ``` /// +/// # Hyphenation +/// If you enable hyphenation or justification, by default, it will not apply to +/// links to prevent unwanted hyphenation in URLs. You can opt out of this +/// default via `{show link: set text(hyphenate: true)}`. +/// /// # Syntax /// This function also has dedicated syntax: Text that starts with `http://` or /// `https://` is automatically turned into a link. @@ -119,20 +124,26 @@ impl Show for Packed { body } } else { - let linked = match &self.dest { + match &self.dest { LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Label(label) => { let elem = engine.introspector.query_label(*label).at(self.span())?; let dest = Destination::Location(elem.location().unwrap()); body.clone().linked(dest) } - }; - - linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))) + } }) } } +impl ShowSet for Packed { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out + } +} + fn body_from_url(url: &Url) -> Content { let text = ["mailto:", "tel:"] .into_iter() From 6fcc4322845482c1810c26ee7f6fc8f6fed20d7d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 22 Jan 2025 14:24:14 +0100 Subject: [PATCH 23/44] Don't link items if container is already linked (#5732) --- crates/typst-layout/src/flow/collect.rs | 32 ++--- crates/typst-layout/src/inline/collect.rs | 20 ++-- crates/typst-layout/src/inline/line.rs | 25 ++-- crates/typst-layout/src/inline/mod.rs | 2 +- crates/typst-layout/src/inline/shaping.rs | 2 + crates/typst-layout/src/lib.rs | 1 + crates/typst-layout/src/math/fragment.rs | 15 +-- crates/typst-layout/src/math/stretch.rs | 3 +- crates/typst-layout/src/modifiers.rs | 110 ++++++++++++++++++ .../typst-library/src/foundations/content.rs | 3 +- crates/typst-library/src/layout/frame.rs | 52 +-------- crates/typst-library/src/model/link.rs | 5 +- tests/ref/issue-758-link-repeat.png | Bin 0 -> 1836 bytes tests/ref/link-empty-block.png | Bin 0 -> 96 bytes tests/ref/link-on-block.png | Bin 2422 -> 2355 bytes tests/suite/model/link.typ | 11 ++ 16 files changed, 184 insertions(+), 97 deletions(-) create mode 100644 crates/typst-layout/src/modifiers.rs create mode 100644 tests/ref/issue-758-link-repeat.png create mode 100644 tests/ref/link-empty-block.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 12cfa152e..76d7b7433 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -22,6 +22,7 @@ use typst_library::text::TextElem; use typst_library::World; use super::{layout_multi_block, layout_single_block}; +use crate::modifiers::layout_and_modify; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. @@ -377,8 +378,9 @@ fn layout_single_impl( route: Route::extend(route), }; - layout_single_block(elem, &mut engine, locator, styles, region) - .map(|frame| frame.post_processed(styles)) + layout_and_modify(styles, |styles| { + layout_single_block(elem, &mut engine, locator, styles, region) + }) } /// A child that encapsulates a prepared breakable block. @@ -473,11 +475,8 @@ fn layout_multi_impl( route: Route::extend(route), }; - layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { - for frame in &mut fragment { - frame.post_process(styles); - } - fragment + layout_and_modify(styles, |styles| { + layout_multi_block(elem, &mut engine, locator, styles, regions) }) } @@ -579,20 +578,23 @@ impl PlacedChild<'_> { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); + let styles = self.styles.chain(&aligned); - let mut frame = crate::layout_frame( - engine, - &self.elem.body, - self.locator.relayout(), - self.styles.chain(&aligned), - Region::new(base, Axes::splat(false)), - )?; + let mut frame = layout_and_modify(styles, |styles| { + crate::layout_frame( + engine, + &self.elem.body, + self.locator.relayout(), + styles, + Region::new(base, Axes::splat(false)), + ) + })?; if self.float { frame.set_parent(self.elem.location().unwrap()); } - Ok(frame.post_processed(self.styles)) + Ok(frame) }) } diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index fcf7508e9..6023f5c63 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -13,6 +13,7 @@ use typst_syntax::Span; use typst_utils::Numeric; use super::*; +use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify}; // The characters by which spacing, inline content and pins are replaced in the // paragraph's full text. @@ -36,7 +37,7 @@ pub enum Item<'a> { /// Fractional spacing between other items. Fractional(Fr, Option<(&'a Packed, Locator<'a>, StyleChain<'a>)>), /// Layouted inline-level content. - Frame(Frame, StyleChain<'a>), + Frame(Frame), /// A tag. Tag(&'a Tag), /// An item that is invisible and needs to be skipped, e.g. a Unicode @@ -67,7 +68,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.text, Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE, - Self::Frame(_, _) => OBJ_REPLACE, + Self::Frame(_) => OBJ_REPLACE, Self::Tag(_) => "", Self::Skip(s) => s, } @@ -83,7 +84,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.width, Self::Absolute(v, _) => *v, - Self::Frame(frame, _) => frame.width(), + Self::Frame(frame) => frame.width(), Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Skip(_) => Abs::zero(), } @@ -210,8 +211,10 @@ pub fn collect<'a>( InlineItem::Space(space, weak) => { collector.push_item(Item::Absolute(space, weak)); } - InlineItem::Frame(frame) => { - collector.push_item(Item::Frame(frame, styles)); + InlineItem::Frame(mut frame) => { + frame.modify(&FrameModifiers::get_in(styles)); + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } } @@ -222,8 +225,11 @@ pub fn collect<'a>( if let Sizing::Fr(v) = elem.width(styles) { collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } else { - let frame = layout_box(elem, engine, loc, styles, region)?; - collector.push_item(Item::Frame(frame, styles)); + let mut frame = layout_and_modify(styles, |styles| { + layout_box(elem, engine, loc, styles, region) + })?; + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } else if let Some(elem) = child.to_packed::() { collector.push_item(Item::Tag(&elem.tag)); diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index ef7e26c3c..fba4bef80 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -10,6 +10,7 @@ use typst_library::text::{Lang, TextElem}; use typst_utils::Numeric; use super::*; +use crate::modifiers::layout_and_modify; const SHY: char = '\u{ad}'; const HYPHEN: char = '-'; @@ -93,7 +94,7 @@ impl Line<'_> { pub fn has_negative_width_items(&self) -> bool { self.items.iter().any(|item| match item { Item::Absolute(amount, _) => *amount < Abs::zero(), - Item::Frame(frame, _) => frame.width() < Abs::zero(), + Item::Frame(frame) => frame.width() < Abs::zero(), _ => false, }) } @@ -409,6 +410,11 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { } } +/// Apply the current baseline shift to a frame. +pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) { + frame.translate(Point::with_y(TextElem::baseline_in(styles))); +} + /// Commit to a line and build its frame. #[allow(clippy::too_many_arguments)] pub fn commit( @@ -509,10 +515,11 @@ pub fn commit( let amount = v.share(fr, remaining); if let Some((elem, loc, styles)) = elem { let region = Size::new(amount, full); - let mut frame = - layout_box(elem, engine, loc.relayout(), *styles, region)?; - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + let mut frame = layout_and_modify(*styles, |styles| { + layout_box(elem, engine, loc.relayout(), styles, region) + })?; + apply_baseline_shift(&mut frame, *styles); + push(&mut offset, frame); } else { offset += amount; } @@ -524,12 +531,10 @@ pub fn commit( justification_ratio, extra_justification, ); - push(&mut offset, frame.post_processed(shaped.styles)); + push(&mut offset, frame); } - Item::Frame(frame, styles) => { - let mut frame = frame.clone(); - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + Item::Frame(frame) => { + push(&mut offset, frame.clone()); } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 658e30846..bedc54d63 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -23,7 +23,7 @@ use typst_library::World; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; use self::finalize::finalize; -use self::line::{commit, line, Line}; +use self::line::{apply_baseline_shift, commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index d6b7632b6..2ed95f14f 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -20,6 +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}; /// The result of shaping text. /// @@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> { offset += width; } + frame.modify(&FrameModifiers::get_in(self.styles)); frame } diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 2e8c1129b..56d7afe11 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -6,6 +6,7 @@ mod image; mod inline; mod lists; mod math; +mod modifiers; mod pad; mod pages; mod repeat; diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index a0453c14f..81b726bad 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,23 +1,22 @@ use std::fmt::{self, Debug, Formatter}; use rustybuzz::Feature; -use smallvec::SmallVec; use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::{GlyphId, Rect}; use typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ - Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, + Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, }; use typst_library::math::{EquationElem, MathSize}; -use typst_library::model::{Destination, LinkElem}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::visualize::Paint; use typst_syntax::Span; use unicode_math_class::MathClass; use super::{stretch_glyph, MathContext, Scaled}; +use crate::modifiers::{FrameModifiers, FrameModify}; #[derive(Debug, Clone)] pub enum MathFragment { @@ -245,8 +244,7 @@ pub struct GlyphFragment { pub class: MathClass, pub math_size: MathSize, pub span: Span, - pub dests: SmallVec<[Destination; 1]>, - pub hidden: bool, + pub modifiers: FrameModifiers, pub limits: Limits, pub extended_shape: bool, } @@ -302,8 +300,7 @@ impl GlyphFragment { accent_attach: Abs::zero(), class, span, - dests: LinkElem::dests_in(styles), - hidden: HideElem::hidden_in(styles), + modifiers: FrameModifiers::get_in(styles), extended_shape: false, }; fragment.set_id(ctx, id); @@ -390,7 +387,7 @@ impl GlyphFragment { let mut frame = Frame::soft(size); frame.set_baseline(self.ascent); frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); - frame.post_process_raw(self.dests, self.hidden); + frame.modify(&self.modifiers); frame } @@ -516,7 +513,7 @@ impl FrameFragment { let base_ascent = frame.ascent(); let accent_attach = frame.width() / 2.0; Self { - frame: frame.post_processed(styles), + frame: frame.modified(&FrameModifiers::get_in(styles)), font_size: TextElem::size_in(styles), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), math_size: EquationElem::size_in(styles), diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 6379bdb2e..dafa8cbe8 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -10,6 +10,7 @@ use super::{ delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled, VariantFragment, }; +use crate::modifiers::FrameModify; /// Maximum number of times extenders can be repeated. const MAX_REPEATS: usize = 1024; @@ -265,7 +266,7 @@ fn assemble( let mut frame = Frame::soft(size); let mut offset = Abs::zero(); frame.set_baseline(baseline); - frame.post_process_raw(base.dests, base.hidden); + frame.modify(&base.modifiers); for (fragment, advance) in selected { let pos = match axis { diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs new file mode 100644 index 000000000..ac5f40b04 --- /dev/null +++ b/crates/typst-layout/src/modifiers.rs @@ -0,0 +1,110 @@ +use typst_library::foundations::StyleChain; +use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; +use typst_library::model::{Destination, LinkElem}; + +/// Frame-level modifications resulting from styles that do not impose any +/// layout structure. +/// +/// These are always applied at the highest level of style uniformity. +/// Consequently, they must be applied by all layouters that manually manage +/// styles of their children (because they can produce children with varying +/// styles). This currently includes flow, inline, and math layout. +/// +/// Other layouters don't manually need to handle it because their parents that +/// result from realization will take care of it and the styles can only apply +/// to them as a whole, not part of it (since they don't manage styles). +/// +/// Currently existing frame modifiers are: +/// - `HideElem::hidden` +/// - `LinkElem::dests` +#[derive(Debug, Clone)] +pub struct FrameModifiers { + /// A destination to link to. + dest: Option, + /// Whether the contents of the frame should be hidden. + hidden: bool, +} + +impl FrameModifiers { + /// Retrieve all modifications that should be applied per-frame. + pub fn get_in(styles: StyleChain) -> Self { + Self { + dest: LinkElem::current_in(styles), + hidden: HideElem::hidden_in(styles), + } + } +} + +/// Applies [`FrameModifiers`]. +pub trait FrameModify { + /// Apply the modifiers in-place. + fn modify(&mut self, modifiers: &FrameModifiers); + + /// Apply the modifiers, and return the modified result. + fn modified(mut self, modifiers: &FrameModifiers) -> Self + where + Self: Sized, + { + self.modify(modifiers); + self + } +} + +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(); + } + } +} + +impl FrameModify for Fragment { + fn modify(&mut self, modifiers: &FrameModifiers) { + for frame in self.iter_mut() { + frame.modify(modifiers); + } + } +} + +impl FrameModify for Result +where + T: FrameModify, +{ + fn modify(&mut self, props: &FrameModifiers) { + if let Ok(inner) = self { + inner.modify(props); + } + } +} + +/// Performs layout and modification in one step. +/// +/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, +/// but with the additional step that redundant modifiers (which are already +/// applied here) are removed from the `styles` passed to `layout`. This is used +/// for the layout of containers like `block`. +pub fn layout_and_modify(styles: StyleChain, layout: F) -> R +where + F: FnOnce(StyleChain) -> R, + R: FrameModify, +{ + let modifiers = FrameModifiers::get_in(styles); + + // Disable the current link internally since it's already applied at this + // level of layout. This means we don't generate redundant nested links, + // which may bloat the output considerably. + let reset; + let outer = styles; + let mut styles = styles; + if modifiers.dest.is_some() { + reset = LinkElem::set_current(None).wrap(); + styles = outer.chain(&reset); + } + + layout(styles).modified(&modifiers) +} diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index ab2f68ac2..76cd6a222 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use comemo::Tracked; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; -use smallvec::smallvec; use typst_syntax::Span; use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; @@ -500,7 +499,7 @@ impl Content { /// Link the content somewhere. pub fn linked(self, dest: Destination) -> Self { - self.styled(LinkElem::set_dests(smallvec![dest])) + self.styled(LinkElem::set_current(Some(dest))) } /// Set alignments for this content. diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs index e57eb27e8..a26a7d0ef 100644 --- a/crates/typst-library/src/layout/frame.rs +++ b/crates/typst-library/src/layout/frame.rs @@ -4,16 +4,13 @@ use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; use std::sync::Arc; -use smallvec::SmallVec; use typst_syntax::Span; use typst_utils::{LazyHash, Numeric}; -use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; +use crate::foundations::{cast, dict, Dict, Label, Value}; use crate::introspection::{Location, Tag}; -use crate::layout::{ - Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform, -}; -use crate::model::{Destination, LinkElem}; +use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform}; +use crate::model::Destination; use crate::text::TextItem; use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape}; @@ -304,49 +301,6 @@ impl Frame { } } - /// Apply late-stage properties from the style chain to this frame. This - /// includes: - /// - `HideElem::hidden` - /// - `LinkElem::dests` - /// - /// This must be called on all frames produced by elements - /// that manually handle styles (because their children can have varying - /// styles). This currently includes flow, par, and equation. - /// - /// Other elements don't manually need to handle it because their parents - /// that result from realization will take care of it and the styles can - /// only apply to them as a whole, not part of it (because they don't manage - /// styles). - pub fn post_processed(mut self, styles: StyleChain) -> Self { - self.post_process(styles); - self - } - - /// Post process in place. - pub fn post_process(&mut self, styles: StyleChain) { - if !self.is_empty() { - self.post_process_raw( - LinkElem::dests_in(styles), - HideElem::hidden_in(styles), - ); - } - } - - /// Apply raw late-stage properties from the raw data. - pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) { - if !self.is_empty() { - let size = self.size; - self.push_multiple( - dests - .into_iter() - .map(|dest| (Point::zero(), FrameItem::Link(dest, size))), - ); - if hide { - self.hide(); - } - } - } - /// Hide all content in the frame, but keep metadata. pub fn hide(&mut self) { Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item { diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 5df6bead4..24b746b7e 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -1,7 +1,6 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; -use smallvec::SmallVec; use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; @@ -90,10 +89,10 @@ pub struct LinkElem { })] pub body: Content, - /// This style is set on the content contained in the `link` element. + /// A destination style that should be applied to elements. #[internal] #[ghost] - pub dests: SmallVec<[Destination; 1]>, + pub current: Option, } impl LinkElem { diff --git a/tests/ref/issue-758-link-repeat.png b/tests/ref/issue-758-link-repeat.png new file mode 100644 index 0000000000000000000000000000000000000000..aaec20d23cb31d58004b500b11d5ea28635ae274 GIT binary patch literal 1836 zcmV+{2h;e8P)XxL|x*>o^{nk*BEk@pKlbgx8XU?6O`=6OJ_s)O1{iUQ+q?>|3 z0j+>mqJUOFE1;DqpcT+cIt%EEii#&so{;`e&HDK93?hEZhFPV#RaG3%a`leO-)Vd0Apig^u2rcl6G7yEG(u@ zooa1uJ!Hs`?%lgPI5-R&Hf-|b$=W_-4IMfZWzwWcqLy!6{{0<#_3A~+pgwZs$fZk{ z)~s2Rk&z*DLqkKcpd34P?AoVrOUf;K75nYuBz=vBJm4N8A45 z#S0V$X;@g;fddD|j2V-VkdU37ee>o`hN_2$$IF*5+ZV`FL9O!a*)w6edi5&lfB^$e zo;=y!|IndB8#iuTwQAL#J$o7(8x0od9zA-D9zEK$RX%j@JkEL^yd4v>$Cj`_Y-7A#mGEOBvh)22e;jB@ZrN*Dh&;2CU{9n2}*5kZCza*3Msm+3~1D5MBsdhp=EnVFfQB6D+d6r`%EN*Lqg<4Kc~lWA~n zZfB+ux=T2l~ z?c2AltgJ?i7{PZQYi3AD2oUVrwd?%(^Syib-nVbx_3PI`!N$gB>C&Y{6&BA?qefAt zMw9oOy9Z-`{rdI5cjd|zUVs9}jvagX^5sE;1}$2&h*zcjxpU_-c)@Gkx^?~f_2UzW zz%pmfoN?pEiRR_xEj^?Zrw_xBI40n<`%Ek)YQZj^z-v0Qk^()f{;Z_TefT&y-4j0o#^Q3 zl`B`CJ9lpX{{1Xkgj3#*`D!=MRu7WuheiDvDxj4ppcT*xXa#`+S^=#@0j+>mKr87A zK$nzOs;h(7e*zkYMqOWt%2LuPqJZvtK(o~y-YRe2yb%^Qhy@#>M?5_}+3NjVE=LI{ zkX?4M^^VT=w+{1g`t)gd19qi1Z{Ez7x8Vala^%R}yLV5SG6l|wJ#Q>CX3QukDA4~D z{?Hbm%VjJ5@ZrOdWa!WWaJw)v+79poFb0q~?Bzp_u$^!CKtmuwh`@j>S+bXKy}&x(FdsrSq}e8 zM;a#35*ip%SW$TvK*B+U<@VjXcMM@U+3{y!a)bk27TWTjuD-Y8FVbRwzjOdkn02=7QpmYp%8*TRJ60RGh{ZSo|89@j*haZ zg9i_?5^&0gW>6G!4kLA8ynp}xKN_ipsUlr~44$rjF6*2UngIE&9=ZSk literal 0 HcmV?d00001 diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png index 8fb7f6c6690680bda5cb700dd4718ab3e0ea043d..eeeb264b9b6696d744a72483832d0a6a0615f748 100644 GIT binary patch delta 2345 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 engsoGZ2tj9SW#bQ3K2K}0000 Text // Error: 2-20 label `` occurs multiple times in the document #link()[Nope.] + +--- link-empty-block --- +#link("", block(height: 10pt, width: 100%)) + +--- issue-758-link-repeat --- +#let url = "https://typst.org/" +#let body = [Hello #box(width: 1fr, repeat[.])] + +Inline: #link(url, body) + +#link(url, block(inset: 4pt, [Block: ] + body)) From 1bd8ff0e0fa7966f4bd2a4426241781bed168df7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 23 Jan 2025 11:16:04 +0100 Subject: [PATCH 24/44] Methods on elements (#5733) --- crates/typst-eval/src/call.rs | 24 ++++++++++++++++++++++-- tests/suite/scripting/methods.typ | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 0a9e1c486..69b274bbc 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -325,6 +325,13 @@ fn eval_field_call( } else if let Some(callee) = target.ty().scope().get(&field) { args.insert(0, target_expr.span(), target); Ok(FieldCall::Normal(callee.clone(), args)) + } else if let Value::Content(content) = &target { + if let Some(callee) = content.elem().scope().get(&field) { + args.insert(0, target_expr.span(), target); + Ok(FieldCall::Normal(callee.clone(), args)) + } else { + bail!(missing_field_call_error(target, field)) + } } else if matches!( target, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) @@ -341,8 +348,20 @@ fn eval_field_call( /// Produce an error when we cannot call the field. fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { - let mut error = - error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str()); + let mut error = match &target { + Value::Content(content) => error!( + field.span(), + "element {} has no method `{}`", + content.elem().name(), + field.as_str(), + ), + _ => error!( + field.span(), + "type {} has no method `{}`", + target.ty(), + field.as_str() + ), + }; match target { Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => { @@ -360,6 +379,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { } _ => {} } + error } diff --git a/tests/suite/scripting/methods.typ b/tests/suite/scripting/methods.typ index 5deea2cfa..566e9d9a5 100644 --- a/tests/suite/scripting/methods.typ +++ b/tests/suite/scripting/methods.typ @@ -31,7 +31,7 @@ #numbers.fun() --- method-unknown-but-field-exists --- -// Error: 2:4-2:10 type content has no method `stroke` +// Error: 2:4-2:10 element line has no method `stroke` // Hint: 2:4-2:10 did you mean to access the field `stroke`? #let l = line(stroke: red) #l.stroke() From 52ee33a275063369673d8802fb820db3825a661f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 23 Jan 2025 12:50:51 +0100 Subject: [PATCH 25/44] Rework outline (#5735) --- crates/typst-library/src/layout/repeat.rs | 2 +- crates/typst-library/src/math/equation.rs | 37 +- crates/typst-library/src/model/figure.rs | 61 +- crates/typst-library/src/model/heading.rs | 54 +- crates/typst-library/src/model/outline.rs | 817 +++++++++++------- crates/typst-library/src/model/terms.rs | 11 +- crates/typst-utils/src/lib.rs | 9 + tests/ref/heading-hanging-indent-auto.png | Bin 0 -> 849 bytes tests/ref/heading-hanging-indent-length.png | Bin 0 -> 1396 bytes tests/ref/heading-hanging-indent-zero.png | Bin 0 -> 859 bytes .../ref/issue-1041-smartquotes-in-outline.png | Bin 3467 -> 3412 bytes tests/ref/issue-2048-outline-multiline.png | Bin 0 -> 1634 bytes ...6-outline-rtl-title-ending-in-ltr-text.png | Bin 0 -> 3341 bytes ...ssue-4476-rtl-title-ending-in-ltr-text.png | Bin 6307 -> 0 bytes .../ref/issue-4859-outline-entry-show-set.png | Bin 0 -> 749 bytes tests/ref/issue-5176-cjk-title.png | Bin 1246 -> 0 bytes tests/ref/issue-5176-outline-cjk-title.png | Bin 0 -> 1218 bytes ...-5370-figure-caption-separator-outline.png | Bin 2078 -> 0 bytes tests/ref/issue-622-hide-meta-outline.png | Bin 2109 -> 2061 bytes tests/ref/issue-785-cite-locate.png | Bin 9191 -> 9441 bytes tests/ref/outline-bookmark.png | Bin 1030 -> 474 bytes tests/ref/outline-entry-complex.png | Bin 14460 -> 8461 bytes tests/ref/outline-entry-inner.png | Bin 0 -> 462 bytes tests/ref/outline-entry.png | Bin 10099 -> 5890 bytes tests/ref/outline-first-line-indent.png | Bin 10837 -> 5539 bytes tests/ref/outline-heading-start-of-page.png | Bin 0 -> 6935 bytes ...outline-indent-auto-mixed-prefix-short.png | Bin 0 -> 1045 bytes .../ref/outline-indent-auto-mixed-prefix.png | Bin 0 -> 5712 bytes tests/ref/outline-indent-auto-no-prefix.png | Bin 0 -> 3101 bytes tests/ref/outline-indent-auto.png | Bin 0 -> 5176 bytes tests/ref/outline-indent-fixed.png | Bin 0 -> 3018 bytes tests/ref/outline-indent-func.png | Bin 0 -> 2884 bytes tests/ref/outline-indent-no-numbering.png | Bin 2924 -> 0 bytes tests/ref/outline-indent-numbering.png | Bin 7101 -> 0 bytes tests/ref/outline-indent-zero.png | Bin 0 -> 3465 bytes tests/ref/outline-spacing.png | Bin 0 -> 2553 bytes tests/ref/outline-styled-text.png | Bin 1481 -> 1416 bytes tests/ref/outline.png | Bin 6743 -> 0 bytes tests/ref/query-running-header.png | Bin 9302 -> 9064 bytes tests/suite/model/figure.typ | 6 - tests/suite/model/heading.typ | 12 + tests/suite/model/outline.typ | 344 +++++--- 42 files changed, 831 insertions(+), 522 deletions(-) create mode 100644 tests/ref/heading-hanging-indent-auto.png create mode 100644 tests/ref/heading-hanging-indent-length.png create mode 100644 tests/ref/heading-hanging-indent-zero.png create mode 100644 tests/ref/issue-2048-outline-multiline.png create mode 100644 tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png delete mode 100644 tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png create mode 100644 tests/ref/issue-4859-outline-entry-show-set.png delete mode 100644 tests/ref/issue-5176-cjk-title.png create mode 100644 tests/ref/issue-5176-outline-cjk-title.png delete mode 100644 tests/ref/issue-5370-figure-caption-separator-outline.png create mode 100644 tests/ref/outline-entry-inner.png create mode 100644 tests/ref/outline-heading-start-of-page.png create mode 100644 tests/ref/outline-indent-auto-mixed-prefix-short.png create mode 100644 tests/ref/outline-indent-auto-mixed-prefix.png create mode 100644 tests/ref/outline-indent-auto-no-prefix.png create mode 100644 tests/ref/outline-indent-auto.png create mode 100644 tests/ref/outline-indent-fixed.png create mode 100644 tests/ref/outline-indent-func.png delete mode 100644 tests/ref/outline-indent-no-numbering.png delete mode 100644 tests/ref/outline-indent-numbering.png create mode 100644 tests/ref/outline-indent-zero.png create mode 100644 tests/ref/outline-spacing.png delete mode 100644 tests/ref/outline.png diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs index e423410ab..9579f1856 100644 --- a/crates/typst-library/src/layout/repeat.rs +++ b/crates/typst-library/src/layout/repeat.rs @@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length}; /// Space may be inserted between the instances of the body parameter, so be /// sure to adjust the [`justify`]($repeat.justify) parameter accordingly. /// -/// Errors if there no bounds on the available space, as it would create +/// Errors if there are no bounds on the available space, as it would create /// infinite content. /// /// # Example diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index a9173c433..1e346280a 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -229,35 +229,20 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.block(StyleChain::default()) { - return Ok(None); - } - let Some(numbering) = self.numbering() else { - return Ok(None); - }; - - // After synthesis, this should always be custom content. - let mut supplement = match (**self).supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - }; + fn outlined(&self) -> bool { + self.block(StyleChain::default()) && self.numbering().is_some() + } + fn prefix(&self, numbers: Content) -> Content { + let supplement = self.supplement(); if !supplement.is_empty() { - supplement += TextElem::packed("\u{a0}"); + supplement + TextElem::packed('\u{a0}') + numbers + } else { + numbers } + } - let numbers = self.counter().display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - Ok(Some(supplement + numbers)) + fn body(&self) -> Content { + Content::empty() } } diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 52dca966d..ce7460c9b 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -156,6 +156,7 @@ pub struct FigureElem { pub scope: PlacementScope, /// The figure's caption. + #[borrowed] pub caption: Option>, /// The kind of figure this is. @@ -305,7 +306,7 @@ impl Synthesize for Packed { )); // Fill the figure's caption. - let mut caption = elem.caption(styles); + let mut caption = elem.caption(styles).clone(); if let Some(caption) = &mut caption { caption.synthesize(engine, styles)?; caption.push_kind(kind.clone()); @@ -331,7 +332,7 @@ impl Show for Packed { let mut realized = self.body.clone(); // Build the caption, if any. - if let Some(caption) = self.caption(styles) { + if let Some(caption) = self.caption(styles).clone() { let (first, second) = match caption.position(styles) { OuterVAlignment::Top => (caption.pack(), realized), OuterVAlignment::Bottom => (realized, caption.pack()), @@ -423,46 +424,26 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.outlined(StyleChain::default()) { - return Ok(None); + fn outlined(&self) -> bool { + (**self).outlined(StyleChain::default()) + && (self.caption(StyleChain::default()).is_some() + || self.numbering().is_some()) + } + + fn prefix(&self, numbers: Content) -> Content { + let supplement = self.supplement(); + if !supplement.is_empty() { + supplement + TextElem::packed('\u{a0}') + numbers + } else { + numbers } + } - let Some(caption) = self.caption(StyleChain::default()) else { - return Ok(None); - }; - - let mut realized = caption.body.clone(); - if let ( - Smart::Custom(Some(Supplement::Content(mut supplement))), - Some(Some(counter)), - Some(numbering), - ) = ( - (**self).supplement(StyleChain::default()).clone(), - (**self).counter(), - self.numbering(), - ) { - let numbers = counter.display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - if !supplement.is_empty() { - supplement += TextElem::packed('\u{a0}'); - } - - let separator = caption.get_separator(StyleChain::default()); - - realized = supplement + numbers + separator + caption.body.clone(); - } - - Ok(Some(realized)) + fn body(&self) -> Content { + self.caption(StyleChain::default()) + .as_ref() + .map(|caption| caption.body.clone()) + .unwrap_or_default() } } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index db131afec..00931c815 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,7 +1,7 @@ use std::num::NonZeroUsize; use ecow::eco_format; -use typst_utils::NonZeroExt; +use typst_utils::{Get, NonZeroExt}; use crate::diag::{warning, SourceResult}; use crate::engine::Engine; @@ -13,8 +13,8 @@ use crate::html::{attr, tag, HtmlElem}; use crate::introspection::{ Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, }; -use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region}; -use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; +use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides}; +use crate::model::{Numbering, Outlinable, Refable, Supplement}; use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; /// A section heading. @@ -264,10 +264,6 @@ impl Show for Packed { realized = numbering + spacing + realized; } - if indent != Abs::zero() && !html { - realized = realized.styled(ParElem::set_hanging_indent(indent.into())); - } - Ok(if html { // HTML's h1 is closer to a title element. There should only be one. // Meanwhile, a level 1 Typst heading is a section heading. For this @@ -294,8 +290,17 @@ impl Show for Packed { HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) } } else { - let realized = BlockBody::Content(realized); - BlockElem::new().with_body(Some(realized)).pack().spanned(span) + let block = if indent != Abs::zero() { + let body = HElem::new((-indent).into()).pack() + realized; + let inset = Sides::default() + .with(TextElem::dir_in(styles).start(), Some(indent.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + block.pack().spanned(span) }) } } @@ -351,32 +356,21 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.outlined(StyleChain::default()) { - return Ok(None); - } - - let mut content = self.body.clone(); - if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { - let numbers = Counter::of(HeadingElem::elem()).display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - content = numbers + SpaceElem::shared().clone() + content; - }; - - Ok(Some(content)) + fn outlined(&self) -> bool { + (**self).outlined(StyleChain::default()) } fn level(&self) -> NonZeroUsize { (**self).resolve_level(StyleChain::default()) } + + fn prefix(&self, numbers: Content) -> Content { + numbers + } + + fn body(&self) -> Content { + self.body.clone() + } } impl LocalName for Packed { diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 84661c1c2..0db056e40 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -1,50 +1,61 @@ use std::num::NonZeroUsize; use std::str::FromStr; -use comemo::Track; +use comemo::{Track, Tracked}; +use smallvec::SmallVec; use typst_syntax::Span; -use typst_utils::NonZeroExt; +use typst_utils::{Get, NonZeroExt}; -use crate::diag::{bail, At, SourceResult}; +use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, select_where, Content, Context, Func, LocatableSelector, - NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, + cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func, + LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, + Styles, +}; +use crate::introspection::{ + Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink, }; -use crate::introspection::{Counter, CounterKey, Locatable}; use crate::layout::{ - BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing, + Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel, + RepeatElem, Sides, }; -use crate::model::{ - Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, -}; -use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; +use crate::math::EquationElem; +use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable}; +use crate::text::{LocalName, SpaceElem, TextElem}; /// A table of contents, figures, or other elements. /// /// This function generates a list of all occurrences of an element in the -/// document, up to a given depth. The element's numbering and page number will -/// be displayed in the outline alongside its title or caption. By default this -/// generates a table of contents. +/// document, up to a given [`depth`]($outline.depth). The element's numbering +/// and page number will be displayed in the outline alongside its title or +/// caption. /// /// # Example /// ```example +/// #set heading(numbering: "1.") /// #outline() /// /// = Introduction /// #lorem(5) /// -/// = Prior work +/// = Methods +/// == Setup /// #lorem(10) /// ``` /// /// # Alternative outlines +/// In its default configuration, this function generates a table of contents. /// By setting the `target` parameter, the outline can be used to generate a -/// list of other kinds of elements than headings. In the example below, we list -/// all figures containing images by setting `target` to `{figure.where(kind: -/// image)}`. We could have also set it to just `figure`, but then the list -/// would also include figures containing tables or other material. For more -/// details on the `where` selector, [see here]($function.where). +/// list of other kinds of elements than headings. +/// +/// In the example below, we list all figures containing images by setting +/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set +/// it to `{figure.where(kind: table)}` to generate a list of tables. +/// +/// We could also set it to just `figure`, without using a [`where`]($function.where) +/// selector, but then the list would contain _all_ figures, be it ones +/// containing images, tables, or other material. /// /// ```example /// #outline( @@ -59,16 +70,89 @@ use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; /// ``` /// /// # Styling the outline -/// The outline element has several options for customization, such as its -/// `title` and `indent` parameters. If desired, however, it is possible to have -/// more control over the outline's look and style through the -/// [`outline.entry`]($outline.entry) element. -#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)] +/// At the most basic level, you can style the outline by setting properties on +/// it and its entries. This way, you can customize the outline's +/// [title]($outline.title), how outline entries are +/// [indented]($outline.indent), and how the space between an entry's text and +/// its page number should be [filled]($outline.entry.fill). +/// +/// Richer customization is possible through configuration of the outline's +/// [entries]($outline.entry). The outline generates one entry for each outlined +/// element. +/// +/// ## Spacing the entries { #entry-spacing } +/// Outline entries are [blocks]($block), so you can adjust the spacing between +/// them with normal block-spacing rules: +/// +/// ```example +/// #show outline.entry.where( +/// level: 1 +/// ): set block(above: 1.2em) +/// +/// #outline() +/// +/// = About ACME Corp. +/// == History +/// === Origins +/// = Products +/// == ACME Tools +/// ``` +/// +/// ## Building an outline entry from its parts { #building-an-entry } +/// For full control, you can also write a transformational show rule on +/// `outline.entry`. However, the logic for properly formatting and indenting +/// outline entries is quite complex and the outline entry itself only contains +/// two fields: The level and the outlined element. +/// +/// For this reason, various helper functions are provided. You can mix and +/// match these to compose an entry from just the parts you like. +/// +/// The default show rule for an outline entry looks like this[^1]: +/// ```typ +/// #show outline.entry: it => link( +/// it.element.location(), +/// it.indented(it.prefix(), it.inner()), +/// ) +/// ``` +/// +/// - The [`indented`]($outline.entry.indented) function takes an optional +/// prefix and inner content and automatically applies the proper indentation +/// to it, such that different entries align nicely and long headings wrap +/// properly. +/// +/// - The [`prefix`]($outline.entry.prefix) function formats the element's +/// numbering (if any). It also appends a supplement for certain elements. +/// +/// - The [`inner`]($outline.entry.inner) function combines the element's +/// [`body`]($outline.entry.body), the filler, and the +/// [`page` number]($outline.entry.page). +/// +/// You can use these individual functions to format the outline entry in +/// different ways. Let's say, you'd like to fully remove the filler and page +/// numbers. To achieve this, you could write a show rule like this: +/// +/// ```example +/// #show outline.entry: it => link( +/// it.element.location(), +/// // Keep just the body, dropping +/// // the fill and the page. +/// it.indented(it.prefix(), it.body()), +/// ) +/// +/// #outline() +/// +/// = About ACME Corp. +/// == History +/// ``` +/// +/// [^1]: The outline of equations is the exception to this rule as it does not +/// have a body and thus does not use indented layout. +#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)] pub struct OutlineElem { /// The title of the outline. /// /// - When set to `{auto}`, an appropriate title for the - /// [text language]($text.lang) will be used. This is the default. + /// [text language]($text.lang) will be used. /// - When set to `{none}`, the outline will not have a title. /// - A custom title can be set by passing content. /// @@ -79,8 +163,10 @@ pub struct OutlineElem { /// The type of element to include in the outline. /// - /// To list figures containing a specific kind of element, like a table, you - /// can write `{figure.where(kind: table)}`. + /// To list figures containing a specific kind of element, like an image or + /// a table, you can specify the desired kind in a [`where`]($function.where) + /// selector. See the section on [alternative outlines]($outline/#alternative-outlines) + /// for more details. /// /// ```example /// #outline( @@ -97,7 +183,7 @@ pub struct OutlineElem { /// caption: [Experiment results], /// ) /// ``` - #[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))] + #[default(LocatableSelector(HeadingElem::elem().select()))] #[borrowed] pub target: LocatableSelector, @@ -121,21 +207,22 @@ pub struct OutlineElem { /// How to indent the outline's entries. /// - /// - `{none}`: No indent - /// - `{auto}`: Indents the numbering of the nested entry with the title of - /// its parent entry. This only has an effect if the entries are numbered - /// (e.g., via [heading numbering]($heading.numbering)). - /// - [Relative length]($relative): Indents the item by this length - /// multiplied by its nesting level. Specifying `{2em}`, for instance, - /// would indent top-level headings (not nested) by `{0em}`, second level + /// - `{auto}`: Indents the numbering/prefix of a nested entry with the + /// title of its parent entry. If the entries are not numbered (e.g., via + /// [heading numbering]($heading.numbering)), this instead simply inserts + /// a fixed amount of `{1.2em}` indent per level. + /// + /// - [Relative length]($relative): Indents the entry by the specified + /// length per nesting level. Specifying `{2em}`, for instance, would + /// indent top-level headings by `{0em}` (not nested), second level /// headings by `{2em}` (nested once), third-level headings by `{4em}` /// (nested twice) and so on. - /// - [Function]($function): You can completely customize this setting with - /// a function. That function receives the nesting level as a parameter - /// (starting at 0 for top-level headings/elements) and can return a - /// relative length or content making up the indent. For example, - /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while - /// `{n => [→ ] * n}` would indent with one arrow per nesting level. + /// + /// - [Function]($function): You can further customize this setting with a + /// function. That function receives the nesting level as a parameter + /// (starting at 0 for top-level headings/elements) and should return a + /// (relative) length. For example, `{n => n * 2em}` would be equivalent + /// to just specifying `{2em}`. /// /// ```example /// #set heading(numbering: "1.a.") @@ -150,11 +237,6 @@ pub struct OutlineElem { /// indent: 2em, /// ) /// - /// #outline( - /// title: [Contents (Function)], - /// indent: n => [→ ] * n, - /// ) - /// /// = About ACME Corp. /// == History /// === Origins @@ -163,20 +245,7 @@ pub struct OutlineElem { /// == Products /// #lorem(10) /// ``` - #[default(None)] - #[borrowed] - pub indent: Option>, - - /// Content to fill the space between the title and the page number. Can be - /// set to `{none}` to disable filling. - /// - /// ```example - /// #outline(fill: line(length: 100%)) - /// - /// = A New Beginning - /// ``` - #[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))] - pub fill: Option, + pub indent: Smart, } #[scope] @@ -188,79 +257,52 @@ impl OutlineElem { impl Show for Packed { #[typst_macros::time(name = "outline", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut seq = vec![ParbreakElem::shared().clone()]; + let span = self.span(); + // Build the outline title. + let mut seq = vec![]; if let Some(title) = self.title(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) }) { seq.push( HeadingElem::new(title) .with_depth(NonZeroUsize::ONE) .pack() - .spanned(self.span()), + .spanned(span), ); } - let indent = self.indent(styles); - let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap()); - - let mut ancestors: Vec<&Content> = vec![]; let elems = engine.introspector.query(&self.target(styles).0); + let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX); - for elem in &elems { - let Some(entry) = OutlineEntry::from_outlinable( - engine, - self.span(), - elem.clone(), - self.fill(styles), - styles, - )? - else { - continue; + // Build the outline entries. + for elem in elems { + let Some(outlinable) = elem.with::() else { + bail!(span, "cannot outline {}", elem.func().name()); }; - if depth < entry.level { - continue; + let level = outlinable.level(); + if outlinable.outlined() && level <= depth { + let entry = OutlineEntry::new(level, elem); + seq.push(entry.pack().spanned(span)); } - - // Deals with the ancestors of the current element. - // This is only applicable for elements with a hierarchy/level. - while ancestors - .last() - .and_then(|ancestor| ancestor.with::()) - .is_some_and(|last| last.level() >= entry.level) - { - ancestors.pop(); - } - - OutlineIndent::apply( - indent, - engine, - &ancestors, - &mut seq, - styles, - self.span(), - )?; - - // Add the overridable outline entry, followed by a line break. - seq.push(entry.pack().spanned(self.span())); - seq.push(LinebreakElem::shared().clone()); - - ancestors.push(elem); } - seq.push(ParbreakElem::shared().clone()); - Ok(Content::sequence(seq)) } } impl ShowSet for Packed { - fn show_set(&self, _: StyleChain) -> Styles { + fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); out.set(HeadingElem::set_outlined(false)); out.set(HeadingElem::set_numbering(None)); out.set(ParElem::set_first_line_indent(Em::new(0.0).into())); + out.set(ParElem::set_justify(false)); + out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into()))); + // Makes the outline itself available to its entries. Should be + // superseded by a proper ancestry mechanism in the future. + out.set(OutlineEntry::set_parent(Some(self.clone()))); out } } @@ -269,93 +311,29 @@ impl LocalName for Packed { const KEY: &'static str = "outline"; } -/// Marks an element as being able to be outlined. This is used to implement the -/// `#outline()` element. -pub trait Outlinable: Refable { - /// Produce an outline item for this element. - fn outline( - &self, - engine: &mut Engine, - - styles: StyleChain, - ) -> SourceResult>; - - /// Returns the nesting level of this element. - fn level(&self) -> NonZeroUsize { - NonZeroUsize::ONE - } -} - /// Defines how an outline is indented. #[derive(Debug, Clone, PartialEq, Hash)] pub enum OutlineIndent { - Rel(Rel), + /// Indents by the specified length per level. + Rel(Rel), + /// Resolve the indent for a specific level through the given function. Func(Func), } impl OutlineIndent { - fn apply( - indent: &Option>, + /// Resolve the indent for an entry with the given level. + fn resolve( + &self, engine: &mut Engine, - ancestors: &Vec<&Content>, - seq: &mut Vec, - styles: StyleChain, + context: Tracked, + level: NonZeroUsize, span: Span, - ) -> SourceResult<()> { - match indent { - // 'none' | 'false' => no indenting - None => {} - - // 'auto' | 'true' => use numbering alignment for indenting - Some(Smart::Auto) => { - // Add hidden ancestors numberings to realize the indent. - let mut hidden = Content::empty(); - for ancestor in ancestors { - let ancestor_outlinable = ancestor.with::().unwrap(); - - if let Some(numbering) = ancestor_outlinable.numbering() { - let numbers = ancestor_outlinable.counter().display_at_loc( - engine, - ancestor.location().unwrap(), - styles, - numbering, - )?; - - hidden += numbers + SpaceElem::shared().clone(); - }; - } - - if !ancestors.is_empty() { - seq.push(HideElem::new(hidden).pack().spanned(span)); - seq.push(SpaceElem::shared().clone().spanned(span)); - } - } - - // Length => indent with some fixed spacing per level - Some(Smart::Custom(OutlineIndent::Rel(length))) => { - seq.push( - HElem::new(Spacing::Rel(*length)) - .pack() - .spanned(span) - .repeat(ancestors.len()), - ); - } - - // Function => call function with the current depth and take - // the returned content - Some(Smart::Custom(OutlineIndent::Func(func))) => { - let depth = ancestors.len(); - let LengthOrContent(content) = func - .call(engine, Context::new(None, Some(styles)).track(), [depth])? - .cast() - .at(span)?; - if !content.is_empty() { - seq.push(content); - } - } - }; - - Ok(()) + ) -> SourceResult { + let depth = level.get() - 1; + match self { + Self::Rel(length) => Ok(*length * depth as f64), + Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span), + } } } @@ -365,46 +343,33 @@ cast! { Self::Rel(v) => v.into_value(), Self::Func(v) => v.into_value() }, - v: Rel => OutlineIndent::Rel(v), - v: Func => OutlineIndent::Func(v), + v: Rel => Self::Rel(v), + v: Func => Self::Func(v), } -struct LengthOrContent(Content); +/// Marks an element as being able to be outlined. +pub trait Outlinable: Refable { + /// Whether this element should be included in the outline. + fn outlined(&self) -> bool; -cast! { - LengthOrContent, - v: Rel => Self(HElem::new(Spacing::Rel(v)).pack()), - v: Content => Self(v), + /// The nesting level of this element. + fn level(&self) -> NonZeroUsize { + NonZeroUsize::ONE + } + + /// Constructs the default prefix given the formatted numbering. + fn prefix(&self, numbers: Content) -> Content; + + /// The body of the entry. + fn body(&self) -> Content; } -/// Represents each entry line in an outline, including the reference to the -/// outlined element, its page number, and the filler content between both. +/// Represents an entry line in an outline. /// -/// This element is intended for use with show rules to control the appearance -/// of outlines. To customize an entry's line, you can build it from scratch by -/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the -/// entry. -/// -/// ```example -/// #set heading(numbering: "1.") -/// -/// #show outline.entry.where( -/// level: 1 -/// ): it => { -/// v(12pt, weak: true) -/// strong(it) -/// } -/// -/// #outline(indent: auto) -/// -/// = Introduction -/// = Background -/// == History -/// == State of the Art -/// = Analysis -/// == Setup -/// ``` -#[elem(name = "entry", title = "Outline Entry", Show)] +/// With show-set and show rules on outline entries, you can richly customize +/// the outline's appearance. See the +/// [section on styling the outline]($outline/#styling-the-outline) for details. +#[elem(scope, name = "entry", title = "Outline Entry", Show)] pub struct OutlineEntry { /// The nesting level of this outline entry. Starts at `{1}` for top-level /// entries. @@ -412,90 +377,206 @@ pub struct OutlineEntry { pub level: NonZeroUsize, /// The element this entry refers to. Its location will be available - /// through the [`location`]($content.location) method on content + /// through the [`location`]($content.location) method on the content /// and can be [linked]($link) to. #[required] pub element: Content, - /// The content which is displayed in place of the referred element at its - /// entry in the outline. For a heading, this would be its number followed - /// by the heading's title, for example. - #[required] - pub body: Content, - - /// The content used to fill the space between the element's outline and - /// its page number, as defined by the outline element this entry is - /// located in. When `{none}`, empty space is inserted in that gap instead. + /// Content to fill the space between the title and the page number. Can be + /// set to `{none}` to disable filling. /// - /// Note that, when using show rules to override outline entries, it is - /// recommended to wrap the filling content in a [`box`] with fractional - /// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely - /// as many `-` characters as necessary to fill a particular gap. - #[required] + /// The `fill` will be placed into a fractionally sized box that spans the + /// space between the entry's body and the page number. When using show + /// rules to override outline entries, it is thus recommended to wrap the + /// fill in a [`box`] with fractional width, i.e. + /// `{box(width: 1fr, it.fill}`. + /// + /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful + /// to tweak the visual weight of the fill. + /// + /// ```example + /// #set outline.entry(fill: line(length: 100%)) + /// #outline() + /// + /// = A New Beginning + /// ``` + #[borrowed] + #[default(Some( + RepeatElem::new(TextElem::packed(".")) + .with_gap(Em::new(0.15).into()) + .pack() + ))] pub fill: Option, - /// The page number of the element this entry links to, formatted with the - /// numbering set for the referenced page. - #[required] - pub page: Content, -} - -impl OutlineEntry { - /// Generates an OutlineEntry from the given element, if possible (errors if - /// the element does not implement `Outlinable`). If the element should not - /// be outlined (e.g. heading with 'outlined: false'), does not generate an - /// entry instance (returns `Ok(None)`). - fn from_outlinable( - engine: &mut Engine, - span: Span, - elem: Content, - fill: Option, - styles: StyleChain, - ) -> SourceResult> { - let Some(outlinable) = elem.with::() else { - bail!(span, "cannot outline {}", elem.func().name()); - }; - - let Some(body) = outlinable.outline(engine, styles)? else { - return Ok(None); - }; - - let location = elem.location().unwrap(); - let page_numbering = engine - .introspector - .page_numbering(location) - .cloned() - .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); - - let page = Counter::new(CounterKey::Page).display_at_loc( - engine, - location, - styles, - &page_numbering, - )?; - - Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) - } + /// Lets outline entries access the outline they are part of. This is a bit + /// of a hack and should be superseded by a proper ancestry mechanism. + #[ghost] + #[internal] + pub parent: Option>, } impl Show for Packed { #[typst_macros::time(name = "outline.entry", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut seq = vec![]; - let elem = &self.element; + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); + let context = Context::new(None, Some(styles)); + let context = context.track(); - // In case a user constructs an outline entry with an arbitrary element. - let Some(location) = elem.location() else { - if elem.can::() && elem.can::() { - bail!( - self.span(), "{} must have a location", elem.func().name(); - hint: "try using a query or a show rule to customize the outline.entry instead", - ) - } else { - bail!(self.span(), "cannot outline {}", elem.func().name()) + let prefix = self.prefix(engine, context, span)?; + let inner = self.inner(engine, context, span)?; + let block = if self.element.is::() { + let body = prefix.unwrap_or_default() + inner; + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .pack() + .spanned(span) + } else { + self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())? + }; + + let loc = self.element_location().at(span)?; + Ok(block.linked(Destination::Location(loc))) + } +} + +#[scope] +impl OutlineEntry { + /// A helper function for producing an indented entry layout: Lays out a + /// prefix and the rest of the entry in an indent-aware way. + /// + /// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the + /// inner content of all entries at level `N` is aligned with the prefix of + /// all entries at with level `N + 1`, leaving at least `gap` space between + /// the prefix and inner parts. Furthermore, the `inner` contents of all + /// entries 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. + #[func(contextual)] + pub fn indented( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + /// The `prefix` is aligned with the `inner` content of entries that + /// have level one less. + /// + /// In the default show rule, this is just to `it.prefix()`, but it can + /// be freely customized. + prefix: Option, + /// The formatted inner content of the entry. + /// + /// In the default show rule, this is just to `it.inner()`, but it can + /// be freely customized. + inner: Content, + /// The gap between the prefix and the inner content. + #[named] + #[default(Em::new(0.5).into())] + gap: Length, + ) -> SourceResult { + let styles = context.styles().at(span)?; + let outline = Self::parent_in(styles) + .ok_or("must be called within the context of an outline") + .at(span)?; + let outline_loc = outline.location().unwrap(); + + let prefix_width = prefix + .as_ref() + .map(|prefix| measure_prefix(engine, prefix, outline_loc, styles)) + .transpose()?; + let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles)); + + let indent = outline.indent(styles); + let (base_indent, hanging_indent) = match &indent { + Smart::Auto => compute_auto_indents( + engine.introspector, + outline_loc, + styles, + self.level, + prefix_inset, + ), + Smart::Custom(amount) => { + let base = amount.resolve(engine, context, self.level, span)?; + (base, prefix_inset) } }; + let body = if let ( + Some(prefix), + Some(prefix_width), + Some(prefix_inset), + Some(hanging_indent), + ) = (prefix, prefix_width, prefix_inset, hanging_indent) + { + // Save information about our prefix that other outline entries + // can query for (within `compute_auto_indent`) to align + // themselves). + let mut seq = Vec::with_capacity(5); + if indent.is_auto() { + seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack()); + } + + // Dedent the prefix by the amount of hanging indent and then skip + // ahead so that the inner contents are aligned. + seq.extend([ + HElem::new((-hanging_indent).into()).pack(), + prefix, + HElem::new((hanging_indent - prefix_width).into()).pack(), + inner, + ]); + Content::sequence(seq) + } else { + inner + }; + + let inset = Sides::default().with( + TextElem::dir_in(styles).start(), + Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())), + ); + + Ok(BlockElem::new() + .with_inset(inset) + .with_body(Some(BlockBody::Content(body))) + .pack() + .spanned(span)) + } + + /// Formats the element's numbering (if any). + /// + /// This also appends the element's supplement in case of figures or + /// equations. For instance, it would output `1.1` for a heading, but + /// `Figure 1` for a figure, as is usual for outlines. + #[func(contextual)] + pub fn prefix( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult> { + let outlinable = self.outlinable().at(span)?; + let Some(numbering) = outlinable.numbering() else { return Ok(None) }; + let loc = self.element_location().at(span)?; + let styles = context.styles().at(span)?; + let numbers = + outlinable.counter().display_at_loc(engine, loc, styles, numbering)?; + Ok(Some(outlinable.prefix(numbers))) + } + + /// Creates the default inner content of the entry. + /// + /// This includes the body, the fill, and page number. + #[func(contextual)] + pub fn inner( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult { + let styles = context.styles().at(span)?; + + let mut seq = vec![]; + // Isolate the entry body in RTL because the page number is typically // LTR. I'm not sure whether LTR should conceptually also be isolated, // but in any case we don't do it for now because the text shaping @@ -511,32 +592,174 @@ impl Show for Packed { seq.push(TextElem::packed("\u{202B}")); } - seq.push(self.body.clone().linked(Destination::Location(location))); + seq.push(self.body().at(span)?); if rtl { // "Pop Directional Formatting" seq.push(TextElem::packed("\u{202C}")); } - // Add filler symbols between the section name and page number. - if let Some(filler) = &self.fill { + // Add the filler between the section name and page number. + if let Some(filler) = self.fill(styles) { seq.push(SpaceElem::shared().clone()); seq.push( BoxElem::new() .with_body(Some(filler.clone())) .with_width(Fr::one().into()) .pack() - .spanned(self.span()), + .spanned(span), ); seq.push(SpaceElem::shared().clone()); } else { - seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span())); + seq.push(HElem::new(Fr::one().into()).pack().spanned(span)); } - // Add the page number. - let page = self.page.clone().linked(Destination::Location(location)); - seq.push(page); + // Add the page number. The word joiner in front ensures that the page + // number doesn't stand alone in its line. + seq.push(TextElem::packed("\u{2060}")); + seq.push(self.page(engine, context, span)?); Ok(Content::sequence(seq)) } + + /// The content which is displayed in place of the referred element at its + /// entry in the outline. For a heading, this is its + /// [`body`]($heading.body), for a figure a caption, and for equations it is + /// empty. + #[func] + pub fn body(&self) -> StrResult { + Ok(self.outlinable()?.body()) + } + + /// The page number of this entry's element, formatted with the numbering + /// set for the referenced page. + #[func(contextual)] + pub fn page( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult { + let loc = self.element_location().at(span)?; + let styles = context.styles().at(span)?; + let numbering = engine + .introspector + .page_numbering(loc) + .cloned() + .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); + Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering) + } +} + +impl OutlineEntry { + fn outlinable(&self) -> StrResult<&dyn Outlinable> { + self.element + .with::() + .ok_or_else(|| error!("cannot outline {}", self.element.func().name())) + } + + fn element_location(&self) -> HintedStrResult { + let elem = &self.element; + elem.location().ok_or_else(|| { + if elem.can::() && elem.can::() { + error!( + "{} must have a location", elem.func().name(); + hint: "try using a show rule to customize the outline.entry instead", + ) + } else { + error!("cannot outline {}", elem.func().name()) + } + }) + } +} + +cast! { + OutlineEntry, + v: Content => v.unpack::().map_err(|_| "expected outline entry")? +} + +/// Measures the width of a prefix. +fn measure_prefix( + engine: &mut Engine, + prefix: &Content, + loc: Location, + styles: StyleChain, +) -> SourceResult { + let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); + let link = LocatorLink::measure(loc); + Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)? + .width()) +} + +/// Compute the base indent and hanging indent for an auto-indented outline +/// entry of the given level, with the given prefix inset. +fn compute_auto_indents( + introspector: Tracked, + outline_loc: Location, + styles: StyleChain, + level: NonZeroUsize, + prefix_inset: Option, +) -> (Rel, Option) { + let indents = query_prefix_widths(introspector, outline_loc); + + let fallback = Em::new(1.2).resolve(styles); + let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback); + + let last = level.get() - 1; + let base: Abs = (0..last).map(get).sum(); + let hang = prefix_inset.map(|p| p.max(get(last))); + + (base.into(), hang) +} + +/// Determines the maximum prefix inset (prefix width + gap) at each outline +/// level, for the outline with the given `loc`. Levels for which there is no +/// information available yield `None`. +#[comemo::memoize] +fn query_prefix_widths( + introspector: Tracked, + outline_loc: Location, +) -> SmallVec<[Option; 4]> { + let mut widths = SmallVec::<[Option; 4]>::new(); + let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc)); + for elem in &elems { + let info = elem.to_packed::().unwrap(); + let level = info.level.get(); + if widths.len() < level { + widths.resize(level, None); + } + widths[level - 1].get_or_insert(info.inset).set_max(info.inset); + } + widths +} + +/// Helper type for introspection-based prefix alignment. +#[elem(Construct, Locatable, Show)] +struct PrefixInfo { + /// The location of the outline this prefix is part of. This is used to + /// scope prefix computations to a specific outline. + #[required] + key: Location, + + /// The level of this prefix's entry. + #[required] + #[internal] + level: NonZeroUsize, + + /// The width of the prefix, including the gap. + #[required] + #[internal] + inset: Abs, +} + +impl Construct for PrefixInfo { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(Content::empty()) + } } diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 1261ea4f4..c91eeb17a 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -1,4 +1,4 @@ -use typst_utils::Numeric; +use typst_utils::{Get, Numeric}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; @@ -7,7 +7,7 @@ use crate::foundations::{ Styles, TargetElem, }; use crate::html::{tag, HtmlElem}; -use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; +use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::model::{ListItemLike, ListLike, ParElem}; use crate::text::TextElem; @@ -160,12 +160,7 @@ impl Show for Packed { children.push(StackChild::Block(Content::sequence(seq))); } - let mut padding = Sides::default(); - if TextElem::dir_in(styles) == Dir::LTR { - padding.left = pad.into(); - } else { - padding.right = pad.into(); - } + let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into()); let mut realized = StackElem::new(children) .with_spacing(Some(gutter.into())) diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index d392e4093..f3fe79d2c 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -276,6 +276,15 @@ pub trait Get { fn set(&mut self, index: Index, component: Self::Component) { *self.get_mut(index) = component; } + + /// Builder-style method for setting a component. + fn with(mut self, index: Index, component: Self::Component) -> Self + where + Self: Sized, + { + self.set(index, component); + self + } } /// A numeric type. diff --git a/tests/ref/heading-hanging-indent-auto.png b/tests/ref/heading-hanging-indent-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..823feb145eac4422dec40e50ce855ff14cb7346f GIT binary patch literal 849 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 delta 3465 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 literal 0 HcmV?d00001 diff --git a/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png b/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c359a1b990069b38118603790d635046de9fee GIT binary patch literal 3341 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ⅈL;bFt8&b1d-{@o7R?=mZ~aNZtg@E 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 JR|hc-`5%sQ_ox5> diff --git a/tests/ref/issue-4859-outline-entry-show-set.png b/tests/ref/issue-4859-outline-entry-show-set.png new file mode 100644 index 0000000000000000000000000000000000000000..33ff442d95767d3c5b26e0c038b8b41f728cb06c GIT binary patch literal 749 zcmVh9?2?dRz1X=`&TEHr6qaEy(Uhlq}kkd~>dv)bF? zl$D#8n4Yt=x^#AaF*7^6y~TcigMEI2!o$aljg>1cHNC&b&(PLvZFAJs*@}ygsH(C_ zOH)@^UOGHP%+A(+e}!&ubANz@qou7~UuW9f|r^P4i^Bv0Hm|Xrhx(vC>1D8Xmx;2TmCAPqClR-67B8lpP%cYl8?y6@X#O;~=5=)5tynch(>;3)x=H}+Pxw-iG_~_{9z{1MY)6=rDvMTo0^)M zIyyQnEiGAFV?jYd;NalS&dzprcAlP|-QC}Ugp8q~p@oN!tE;PheSQ1;`>n05uCA`G zuCP#0Qha`fe}RdCfq^M0DZRbDb8~aFw6r!hHsa#qx3{-BIXUU+=~-D>jg5`!>g+#1 zLBhhqSXfw-lat%q+rq@m;o;%U&CL!D4&2<_$;r#h%gwH^xNU81d3kxv&em;jcdxOz zQdC@ohK|(K)UdL;WM*#8(Acf7x3#yws;snTXmGy3$Y*JBXlim^U}%nzn3~cpp;}yIqNJ>Se}_m&O2ELvu&}a`lbgA_!+3gvUS437mY&GU(#*`v z8X6iQBP+49yVlm$K0ZEygNtctY4P##xVX5%!NFHoSG>N)DJwIeprE3nqH1btnw+Fg zP*_S#RGy%yOG`_%w!X^B%+S!%I66X5Qd*CZnYg;b!^O?m+Tz;W!2ng-%?J_bl>gwwJ{QTwRHAC3-YwThxuP(g8md+)vXmWq4ty>Kh;y%&fI zZ2`fe;#N?ER~{|*rrSu3n- zvzj7&DBHSj_0sT%?FWUkz$tpG7XBC_EDngXIp^jHB^=!bPi+ma-_R!sSFNAB)%5Ja zhxZasE7-D`_^xpM8bhbDeJ93IqZyI$mz11@p-!4oFX1b_cR$WTFyvFG&&VZn=S-Ri z7}~;ji4KuOwa54a?$pq&JKlFtUCGvMc%tEU1o6|@u?t4(m^q`NG+7^zsCsTPnR*c% zNxMY4=-F~76tY~dvQ^i!_(WDMY*_f;-n+YVWEWck0_tBbQg@XW;1>Qb&}^FsdI8b!sjf!a^WIDF{H*kiiHOc0k;c&fnJABuT10I z_`g9E#7ZPoLZotTd5~h2*NHI=C0D%!9=%@g)h=lb4UI@~07*qo IM6N<$f|jtSP)+9{Zw7jvi zyTr!MsjIV@o1?Y2zpbygzQD+YhK_=SjG>{SuCA`Px3|vF*k)*OXK8V0YI0;|Zkn2! z`1ttk?d|#b`RwfM_4W0=zsFEgT3%piYHDh0ZFO&PdF}1*MMX#M?(X{f`YFMeH z{r%|Z=$DzHpP{LclA2{_Z^gyO+}z%hl$>K_ZSwN+^z`)c@$q3}Yg1KSy}iHV(^)ZEe2+YAmC<>loK4;RSF(v+5-Iy^+`>gxRb{Fa!YI66W(J3}@&Kq)ITAtNgq z93R89VN51z0J+f)6>;|fr)&6hIo2{uCTbTvAI%ITv}XYrKhh-OjK1@ zUtV5d*x1_K-sZWx!;zDlu&}c6@$Acm#V=g zP>Sfqdg_uQB2;XlK{55_p@ON@Fn7SL=S@S&PG;6WGGc?W@f#akXIltMqMK=DzxLS4idxy3lI zQmn-I;x1WamC~mJhJR|W(0#!>Q4B!mN#M#jaO(!dnxpXM70}QFycmQ?|1;RM9{4o{ ze0>Drb6s%nHqg=voag{P2H^YoNx&aau3+9`r#w+x-R{*XaXOu%(qpZ*wQg+7MvW>P zbrz{?lo`Y6aZ|LYXl7*IGc*$&-j?3P@_Lt$U%PpT{`YDO{L?CDU9L{aV{CFwr>f^0 zkbJk(-VbMFx|yIgiXM{E7NtI4@q(J9QV&`96J1z2RNy||{YA=N3e$$4)jvv_xI6lJ z%pEegSa!f!ucSm7TwDr3wSE2(06!260RkXe6bfdSzj?UtpNL5I(V~L4lSYn7$!|X} zvI_XN2apt88jiv71n_PGaJ`2}JPwWhfSL;ou7mK{c1R?EaRJm$;M08wj7|cc)0UAq zdD+S<4Ui;5tA$qM{P0tja{%sCGCU gRpudWGMQ$W@V*iY0>zS1`+Kd+KVy`nnZ(&_AV;gL_~=dlu0F7YGu)$RJ0@7J2l$V z{kivE@At#QckxqsKi*J#mFG{$Ip^Mc?m7Saoafvd`qK=eN|7cOh|yxSNQ@Sv#b}Wj zEk=vP=zo=-o__P@&4h#mgL%kGGb&Fnp92RD%$zwhFE6ig+x_fwT+_({Su-MY17$Btvij%8$I==ZvQ{rZg?H?CZ{ zqLsJ$_3Jli&>$vQ4o2tX7+V88$tHSSE9 zFrj<*?znGiR@t;^Q{B3C`}FBU)!p5_Y15`e$LZ6j@7=pcZ{x?0C#AG*-J1Jy3p+bI z9OvP^efyTQ!wPveN!f zwQAKGF=9mVDltw5B<)fr!!ifj`}gm!nWzpOI*c1Pj+L=!(ISi^ke!{Kv9(^kdi(b6 z>(QeJ=crrjvbMIqeEBjN49jq{Ns}fF!`9Z8U{@LG*|TR65fP(DkJc}F_39OAnr;ac zwG&jlOpj=$#2nZ(^%j;S;WlK*5DM7crcIj{FJ6#RVq;^e1_cF`NFW@dokCQRif>kk z=pzP?A3u&qxkaLdOLzuEYhhs_d4rP~0Egn^<5@jy8$>I2p@r3^)?9XWHieEIJF@Yv zU%wth^>%gwM#v+vva;eTeRuuv;e*P1D(bl@506ErHwFd}4j(>DSTJn|2Z!0SXEQ=N zpEYY1u^bZ<)4O+XH&+)M8*2uD%jBQw)2H)P$mt9~h0e#1A16(kL_+iM@bK~R898z! z?*IJxlc&3N>(*`Cw(Z})pK|l&&FLgPJu5IM`26|vM9IR13!NN|E*C>}QD|uBnl)=w z8AnJ&^1)Ie%FoXyLU|!MdGh4br%&a}P(Hy$Vzd}75~IavFi_s!8HJbk@a4-Wg zjB0MQrfG3zAcj%>jV`mj+7H806IF`DXfaxh7Kzbfv=}WGh|yxSNQ@Sv#b}WjEk=vP zXfaxh7Kzbfv`CB=qs3^E7%fJN#Aq>Ej22ab43#Tz01jp#hEdIb$x;1v^&c0*kY7ZX zUqly)(PFenjQ+1cq5z_(8b&{U{CM!-!O*P$P=RwWm@cp;AZJRq)90{Z!@!pSZi8uJ zyc4)JuxAXW2+Run6rf90CZaJqEG&!yL?rMqCS40ihS%E@Cr*?;1Z)RL6$KzwFlmhU z10M~JlEFkluK{mUW&VbxX3d(g$rNo3j1hPorVVCi*REYa(|CJjc@j7P-AV(yK=DkS zI+e@I>{bHE``pL7}VdcNyu0SE?F3n&?fU=OZaw@yD1xaxFjg0z4OUy!!17gN$VEMRQISy+RvKy&G(b?cG(XK|w zV2E)dW6YQ_>L{XHat;_Y5)@aNY3ksu1AG)Yk(uak3$RJCkGF-@t5;Ka`SRtwdGp{0 zGArIp;I-+O^GG-u(ieWQsLA8{A>e3$+Z2+D7=(YxW57h{Z}>tZTPW9(TPefTRB6Gx zNZf^I!?mOr4ndA%CxFGq3Zn;JHrPejFgS->Gjbq&v=HEoP~M}^2z|r+|LIZ zi+NM6R;%4^8w`er6Ap*TWHKbhV$tPtIUJ61xeUX4y?*cdR4S#>Xy7ZD@p$}jI1~zn zXfz5#Sjy+~xqsd6@p!!3?fibfUaubv24=I_slZ_}nGh8Ug;*?xY1!hiLLM;t&CiNx0z1tN6xBSJBNRw|Xyr10iEYJMiaroftzeEHVC`Lr~4Mq%84 z(Zoz=rWVnHpv+iX6*p37)ow})>Pm5|*oElIFHnjV#FZPNUAXX52wF{SL9MN5qqO*^ z)XbPTCNstwXEHH~Cei5+4qQsEL~3GTX3immCx7w!-ZxKf-g}<&oHvo+?65pJIXN*g z@rExQ#MIPOX=y2m2bb;W=;+wknB)?*wY99pEDM#Dm8>BwyIj%D&CPrbot>Sm7_tg&8Xg{wMx))`-K-ev>+AeJ0R8>_u~=+mWW;W_b0I@?Lqh{M z34flKmzUp)JQ`9+xn^f)kB^W4orl2!Yc%Zea(8!^#gxOiiNOmP1||EJ6&%b(?Ql4@ zwzj~bP)H8rC$hP@3E*Wo9A;R?WGUyE?d|QEnHj!RRfZZ$v=XgEYbepLe-z}Tjc11^ zJd>!AmetqS=V+J$5~&bWRaL!TUXtdpYk!E2oyB>6yyso|`IiR|KTulDf!3v5Gc+`m zkuoHx>>!b;*df@IR&#W8bfC@vnG%hZpw>af4l$y%-d3$65C~A#XlrYu9EvhD-bdH{ zZvag%We{|uluaU$h?J|4tSEH=%9BD00AYzz1@Jz91@J?pR4Y{!M#%2mfg5)7Xku@ zK5J1v;P0cSg_c~vNidL~cL@l+IL^&U0$zrKuP)<&$mt6v1~`j_&(BT)@$*P5`Vw$? zaef+p4&X@s5s;UAA@eWDf5n&p#(zHw_yIO+!Qrz5z#V&$-{%D|tAi(d0Oq;xePql( zh1u7w8dDe)IGW{}k(5d|2i5%D||@1ImIj^!a?#(|^;X`UnA~ z)f_gPEjtptqN1Xwrw5O9b#+x~y)6k&p@Y&CWm1GJ-5T;H7q|eNQvq{xa~uE=A66Qd zM5nr3E_6I#XJ^Ob@c<|);?DxGao5(?L|ZQ{EdjXkO5YQsq4-nI!g zsth%cR-!eOXeC-BLvxlpti@uXN2{cygtj?N)KY@9#O-$1)zxKW&N`ewCX$z5eEGR# zPI$G-ths}k9>Gk_S*O$4)YK%ny}ey&H3v=HSx|;ns})(HY~H#uq<=M^E_B2mQVl^R zZ&LI?^@$TIS_`XGz7JHH=s=NM0;suU7s$xQCINzNFCqwOEEYjXe}vezOcj|YmM%W8 z*r_7qIFjQ1sx;?jb}jx5M0SxJbwi76@O=0)-h zP;9f{mWs=XPbw~_*ng%xqLGjQOjQB+r`H1GU|d(MQn6wgR;*aDbQAF?a{w?60nAXm zUHn{2K@I?imj{5CGXJ8&JY)V$TjtMDGJl!&y}QMn?#lTaMs3rlAFDa5d9)I(p+vvA z%5cu{6E|!25H>b8VkKynmL|9{X41BsocY1YUf1uwpP&g(HGhja*cIsa`v(UHA^PU& z9nLJ9HQDPrFff4k5s#|`PmR{rR;>p5kNToU(zSx3`yf()y&=JEzRaFmt77C5qW_br%*E#7i~o-IbLU5&DMDYq+a) z$Yb~#Hmf+l?0Lf#m5kpi*9ZQ+7_ZVxZ+O9m1Iv;qrm$hfBIucIiYIHfi3vtPtgAVz zd9)I(p+qau8cMVhtwd`m(Mq(260Jll(Hcs$60M;`E73}{h7zqrYbeo5v=XhML@Uu6 eO0*LF_QXH+?ZR+&DOFGa0000B|D zB#a^@3>Xa-qD(#pk+NW-7*G_2qcLa-esT(A zD|olt?e%&(osLiTRd6i0quMjht%<*_Em&=Jn z0JRXnXaERU3>osaNo6TS_@Or&An{76m zefWhX5C~8xl%KO-tyZa2DpI@IY|`oUdcA%=pZR>gR4VQF`%0x!AP}Go-eGw74W^gc*99A+|^&%nj zf>D5o9v6$nAOOm6iVlb4bUJ-_bjam$glsOCBb3Qxg5M*WcemSN{1M_b8jZzb{TviL zaSX&0#ebK}MWIljNs&234MB({*c0UY`!BoZ^O8Um#PM@zkc8UJLr_`x4=CQfDPFxq zU___>1CcIax2|666bc?hq)s7qQ7rrgS|rgb!fqi81{u|d9>}ul$-3Bk$TH5(e&4*= z+3|hH9Wpz~o=hgWRrBBL$Ye5hyPb^(&p4@?NPi^6Uc&8mQ;R7JE|-gxr0m*kHm}#q zZ3u-zB(f+4)DWM~mrA859S(=X>2!v};aDs-7z`qj2#H)OmD1@nLNppBbID?h#S)Ll z3AD%K;YWgFp-}i2`)Go~mMfRbsj6z+hY!7pa_xtJCLD=Mf+2bP^48qQS;dKM3M1P~vpkoIakzxI-_d0sL9&L?4AV515W;oK5 z;Nl2rq=SKARKlL=j?xS_M0Y!X;d2JO%7D z5S%e7aTvG9LB!w^*zBx!A{3P_nVv!J&>58I2vfV@Wt`>|tE$)Q0t9N#Jwr)fX#~=0wOFJ8;fRa| z5SjsiV2P5YLe@J!1A(ss1Ov=4o(KXsQ!tSLVQL(}4V=Lx97BndfZ#A7IL{JThydZp zShOzOw;EfUYhR!5@&%gjcFx|lsedpE?5xY6miU@UZ5(lYY z;Kw3Z!m?MF7-K-%k8;` z_n!Qk+rCeqL$r?;XoWx@QqZ;9XZ~rtyZd!-kG~dw#LE{jfsB2H0{wDh`+v;Y3*LvF zlc!Fjft3FB*+nk^u~ko12>`rEE4^sZ<(`20k~+ zn@b3-L?VI2Di({_tcZBN<$sqHmr#!w-bgHz6(cw(G?KO;PYlluGK||{x1;Xt($>}% z`pxq^tXrNfrVr?ZOQ`2$GKspQ!(1+hCZ-=-iYE`}9*M}@6GKwnZr3%8^0tPX&1Nc< z>TQ_EpN4s*G|WwV8%9P38+M4Wo^9|-%24}gfmR5#Kr2F;vrkvo*MDAY+`e;fIld|t z9yDja|7d&fKZZ4D*VYs9Cu?TWF0VXC(F?Bcjd=fIdX{5j&QXKdYX~!jZY{}?bLz&cMkYRG!-xc_>NR)0^o!Fpa--+H`9U%f+Q&T?4LCypPzd1I+o-NB{hJQ6uV(tKs! z>HLctSxkl__alSG#s9XRXxHeIp`Ko8$klUBhJ5&XGt2*7k7T#{cDrp3J>Al?tLL?l zF^ioVU%mCyTkp)-X5(AzSXAb$_R#{Z5a`1T`d!vby?XODoqzrL>o=pM5-h%8ju|ZW z6$+HGn7ptGXTv@t>GSt{^P#HdWv~ub4wXn(%XLfXSbZl&Fad8owgFTR>hcBk5 zr`h}s^Hahw!trcG))$dTWM5?iciHZo9sAVOl*sm_6!slmtyWP%rBVqebYWq^<;}Rn zkmZ99>h-$wK!3R<&L&E%P$+nwht_hQDb8jllQAck)ev275e!*uSr$S5x<$<1Wf6YB zy^COymdj<_)xL{h%l0l}mTNwr?_H$R>DVF+D00000NkvXXu0mjfZ@mP7 diff --git a/tests/ref/issue-785-cite-locate.png b/tests/ref/issue-785-cite-locate.png index 5240aa772cce985eaa73e85b8f9c9c30c856792e..d387ed0d58686504f8956e8f8df20793558fee8a 100644 GIT binary patch literal 9441 zcmZX41yCKZ(k^gtcXxLv?(Xg!q(zEDaV^EYxWmEyP>Q>|yE}y*+}-8%{_oBFcjnD( zHknK&$?oj8Np=&Zp(c-pOpFW#1%;-lAfxrKZunPP5MloD!Ec)6P*AjBMHxvQ@3m8q zkBaUv!EiO}H4vAL65OMX`LNmcX8f1fsFzIJd|l=fvCC7xUzA)0USpMvW;BXi4;)-< z6T7BMyssq7&qziLV~w85Kn%%sb-~GT)ie9r@U*6zx38|}6Pz1f2f_#0f(JI@%#6Um zQn8?5>WKfJ`T{~o>FDU3wmN1XE+3!o>_cB)UkeMV@8CWLYv%k3u&}Vm$;tKf^w3E7 zNX7kM1EB$@XJ-!=f2K7cL$PmfZ@ggvpWYM1`oqyGbjs(R?@snyw3#sZ95=kTd!Ri! z-rGR{oeI6!rJ*4?@(2tHa$MX{PV<3Cy-GtWL{9$l@^ZN%m;H&%&33QpkrD5!zi_2x zEiD-Z2h({p-QVxVM4o1ghMZ5#^<*lr&%*+ae`U6lXhlRH%GP#H2$lToB zv$-sPCMRp3ELD~S%ofUiZgY={i@U$OE5QN4;(dQO|J-P|I2cVJ?)U6|x!pT5U#j}` zdagt{CnqP1*S^W;W*@|BKb6HFug0U@?6_$)og4e6q@q&HAS~Q28;$>O+LM`r{x~w> z1wCD3SBI6FAT)GD#DxA%i&5-_xSSj^E>Q=&>0D6)R-i_^=h@zPIs!aA-?ugQiHuL? zO~%H?^r|3QuCs-5?ahr18S9BW@x0N4sT>l1N76U@uY5fD-g_SnJM%;jfuvm6%G#Q_zyJ>=4U`UCW@=P9sJ7cZSO_)F*a&P^nw8GN_> zi69E&Z%e-oic+C5Wiy~1H(Dg4uV;imjS#RK6AHOA%Kc1rv0QM}6{rs$mdHv@zNrvK zmy8PzhOvMZ0ZjQ>SnU3jHH+Vg-ECuY)AnMcRr=+0mAmsLDt;81K3S1m*y~)S-Fc_a z_u;&xq@)s98$3)U9p2K`mMiL;hMxwm{N=2_3I;RYhtr%xAR%Y`50HehSqwC^J*gne zPlTufq-exEJ-kuT(aD{|f|hFkl*>tqxDp+LfDs-R=JK%fiIz6hmdkCsC+HT6bkV5U zadtE>U&Ps?v!jFkO9z-e4Ub*w`Fc-TO)x7WNg_=RDgGMq@W61k_Umv)Muu*Mo@$<0 zP6dY}!n2&!R(F6&=*EUImIYG}^&Cx)r^t^-jXyWLqsi@F7k}fxe$Qq5dY8KPi@z-6 z>f8zjRlpn-BUjvrwO`TmM+&4PoW)uUFrl?g^b#Qi#bsp|Tb(bLJN<-McdB@TxVZn9 zwuUHV6d->eTz67x_zTd3F$c;;L`55~hf`Vfj~6TClho@`9b4lm#OZy@I#E7MVeK6T`4FF}lmvQz_FqZ@Yk65pV%8s6Vt z?9(Ji?)E3Ma)dnQSOXs&^z`tbi2V+32*PY_y;e@u#nD`=jwxv&zhn(H=Ox7hHkV1n z-VbTeN4xx9!VwVc(`tpq{GClHrL@coj3usarS-*kvZnAk047sS@xfbIaeOj9gUh#l^J24q9I& z+*A2eGTzSCn|Te6hxrHT=%|(9q1eJCaMAldM-8PvIGP37rs%#oOwm|!=}04};mSW}ZA-sk~xo&1H!SLYu;M4bZ~-G$5;joU{r&x`+;iT2`Ut+hu)Y75R3xR=;FjQpStuToxRwl74-Uo; z4aEkg_2&Uxcx7M~O?lo<8SmlZ|dRC8CEluC4=;w}+6Oh~{O#4BG z<#P6NQD0Bte>K?Q)b+&owAMJHng~MQ+WK&#Hu(HbDCkp&U||8vU4FjU{vA+L8p-sk16#c$JP2#DKn2#kQ&c3fy5WJZ zN{XW-?1o(6HluB{g6sMAt4Yu=g^fixTH8r1wz1ZK6S+4t|6r&nNMK_Hk%k0Q)|Bg0>C$Fhm8-GIyGA_29A1D6=c;Bx}+aS|H2SI7o2?Xg= zMGz$&mMw%J?Jol~Lh;cgHHmx!hvvG)pW};7^Thm|%`d4~-&71$QeO#fV^I@Ll+?RF zH&^|!&I=&N<^oZKRTVm_%2hKk9PI9cOkyl=x5p#l`(nmm?d@$1x_8ggXT~TF^^KiG z>{l*~h9T2gc<77=8POpku0X*b;P3bOaBK*Zt*mzM@?UQ?pi6+P{B;O4;KN&ZNz zJ1pn&A;KSp`Pa2IC8jaOjB*rahp!4_vGvbI{{$k9f?k}LLOvxTKm5kGSYXQ6!u}o8 z6v*yKjQI*{OueaZ@k?cd8kmvOxqj7@D|aC-rt+lCmKZcp%_Pp)B!f9 z2Q|pIQN6r=Hvjb6Ykbv2TlFL@k}0B>iX1WjQf~XjsdT=&s@|$94Vr$U>CQghY52Sz zk~BsvL$#5>>2oMY5G@;Z*;4rzHu@N%avCJf1CS47bq1heWLrf;k04lYJ?S&~?gY=M zZ1IAy%8}YF=2SIw7k`NHyJ!g6N(JHYaQTJ+;!zWe*#ZGD5ue_#2aVYUO$E>;^{QNn zega27fh=MJVc-mCKenaNelTiGREf7}YK#zyU?5Pa1nz(`FzV-DQ<9G6LUe5lIkL}E zjRCESI=yDlKV)IJfth zq72Rf3v9k*^6dgFPp1M89~)V&b;Mq&{MUb!1VzO$@*@qV{C$NQM!P`13UWk$8FpGLJ+AwiFXKLzk{37(g+yQx0W1gTb>E*ZIN!Y0L z`SC&pZxIwoAuAUTVYq4n(@p25)L@JGc^g_3>#c+#Wg^;We~+vTBG)g8uwb z%17&WjP&5JV1w*d6ubilja6p>8K(XuZqXpERZ>u_dvH%O45Dtmvl-NO;KJ43$j&q- z)svKlnKNeAmA6V_he^7-saT$`C0|x%1r?`+{ulOhL=o0NKcvby!dQ_pWg-Vc9V~AL zD=FX(4cb^opJPmLou+l9;a$(KkP-PQ;_?~q8f#jTIhD7rp>Ie-1sOt7%6p*zS*QX@ zUCxI#69sk7@P0)V zBwKXy>?)WanIqA_#N0#r$=Rxo&JkAUY37`NDv# zrB978aPEk(qG4Wjx?gz5vYL4Yif{9Jp;J2@J!*mp!>XyB&2<_b)pVosy<@)ke7ZoL zVYQ(NOX0RNY`#3g)3&OXr$Bl>mIa>M@XR^xvl&^DGRZpQ5eWkdEsjEZdmeoly30na z>J{@JzqrA>LF0y>)_H*H^m%rcM+R8;DntyjK4cLKiyI1Ygl5aCHG%D1uPb6qBfH*J zpFuU^`F*${$Zo_Ou6vZ=RHX1#J$NJJ-w)KLpBF$5f>0x25-P4RT8QpeNu2t#ZBaAk z7N?rP6YLOfCNjk{muRJ4<8TJH#u2$Pk5Oj!AC|2 z=hYa>k;N|^x!gYxQ@=L$ZM<}i(DGfLAJN;7LVK3q403S+%vs(Je;Fr!qnI}m%oYk} zGubS64R9SG!!W7SCvR?g-JcVd#Zt!nZAqcmm)r;p2C-<&I0J-enHiZFTcmL{OLpMh z%ac%Kr_{gCrjNv-exek<{8CF?ZSA#`+sqBQdp&ru`BhFXFUw4JTY7bwWOB#4UuZY9 znCH7msK;XdO+{2wiRbe|jsDl|aX}%Wzwnj$>;SJQY=}RCYq43Z^rgV?@UU1gd2eQ; z(097G6mr#yZTeYymvHtdq!h<9mb^aeS$-lzre$!Z6YXui2C8VOG8c)Doa>)%zeRA% z$iqvhj&-#sfiPciF~U?gFF{XtQge~?f7#+kky>G; zP;ZM}v(|t2|GyDe*j!(QqYK{i_2V9F)2@U%!H4Pq*^ zY6ID>xWTP0(ZeZ>by=e+?#|Ln#(%b*2WE>^aS%u|$iDEu)(fiChr!WkxBz$3`L}^-dOBI=$W_sItC_o4D7;mMbUZ9X0ZpQzWZsg_4!Zo^+7Wv zmJBeU1xg2)8cCf7>Pdt_Gk?fv=_|GbO6MyesL2pMi0+9z!tal_yODp}f|AOhBg`(u zA&0{soL8<5u6b3Arw4VzJD~`yIbu8Ykc1|!kOtM}R<*Ya1M2~Ffypgo#U0;yd&M(E zf^`N^=U=fBq2wG$#ZZ+(5t9)3B9hy?x`tS(hT#3Aae7g&NOP??wXE=dSgB&$QZb5#(a*yYz;pq4FF2t8k5vNZ9kRHdoC zZnx;Y^`{>blr<_KZdBfl7Uj*K*e^EEG9n&9^FjoF82a@_#8;)i4_+fq3Gp*l1Cij1qi> zyqc-MB`MX12RLmvnv&&PyoI%&aYg+sG4)0|#5DI)P-+PX zF@I*b1yfj#BbaS?vQ$$){K@#rqpKE9BZm=%8L2ucAVG@@K=F9J4aU3;r$i@eFTO1) zv=+}d@IPYW-wD=R{4~ty^?I?scY2Ci_qv>sU#}W#)nk*Ti+#Ol-!|W?-c({Y(}LV_Kzp0I^gS@8@I{F$w}3F`vV-SnHk+v&dScj z(zo!!IXkk>DIhUaG!5;2V(>yImGzQp!YWAYr>7{`CS=c;_E%1s0c_25FaZGpd~qvz zKbd<+GGPV@#?fZ6;38rS_wcu#6J}A{uW;R;EeDT5Uk`TG z7y70-z!-1Hzd%L51pkYP9jwI3=v+bB5wI8SuLwrr@KVm>TMn7&fq`*Y6ncnv@&3dRp%iimn5aZX4DlXfQmn=TzTe)u zPr72)bZpo0Jl{TztKXlOHkMN&11{CtJn7ye9iQFWZUAf`YLKbje1q-=qaNl&pWY`W zTv}$Pj4+mx!#)g|x@J5hn^ZoObdMbXLio5U5^@Pof+TzL=tgGizi^!O_sJLXSPml; z{e!Vt>Pd!{&b!lCtc{JxxNmK^K&8vTT}xx6TTeM6F*fs*RtmsW$f07K|8@3+@83A2OkTKp_^9%~O;yjRhTT z7f=1F<0#knXyau{ch}=S6fLv7Ba1OQ@FO=N#$Y!J>^1Ub_ogGp9Ep~A8dOi!^yfT~>U>WUoXQAY5HiME{4G;w!0$*b-(_3A$zk)&Sm5ZP`K};r2y4 z$tlT@p&EPx=G0ee%=aabZR8{o?PPR3>_*Z>idkj|l6sLh1&-QONQlud=LkY-0YXw~ z87v87gBZHuVc<^D#FEu=CKJ@UWf>(73{lPUL2jB|6t)B2?E*RV!{DGSEa7DZHToBT zud;f+Uac{|6l{?t|0TFeKO_E;|HH4r)V9e3f-*mazviAVJJnxj=HenhYXTzJ zM2sGwGqW2}J*R4uJO+`WAuX|prLIyD2eHGGg!iF=`EjVI$tK3X{|-6=VgotslCve? zFVL8#1X6^0`z5b(C2#4JV6?0eW^>;w`bXbMHfa2~EmBVU@@pJo#D&|%OQ$(k*0 zE{vj5_DL~L8bnpBe@yUA;ppX74>70Y*u*`nc_l2cNf~$fdJeX6#v@@guEL&Bx&O1WlEGr# zv$wkh88bgs!~4pdk`5L?4hyDX#xa|$y2l0RLEoGc}>$k91YXpIb=)ZTmh zain?uL=qa$^oIp9{O&Z{P+~AsgQSvw43fe1?w>-sPTMR#@% z<3@{*ubaKE$G5kIqo@OpG!pgP908yG$#t(25+k>pWhb@`pGE)58+Xg|za``LfB21A zxNmIf{^$jylvz#XbYC9bEN^a#1736cZBu57)UFWtI4o~~{SQ15YXUL`9gOBbwcAz# zOmAGVMe5%~b?z_Gj%r>lFZ{K#BZCFvd|@FiGs(M*f2sStgN%Dx6mdt_S~Lerf+^WW zrDswNeue!VT~>3DIy`G2lXdfwL^z={?y$C79L8!1a# z8%cxjGC&;%bf9nncUK4l_Y5+JrQwDQH@zy#!QbxwT`K)wcQau^k0pj6;y4la)LoZ) z)>=8&ZdHcWELGl=_p|i$lQnE2kOZUb{4^GXa-8Xd9;xE4G4ooDj zCw|0yM9s0I=E#sEH4y%h~fc6~sW{I{h9WZfO)K)bE{U~p(DDzE|ad*^i zTrNSmABIksEiIZI2|Xmk_c-udJ6x^Wfpv)a&n6_pjT`XU*8-6K#0TJ2hJQ=poVwlyS=Zgc3W(~m^O^{Kg_%bM@ z=gVXhu>dd|2myV6^C*Ao3`n&4_v)H}nUK8gnMWxNMnI%JB#(s+Bf7?G9wBB}@{~u7 z3K*f}F~m1sA;NK=ZQqxHNv{qq61w`WS3%v4h6h^KJzsc{f$06T24}nTG<7K|2z-)W z3VwtcU4=Q|SXt}ZaM<{viM#5+*B?$K$c;O+qI@YIlo5WI?}H6{Jr)}F31GhC6vh)3 zN3a-$AosHwf&t&;`I`w_d4IBbv+*EN9EqXjuY#Gn%1k5TF>Z5(OWdq^YN#IQD24Xwm9`%LW5tyE6R-S715PAcbjt*6L;TYa(UaySZYP9xe5L*iUP8 z_KP61@0w!IQg$RZ^wbUI?3Xy$L$12iJ$KJ)f;*zG*g9{*Cd4zaDf>+A&`Vm@(pF zAOb0VM&R#a`Tbo%N7{_IrW-Y?Fn7(fHa+<0@F5BJ;0Vu>lkcqfMNi%$QwKMgZS=`e z?n6w)S6cA1CwIqp&k^5+{98Ai+ag<4YJf3N!T3oqkHMJNc8$a_3wDibj?FtRvPGKx zoEhy=@fg)Xv!0|P_}9t;csY;r;4kZY?2koj>awf$!RF5A}$<5Hrv|6(cTgU1k3WYQPxwA03aiiUUKpl=a|vfJ zf58McVW{z?REYH6&M(37tLh1|gn$3J^E}9JuF+7wEd*>mKpKz|a_Qw#gL|UXaC`J~SMD4#8CIJeJ z3CwhMco1E@I0iWH5!l?>-}txfXH2I}gD%)Xa*c0r%g;=j*GsN;Vir+PmSTAav%kT! zb*wc$6@_%A*hwQpt|k=R=X7i=#@JTH5lrPBn7U+f2XS4rsrJ(fW@x##zJD2L2 zxK9v?Ls6=G(xqdj2T9ZC5aBabsyyTB3EX5!7c^=oyIP9Ul^VcQivwsGgC&4l@4CCA zAAD>dAT{?Q`g6hJ$=3gFD3pZk#+?3IV49L;Kzka0VdPS2qgY09%;#H5zTBqFI-~ZN zfee}$BtbORQORY?$Q0M>L#QmU`1YD&=LsI=A`j`E(s;5FA`%r;Be&qH7C%PAvaS`h zInH9w;ZU(w7oc;`pmjEg>qynycQ3|5c2qHR4qRG$eg_je9vnk>3}$?{vxt@t4)up2 zSun4RQ<}%U9M#Qx4pvO2EOFMXIqHs$i(l73)}s1h!KI zs%SLMQOiMba>^ewPYJ?)zR@Qkil$6VPu1%pc156u!bcF5SZS$!^)`VYz8H?YVL*)TU0J0u7x5L*#1E_R6mP(Y`Llo|{(Pz2MhcG_ zA)VruJu4))mLWVNPmcvlaaQ|%klZ|TBPnVqng{roU2qsuD^lrvuB2aK=yQ7yfXvTx zv_Qv@<6{${a*Z&BH(4T@$6MEQ5kbhzI}xP~>DK)&JR+f1?8)^4~n-s749^P}RvvifMXpTx2TH z)tKRrMH+HOxJ}SZme7?nCQ61mU=K-8SQfHHAL^@1!YawJ6QFi7(PcLdh^xyo)ljdq z9X7dS|FWEZ&b?dCP|l5ScFpPe99jMJ+5YWUzWw|2;mfQbVs`L=a{;;xIr%Wf|IZEg z@a>;Je_mc*RzhuC^(orh+f`Il_M!Xh_+DOL0|EjrFD{0LhFbI;t*pwLo9`|zm=p#F z2P-Qp|E=-Rqi0|cCzdW&FMuxZq#*ha>~ok*X!!*in78Df3!os2j0+;N#XC^1*p;Pnn>eMSe z1h$r!wUm`nd;0qFcJuN`(a;hqDk|pZ)q;XPS5~xhq>C}+xOsUaBO{$19rwh9=I7^O zLn&BTob2r6DQ@<>>-i!gB4k~prAH?w5UjkscqfZKgAXVw@5wGNFF_!XZ?7;fFE0m2 zQ%_Hh)=*DRkDZ+z^-ObJ-OSk7@BMwq?Ufbjo0k^>6qFAT2%>r2(8%cL!rI!}<0DV? z;^LyRvNEL>8Cs;*Xj*!DdTJ^Y3kyfT>Bi<}G4dpeVWp;kfPlK>?h+v%bcH5Eo_cI| zclWeqic>@Yc?Aeh3Ap&@kDI!BvVQGo)W%a(YASZFvW}V>h5Ccs2n7{0^JOi4jA;F8 zdrM0Jnuv%9Eb)k7uqw4>aBeP1_Rq3qJ8r@_V!+PY+W72jxoo4mhDOTxip-BkT5Me0 zZmld9_6@o|~#ayb?p+28kfp$Ut;u&KZZ48`F5MoMAfKsUFh`uh69!okJG zf6*aGMW>c)_}HbjK`zKL{V;EoZElR6no^K9uU=;ObKn2bE?!TFj!wSQ#5o6~rv9FS zv;PDd92A>`z|_;#Wy9}lwjV99l~ThGJjEASVqppKP_2M*b+?PDR-iTdCF$XY&b#V) za%`$_xrAL6La)u1UIkO#GAKrG7l)%}!>FWMwHs=ME-%t^bPLa}L}}#_a1fc z>+!_mZ@8@w{#q^rxD11*+gn(Lim%V@8!PLf?N2G%3J311ryj?5&+|6JpV=<|L;z() zLSIExj1I9;zm~1N92H>b+G$Acz>}?{&-_NENDw??u6G(#3~>@d(fVg2{r+}Q_-5kW z&+C&(hjq%W*Vv1XxH>Vgp~I97-rS&4(_c8hMZd!F#8`wI(Is?xC)YK@KcXwg~oGC+5U>IEJse*vD0T_{{8@^3 zwT0u$u5|}xAlAlG>P>uYBJ4EPMhGhxVcd|GKkzvCVyO&)1Yag);_%ix43=#8ENn!% z{0E@o*nvj@F9izDLJHcqd#g;R#WmS^bg|l)a2ZKAgNM}tk~q`2o;73YT>WvNxTh>a zoMNL?ogO!4es>!SapEgxLCQgtbaBWRBxuNc@$HBoW>*m70#t)S^h5_%bnb-6usIr@ zxWw;aYMuntwaa!Y^CU=&z-rANOv;nU z9~At-WSA^xyW|o3w|w@6cpmwKf&HJ*^eF7mnWChaX|N?JveZMaO)h5y!_IOR`z5Zy z3@}3&PxwOj9AQu^w>dPTtg4b3vec>1nJW%F5@NF1cpUlg7)^h=20>$UG;Ad3luKLi z46XXLP52d9$;1D|>om+56S;<|7dbSEVv#L^c!0U2B=oZQTvHwIM6q^nVnidBoZLAn z6I4`l5F|s9&WLS7f&sG+jx!NpvG1XPE8miK;om za0#qn=D|<)8itJvO6^xEf!WP|vp?=2S$leBkyg`aRb~RN-1FV9-0R$P-Sh8y>&`Xu zhvwofG!c-ZT}9zZihS4L)#bF~-B`LWI6$h~Q))#B<7&ZgBqx&FVbq?&qSSyUQ+TQC zt?pHW?1bT|%eT8(lMa`{T=QD9ERDME6rj>Zi!Bz*DykdXsHQHYbtXfn^Nuio0+P_5 zajJO!> z)_Fd%iT(Vz>6{<%xK3%{PBT|x%D^OcwlmRRUpbbCtOF&zxj$nfc%D+6LCmI~Wwlyo zFu*=Yjrh`HqQz(@;0*)Bu>zM zhojBGQ*;y%1Z|~49fw9-78MX8w4EWoielGsh4GmhKA|% zEmvv{tiON}rNph6I2Yi` zj_HB1YCC&a?8|X09c`0@hzKx59Ikj6ejX2jvuXW~-+X_k=d<1DNQ{nL8AJdF2j!vHK@ESd{08y^ z1I|*zW1PHn&n}GLCMb8sJ6IDjPEc;N+R7@tkK-q*}*q+gmBYTXqC_98@L>( zW(S&{Hj3%+3;N4|`DnDu5z*pI z)OdfG9@3EdV!?-Qo{&v4O};&Jw5($iFoL5)m#dhDfK64FL3+G|lpKPqo@lhKU!VQ7 zgvfZHbA}3UxE*TRxjQy_8J`|CMjRLNj@Q63m&!+SJpW3RJK7un<-Deui|&Lb6J%t% zy5K2v*D^y|2=9gUJ=Ki>o8(!gS&X)oz^K^%#WQnQjEZP%MQ$D-+vD~S_5fm0q;B9t zb|B?Sg`!R$6~S#xE&Ktgdo8956r((3J7( z)9PogAUK%{PsSEgrJ?%Edv^@iTj9#vszq?F)SAg*w6Xt`q)%!!nlc^|Jy4||Kq)Dp z4>gAd2weRok8i2Dg*Rn8Y|vq&8qZu}XUr_0wkZh_69?`gYhVIZ1(*??_l%~M(zPdB z?M8VakM$NRsJGCFoeG18rJWr+|Ss{3n6Jbb;PsF53q>sq>O-(gYlDo#=WWh-OkR;)K$YmBMW{L(IO z5-H{*C`{&dh=P(X^FdlxYE{o%;P72x;N?fQ*IiGyIXqRifVwQ%S2W^|=mY zSru{8aY;(8&Gc;+T-qgWm<-w!!P2cyi`l>g`roGsPhx*WEGJXP56gzCo}4)J!@vkq z6xq`Dh5?OY1Euf*k1;G=-}Q^WdY6`yn^OtQu@BD;-M3ki7U8jSl(rXf>HvFX8SaiH zqcm7>{7CyMD1j=I3CTxNY)(py7UH8O{qr$S83ysXLcZ)~>h8Pqmjf$EUOPH6C8lu#eBjuMxK7#%e0!${;=jtfh-v?H6 z3u)JE3)}J5#^t^7jqrd`jC~Y2v6t;+PERaqt3{dlGYQ5GbN;lF<@oYO`0xOx z@w;Le+cS$!2|;5Z?(vF*6|Y&2UAShpsR+AvEG>x@U@z81@pGl5Tb4tKDO1Mq#mhG9 zZ@vNP#d?(dhfhy-VQQGg}4XM_DZF3AkArT7?;DRn893htacBB2D`O1IgEbKf;#=gx<7?aRHPNIP0 z5344#G_*naOfFyUra4oC=ty!?rz_>p^TfR9hlasJ#hxPBS@(>q%NIk*v?KGK_1w(}-saezA9P}XLtm)lioq-Rpo+Z& z2b(asjk?*L<5)0In74WB>$DK^WLUjDgNOgw0v?wsEKQ8P1qzPI`GqPxG1Yue(Nn+U zW?NK?gdHm#Y$l&ed8Tq{Qhbm1=MtJN;kMUKz_i=kSM1KfcVE&l`b=BNZBT3cRr%(O z1?wxpmP2WRQ*lF}d35nuIk))<2pGtl_rR8Slv&bsT9(}cJRLe{0Q{6E9bf-crI9W< zIcO#fX%DkqYJ}g<$1MlQ+;P3}IBzSu$Me`UX-#B1_ho7`?-kDY!p}~Wq)m|`d{YQX ziL`wDLTQWMzlGsuoYPeba2*$iW~6dJwvDFS|6@MQFC2rz;?t(xHLMG2GHQo+ED{5* zJ$AM)w`L`8Ff40W$adH3HAk$K_sa`r8bMho$jjFryG?LWa_jp`kS&W4(W437CmL)y za7&(N=k1NpAxa=G*`JW#PgdpnskBcqVROciOMvOl zk{Nx8PgrQA=m^GjI;LkTilSC!xl)6xDVAUI&Ez?CPDB_bb{J)^#Cq@~xhCNYSxqIB z#V4Z{Ucr6)ooq^ea{w~rO3)};pTA^SRw8Okfq_#W5ZcA5hflSxO-{mj`_oIE~wlZuPh+In| z#CAkVlM$PWI*e!!9$YhH;LmZs;1HXXS8Kkohv2Ayo0SVH@1Re$p~ns6X0Z0R(9aea z*Zre)^Eu;Wid&Coy|9I1$#^)w^k@z|wqQy(H6~7ipv5w!I~9C%exZlk1dAb+Kav9V z%wK8?e$VD7NGXO_g0t{n7AhD5-ae!xrf8qXrmeOGT0Ac|)+9JF!7u|qVWrnJdB-sG zWoiU5wU=_DULjskVRbEynqj4A&QK%!2wf#JkkxlMs zI1Tj;HJTm{u6TpwVbg8F9Xum;K&;MN5Xhr{#)_ck$N{F=^+4Jpl|5X@?=exLtOV?4 zQ7lFVhB3rEE<><1OP$_DmLa>LHIq3xi{TAHWdONXT;%-1Oiysi` zOMWx)-V66$nj9?|oeY(eiD}fds*U5U2^&tN^PUQ2(#*8)$>rkxiiKq>=#h#b5bNiLL*X(-Xw|N;d zoghUvaKQmh`JSrEOuP3O22{$BsShgY>!RS44H%E<+r9gqoX96q|I^gxODGgs5VHR_ z`H7f;13p^|nqCjGgx6iAJJ-ZSmtRT4CmqGgV~eP=J{sS#U8nJ54rY`|Fh`O32ee32 z@kO{b_Nfsm4)rped!Da8pq!0`tUvaCe6DjBUx8cnC^NE&%^7=>1jeaxY*YrFTA>RJ zQ6qDY6*95-tg|8Gh?ifyuFigS91gpxFlM0*RS_Lj5Vj%4&bqciSz6cXzl{<(do+#Q z21C!(clHDP#S_uZJrXQ*>CK*UBRh0cO?np|&pcSqQ5YB=rjxszq(Mm|#(l6vep zD3Xei#APJrbwAO(`7TzAkTv%U^QI7@*DUp1WQ`z+8G;Ln9i7&s0Mi@hW{8HVK#gc( z5AXQHyHKJRq6fI zr;>!f7yq*WWQ9AxhJPh$B^d&TP94m)D?W_;NYmcHouiwFLYVLt3K=F{4KwQc0~*EG zBA%MfpP6qz3J#HR+}Q}`r+uouuL;-3pvbp0>B!MzUIfhe=>|K{Cu}m7g&9#pt2GVQ zx(VI*;zFrHVds4g0iu8c#)1S09ML5##tx0$jewMhoP+$4F55h(R>6JT`8yA3m_~?= zZB?vH*3>o4IYGivPXvS3Wn=`gSTcklMl|=(llb=ubE=e0dZxaiwMz~bFMpTFuBuRZ zb(1$&_P7kbg!eCV%4YyLOZ5%F)6Gb9+8j8HzIFdqE?b*PavCYgnxp*+;)iAh*PFN- zkqvOjc%@l)c#HHp{RvIZ8Dy9;Zrh4rb(u*cE3zl`1w-UPExIyKI226vWXvG*b!LOPmJd)G5H;HQBbd?yT{v{LtuqJlrldL^vJm!N0 zIR`#}d_%%}ANhE%pZT25ujzR;4z9y`)3VM1PTy`1N|@~2tc^N5U_DJ+f8Iw0z3}Ah zAbeDDM#K$VNJ|`J;}dw!nH!@Z3@C8yPN z@LA|Vh!Pwm%q2G9qIw3OC(B z+Gg4>lWfgBk;ZEODVgT+kP*;sz=D!%5|IQp&Ye2Ts z7Z53`nY`_NPz#Mp?Ctpc2M6+bE9i5>sJoAKE>PrU-t03pu-og}=d?6vz3r0B&F5w} zsOR$o4UQ)8?^fvRUo!8@)>XfK89Jw(KAo$+SERALz{C7sn!CAPAHA{O)J^-$FmqS0`-qQm!-nL18 z<|=&yD;%R;*t3T%yd@k8j<3)L$jxxsOf1B=Ebn0u6?+hE6@{G)<~g5sR78D3O3ckF`Rbb7Zae#re<|fNhDCK5q9c8KpR$74n@A)@&O5&ixD?G=Er5OWb5g zBsF*X@7|2mkvJM}c7I51{5;qK5vI9m0p+(TaOFE93Y8u5fjEzR%fEDq3bHd-8W_Pr z8C-z};qaBUcH@QtT(b9aN7qW0G^RxQ-BAfVors(cgD2l}M~|Zvg?jq{ahlAD>Pu!{ zyksvyJnd2*s*Y;jrA~x-jBKjdhupkJTCBthh@Q%o0~%S&As=vQ;?CKN$fP{xK9SuB z?_sP57_|ynvsO{PxZAYqT+DWxOf(Xh^z9O zoa5b8Y7RNpeVjVqms+j~lXeV>B#DTo_(n8FV?jb``9UmsuT`)GFHovG{9?1sf4s9H zCcxP4^=lL~sTy71p=i|K^g3EPQY}8wB;&E)u1SZPaecr&$>a>XkbcL%jp7o@HtY~jH@czTwxl`d`IBPx_`OkI=LYU(~RN)@Fq5A3dc z!(jv@Gj79@H-uwn^7}rs$4lckf1G{_S?{;1DXg4b(9tKmzBG98SQO|J`ge0pl z5`Mb&NEyZ~b;ko5%l0aClFM*vlc}M_n1M-gUAD4*MwYm$j#}JoUBZ=ztOf0c%y zQO^4uwCvxl1*ItuBe>(fjUiFztfq=`_Y~KxRVOjwBJ96JtqtG`UDNR@i3xX3X#?18 zF^5wrEwb!?waxml3%zN`lwJ3{A8V)tDJ*Lt;T=F}MCtolhX!|&LF!5Mrv2;m!b7>2 z)S2gmCIB-w#&xMNyOlG0DBQ_&SlQ;Cf2dqm>nb1gHbb`fc) zMd9AGf&8x?I;Z=3Z|tnP84WBY^m61ON-Fp@u_%p%;@}Zbag{IL7p1O}k3-O&?93^{ zgXS|2ie7z5WDb4>^BzcH4KnH-Q==9WLF9)~+LuzGQGI#4FIGd}L5}e~gZ)|WHveSLYCE!%ISVgwp959J?g%8-E3oi^)u&^ zbIWeVZ3}Upqx-cL68<|VzEL)xqOY%p<@2dIzYY~Vz$TWz+lT5O;$Cz1I;Ru*tvjJ& zx6fa77ayy9mNQ0v%8H23GpD9y>*rY2JHZ6>my^0{(HIvZgf_A22Ng0fxvkm5Ar$M? z@I#l1hOoV6z^|8S_l!ne1UB4=YEO|Zz^L&0<;wbM zb7etj=W~1LX0DP;tfPFj{g8BsfphONTo+9APk@&zra}QvYSp|7*0HJ7l5+k#jgc~} zV!rTJx_j(A;${oG*P1X#)Y|S3A?jYOJ_WuBKrPbz`5SG^130_fnpaDm36p z$dcTw$R<%ku{GX;6HZp$no=k2C39;$}x5=!5F&CKIFc`1tE>k4#~ zBP=a;+oQl{4cW#YMm=@FK~V?-?teEg{@ZiMj>CIr-6y^Cgs(nq`uBkcASb0PSto89 F@*hG5mlps4 diff --git a/tests/ref/outline-bookmark.png b/tests/ref/outline-bookmark.png index 66e5329d88516a4cb6c8c2229682b8119df0bfcf..83c74444ae86a35f8d23115e43d9e784bb73b456 100644 GIT binary patch delta 441 zcmV;q0Y?6Y2-*XX7gQSv0{{R3fZDqh0002zP)t-s|NsBp-{1ymfbf!NbeByTjk#-`{7+NB{r;u1Q2eRCwC$ z)HMOx>6h(?W+l!bE#X>9^F?9T# zFObUb0}&~gfGE(!*XVYIOzVJG9In)X1JLT@V>mcLDz`@*t|oc7-2CWXY)8oC8i~WHG~H?|YN6bGHl<5#L*;Qe#}guy$+Ba! j67%KQ?COsyit?v7W=as7=W>hw00000NkvXXu0mjfbC%;1 delta 1021 zcmV+9=kYHDh0Z?CYl$H~!hbbQUv*KKciWM*!%w7jUQvVMSw zn3|$@d4V=KKsPrx0s;aO5)$t2?tFZFGBPscP=W?EcisjIVgcz}zIm4t?lfP#v7d3m0mo|u@JWMpJ# zXJ^RB$i&3Ny1Kgb^z`uX@M&pjXK8VRg^jhhzueyDPf=NEYjbLBbgi$qtgg1k$k4sN z$15#0qNJ?m=YQ?<^Y+5S$8K+PZg6;%mY$E1ndFMz_6`mX6ciM2aB!WSoyy9}(9qDNq@;Lwcsx8j zhK7bOFE1P%92y)X*xKTooSgtk`lD@va z#>U2(nVD>CY!43)UteFEnwq7hrTY5%&(F`Yvaa&x8-r*)8Z5kljZu)%5h2kiX#`O$e18K4K^vLI7m!2;W(Ce92SO&@*#`pq z_X3NxVSnF86h%=c8o++?Q6QxkE)@qfs6_8}X_gF)==3R(Dv>(z{3C^*43cnxjGb}2 zdt(a>8Uonr+jX)v+wv;G8vrN^z{rGDB4ww=+2o$Pt%00^x3szD`&2iMf!p8M+a_BV z{eOX!NS)K=&(d)VCO_%rxLf{3cFsy8uF}oudEni9@^=B4iE}iHq9}?Im&O2IH3(13 za$S_Lhkb=56ZVb32R+qouJ=mCx=czx)&*~TRSy_fp#~Mr(NQbUH9Z-&aczDcCNN-4 z(cyn;Iq-R_{}mQpZ<>zEce9VMjb{*xgmy{XlMWuP+zK1)F2O1uuGuF^c29cqj6nE(OR+qPgQs?2H9J<00000NkvXXu0mjfeBuqD diff --git a/tests/ref/outline-entry-complex.png b/tests/ref/outline-entry-complex.png index d0491179b803f8ada1894fea31a233c02ae19af4..d2ad49e79963ab19427e29312089345e4727bbcc 100644 GIT binary patch literal 8461 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 Yc)SGlL+9VOZw>$%Nd<`-h(X~006Aa`Hvj+t literal 14460 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 WT8wU--@m^K!DJ*9#p^|lL;gRfvkSEV diff --git a/tests/ref/outline-entry-inner.png b/tests/ref/outline-entry-inner.png new file mode 100644 index 0000000000000000000000000000000000000000..5376c9961453ad7c1f3acf1fb40e93a606fae0a8 GIT binary patch literal 462 zcmV;<0WtoGP)vq&_AXr!W1 zt5&!$0+d=mA#Afy!76i$X#A!_>QY)O6~GW<`%YaJ`|#}K%9qIwvONVGxRT+L2L zqRHNxfxwOV9KsE2!#x%NT~*a=mgsn*VVu)Kngn3bm`zm6BsU1sq(xLaM(%M*GX-*v zFnxwbb2X1d6EL_5*e)&~C0?lXsYG0*XL1HFjpm8FY)~B@YG%kDuIM#(X~rZT=+yXr zu2#5G4-9QX+y?fmhc~*H)(;?Vde5{x5;82qGW@5*U*6A7eiyuS@c;k-07*qoM6N<$ EfkPy|dRtBHO-RJ4Ol$onC8g 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 Kt{P|=_WuAvVO=`_ literal 10099 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 diff --git a/tests/ref/outline-first-line-indent.png b/tests/ref/outline-first-line-indent.png index e40b440949a8bc1ab590393a19c186bc0d793152..e3341295cdf508f9425b4553211321fcd4aa2125 100644 GIT binary patch literal 5539 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 literal 10837 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 diff --git a/tests/ref/outline-heading-start-of-page.png b/tests/ref/outline-heading-start-of-page.png new file mode 100644 index 0000000000000000000000000000000000000000..e6dbbb5f1ddaf02145241e91eb2e01dab11af21c GIT binary patch literal 6935 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 P00000NkvXXu0mjf-RwE) literal 0 HcmV?d00001 diff --git a/tests/ref/outline-indent-auto-mixed-prefix.png b/tests/ref/outline-indent-auto-mixed-prefix.png new file mode 100644 index 0000000000000000000000000000000000000000..097e0bf88f1858e0d927b48233d7f39ede2ce5a8 GIT binary patch literal 5712 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 literal 0 HcmV?d00001 diff --git a/tests/ref/outline-indent-auto-no-prefix.png b/tests/ref/outline-indent-auto-no-prefix.png new file mode 100644 index 0000000000000000000000000000000000000000..e746b35b62c7603710b7ee3025693e3c7eef0bd9 GIT binary patch literal 3101 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 literal 0 HcmV?d00001 diff --git a/tests/ref/outline-indent-auto.png b/tests/ref/outline-indent-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..53517abd8352ac231a80fb3885def697d86ae548 GIT binary patch literal 5176 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≪BIk|3#CngLkaB*0~+Kpu=QfbrX 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 literal 0 HcmV?d00001 diff --git a/tests/ref/outline-spacing.png b/tests/ref/outline-spacing.png new file mode 100644 index 0000000000000000000000000000000000000000..897a5f74609d16efe885a14d491024be77afc562 GIT binary patch literal 2553 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 DgvX)g delta 1463 zcmV;o1xWgc3&{(RB!7@eL_t(|+U?t2Op{j_2XF@q;lj(g#h92dbz8D5yKu%V(~Frb zBUy;aGUJ6a8eo#mp-d1cIDvpbEngMIj4`qa#R9dZ&U85ZK(Lh377LX^X-m;UTj&>1 zzU-I#o7dC^g_&t)g3ptalXIT)yf1lwd2@Q2lr;}2{+C!o1b+lg&=dqs&;(6E&;(8K zDxl?Z`JFp=JRXlkBAJ?+BGdl@&6OB7u@qIUMx)6pmW^)9S0&XB3>&SOGR-t<&0ysn z{fX75(Qp6s^J_P{kPL6D*v?{g;sLMUly!BhF?bmah5mNiAyzh$q9tPpi7wQEm z$FeZqv-iM__J8iz3L4M;;YZs9Ldi>je*c6*Gi+fRpj#F9k*seYIV&;b3UnAh4P8)J z#^zM!<#Y7L@m2pu|L|klCDC6m0Xin5r~HbZY1ldd4e`$kdoSKH#g?jTuZ=8$uKk^# zb)o=MK(n&*3Ho`VKRBU8&$Qj#en9g(OkY>_%QR+bzkgY0v@C&6OFzjeJ%=g&JTe6R zT+sO3Tc0KmL4Q%A7D`9^b>rAi`i8~_^%j+Oth%QD`@^YNjoZCf(%N3%AjANhMiHBf z4J34{?(S}{*E={kXt&!ZCntx7h8{e4fD;RrIyySgV~h;)SVav6sK-@H3UoT%#KZ&& zaH}N+J%2qti;IiH!^1Y4&EarpwOXIgCzHuS1qy{?VPQe9*ITVtr_(twFyQz5pDr*M z3@C89TzBu@ML}0rSExX#RL;-OhePi_kW|OJ^rFzUoz^7Y$OWm*<@Vedupho?%KP)a z-RWtP+m1Avs&83bE>BKwL26p&&Gy?zj;3CRG7@4M~CamIlPzg;Ahl8bzj12UprKKPi2m}D_?d|3B z`M4pQ%?=fmmzM)bC=^0ptyVWQGyto(xR@yj27?t96&8y{Boe`{udk2CguMar%#D=W=rGo}cSXTGCThNp&r z)#jitEcDHgpeYENpeYENpb46Spb46Spb46wDF~XNDF}L1L;HNbnVFf;)ZuUt^s>=z zoIlg)J|}gf7im1m+;{u9o$j-@-Icf96@MMBVDK4N84QM9yLLrKM`veeV_H>JwPC}C zh=>TM(@D_J4$ornn?J_iNYi~%G3uD}BjXP|Jl{2iOyqH$dVF!Y3wb;qWOH(InCaH7 zTQS88PSDE+!CT-yG4*&n1ifthB6|d#dVl!| zv}M|l%zMQn;q=&B^4lE!mc{3R)@rqEHajaTOCphAD<|kD(A&@4%aYicmMoeGhc-IA z>+=kPeox_bXHF^wlR1p+kp|7mLLN{V!VwP91NZG`y2*z?p<;x=HT} z(6h6%7;N0Qk;#{pm0^m*2|+)GKDPWG{dTV5&2*i8*3V4my+QOMktqDsv17-M(5Yj~ zmMu|HQTRa+#vBd@d7S?U`XROguzp!>4rTt<%^?$=IgG;}ra!kjB97}mE`J@(x$5|~ z!OH8M2?_4ow-4iBFvy&vRVo#xH8nMnk&!7WDS<#BHa0dUCI+wh>kfT+U>3V`Vv`Mh zb@vo9BTnyk%{F8bnr!$~#MTiAK6>l-To^xa(b_sR$NYjVK0ZD%F%i8`C@d%_NJvP) zG$|>G`ImsUwzlNt^;cBgyQf1+x>1pmj-k7ePU)78p&JGekWK;Vkw&DuyQD_x2I=mudwk#Tz3Z;~ z7u@s9JZsiIXYI3}_{51&RhGqiLG}U;4i4+RoRm87>jeh~pMZt{L~jIEec|B9sNPG7 zYkJKeW}&)iX-yBvTDe(Hk`C`+a=m?9Y+{}tPJp_L#wdl>7NIFF5dnHbVDk+bLEMMi z7LD@FwGpQW(qOGrv;fTgOcsv1@T?j!K& z)xlEIQAB0BHI^RmSknuREi6Jp6bm-bPmh+&dbPz?CJ~B)Mdo&Uw)y;Y-SLpr?tg!@)WXXyE-NF`y#s7j`BE)M#GlIl zLXE;xPHs^2`BvqGQpE2jL6&MLjf-Bn&|)|x`cHm7jSw91C!Do(!zP!_onLwUE*rBo zRw_%)?m6CDq#~$a9#W1+zr53M*&Z|ep~KN4gd_UEt{r~vtq-g(H*7hxF>H22SA_L^ z#`)a+mBU=!|Ds6ru|zpbfTYFq*dwpq@AmBWe0u0*0X;zRjPg5yk8E5zP3l}u5wtY&xYQ{x;p*ojhK}T!|W3AJfiUEizGbmczyzR zJQ6Kzpf`J;Z^8A6L7wkd$U)X7SKB#|<^1>f=b}OAWOa5k*e{dll)7amid1;F^;R`mjTOd~(!@$hls zYl`QW6<9mdW%{mkh*uLTqMM>YqsXfhPJ+^r1iOhF zFnG-Qt)I$5{OkkxEkBU__b#l+c^!JMj)H|ypNo3H8{ z;Bm+rf4v&>*;*MN&!ik*$#@4tXW|9f5dBdldiP;IS~4fw=jfKWO$>*EqRwK$XlzIX zddsEXetC7!6vc_*GYC`r6c+8q5cOP?S(C%2XDj{32lMk&S$u4urRo5S;vlfRv<428 zYIkt60}|IRzM1deFg!ZkD5)|PzKbK{LZ11RYvCKz&H-z?$KIVRNX@%Fevr;G%-v8J z+nV|ztAAieo)Kq#N&96XB zH86dpVyr9QXRFrX;^F!|( zA-bJPc5|Sxf4@*bv-ZJ7lh7d+6=$$)r~CU1mhMEQV0n|ZH+&eu^6vY*o7;CWf?v-@ zAuu_eU8aU>o9x7 zoy%g+$M;A0Zz7g_!}MQ6?#B@QsAUungix6KAx1w|cLlQjn&T;PUh!qp2HS56G@4rw zfucY}n(6Eu&1MDA^g1$4uM$o$zfq}&*V&K&rl@f{+nG?icg9ic@W6i-aA z-Zm9kyZ{)CftmajpSY~XXap5h26Tl?=PFG*C2`XQqI}%C6dzM;y!P;>+eyRifUC*ZsY)=G__PbgS4tIV2M@Il5?IsCU6S)O++KJ0F>wqA zQWuACn845iVoCVFnQv#WqEscY7+xj&qs9utDzr*lymoDC?n80jUcZeO>)`d_gb!+B;?U~W6$15M~v`|dO%6f3{-->H*p)c6HAqJwts|2 z=9~^HE^XB8;hZnh4hn{-!UozxP<_6}IaP&bdDo^p^mG{eBBM+q;yp5+wa}X{LxMEr zU*5qoc#bVGyRE$@Z-^DNei+rf5f?ls#|A04Y9EU)A{yRIsy%siRX-p{;q%q`(uwku zutU*loCku@dHquKIFZS!ikA0xZ+;gj9H94}5JqsjZjViAoR0=Rt$lt4Ou4ty)e`Vx0AZq(2V`GGB)&=@7B#a$m30vKD{LSc={{ z3!y384Y{sN%j1%Dfkw>MVH`$^J4M=2c|qr@Q@F)xw{+q) z31*||+$5xt7X<)IQ&i+eCbkYrK@wOEmbV%UuoS0VnJL_GXh%)r1&^PMtkWx37}Skj z*8-gpm&0x$HMx_2In>%l8!X>iO;f8}m&!OMRd@c$!Fr7)$ne3i41f}9|Ap`}u8tViMMqHID z3LFHLhe#$@AdnA+bMT(KvbdVmZ-=-@B$6UPHCFAO^>fh!8RoT-e)+oLsF6&F2cFW477GW#ACka01Fi z?_VdCPeF?oKfk=$@-D;Hhdivm+7(cOCl4Bf19|6rpKYY^g0tr?4hxyyDVaBb zR~+eKgOkn4cahVp50L@YZY@MRm}2;6sHH`0{e(^-M^&@omdn?Rt~fDY{AWJwO_j>7 zUx+@e;ea*?frS(Dk3pzZosC308yg?{nV0tbGa_M}S@oB+3MsuoVg}HOi3M<%@S_4P|m$djM) z$zHf*#yt^>UL);*?{Qm$+wO0KEOD^1-Z*%^7M+|@*yjuOO5hfiwS%Bd@zcZkxcO^O z>^SO09tYE#i`@x;W^S4F_V%teA`7&MFExOTYfhU=wJWx;n{1~T=fAsd^bs4^THzHT zqGCIB|4e2g9d0dPV548ag<-h)Oi1)sfFoqg$tWm(&`17&cZgtXA1AaP`O0BCm(vOqP-MX_353bXDXU%!*pE+S^`Ce=M)J2s)B4)dTV!r_Qq zKSO9Li~ff)J#KyE&-N8YZSr3jaLLK-Vw{45gIl|AnFlJ#hW~U9CcIr=Uq5a=9|IUT znp;0KiiqWBwwS^z2IF=i885FJKP|f5E;<`tKXU&%20561+=%D~la8R@~f` z!t&Z2d0HLk1U*H)htI;eFf{-9s-c4;(qFvh43C&twi$i}@o#0kH_msc9Q;$)O-V+! zmv&R-wySOo19Z>sQj2G~K~r~k_pBEaY_|lEd4Dy6Q}-J6b;^cpH98(1Uiqc~kfIP_ z$3?y%Wfftci%x~cjV2z4`O1LsuAeD&5}Qp3Fd;nyj0-?bRcd*$Bf>Cs#FALec_qIB zt7;8fJRpzz#zSvcO{AodB$t^;qx0of6Ky4Nw0`3Qstn69XiN-q4fm~#$;l6%BUz<% z&`{^=WVE`Qr5SZ&}daVx_>8dC-gzDe=hC862F-g14UYx3* zp1rkBcgDN1O20m+8L#G+8+<@ccY~Nc)K5x$ksN$LNFT(l!X{?%c{m+lGj3!lsH-XUWU z5wM8{Tk^;?!|NZDF8$dwhG}lU_7B9w8tr&QyBng<0evzMnEG%|Y@OKkbe7r?zab|T zfoWK68ZyELMSqik8@ck@H1#* zKo_^LWB`hikh3E&HahwvaVYyOC1{~?c8WzY098pHRUa#K=omWp*#J<)3moA^*}}d& z@N{a!KCtj!!IWwStxh7R?;dDr*?IrVeE1(1;(r3D2ACU=-4^TpF$jmoYC<7fBW~S< z)*Vr6icQ8rS1s0oOfUpWG3YzptE~+1i&}_uB06_A!@Uwd-haQt-

dMPzvt`W>FK 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

=bDhhvkJjjiic|Uacbz10ty+0J? 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 literal 9302 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 (0pt, 1em, 2.5em, 3em).at(n)) + += A +== B +=== C +==== Title breaks +#set heading(numbering: none) +== E += F + +--- outline-indent-bad-type --- +// Error: 2-35 expected relative length, found dictionary +#outline(indent: n => (a: "dict")) + += Heading + +--- outline-entry --- +#set page(width: 150pt) +#set heading(numbering: "1.") + +#show outline.entry.where(level: 1): set block(above: 12pt) +#show outline.entry.where(level: 1): strong + +#outline(indent: auto) + +#show heading: none += Introduction += Background +== History +== State of the Art += Analysis +== Setup + +--- outline-entry-complex --- +#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt)) +#set heading(numbering: "1.") + +#set outline.entry(fill: repeat[--]) +#show outline.entry.where(level: 1): it => link( + it.element.location(), + it.indented(it.prefix(), { + emph(it.body()) + [ ] + text(luma(100), box(width: 1fr, repeat[--·--])) + [ ] + it.page() + }) +) + +#counter(page).update(3) +#outline() + +#show heading: none + += Top heading +== Not top heading +=== Lower heading +=== Lower too +== Also not top + +#pagebreak() +#set page(numbering: "1") + += Another top heading +== Middle heading +=== Lower heading + +--- outline-entry-inner --- +#set heading(numbering: "1.") +#show outline.entry: it => block(it.inner()) +#show heading: none + +#set outline.entry(fill: repeat[ -- ]) +#outline() + += A += B + +--- outline-heading-start-of-page --- +#set page(width: 140pt, height: 200pt, margin: (bottom: 20pt), numbering: "1") #set heading(numbering: "(1/a)") #show heading.where(level: 1): set text(12pt) #show heading.where(level: 2): set text(10pt) -#outline(fill: none) +#set outline.entry(fill: none) +#outline() = A = B @@ -23,66 +208,28 @@ A == F ==== G +--- outline-bookmark --- +// Ensure that `bookmarked` option doesn't affect the outline +#set heading(numbering: "(I)", bookmarked: false) +#set outline.entry(fill: none) +#show heading: none +#outline() + += A + --- outline-styled-text --- #outline(title: none) = #text(blue)[He]llo ---- outline-bookmark --- -#outline(title: none, fill: none) - -// Ensure 'bookmarked' option doesn't affect the outline -#set heading(numbering: "(I)", bookmarked: false) -= A - ---- outline-indent-numbering --- -// With heading numbering -#set page(width: 200pt) -#set heading(numbering: "1.a.") -#show heading: none -#set outline(fill: none) - -#context test(outline.indent, none) -#outline(indent: none) -#outline(indent: auto) -#outline(indent: 2em) -#outline(indent: n => ([-], [], [==], [====]).at(n)) - -= A -== B -== C -=== D -==== E - ---- outline-indent-no-numbering --- -// Without heading numbering -#set page(width: 200pt) -#show heading: none -#set outline(fill: none) - -#outline(indent: none) -#outline(indent: auto) -#outline(indent: n => 2em * n) - -= About -== History - ---- outline-indent-bad-type --- -// Error: 2-35 expected relative length or content, found dictionary -#outline(indent: n => (a: "dict")) - -= Heading - --- outline-first-line-indent --- #set par(first-line-indent: 1.5em) #set heading(numbering: "1.1.a.") -#show outline.entry.where(level: 1): it => { - v(0.5em, weak: true) - strong(it) -} +#show outline.entry.where(level: 1): strong #outline() +#show heading: none = Introduction = Background == History @@ -90,85 +237,54 @@ A = Analysis == Setup ---- outline-entry --- -#set page(width: 150pt) -#set heading(numbering: "1.") - -#show outline.entry.where( - level: 1 -): it => { - v(12pt, weak: true) - strong(it) -} - -#outline(indent: auto) -#v(1.2em, weak: true) - -#set text(8pt) -#show heading: set block(spacing: 0.65em) - -= Introduction -= Background -== History -== State of the Art -= Analysis -== Setup - ---- outline-entry-complex --- -#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt)) -#set heading(numbering: "1.") -#show outline.entry.where(level: 1): it => [ - #let loc = it.element.location() - #let num = numbering(loc.page-numbering(), ..counter(page).at(loc)) - #emph(link(loc, it.body)) - #text(luma(100), box(width: 1fr, repeat[#it.fill.body;·])) - #link(loc, num) -] - -#counter(page).update(3) -#outline(indent: auto, fill: repeat[--]) -#v(1.2em, weak: true) - -#set text(8pt) -#show heading: set block(spacing: 0.65em) - -= Top heading -== Not top heading -=== Lower heading -=== Lower too -== Also not top - -#pagebreak() -#set page(numbering: "1") - -= Another top heading -== Middle heading -=== Lower heading - --- outline-bad-element --- // Error: 2-27 cannot outline metadata #outline(target: metadata) #metadata("hello") + +--- issue-2048-outline-multiline --- +// Without the word joiner between the dots and the page number, +// the page number would be alone in its line. +#set page(width: 125pt) +#set heading(numbering: "1.a.") +#show heading: none + +#outline() + += A +== This just fits here + --- issue-2530-outline-entry-panic-text --- // Outline entry (pre-emptive) -// Error: 2-48 cannot outline text -#outline.entry(1, [Hello], [World!], none, [1]) +// Error: 2-27 cannot outline text +#outline.entry(1, [Hello]) --- issue-2530-outline-entry-panic-heading --- // Outline entry (pre-emptive, improved error) -// Error: 2-55 heading must have a location -// Hint: 2-55 try using a query or a show rule to customize the outline.entry instead -#outline.entry(1, heading[Hello], [World!], none, [1]) +// Error: 2-34 heading must have a location +// Hint: 2-34 try using a show rule to customize the outline.entry instead +#outline.entry(1, heading[Hello]) ---- issue-4476-rtl-title-ending-in-ltr-text --- +--- issue-4476-outline-rtl-title-ending-in-ltr-text --- #set text(lang: "he") #outline() +#show heading: none = הוקוס Pocus = זוהי כותרת שתורגמה על ידי מחשב ---- issue-5176-cjk-title --- +--- issue-4859-outline-entry-show-set --- +#set heading(numbering: "1.a.") +#show outline.entry.where(level: 1): set outline.entry(fill: none) +#show heading: none + +#outline() + += A +== B + +--- issue-5176-outline-cjk-title --- #set text(font: "Noto Serif CJK SC") #show heading: none From dda486a412b31acbf767087c748a62ccc6b510b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Thu, 23 Jan 2025 13:08:48 +0100 Subject: [PATCH 26/44] HTML tables (#5666) --- crates/typst-html/src/encode.rs | 5 +- .../typst-library/src/layout/grid/resolve.rs | 2 +- crates/typst-library/src/model/table.rs | 66 +++++++++++++++++-- tests/ref/html/basic-table.html | 35 ++++++++++ tests/suite/layout/grid/html.typ | 32 +++++++++ 5 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 tests/ref/html/basic-table.html create mode 100644 tests/suite/layout/grid/html.typ diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 62146f867..71422a0fc 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -120,7 +120,10 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { /// Whether the element should be pretty-printed. fn is_pretty(element: &HtmlElement) -> bool { - tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta) + matches!( + element.tag, + tag::meta | tag::table | tag::thead | tag::tbody | tag::tfoot | tag::tr + ) || tag::is_block_by_default(element.tag) } /// Escape a character. diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index 504159e83..f6df57a37 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -602,7 +602,7 @@ pub enum Entry<'a> { impl<'a> Entry<'a> { /// Obtains the cell inside this entry, if this is not a merged cell. - fn as_cell(&self) -> Option<&Cell<'a>> { + pub fn as_cell(&self) -> Option<&Cell<'a>> { match self { Self::Cell(cell) => Some(cell), Self::Merged { .. } => None, diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index fa44cb58a..ba7924422 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -7,7 +7,11 @@ use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, + TargetElem, }; +use crate::html::{tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; +use crate::introspection::Locator; +use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use crate::layout::{ show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, @@ -258,11 +262,65 @@ impl TableElem { type TableFooter; } +fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { + let cell = cell.body.clone(); + let Some(cell) = cell.to_packed::() else { return cell }; + let mut attrs = HtmlAttrs::default(); + let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string()); + if let Some(colspan) = span(cell.colspan(styles)) { + attrs.push(HtmlAttr::constant("colspan"), colspan); + } + if let Some(rowspan) = span(cell.rowspan(styles)) { + attrs.push(HtmlAttr::constant("rowspan"), rowspan); + } + HtmlElem::new(tag) + .with_body(Some(cell.body.clone())) + .with_attrs(attrs) + .pack() + .spanned(cell.span()) +} + +fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { + let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack(); + let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect(); + + let tr = |tag, row: &[Entry]| { + let row = row + .iter() + .flat_map(|entry| entry.as_cell()) + .map(|cell| show_cell_html(tag, cell, styles)); + elem(tag::tr, Content::sequence(row)) + }; + + let footer = grid.footer.map(|ft| { + let rows = rows.drain(ft.unwrap().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))); + if header.is_some() || footer.is_some() { + body = elem(tag::tbody, body); + } + + let content = header.into_iter().chain(core::iter::once(body)).chain(footer); + elem(tag::table, Content::sequence(content)) +} + impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table) - .pack() - .spanned(self.span())) + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + Ok(if TargetElem::target_in(styles).is_html() { + // TODO: This is a hack, it is not clear whether the locator is actually used by HTML. + // How can we find out whether locator is actually used? + let locator = Locator::root(); + show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles) + } else { + BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack() + } + .spanned(self.span())) } } diff --git a/tests/ref/html/basic-table.html b/tests/ref/html/basic-table.html new file mode 100644 index 000000000..6ba1864ef --- /dev/null +++ b/tests/ref/html/basic-table.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Thefirstand
thesecondrow
FooBazBar
12
34
Thelastrow
+ + diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ new file mode 100644 index 000000000..2a7dfc2ce --- /dev/null +++ b/tests/suite/layout/grid/html.typ @@ -0,0 +1,32 @@ +--- basic-table html --- +#table( + columns: 3, + rows: 3, + + table.header( + [The], + [first], + [and], + [the], + [second], + [row], + table.hline(stroke: red) + ), + + table.cell(x: 1, rowspan: 2)[Baz], + [Foo], + [Bar], + + [1], + // Baz spans into the next cell + [2], + + table.cell(colspan: 2)[3], + [4], + + table.footer( + [The], + [last], + [row], + ), +) From e61cd6fb9e9a90de8d78f05a43246f08feddcf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Thu, 23 Jan 2025 13:18:46 +0100 Subject: [PATCH 27/44] Support `start` attribute for `enum` in HTML export (#5676) --- crates/typst-library/src/model/enum.rs | 24 ++++++++++++------------ tests/ref/html/enum-start.html | 12 ++++++++++++ tests/suite/model/enum.typ | 7 +++++++ 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 tests/ref/html/enum-start.html diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index eb3c2ea45..2d774cbbb 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -229,19 +229,19 @@ impl Show for Packed { if TargetElem::target_in(styles).is_html() { let mut elem = HtmlElem::new(tag::ol); if self.reversed(styles) { - elem = - elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed"); + elem = elem.with_attr(HtmlAttr::constant("reversed"), "reversed"); } - return Ok(elem - .with_body(Some(Content::sequence(self.children.iter().map(|item| { - let mut li = HtmlElem::new(tag::li); - if let Some(nr) = item.number(styles) { - li = li.with_attr(attr::value, eco_format!("{nr}")); - } - li.with_body(Some(item.body.clone())).pack().spanned(item.span()) - })))) - .pack() - .spanned(self.span())); + if let Some(n) = self.start(styles).custom() { + elem = elem.with_attr(HtmlAttr::constant("start"), eco_format!("{n}")); + } + let body = Content::sequence(self.children.iter().map(|item| { + let mut li = HtmlElem::new(tag::li); + if let Some(nr) = item.number(styles) { + li = li.with_attr(attr::value, eco_format!("{nr}")); + } + li.with_body(Some(item.body.clone())).pack().spanned(item.span()) + })); + return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); } let mut realized = diff --git a/tests/ref/html/enum-start.html b/tests/ref/html/enum-start.html new file mode 100644 index 000000000..8a4ff37f9 --- /dev/null +++ b/tests/ref/html/enum-start.html @@ -0,0 +1,12 @@ + + + + + + + +

    +
  1. Skipping
  2. Ahead
  3. +
+ + diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index 258c6f6bc..e957ae9e8 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -101,6 +101,13 @@ a + 0. [Red], [Green], [Blue], [Red], ) +--- enum-start html --- +#enum( + start: 3, + [Skipping], + [Ahead], +) + --- enum-numbering-closure-nested --- // Test numbering with closure and nested lists. #set enum(numbering: n => super[#n]) From b3fb6c2326ac6d585cc17d1f643bc06e076be042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Thu, 23 Jan 2025 13:21:34 +0100 Subject: [PATCH 28/44] Support quotes in HTML output (#5673) --- crates/typst-library/src/model/quote.rs | 81 +++++++++++++--------- tests/ref/html/quote-attribution-link.html | 15 ++++ tests/ref/html/quote-nesting-html.html | 12 ++++ tests/ref/html/quote-plato.html | 21 ++++++ tests/suite/model/quote.typ | 23 ++++++ 5 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 tests/ref/html/quote-attribution-link.html create mode 100644 tests/ref/html/quote-nesting-html.html create mode 100644 tests/ref/html/quote-plato.html diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 2eaa32d4c..774384acb 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -2,13 +2,14 @@ use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, - StyleChain, Styles, + StyleChain, Styles, TargetElem, }; +use crate::html::{tag, HtmlAttr, HtmlElem}; use crate::introspection::Locatable; use crate::layout::{ Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, }; -use crate::model::{CitationForm, CiteElem}; +use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget}; use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// Displays a quote alongside an optional attribution. @@ -158,6 +159,7 @@ impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let mut realized = self.body.clone(); let block = self.block(styles); + let html = TargetElem::target_in(styles).is_html(); if self.quotes(styles) == Smart::Custom(true) || !block { let quotes = SmartQuotes::get( @@ -171,50 +173,65 @@ impl Show for Packed { let Depth(depth) = QuoteElem::depth_in(styles); let double = depth % 2 == 0; - // Add zero-width weak spacing to make the quotes "sticky". - let hole = HElem::hole().pack(); + if !html { + // Add zero-width weak spacing to make the quotes "sticky". + let hole = HElem::hole().pack(); + realized = Content::sequence([hole.clone(), realized, hole]); + } realized = Content::sequence([ TextElem::packed(quotes.open(double)), - hole.clone(), realized, - hole, TextElem::packed(quotes.close(double)), ]) .styled(QuoteElem::set_depth(Depth(1))); } + let attribution = self.attribution(styles); + if block { - realized = BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(self.span()); - - if let Some(attribution) = self.attribution(styles).as_ref() { - let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()]; - - match attribution { - Attribution::Content(content) => { - seq.push(content.clone()); - } - Attribution::Label(label) => { - seq.push( - CiteElem::new(*label) - .with_form(Some(CitationForm::Prose)) - .pack() - .spanned(self.span()), - ); + realized = if html { + let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized)); + if let Some(Attribution::Content(attribution)) = attribution { + if let Some(link) = attribution.to_packed::() { + if let LinkTarget::Dest(Destination::Url(url)) = &link.dest { + elem = elem.with_attr( + HtmlAttr::constant("cite"), + url.clone().into_inner(), + ); + } } } + elem.pack() + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack() + } + .spanned(self.span()); - // Use v(0.9em, weak: true) bring the attribution closer to the - // quote. - let gap = Spacing::Rel(Em::new(0.9).into()); - let v = VElem::new(gap).with_weak(true).pack(); - realized += v + Content::sequence(seq).aligned(Alignment::END); + if let Some(attribution) = attribution.as_ref() { + let attribution = match attribution { + Attribution::Content(content) => content.clone(), + Attribution::Label(label) => CiteElem::new(*label) + .with_form(Some(CitationForm::Prose)) + .pack() + .spanned(self.span()), + }; + let attribution = + [TextElem::packed('—'), SpaceElem::shared().clone(), attribution]; + + if !html { + // Use v(0.9em, weak: true) to bring the attribution closer + // to the quote. + let gap = Spacing::Rel(Em::new(0.9).into()); + let v = VElem::new(gap).with_weak(true).pack(); + realized += v; + } + realized += Content::sequence(attribution).aligned(Alignment::END); } - realized = PadElem::new(realized).pack(); - } else if let Some(Attribution::Label(label)) = self.attribution(styles) { + if !html { + realized = PadElem::new(realized).pack(); + } + } else if let Some(Attribution::Label(label)) = attribution { realized += SpaceElem::shared().clone() + CiteElem::new(*label).pack().spanned(self.span()); } diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html new file mode 100644 index 000000000..4da8b47f5 --- /dev/null +++ b/tests/ref/html/quote-attribution-link.html @@ -0,0 +1,15 @@ + + + + + + + +
+ Compose papers faster +
+

+ — typst.com +

+ + diff --git a/tests/ref/html/quote-nesting-html.html b/tests/ref/html/quote-nesting-html.html new file mode 100644 index 000000000..c652bd97b --- /dev/null +++ b/tests/ref/html/quote-nesting-html.html @@ -0,0 +1,12 @@ + + + + + + + +

+ When you said that “he surely meant that ‘she intended to say “I'm sorry”’”, I was quite confused. +

+ + diff --git a/tests/ref/html/quote-plato.html b/tests/ref/html/quote-plato.html new file mode 100644 index 000000000..fc052d10c --- /dev/null +++ b/tests/ref/html/quote-plato.html @@ -0,0 +1,21 @@ + + + + + + + +
+ … ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. +
+

+ — Plato +

+
+ … I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either. +
+

+ — from the Henry Cary literal translation of 1897 +

+ + diff --git a/tests/suite/model/quote.typ b/tests/suite/model/quote.typ index 2c93f92cd..d0dcc55dd 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -84,3 +84,26 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. // With custom quotes. #set smartquote(quotes: (single: ("<", ">"), double: ("(", ")"))) #quote[A #quote[nested] quote] + +--- quote-plato html --- +#set quote(block: true) + +#quote(attribution: [Plato])[ + ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι + ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. +] +#quote(attribution: [from the Henry Cary literal translation of 1897])[ + ... I seem, then, in just this little thing to be wiser than this man at + any rate, that what I do not know I do not think I know either. +] + +--- quote-nesting-html html --- +When you said that #quote[he surely meant that #quote[she intended to say #quote[I'm sorry]]], I was quite confused. + +--- quote-attribution-link html --- +#quote( + block: true, + attribution: link("https://typst.app/home")[typst.com] +)[ + Compose papers faster +] From f7bd03dd76533cda2d2626d6470d3bb55e03b012 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Thu, 23 Jan 2025 07:27:38 -0500 Subject: [PATCH 29/44] Fix delimiter unparen syntax (#5739) --- crates/typst-syntax/src/parser.rs | 4 ++-- tests/ref/math-lr-unparen.png | Bin 0 -> 493 bytes tests/suite/math/delimited.typ | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 tests/ref/math-lr-unparen.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index a65e5ff6b..f9fb8b616 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -442,10 +442,10 @@ fn math_unparen(p: &mut Parser, m: Marker) { if first.text() == "(" && last.text() == ")" { first.convert_to_kind(SyntaxKind::LeftParen); last.convert_to_kind(SyntaxKind::RightParen); + // Only convert if we did have regular parens. + node.convert_to_kind(SyntaxKind::Math); } } - - node.convert_to_kind(SyntaxKind::Math); } /// The unicode math class of a string. Only returns `Some` if `text` has diff --git a/tests/ref/math-lr-unparen.png b/tests/ref/math-lr-unparen.png new file mode 100644 index 0000000000000000000000000000000000000000..d418b14eaa978f7975cc8185ce6e25b52c7a5056 GIT binary patch literal 493 zcmV+IEiC>vD{QupfuT z9}}B_3aZl+)==5U9`ix0&-3GO`8c%!#QM3`o5~iyRK5vj@A!tp;%~Z#!0a=RsT{{U zjeg>fkFQODRL{R~S^T^n z(x3QBU5nqBL)c}1a9I4S2vS2ezon|h|01^>Ja}OJWP>bRL7lby;K75NX4q3XAAeW` z1^01S{A&qR!Ij}uN!)xzz~a|8XkziNg9Lp1Zy#-Zd@}nVev404zoCi6pGqDR@Nvdt z>RKGM{TPUPI~N?s`?g1;D~R3>4&*=6z!o3d9z-RJw=LR?#9O)OAi9Esi&i4>HZR&X j+G!iLc+}!ii)je}qm_{qY}NN400000NkvXXu0mjf^L^=Q literal 0 HcmV?d00001 diff --git a/tests/suite/math/delimited.typ b/tests/suite/math/delimited.typ index 226740501..ca82427dd 100644 --- a/tests/suite/math/delimited.typ +++ b/tests/suite/math/delimited.typ @@ -125,3 +125,11 @@ $ lr(size: #3em, |)_a^b lr(size: #3em, zws|)_a^b --- issue-4188-lr-corner-brackets --- // Test positioning of U+231C to U+231F $⌜a⌟⌞b⌝$ = $⌜$$a$$⌟$$⌞$$b$$⌝$ + +--- math-lr-unparen --- +// Test that unparen with brackets stays as an LrElem. +#let item = $limits(sum)_i$ +$ + 1 / ([item]) quad + 1 / [item] +$ From 58dbbd48fe415c5a345fb1665aab478a03b5df82 Mon Sep 17 00:00:00 2001 From: SekoiaTree <51149447+SekoiaTree@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:35:29 +0100 Subject: [PATCH 30/44] Add lcm as an operator in math mode (#5718) Co-authored-by: Laurenz --- crates/typst-library/src/math/op.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs index ef24705a7..5b3f58beb 100644 --- a/crates/typst-library/src/math/op.rs +++ b/crates/typst-library/src/math/op.rs @@ -17,9 +17,9 @@ use crate::text::TextElem; /// # Predefined Operators { #predefined } /// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, /// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`, -/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, -/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, -/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. +/// `gcd`, `lcm`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, +/// `limsup`, `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, +/// `sinc`, `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. #[elem(title = "Text Operator", Mathy)] pub struct OpElem { /// The operator's text. @@ -75,6 +75,7 @@ ops! { dim, exp, gcd (limits), + lcm (limits), hom, id, im, From ce299d5832095013bbcf2baef38552df6d2fc21b Mon Sep 17 00:00:00 2001 From: wznmickey Date: Thu, 23 Jan 2025 07:52:20 -0500 Subject: [PATCH 31/44] Support syntactically directly nested list, enum, and term list (#5728) Co-authored-by: Laurenz --- crates/typst-syntax/src/parser.rs | 6 +++--- tests/ref/issue-5719-enum-nested.png | Bin 0 -> 800 bytes tests/ref/issue-5719-heading-nested.png | Bin 0 -> 217 bytes tests/ref/issue-5719-list-nested.png | Bin 0 -> 506 bytes tests/ref/issue-5719-terms-nested.png | Bin 0 -> 921 bytes tests/suite/model/enum.typ | 10 ++++++++++ tests/suite/model/heading.typ | 4 ++++ tests/suite/model/list.typ | 8 ++++++++ tests/suite/model/terms.typ | 6 ++++++ 9 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tests/ref/issue-5719-enum-nested.png create mode 100644 tests/ref/issue-5719-heading-nested.png create mode 100644 tests/ref/issue-5719-list-nested.png create mode 100644 tests/ref/issue-5719-terms-nested.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index f9fb8b616..5b9e66e28 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -160,7 +160,7 @@ fn list_item(p: &mut Parser) { p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| { let m = p.marker(); p.assert(SyntaxKind::ListMarker); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::ListItem); }); } @@ -170,7 +170,7 @@ fn enum_item(p: &mut Parser) { p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| { let m = p.marker(); p.assert(SyntaxKind::EnumMarker); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::EnumItem); }); } @@ -184,7 +184,7 @@ fn term_item(p: &mut Parser) { markup(p, false, false, syntax_set!(Colon, RightBracket, End)); }); p.expect(SyntaxKind::Colon); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::TermItem); }); } diff --git a/tests/ref/issue-5719-enum-nested.png b/tests/ref/issue-5719-enum-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..7670454498688ea2701f53fc5a56f137f42db0f9 GIT binary patch literal 800 zcmV+*1K<3KP)pUv$QdP>})$aKd}za*#x~%JkPm#o~!rw>c#e)^W}s=rW3|s1~Zt! zM*@EdX+FkJ3-;^75j-E#duYKxOi1g;(`121wfzxpIbf}AGQWiuyb}S8a{tq=17WwBu+;dykUx%`im@Ko7mG-=o4OC({rkH$hN zVb=;yZ;`>BND71Mje-#=VW7Q1OfCdYsJ~^D6do{PPgo5^x+H`bwP@L#=7nJ;7MKvS zHx2w*w}kNL+e#Hm=Y^pn&5B%4pgct?lWC4E5C${&(D1UwC$`m4`Cx<++}|{sO8VPF zVCe}Zcxr9y6zSvz08UCUU~DH9?xT6fU2S5N zWG+yiyrd>m3C|V?gBg5S*uO-ID=(;hHc1Pfs5L(&-FXL&ceLPPhd+vRzXmfD@A#uZ zgyU-N(u2ng2>HeH)Zkw_Sg<3sMFW^=!DUIxn@HdS$!TI6bm?q?Fqpvygf~ePsx_I} znd!eN!Hwl&Mkrb_bKyH3_(xua*jE%ofNhQnyshuv4KFxB1+MBgA>)2TwhoH4F1&{UW)IrAHqpg^vSY79LE%SoE&g{RdwY6+tmza@Dahh zJn*32oLeXZJd*;TSO!?vBGVmT9Rzwy$LPaaJ%Hoe^x>s51K5uA(TCxD`{wm5+guE0 eFoPK!dj0`Pr%E}6wyz8T0000EZxfpUZ5a)k*$4xaxv z>s)8{(w=u8|Nno^ZG3ai;`K-E>uP>~1**-r-}g^yt%{qj+}4UKj~vqt8UFlC6*5~Z QR|s;mr>mdKI;Vst06Z99ivR!s literal 0 HcmV?d00001 diff --git a/tests/ref/issue-5719-list-nested.png b/tests/ref/issue-5719-list-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..9c9a7cc62f9fabfc6e15ce6b01c1c108c0bd069a GIT binary patch literal 506 zcmV0005ONkl9oL%^2oD;09igAHG z#u*DD{MxvYO4BxVHzIt>s>KkN2}0#N9t_qeajKjEOI!FmzLA+Ax;){b0wI{-?7Z#y)1gCfcOl$3*sBvmntIDD?q%G&NZ6$rru{~rDts;O=6Ul!0|2opF07*qoM6N<$f|hsQS^xk5 literal 0 HcmV?d00001 diff --git a/tests/ref/issue-5719-terms-nested.png b/tests/ref/issue-5719-terms-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..8428ae4eee55e9d02e2d5cbf4db5837a565307a6 GIT binary patch literal 921 zcmV;K17`e*P)N5jb^JU=$x0tnPP79 zek*fMx14xE#k`doSfDepyybGJ+0>mUr$@{fe(4;7c+aQzJ^w%aIETaYa@N2_11gBU81EW6}yM&SDk|<0iK41n#HFS z?q%oWJzZ5?--TrSiQoR^-WfLF^y6)uY8@)kQR`FsiSia=*pf(P5~7)1*!5gPgQ*S~ z0XF7O5;Z+UHvmxF`UYD+_TcAe5QJNb3-www1m<6b@I|5X_6L^~R!Rn^ZI5q9 zvj$gz5{>_8w$^%qOu1;7C72$WR7i;5Y{bE22i0NmE&F^Io+VHqje|R|3(JnwajL6` z%0^h9@dfGHiNOW9QM9j>UD)M9XXjiUv3N+JnjX}>0Gq(pn>|>D20*-7o}t$egD|&F zK7>^7Q4;{fqp8*nNvy&BqF^b^Ci-eYD#!)3Kvp40D-3uyIJF36mk-P2;b*Q4|=^<`ZQJYS1U>LrseHvr=w8_ z-0`+iubW>`Hd)^Y%?G00@{H&m+}%%qQsb227{iCeeWyq;-m^=D2q!s|I*LmiW99R= vz!c=<9}_!8*XknUd+-3kForSwzr()(Ihd{iz4HV~00000NkvXXu0mjf1~sp1 literal 0 HcmV?d00001 diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index e957ae9e8..288392d45 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -199,3 +199,13 @@ a + 0. + f #align(right)[+ align] + h + +--- issue-5719-enum-nested --- +// Enums can be immediately nested. +1. A +2. 1. B + 2. C ++ + D + + E ++ = F + G diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ index 45e5b50ae..4e529fdf6 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -148,3 +148,7 @@ Cannot be used as @intro // Hint: 1-16 HTML only supports

to

, not // Hint: 1-16 you may want to restructure your document so that it doesn't contain deep headings ======= Level 7 + +--- issue-5719-heading-nested --- +// Headings may not be nested like this. += = A diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index b3d9a830b..96ddf3c18 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -276,3 +276,11 @@ World - h #align(right)[- i] - j + +--- issue-5719-list-nested --- +// Lists can be immediately nested. +- A +- - B + - C +- = D + E diff --git a/tests/suite/model/terms.typ b/tests/suite/model/terms.typ index 61fe20b0d..23ac6e513 100644 --- a/tests/suite/model/terms.typ +++ b/tests/suite/model/terms.typ @@ -90,3 +90,9 @@ Not in list / h: h #align(right)[/ i: i] / j: j + +--- issue-5719-terms-nested --- +// Term lists can be immediately nested. +/ Term A: 1 +/ Term B: / Term C: 2 + / Term D: 3 From b7546bace7fb8640d1e7121b8bd7baf3cdb576e1 Mon Sep 17 00:00:00 2001 From: T0mstone <39707032+T0mstone@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:05:12 +0100 Subject: [PATCH 32/44] Ignore shebang at start of file (#5702) --- crates/typst-syntax/src/highlight.rs | 1 + crates/typst-syntax/src/kind.rs | 9 ++++++++- crates/typst-syntax/src/lexer.rs | 6 ++++++ crates/typst-syntax/src/parser.rs | 2 ++ tests/suite/syntax/shebang.typ | 7 +++++++ 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/suite/syntax/shebang.typ diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs index de8ed65c9..c59a03384 100644 --- a/crates/typst-syntax/src/highlight.rs +++ b/crates/typst-syntax/src/highlight.rs @@ -287,6 +287,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Destructuring => None, SyntaxKind::DestructAssignment => None, + SyntaxKind::Shebang => Some(Tag::Comment), SyntaxKind::LineComment => Some(Tag::Comment), SyntaxKind::BlockComment => Some(Tag::Comment), SyntaxKind::Error => Some(Tag::Error), diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs index 0a7c160b4..b4a97a3e0 100644 --- a/crates/typst-syntax/src/kind.rs +++ b/crates/typst-syntax/src/kind.rs @@ -9,6 +9,8 @@ pub enum SyntaxKind { /// An invalid sequence of characters. Error, + /// A shebang: `#! ...` + Shebang, /// A line comment: `// ...`. LineComment, /// A block comment: `/* ... */`. @@ -357,7 +359,11 @@ impl SyntaxKind { pub fn is_trivia(self) -> bool { matches!( self, - Self::LineComment | Self::BlockComment | Self::Space | Self::Parbreak + Self::Shebang + | Self::LineComment + | Self::BlockComment + | Self::Space + | Self::Parbreak ) } @@ -371,6 +377,7 @@ impl SyntaxKind { match self { Self::End => "end of tokens", Self::Error => "syntax error", + Self::Shebang => "shebang", Self::LineComment => "line comment", Self::BlockComment => "block comment", Self::Markup => "markup", diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index 6b5d28162..17401044f 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -103,6 +103,7 @@ impl Lexer<'_> { self.newline = false; let kind = match self.s.eat() { Some(c) if is_space(c, self.mode) => self.whitespace(start, c), + Some('#') if start == 0 && self.s.eat_if('!') => self.shebang(), Some('/') if self.s.eat_if('/') => self.line_comment(), Some('/') if self.s.eat_if('*') => self.block_comment(), Some('*') if self.s.eat_if('/') => { @@ -151,6 +152,11 @@ impl Lexer<'_> { } } + fn shebang(&mut self) -> SyntaxKind { + self.s.eat_until(is_newline); + SyntaxKind::Shebang + } + fn line_comment(&mut self) -> SyntaxKind { self.s.eat_until(is_newline); SyntaxKind::LineComment diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 5b9e66e28..5de71cafc 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -93,6 +93,8 @@ fn markup_expr(p: &mut Parser, at_start: bool, nesting: &mut usize) { p.hint("try using a backslash escape: \\]"); } + SyntaxKind::Shebang => p.eat(), + SyntaxKind::Text | SyntaxKind::Linebreak | SyntaxKind::Escape diff --git a/tests/suite/syntax/shebang.typ b/tests/suite/syntax/shebang.typ new file mode 100644 index 000000000..c2eb2e43c --- /dev/null +++ b/tests/suite/syntax/shebang.typ @@ -0,0 +1,7 @@ +// Test shebang support. + +--- shebang --- +#!typst compile + +// Error: 2-3 the character `!` is not valid in code +#!not-a-shebang From 2d33393df967bbe55646b839e188c04380d823fe Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:24:35 +0100 Subject: [PATCH 33/44] Add support for `c2sc` OpenType feature in `smallcaps` (#5655) --- .../typst-library/src/model/bibliography.rs | 6 ++- crates/typst-library/src/text/mod.rs | 10 +++-- crates/typst-library/src/text/smallcaps.rs | 35 ++++++++++++++---- tests/ref/smallcaps-all.png | Bin 0 -> 512 bytes tests/suite/text/smallcaps.typ | 4 ++ 5 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 tests/ref/smallcaps-all.png diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 95db8a222..762a97fd9 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -38,7 +38,8 @@ use crate::model::{ }; use crate::routines::{EvalMode, Routines}; use crate::text::{ - FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta, + FontStyle, Lang, LocalName, Region, Smallcaps, SubElem, SuperElem, TextElem, + WeightDelta, }; use crate::World; @@ -1046,7 +1047,8 @@ fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Con match format.font_variant { citationberg::FontVariant::Normal => {} citationberg::FontVariant::SmallCaps => { - content = content.styled(TextElem::set_smallcaps(true)); + content = + content.styled(TextElem::set_smallcaps(Some(Smallcaps::Minuscules))); } } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 6cca24587..edbd24139 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -755,11 +755,10 @@ pub struct TextElem { #[ghost] pub case: Option, - /// Whether small capital glyphs should be used. ("smcp") + /// Whether small capital glyphs should be used. ("smcp", "c2sc") #[internal] - #[default(false)] #[ghost] - pub smallcaps: bool, + pub smallcaps: Option, } impl TextElem { @@ -1249,8 +1248,11 @@ pub fn features(styles: StyleChain) -> Vec { } // Features that are off by default in Harfbuzz are only added if enabled. - if TextElem::smallcaps_in(styles) { + if let Some(sc) = TextElem::smallcaps_in(styles) { feat(b"smcp", 1); + if sc == Smallcaps::All { + feat(b"c2sc", 1); + } } if TextElem::alternates_in(styles) { diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index 1e88974f5..924a45e8c 100644 --- a/crates/typst-library/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs @@ -12,11 +12,11 @@ use crate::text::TextElem; /// ``` /// /// # Smallcaps fonts -/// By default, this enables the OpenType `smcp` feature for the font. Not all -/// fonts support this feature. Sometimes smallcaps are part of a dedicated -/// font. This is, for example, the case for the _Latin Modern_ family of fonts. -/// In those cases, you can use a show-set rule to customize the appearance of -/// the text in smallcaps: +/// By default, this uses the `smcp` and `c2sc` OpenType features on the font. +/// Not all fonts support these features. Sometimes, smallcaps are part of a +/// dedicated font. This is, for example, the case for the _Latin Modern_ family +/// of fonts. In those cases, you can use a show-set rule to customize the +/// appearance of the text in smallcaps: /// /// ```typ /// #show smallcaps: set text(font: "Latin Modern Roman Caps") @@ -45,6 +45,17 @@ use crate::text::TextElem; /// ``` #[elem(title = "Small Capitals", Show)] pub struct SmallcapsElem { + /// Whether to turn uppercase letters into small capitals as well. + /// + /// Unless overridden by a show rule, this enables the `c2sc` OpenType + /// feature. + /// + /// ```example + /// #smallcaps(all: true)[UNICEF] is an + /// agency of #smallcaps(all: true)[UN]. + /// ``` + #[default(false)] + pub all: bool, /// The content to display in small capitals. #[required] pub body: Content, @@ -52,7 +63,17 @@ pub struct SmallcapsElem { impl Show for Packed { #[typst_macros::time(name = "smallcaps", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body.clone().styled(TextElem::set_smallcaps(true))) + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let sc = if self.all(styles) { Smallcaps::All } else { Smallcaps::Minuscules }; + Ok(self.body.clone().styled(TextElem::set_smallcaps(Some(sc)))) } } + +/// What becomes small capitals. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Smallcaps { + /// Minuscules become small capitals. + Minuscules, + /// All letters become small capitals. + All, +} diff --git a/tests/ref/smallcaps-all.png b/tests/ref/smallcaps-all.png new file mode 100644 index 0000000000000000000000000000000000000000..f3be53f8290996cd1ba6dccc177412d3631f12d5 GIT binary patch literal 512 zcmV+b0{{JqP)mHk zcidp#+I|6{I1D$=34uHCR1nB!vl-BImi4Tj2EP7l_VPP_lwP-XQ3wo8cIZ0cyO;7c zDq)M~q(=b?R~N5=b&OS=GXi1GZT4s0SFUB2C}0W5XUmj>!SzQZZ1Oh4mn#|+0B{!V z4mtRzA3?DD8DL_u`xA~%I0b+j419x4V zz*DF3|G`6521LRVJ_tPjeFHWoVXb9M9K5~fA^9Y3W!n4}qs+J&5duGVovtxQ&pWEU zX|={(_wMS;+e!G?z$J|~JIrF~5(394T#um)O(#M{%gGpz`_ekt+PO7DyB&yF5dyOd zgVm|R-^+vS!T=Smm@_#zQE7;RFQlM!5P$2+hz*n=K=*5-6k1+DnGx}efk*E*5mo9w z{OGB2O|W2b0`R$Zie$MhoG30S4$FW@Si%xM&hQR$(n#()v83?;0000 Date: Sat, 20 Jul 2024 21:21:53 -0500 Subject: [PATCH 34/44] Just add MathText SyntaxKind --- crates/typst-eval/src/code.rs | 1 + crates/typst-eval/src/math.rs | 14 +++++++++++- crates/typst-syntax/src/ast.rs | 32 ++++++++++++++++++++++++++++ crates/typst-syntax/src/highlight.rs | 1 + crates/typst-syntax/src/kind.rs | 3 +++ crates/typst-syntax/src/lexer.rs | 9 +++++++- crates/typst-syntax/src/parser.rs | 14 ++++++------ crates/typst-syntax/src/set.rs | 1 + 8 files changed, 67 insertions(+), 8 deletions(-) diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index 34373fd4a..2baf4ea9e 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -99,6 +99,7 @@ impl Eval for ast::Expr<'_> { Self::Term(v) => v.eval(vm).map(Value::Content), Self::Equation(v) => v.eval(vm).map(Value::Content), Self::Math(v) => v.eval(vm).map(Value::Content), + Self::MathText(v) => v.eval(vm).map(Value::Content), Self::MathIdent(v) => v.eval(vm), Self::MathShorthand(v) => v.eval(vm), Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index 51dc0a3d5..f93f147eb 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -5,7 +5,7 @@ use typst_library::math::{ AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, }; use typst_library::text::TextElem; -use typst_syntax::ast::{self, AstNode}; +use typst_syntax::ast::{self, AstNode, MathTextKind}; use crate::{Eval, Vm}; @@ -20,6 +20,18 @@ impl Eval for ast::Math<'_> { } } +impl Eval for ast::MathText<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + match self.get() { + // TODO: change to `SymbolElem` when added + MathTextKind::Character(c) => Ok(Value::Symbol(Symbol::single(c)).display()), + MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())), + } + } +} + impl Eval for ast::MathIdent<'_> { type Output = Value; diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 19e123727..014e8392e 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -123,6 +123,8 @@ pub enum Expr<'a> { Equation(Equation<'a>), /// The contents of a mathematical equation: `x^2 + 1`. Math(Math<'a>), + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. + MathText(MathText<'a>), /// An identifier in math: `pi`. MathIdent(MathIdent<'a>), /// A shorthand for a unicode codepoint in math: `a <= b`. @@ -233,6 +235,7 @@ impl<'a> AstNode<'a> for Expr<'a> { SyntaxKind::TermItem => node.cast().map(Self::Term), SyntaxKind::Equation => node.cast().map(Self::Equation), SyntaxKind::Math => node.cast().map(Self::Math), + SyntaxKind::MathText => node.cast().map(Self::MathText), SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), SyntaxKind::MathShorthand => node.cast().map(Self::MathShorthand), SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), @@ -297,6 +300,7 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Term(v) => v.to_untyped(), Self::Equation(v) => v.to_untyped(), Self::Math(v) => v.to_untyped(), + Self::MathText(v) => v.to_untyped(), Self::MathIdent(v) => v.to_untyped(), Self::MathShorthand(v) => v.to_untyped(), Self::MathAlignPoint(v) => v.to_untyped(), @@ -706,6 +710,34 @@ impl<'a> Math<'a> { } } +node! { + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. + MathText +} + +/// The underlying text kind. +pub enum MathTextKind<'a> { + Character(char), + Number(&'a EcoString), +} + +impl<'a> MathText<'a> { + /// Return the underlying text. + pub fn get(self) -> MathTextKind<'a> { + let text = self.0.text(); + let mut chars = text.chars(); + let c = chars.next().unwrap(); + if c.is_numeric() { + // Numbers are potentially grouped as multiple characters. This is + // done in `Lexer::math_text()`. + MathTextKind::Number(text) + } else { + assert!(chars.next().is_none()); + MathTextKind::Character(c) + } + } +} + node! { /// An identifier in math: `pi`. MathIdent diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs index c59a03384..cd815694d 100644 --- a/crates/typst-syntax/src/highlight.rs +++ b/crates/typst-syntax/src/highlight.rs @@ -171,6 +171,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Equation => None, SyntaxKind::Math => None, + SyntaxKind::MathText => None, SyntaxKind::MathIdent => highlight_ident(node), SyntaxKind::MathShorthand => Some(Tag::Escape), SyntaxKind::MathAlignPoint => Some(Tag::MathOperator), diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs index b4a97a3e0..c24b47fe7 100644 --- a/crates/typst-syntax/src/kind.rs +++ b/crates/typst-syntax/src/kind.rs @@ -75,6 +75,8 @@ pub enum SyntaxKind { /// The contents of a mathematical equation: `x^2 + 1`. Math, + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `|`, `[`. + MathText, /// An identifier in math: `pi`. MathIdent, /// A shorthand for a unicode codepoint in math: `a <= b`. @@ -408,6 +410,7 @@ impl SyntaxKind { Self::TermMarker => "term marker", Self::Equation => "equation", Self::Math => "math", + Self::MathText => "math text", Self::MathIdent => "math identifier", Self::MathShorthand => "math shorthand", Self::MathAlignPoint => "math alignment point", diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index 17401044f..b8f2bf25f 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -685,6 +685,7 @@ impl Lexer<'_> { if s.eat_if('.') && !s.eat_while(char::is_numeric).is_empty() { self.s = s; } + SyntaxKind::MathText } else { let len = self .s @@ -693,8 +694,14 @@ impl Lexer<'_> { .next() .map_or(0, str::len); self.s.jump(start + len); + if len > c.len_utf8() { + // Grapheme clusters are treated as normal text and stay grouped + // This may need to change in the future. + SyntaxKind::Text + } else { + SyntaxKind::MathText + } } - SyntaxKind::Text } /// Handle named arguments in math function call. diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 5de71cafc..55d5550b6 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -252,7 +252,9 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { continuable = true; p.eat(); // Parse a function call for an identifier or field access. - if min_prec < 3 && p.directly_at(SyntaxKind::Text) && p.current_text() == "(" + if min_prec < 3 + && p.directly_at(SyntaxKind::MathText) + && p.current_text() == "(" { math_args(p); p.wrap(m, SyntaxKind::FuncCall); @@ -264,10 +266,10 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { | SyntaxKind::Comma | SyntaxKind::Semicolon | SyntaxKind::RightParen => { - p.convert_and_eat(SyntaxKind::Text); + p.convert_and_eat(SyntaxKind::MathText); } - SyntaxKind::Text | SyntaxKind::MathShorthand => { + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { continuable = matches!( math_class(p.current_text()), None | Some(MathClass::Alphabetic) @@ -316,7 +318,7 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { let mut primed = false; while !p.end() && !p.at(stop) { - if p.directly_at(SyntaxKind::Text) && p.current_text() == "!" { + if p.directly_at(SyntaxKind::MathText) && p.current_text() == "!" { p.eat(); p.wrap(m, SyntaxKind::Math); continue; @@ -414,7 +416,7 @@ fn math_delimited(p: &mut Parser) { // We could be at the shorthand `|]`, which shouldn't be converted // to a `Text` kind. if p.at(SyntaxKind::RightParen) { - p.convert_and_eat(SyntaxKind::Text); + p.convert_and_eat(SyntaxKind::MathText); } else { p.eat(); } @@ -535,7 +537,7 @@ fn math_arg<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) -> bool { } let mut positional = true; - if p.at_set(syntax_set!(Text, MathIdent, Underscore)) { + if p.at_set(syntax_set!(MathText, MathIdent, Underscore)) { // Parses a named argument: `thickness: #12pt`. if let Some(named) = p.lexer.maybe_math_named_arg(start) { p.token.node = named; diff --git a/crates/typst-syntax/src/set.rs b/crates/typst-syntax/src/set.rs index 9eb457b84..a7b9a594a 100644 --- a/crates/typst-syntax/src/set.rs +++ b/crates/typst-syntax/src/set.rs @@ -64,6 +64,7 @@ pub const MATH_EXPR: SyntaxSet = syntax_set!( Semicolon, RightParen, Text, + MathText, MathShorthand, Linebreak, MathAlignPoint, From c47b71b4350434a73734789ebde1374b791dc88e Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski Date: Mon, 29 Jul 2024 00:25:03 -0500 Subject: [PATCH 35/44] Basic SymbolElem addition --- crates/typst-layout/src/math/mod.rs | 10 +++++- crates/typst-library/src/foundations/ops.rs | 12 ++++--- .../typst-library/src/foundations/symbol.rs | 33 ++++++++++++++++++- crates/typst-library/src/foundations/value.rs | 6 ++-- crates/typst-library/src/math/accent.rs | 9 +++-- crates/typst-realize/src/lib.rs | 10 ++++-- tests/suite/foundations/content.typ | 12 ++++--- tests/suite/math/symbols.typ | 4 +-- 8 files changed, 73 insertions(+), 23 deletions(-) diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 06dc6653b..905e159ab 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -17,7 +17,9 @@ use rustybuzz::Feature; use ttf_parser::Tag; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; -use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain}; +use typst_library::foundations::{ + Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem, +}; use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem}; use typst_library::layout::{ Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem, @@ -535,6 +537,12 @@ fn layout_realized( layout_h(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { self::text::layout_text(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + // This is a hack to avoid affecting layout that will be replaced in a + // later commit. + let text_elem = TextElem::new(elem.text.to_string().into()); + let packed = Packed::new(text_elem); + self::text::layout_text(&packed, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 85a041b6c..7dbdde8ff 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -6,7 +6,9 @@ use ecow::eco_format; use typst_utils::Numeric; use crate::diag::{bail, HintedStrResult, StrResult}; -use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value}; +use crate::foundations::{ + format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value, +}; use crate::layout::{Alignment, Length, Rel}; use crate::text::TextElem; use crate::visualize::Stroke; @@ -30,10 +32,10 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())), (Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Str(a), Content(b)) => Content(TextElem::packed(a) + b), - (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), (Args(a), Args(b)) => Args(a + b), @@ -130,10 +132,10 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())), (Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Str(a), Content(b)) => Content(TextElem::packed(a) + b), - (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 3045970de..8a80506fe 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -9,7 +9,10 @@ use typst_syntax::{is_ident, Span, Spanned}; use typst_utils::hash128; use crate::diag::{bail, SourceResult, StrResult}; -use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _}; +use crate::foundations::{ + cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed, + PlainText, Repr as _, +}; /// A Unicode symbol. /// @@ -425,3 +428,31 @@ fn parts(modifiers: &str) -> impl Iterator { fn contained(modifiers: &str, m: &str) -> bool { parts(modifiers).any(|part| part == m) } + +/// A single character. +#[elem(Repr, PlainText)] +pub struct SymbolElem { + /// The symbol's character. + #[required] + pub text: char, // This is called `text` for consistency with `TextElem`. +} + +impl SymbolElem { + /// Create a new packed symbol element. + pub fn packed(text: impl Into) -> Content { + Self::new(text.into()).pack() + } +} + +impl PlainText for Packed { + fn plain_text(&self, text: &mut EcoString) { + text.push(self.text); + } +} + +impl crate::foundations::Repr for SymbolElem { + /// Use a custom repr that matches normal content. + fn repr(&self) -> EcoString { + eco_format!("[{}]", self.text) + } +} diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index efc480d3f..8d9f59332 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -16,7 +16,7 @@ use crate::foundations::{ fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, - Styles, Symbol, Type, Version, + Styles, Symbol, SymbolElem, Type, Version, }; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; use crate::text::{RawContent, RawElem, TextElem}; @@ -209,7 +209,7 @@ impl Value { Self::Decimal(v) => TextElem::packed(eco_format!("{v}")), Self::Str(v) => TextElem::packed(v), Self::Version(v) => TextElem::packed(eco_format!("{v}")), - Self::Symbol(v) => TextElem::packed(v.get()), + Self::Symbol(v) => SymbolElem::packed(v.get()), Self::Content(v) => v, Self::Module(module) => module.content(), _ => RawElem::new(RawContent::Text(self.repr())) @@ -656,7 +656,7 @@ primitive! { Duration: "duration", Duration } primitive! { Content: "content", Content, None => Content::empty(), - Symbol(v) => TextElem::packed(v.get()), + Symbol(v) => SymbolElem::packed(v.get()), Str(v) => TextElem::packed(v) } primitive! { Styles: "styles", Styles } diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index b87e527f2..b162c52b1 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -1,8 +1,7 @@ use crate::diag::bail; -use crate::foundations::{cast, elem, func, Content, NativeElement, Value}; +use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem}; use crate::layout::{Length, Rel}; use crate::math::Mathy; -use crate::text::TextElem; /// Attaches an accent to a base. /// @@ -142,8 +141,8 @@ cast! { Accent, self => self.0.into_value(), v: char => Self::new(v), - v: Content => match v.to_packed::() { - Some(elem) => Value::Str(elem.text.clone().into()).cast()?, - None => bail!("expected text"), + v: Content => match v.to_packed::() { + Some(elem) => Self::new(elem.text), + None => bail!("expected a symbol"), }, } diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 6ab6d81c5..99db2ef1b 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -16,7 +16,7 @@ use typst_library::engine::Engine; use typst_library::foundations::{ Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles, - Synthesize, Transformation, + SymbolElem, Synthesize, Transformation, }; use typst_library::html::{tag, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; @@ -221,7 +221,7 @@ impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> { /// Handles an arbitrary piece of content during realization. fn visit<'a>( s: &mut State<'a, '_, '_, '_>, - content: &'a Content, + mut content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<()> { // Tags can always simply be pushed. @@ -230,6 +230,12 @@ fn visit<'a>( return Ok(()); } + if let Some(elem) = content.to_packed::() { + // This is a hack to avoid affecting layout that will be replaced in a + // later commit. + content = Box::leak(Box::new(TextElem::packed(elem.text.to_string()))); + } + // Transformations for math content based on the realization kind. Needs // to happen before show rules. if visit_math_rules(s, content, styles)? { diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ index 31ef1c54c..9ddee5975 100644 --- a/tests/suite/foundations/content.typ +++ b/tests/suite/foundations/content.typ @@ -50,12 +50,14 @@ `raw` --- content-fields-complex --- -// Integrated test for content fields. +// Integrated test for content fields. The idea is to parse a normal looking +// equation and symbolically evaluate it with the given variable values. + #let compute(equation, ..vars) = { let vars = vars.named() let f(elem) = { let func = elem.func() - if func == text { + if elem.has("text") { let text = elem.text if regex("^\d+$") in text { int(text) @@ -74,7 +76,7 @@ elem .children .filter(v => v != [ ]) - .split[+] + .split($+$.body) .map(xs => xs.fold(1, (prod, v) => prod * f(v))) .fold(0, (sum, v) => sum + v) } @@ -83,13 +85,15 @@ [With ] vars .pairs() - .map(p => $#p.first() = #p.last()$) + .map(((name, value)) => $name = value$) .join(", ", last: " and ") [ we have:] $ equation = result $ } #compute($x y + y^2$, x: 2, y: 3) +// This should generate the same output as: +// With $x = 2$ and $y = 3$ we have: $ x y + y^2 = 15 $ --- content-label-has-method --- // Test whether the label is accessible through the `has` method. diff --git a/tests/suite/math/symbols.typ b/tests/suite/math/symbols.typ index 65a483162..6dd9db622 100644 --- a/tests/suite/math/symbols.typ +++ b/tests/suite/math/symbols.typ @@ -2,7 +2,7 @@ --- math-symbol-basic --- #let sym = symbol("s", ("basic", "s")) -#test($sym.basic$, $#"s"$) +#test($sym.basic$, $s$) --- math-symbol-underscore --- #let sym = symbol("s", ("test_underscore", "s")) @@ -16,7 +16,7 @@ $sym.test-dash$ --- math-symbol-double --- #let sym = symbol("s", ("test.basic", "s")) -#test($sym.test.basic$, $#"s"$) +#test($sym.test.basic$, $s$) --- math-symbol-double-underscore --- #let sym = symbol("s", ("one.test_underscore", "s")) From fecdc39846959e0dae12e51282bb35d3d417547e Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski Date: Wed, 22 Jan 2025 11:04:01 -0500 Subject: [PATCH 36/44] Use SymbolElem in more places and add `char` cast for content --- crates/typst-eval/src/call.rs | 9 ++++----- crates/typst-eval/src/math.rs | 6 +++--- crates/typst-layout/src/math/attach.rs | 10 +++++----- crates/typst-layout/src/math/frac.rs | 7 +++++-- crates/typst-library/src/loading/csv.rs | 16 ++++------------ crates/typst-library/src/math/lr.rs | 9 ++++----- crates/typst-library/src/math/op.rs | 5 +++-- tests/suite/loading/csv.typ | 4 ++++ 8 files changed, 32 insertions(+), 34 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 69b274bbc..f59235c78 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -7,12 +7,11 @@ use typst_library::diag::{ use typst_library::engine::{Engine, Sink, Traced}; use typst_library::foundations::{ Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, - NativeElement, Scope, Scopes, Value, + NativeElement, Scope, Scopes, SymbolElem, Value, }; use typst_library::introspection::Introspector; use typst_library::math::LrElem; use typst_library::routines::Routines; -use typst_library::text::TextElem; use typst_library::World; use typst_syntax::ast::{self, AstNode, Ident}; use typst_syntax::{Span, Spanned, SyntaxNode}; @@ -402,16 +401,16 @@ fn wrap_args_in_math( let mut body = Content::empty(); for (i, arg) in args.all::()?.into_iter().enumerate() { if i > 0 { - body += TextElem::packed(','); + body += SymbolElem::packed(','); } body += arg; } if trailing_comma { - body += TextElem::packed(','); + body += SymbolElem::packed(','); } Ok(Value::Content( callee.display().spanned(callee_span) - + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) + + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) .pack() .spanned(args.span), )) diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index f93f147eb..bfb54aa87 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -1,6 +1,6 @@ use ecow::eco_format; use typst_library::diag::{At, SourceResult}; -use typst_library::foundations::{Content, NativeElement, Symbol, Value}; +use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value}; use typst_library::math::{ AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, }; @@ -25,8 +25,7 @@ impl Eval for ast::MathText<'_> { fn eval(self, _: &mut Vm) -> SourceResult { match self.get() { - // TODO: change to `SymbolElem` when added - MathTextKind::Character(c) => Ok(Value::Symbol(Symbol::single(c)).display()), + MathTextKind::Character(c) => Ok(SymbolElem::packed(c)), MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())), } } @@ -114,6 +113,7 @@ impl Eval for ast::MathRoot<'_> { type Output = Content; fn eval(self, vm: &mut Vm) -> SourceResult { + // Use `TextElem` to match `MathTextKind::Number` above. let index = self.index().map(|i| TextElem::packed(eco_format!("{i}"))); let radicand = self.radicand().eval_display(vm)?; Ok(RootElem::new(radicand).with_index(index).pack()) diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 8a67d53b3..e1d7d7c9d 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -1,10 +1,9 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size}; use typst_library::math::{ AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, }; -use typst_library::text::TextElem; use typst_utils::OptionExt; use super::{ @@ -104,13 +103,14 @@ pub fn layout_primes( 4 => '⁗', _ => unreachable!(), }; - let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; + let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; ctx.push(f); } count => { // Custom amount of primes - let prime = - ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame(); + let prime = ctx + .layout_into_fragment(&SymbolElem::packed('′'), styles)? + .into_frame(); let width = prime.width() * (count + 1) as f64 / 2.0; let mut frame = Frame::soft(Size::new(width, prime.height())); frame.set_baseline(prime.ascent()); diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 63463d761..6d3caac45 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -1,5 +1,5 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem}; use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; use typst_library::math::{BinomElem, FracElem}; use typst_library::text::TextElem; @@ -80,7 +80,10 @@ fn layout_frac_like( let denom = ctx.layout_into_frame( &Content::sequence( // Add a comma between each element. - denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1), + denom + .iter() + .flat_map(|a| [SymbolElem::packed(','), a.clone()]) + .skip(1), ), styles.chain(&denom_style), )?; diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index e5dabfaa6..1cf656ae2 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -136,18 +136,10 @@ impl Default for Delimiter { cast! { Delimiter, self => self.0.into_value(), - v: EcoString => { - let mut chars = v.chars(); - let first = chars.next().ok_or("delimiter must not be empty")?; - if chars.next().is_some() { - bail!("delimiter must be a single character"); - } - - if !first.is_ascii() { - bail!("delimiter must be an ASCII character"); - } - - Self(first) + c: char => if c.is_ascii() { + Self(c) + } else { + bail!("delimiter must be an ASCII character") }, } diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs index 965f53516..7558717af 100644 --- a/crates/typst-library/src/math/lr.rs +++ b/crates/typst-library/src/math/lr.rs @@ -1,7 +1,6 @@ -use crate::foundations::{elem, func, Content, NativeElement}; +use crate::foundations::{elem, func, Content, NativeElement, SymbolElem}; use crate::layout::{Length, Rel}; use crate::math::Mathy; -use crate::text::TextElem; /// Scales delimiters. /// @@ -19,7 +18,7 @@ pub struct LrElem { #[parse( let mut arguments = args.all::()?.into_iter(); let mut body = arguments.next().unwrap_or_default(); - arguments.for_each(|arg| body += TextElem::packed(',') + arg); + arguments.for_each(|arg| body += SymbolElem::packed(',') + arg); body )] pub body: Content, @@ -125,9 +124,9 @@ fn delimited( ) -> Content { let span = body.span(); let mut elem = LrElem::new(Content::sequence([ - TextElem::packed(left), + SymbolElem::packed(left), body, - TextElem::packed(right), + SymbolElem::packed(right), ])); // Push size only if size is provided if let Some(size) = size { diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs index 5b3f58beb..55696e534 100644 --- a/crates/typst-library/src/math/op.rs +++ b/crates/typst-library/src/math/op.rs @@ -1,6 +1,6 @@ use ecow::EcoString; -use crate::foundations::{elem, Content, NativeElement, Scope}; +use crate::foundations::{elem, Content, NativeElement, Scope, SymbolElem}; use crate::layout::HElem; use crate::math::{upright, Mathy, THIN}; use crate::text::TextElem; @@ -38,6 +38,7 @@ macro_rules! ops { let operator = EcoString::from(ops!(@name $name $(: $value)?)); math.define( stringify!($name), + // Latex also uses their equivalent of `TextElem` here. OpElem::new(TextElem::new(operator).into()) .with_limits(ops!(@limit $($tts)*)) .pack() @@ -46,7 +47,7 @@ macro_rules! ops { let dif = |d| { HElem::new(THIN.into()).with_weak(true).pack() - + upright(TextElem::packed(d)) + + upright(SymbolElem::packed(d)) }; math.define("dif", dif('d')); math.define("Dif", dif('D')); diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ index 415488fcc..93545fc49 100644 --- a/tests/suite/loading/csv.typ +++ b/tests/suite/loading/csv.typ @@ -25,3 +25,7 @@ // Test error numbering with dictionary rows. // Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3) #csv("/assets/data/bad.csv", row-type: dictionary) + +--- csv-invalid-delimiter --- +// Error: 41-51 delimiter must be an ASCII character +#csv("/assets/data/zoo.csv", delimiter: "\u{2008}") From 7838da02ec8a9ffbdfa61ed3dfedb24557a0e49c Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski Date: Mon, 29 Jul 2024 00:25:03 -0500 Subject: [PATCH 37/44] Add SymbolElem to realization --- crates/typst-realize/src/lib.rs | 53 ++++++++++++++++++++++------- tests/ref/cases-content-symbol.png | Bin 0 -> 191 bytes tests/ref/cases-content-text.png | Bin 0 -> 184 bytes tests/suite/text/case.typ | 8 +++++ 4 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 tests/ref/cases-content-symbol.png create mode 100644 tests/ref/cases-content-text.png diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 99db2ef1b..ff42c3e95 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -221,7 +221,7 @@ impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> { /// Handles an arbitrary piece of content during realization. fn visit<'a>( s: &mut State<'a, '_, '_, '_>, - mut content: &'a Content, + content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<()> { // Tags can always simply be pushed. @@ -230,12 +230,6 @@ fn visit<'a>( return Ok(()); } - if let Some(elem) = content.to_packed::() { - // This is a hack to avoid affecting layout that will be replaced in a - // later commit. - content = Box::leak(Box::new(TextElem::packed(elem.text.to_string()))); - } - // Transformations for math content based on the realization kind. Needs // to happen before show rules. if visit_math_rules(s, content, styles)? { @@ -247,7 +241,7 @@ fn visit<'a>( return Ok(()); } - // Recurse into sequences. Styled elements and sequences can currently also + // Recurse into sequences. Styled elements and sequences can currently also // have labels, so this needs to happen before they are handled. if let Some(sequence) = content.to_packed::() { for elem in &sequence.children { @@ -301,7 +295,14 @@ fn visit_math_rules<'a>( // In normal realization, we apply regex show rules to consecutive // textual elements via `TEXTUAL` grouping. However, in math, this is // not desirable, so we just do it on a per-element basis. - if let Some(elem) = content.to_packed::() { + if let Some(elem) = content.to_packed::() { + if let Some(m) = + find_regex_match_in_str(elem.text.encode_utf8(&mut [0; 4]), styles) + { + visit_regex_match(s, &[(content, styles)], m)?; + return Ok(true); + } + } else if let Some(elem) = content.to_packed::() { if let Some(m) = find_regex_match_in_str(&elem.text, styles) { visit_regex_match(s, &[(content, styles)], m)?; return Ok(true); @@ -314,6 +315,14 @@ fn visit_math_rules<'a>( visit(s, s.store(eq), styles)?; return Ok(true); } + + // Symbols in non-math content transparently convert to `TextElem` so we + // don't have to handle them in non-math layout. + if let Some(elem) = content.to_packed::() { + let text = TextElem::packed(elem.text).spanned(elem.span()); + visit(s, s.store(text), styles)?; + return Ok(true); + } } Ok(false) @@ -792,7 +801,7 @@ static HTML_DOCUMENT_RULES: &[&GroupingRule] = /// Grouping rules used in HTML fragment realization. static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; -/// Grouping rules used in math realizatio. +/// Grouping rules used in math realization. static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; /// Groups adjacent textual elements for text show rule application. @@ -801,6 +810,9 @@ static TEXTUAL: GroupingRule = GroupingRule { tags: true, trigger: |content, _| { let elem = content.elem(); + // Note that `SymbolElem` converts into `TextElem` before textual show + // rules run, and we apply textual rules to elements manually during + // math realization, so we don't check for it here. elem == TextElem::elem() || elem == LinebreakElem::elem() || elem == SmartQuoteElem::elem() @@ -1124,7 +1136,16 @@ fn visit_regex_match<'a>( m: RegexMatch<'a>, ) -> SourceResult<()> { let match_range = m.offset..m.offset + m.text.len(); - let piece = TextElem::packed(m.text); + + // Replace with the correct intuitive element kind: if matching against a + // lone symbol, return a `SymbolElem`, otherwise return a newly composed + // `TextElem`. We should only match against a `SymbolElem` during math + // realization (`RealizationKind::Math`). + let piece = match elems { + &[(lone, _)] if lone.is::() => lone.clone(), + _ => TextElem::packed(m.text), + }; + let context = Context::new(None, Some(m.styles)); let output = m.recipe.apply(s.engine, context.track(), piece)?; @@ -1147,10 +1168,16 @@ fn visit_regex_match<'a>( continue; } - // At this point, we can have a `TextElem`, `SpaceElem`, + // At this point, we can have a `TextElem`, `SymbolElem`, `SpaceElem`, // `LinebreakElem`, or `SmartQuoteElem`. We now determine the range of // the element. - let len = content.to_packed::().map_or(1, |elem| elem.text.len()); + let len = if let Some(elem) = content.to_packed::() { + elem.text.len() + } else if let Some(elem) = content.to_packed::() { + elem.text.len_utf8() + } else { + 1 // The rest are Ascii, so just one byte. + }; let elem_range = cursor..cursor + len; // If the element starts before the start of match, visit it fully or diff --git a/tests/ref/cases-content-symbol.png b/tests/ref/cases-content-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..b0b8a65e322ce257658328c02455efa3f21dcdc7 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS_0VEhE<%|3RQf;0tjv*Ddl7HAcG$dYm6xi*q zE9WN`Z)@Wqme138AzrP)-EMf%64NzaW>$N&Gof5Qv4 m3E>~}w>**BS^=@pjg{dlLr2oy8~h-LF?hQAxvX;IkOyX9M(PwfByU;F>^%|{*g7KX+Po-~LBj{NKa Date: Mon, 20 Jan 2025 14:39:26 -0500 Subject: [PATCH 38/44] Update math TextElem layout to separate out SymbolElem --- crates/typst-layout/src/math/mod.rs | 6 +- crates/typst-layout/src/math/text.rs | 238 ++++++++++-------- tests/ref/math-equation-auto-wrapping.png | Bin 160 -> 159 bytes .../math-mat-align-explicit-alternating.png | Bin 1009 -> 1035 bytes tests/ref/math-mat-align-explicit-left.png | Bin 992 -> 989 bytes tests/ref/math-mat-align-explicit-right.png | Bin 1028 -> 976 bytes tests/ref/math-mat-align-implicit.png | Bin 1027 -> 1046 bytes .../math-vec-align-explicit-alternating.png | Bin 1009 -> 1035 bytes tests/suite/foundations/content.typ | 2 +- tests/suite/math/alignment.typ | 8 +- tests/suite/math/delimited.typ | 4 +- tests/suite/math/stretch.typ | 6 +- 12 files changed, 151 insertions(+), 113 deletions(-) diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 905e159ab..702816ee6 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -538,11 +538,7 @@ fn layout_realized( } else if let Some(elem) = elem.to_packed::() { self::text::layout_text(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { - // This is a hack to avoid affecting layout that will be replaced in a - // later commit. - let text_elem = TextElem::new(elem.text.to_string().into()); - let packed = Packed::new(text_elem); - self::text::layout_text(&packed, ctx, styles)?; + self::text::layout_symbol(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 7e849c46c..6b9703aa2 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -2,7 +2,7 @@ use std::f64::consts::SQRT_2; use ecow::{eco_vec, EcoString}; use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, StyleVec}; +use typst_library::foundations::{Packed, StyleChain, StyleVec, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -22,54 +22,66 @@ pub fn layout_text( ) -> SourceResult<()> { let text = &elem.text; let span = elem.span(); - let mut chars = text.chars(); - let math_size = EquationElem::size_in(styles); - let mut dtls = ctx.dtls_table.is_some(); - let fragment: MathFragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| dtls_char(c, &mut dtls)) - .map(|c| styled_char(styles, c, true)) - .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span)) - { - // A single letter that is available in the math font. - if dtls { - glyph.make_dotless_form(ctx); - } + let fragment = if text.contains(is_newline) { + layout_text_lines(text.split(is_newline), span, ctx, styles)? + } else { + layout_inline_text(text, span, ctx, styles)? + }; + ctx.push(fragment); + Ok(()) +} - match math_size { - MathSize::Script => { - glyph.make_script_size(ctx); - } - MathSize::ScriptScript => { - glyph.make_script_script_size(ctx); - } - _ => (), +/// Layout multiple lines of text. +fn layout_text_lines<'a>( + lines: impl Iterator, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + let mut fragments = vec![]; + for (i, line) in lines.enumerate() { + if i != 0 { + fragments.push(MathFragment::Linebreak); } + if !line.is_empty() { + fragments.push(layout_inline_text(line, span, ctx, styles)?.into()); + } + } + let mut frame = MathRun::new(fragments).into_frame(styles); + let axis = scaled!(ctx, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + Ok(FrameFragment::new(styles, frame)) +} - if glyph.class == MathClass::Large { - 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()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(ctx); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. +/// Layout the given text string into a [`FrameFragment`] after styling all +/// characters for the math font (without auto-italics). +fn layout_inline_text( + text: &str, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + if text.chars().all(|c| c.is_ascii_digit() || c == '.') { + // 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![]; - for c in text.chars() { - let c = styled_char(styles, c, false); - fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); + 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), + _ => {} + } + } + fragments.push(glyph.into()); } let frame = MathRun::new(fragments).into_frame(styles); - FrameFragment::new(styles, frame).with_text_like(true).into() + Ok(FrameFragment::new(styles, frame).with_text_like(true)) } else { let local = [ TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), @@ -77,64 +89,97 @@ pub fn layout_text( ] .map(|p| p.wrap()); - // Anything else is handled by Typst's standard text layout. let styles = styles.chain(&local); - let text: EcoString = + let styled_text: EcoString = text.chars().map(|c| styled_char(styles, c, false)).collect(); - if text.contains(is_newline) { - let mut fragments = vec![]; - for (i, piece) in text.split(is_newline).enumerate() { - if i != 0 { - fragments.push(MathFragment::Linebreak); - } - if !piece.is_empty() { - fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); - } - } - let mut frame = MathRun::new(fragments).into_frame(styles); - let axis = scaled!(ctx, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(styles, frame).into() - } else { - layout_complex_text(&text, ctx, span, styles)?.into() + + let spaced = styled_text.graphemes(true).nth(1).is_some(); + let elem = TextElem::packed(styled_text).spanned(span); + + // There isn't a natural width for a paragraph in a math environment; + // because it will be placed somewhere probably not at the left margin + // it will overflow. So emulate an `hbox` instead and allow the + // paragraph to extend as far as needed. + let frame = (ctx.engine.routines.layout_inline)( + ctx.engine, + &StyleVec::wrap(eco_vec![elem]), + ctx.locator.next(&span), + styles, + false, + Size::splat(Abs::inf()), + false, + )? + .into_frame(); + + Ok(FrameFragment::new(styles, frame) + .with_class(MathClass::Alphabetic) + .with_text_like(true) + .with_spaced(spaced)) + } +} + +/// Layout a single character in the math font with the correct styling applied +/// (includes auto-italics). +pub fn layout_symbol( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + // Switch dotless char to normal when we have the dtls OpenType feature. + // This should happen before the main styling pass. + let (unstyled_c, dtls) = match try_dotless(elem.text) { + Some(c) if ctx.dtls_table.is_some() => (c, true), + _ => (elem.text, false), + }; + let c = styled_char(styles, unstyled_c, true); + let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) { + Some(glyph) => layout_glyph(glyph, dtls, ctx, styles), + None => { + // Not in the math font, fallback to normal inline text layout. + layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? + .into() } }; - ctx.push(fragment); Ok(()) } -/// Layout the given text string into a [`FrameFragment`]. -fn layout_complex_text( - text: &str, +/// Layout a [`GlyphFragment`]. +fn layout_glyph( + mut glyph: GlyphFragment, + dtls: bool, ctx: &mut MathContext, - span: Span, styles: StyleChain, -) -> SourceResult { - // There isn't a natural width for a paragraph in a math environment; - // because it will be placed somewhere probably not at the left margin - // it will overflow. So emulate an `hbox` instead and allow the paragraph - // to extend as far as needed. - let spaced = text.graphemes(true).nth(1).is_some(); - let elem = TextElem::packed(text).spanned(span); - let frame = (ctx.engine.routines.layout_inline)( - ctx.engine, - &StyleVec::wrap(eco_vec![elem]), - ctx.locator.next(&span), - styles, - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); +) -> MathFragment { + if dtls { + glyph.make_dotless_form(ctx); + } + let math_size = EquationElem::size_in(styles); + match math_size { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} + } - Ok(FrameFragment::new(styles, frame) - .with_class(MathClass::Alphabetic) - .with_text_like(true) - .with_spaced(spaced)) + if glyph.class == MathClass::Large { + 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()) + } else { + glyph.into_variant() + }; + // TeXbook p 155. Large operators are always vertically centered on the + // axis. + variant.center_on_axis(ctx); + variant.into() + } else { + glyph.into() + } } -/// Select the correct styled math letter. +/// Style the character by selecting the unicode codepoint for italic, bold, +/// caligraphic, etc. /// /// /// @@ -353,15 +398,12 @@ fn greek_exception( }) } -/// Switch dotless character to non dotless character for use of the dtls -/// OpenType feature. -pub fn dtls_char(c: char, dtls: &mut bool) -> char { - match (c, *dtls) { - ('ı', true) => 'i', - ('ȷ', true) => 'j', - _ => { - *dtls = false; - c - } +/// The non-dotless version of a dotless character that can be used with the +/// `dtls` OpenType feature. +pub fn try_dotless(c: char) -> Option { + match c { + 'ı' => Some('i'), + 'ȷ' => Some('j'), + _ => None, } } diff --git a/tests/ref/math-equation-auto-wrapping.png b/tests/ref/math-equation-auto-wrapping.png index 9c600172e63bac08577921144c30027a5772d275..2476d668caa919892baeac9574ea57d771691ac2 100644 GIT binary patch delta 130 zcmZ3$IG=HXN_Cp2i(^Q|t>ho}4h@M{9tC#0>n@vmG(4PdO8;xNQ;%*m?`A*t=+yt6 zCEI`e{1n^&Z~5LAi?$z~|F`~X&X4~e&F(&~xU}Sd;(InF>#H(kb`^EhjfWXt$&t;ucLK6V=KSB5a delta 131 zcmV-}0DS+S0iXepBz$K{L_t(|+GF@XK!9P?;!%r7EvA{p-LD78;;erIWU lMdql*qZW@^JZdpc008krkH9a7&glRE002ovPDHLkV1nN-L!JNt diff --git a/tests/ref/math-mat-align-explicit-alternating.png b/tests/ref/math-mat-align-explicit-alternating.png index 37e8dc06a7e06a903d87ef61d608dc8b92589097..1ebcc7b6847d96c69e3e5787510299955d3c7003 100644 GIT binary patch delta 1013 zcmV5X$Yw>YHx!K*VnU3dFnK@$4M zIUcAvM|UOfHq zy%c(#61ny`VXU1??GtPaa+Z`WQ)M?C*v(y+(8 zOAGv9HGixIChSd%$t8E0%bHV)eK#+tNjNy4O1#LBG+#?!H`Nc}P>W0l>gtIO00a^xXSwF z_gHWx#z=Sk6sF8s_)7 zawnCzW-n{;mQz&XyM+Wkdzf&l%|nPBk0rjn7eTW1x-Ci=w)+YB4hJFn`!~Zeg`~r-*^d#evVLy(K0wqY|}3f!RWbZ zSAPR=tX29}^LG7`xSV*-rvHV6?xs=;aH!?tMxZwf(TPvj;f*t2Q1&Je5|#`a>wWMK zsEsOs9fiUwmz?;%O3dRZhenKiN#+GXVlhEiX-saweN7`RR{=8EBcRag8$;} zcl3YWz_gWj=2Xgw&4)U&sxd?PeY;BsaBfg_XccgG3nnSWsJsVZBZs17*4p$|eFCh+ jfy_ErMw}66#H{NRi#W{02W|T?00000NkvXXu0mjftI+K7 delta 987 zcmV<110?*52=NDyB!9R`L_t(|+U?l?Pt;`qz;XWte{E~J^+P|prB>jY|WW#}Y6%D>SAh@jS2piTp5Qc^!)LPd92)FG3>YjjPxFHV> zBPaS^0$4CQ4%V^_4bre0Vr!eR|oL9Su&i`gNC?q@0$SYt(SNB0Diyz zK^=gj<>CQCb$?t-ZvwEloHqj4UY852&jr!2jhh8iz+Z2866}Dwd$`gx3FMZ{rc8#O z^vjOo%hSLoRpW=mMBo`!D*Q(jS~8V)i+=;mXNGs>14}W=sjNSN)c*R|pZVWRta3c7 zAJ~7VI^F^h+a(qL!GIRkrj8&?53bDsto5!t5pK^-`hN-G>)jYf7?@X7fUFX!aCSC2 z%l}j=Y$!!%6=_o8eP(oaWuH_y#)8f)F;d~J1?a3`>r=2}`LM6!J_!B2ASmr#gw=M1 z@aAp7#|R&NkU0p@V6uD*;4;o6Bc#n`*a^p)Yl{N;@a7ys^_Wjq0z7CkB>?zaYj=17 zy3Cn%gnzTstsFtf*(?>_ZX!f**WKFk2_WzL)i^PBCvC(J1O6AC6{d+W-IZu+E-3(} z?T>KO-o5tR^jkp2&HBA&py{)|=&yj~SM8_Y?|-kVQCNcJAR2T**Y<^sa=ZmBT4#j;>q^&S^MU;W7nfZ0Y<0>D4XoNGqIMoaZJ zgn#bZ3=6Fl3{lY(EgEie|7+TaRXl*=>k5yR{hEq!1?vB(U;I*KHC{p0Jsuu zA7=~#hR({kt3c9{WSG-EfKS1LFcx$pOjNCU5ZudN4_Z9a0WUys3^#(ZwgPmXMK-)- zL`MhbrNcDWpqp=6kPlPUhEAGF0r@a9=wv4H&lL$r!jZ7v{sWMZ&8QxdpnU)U002ov JPDHLkV1j|p*Ny-H diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png index 09ce93982ad85083cf89b7a3b488ebed9fb05fc4..cb9819248275a76b4ae40dff2472c993c64ee49d 100644 GIT binary patch delta 967 zcmV;&133KP2i*sdB!8qyL_t(|+U=N0OcPNYhdt}X#DfPtY2wY8s1Px3(FVmO6-(R@ z+yKS6KqN|B)5MslxS%mXt0uG*1j~{rg-AdPr3944(AZEZYg=ebSxP%lrtRPNriWp4 z%uHY=a_Dz>-;Xepuh4W3T?GG$MUikM90^Clf`&aALME1u3C<=X^o zdJ)>7|zu+XrD5Rf#;p z%U$U3ZL!@*uVW2svQ07fkjJ-MuU^)paj30OQie<~rQEpTG0b9o1$TI8F{PQ7={{g* zxO3WpJ>B9DU-H9cCxNl-KM5z}o|RV4aPt}vfelTZ;eQXi#0bZgaE4zbiV;qH!5My* zBt|&t8D}^xT8waX+5-KH>F`wD08q5Gz#mv^3Cg}&>t7F-)_I3d+b(os7b(jh0fUib z$3bcwbETMp!CQ^`<($K^)s&hw847G$g)$BpklKQc1Rjl=3u<66Q2s{79$qcu49lhX z48?R6k#tg{XKXY848|?w7&y=v1219DSL;51a_zwJV=?}c|e6B-3EeUJq7&2?sP%B{=;sbVd#_#8P4he pzF}|(8Foaz=Zb_Q;Ye6megldE%v2mRp*jEn002ovPDHLkV1k1o(oO&X delta 970 zcmV;*12z2J2jB;gB!8z#L_t(|+U?j|OjCCN$8n#xmnD1H!}c_jy)7mxM7F485(Kve zw8UK)Ac|y|&A=p%Wo}C1mTY4RBTH1A3*|Ok9V`xTGrFObMpR&F1EEqVwiMcO?d4$U z{xAP^JSFQ=;Z!&kPK6U1_J389Ku7&r!_mFdB+x>6 zRAczteiF*~y|%DRM}qB}))YQ61R#xqviX4!!rFir1j`u2FOMmV%*`q-;l&I9%V&4q zMA2fgy#S%uvEK&J+;P4MbSJ--xy zd`Z2X8<6|zf`5kaNBux#BbV!EQF7yXMC5=Xu7;45cP6Er@oktEEfjvX*S$}|R>1!>Uene*ZW%cmva|q$#$T-U8%n#iF z4+lebg#W#+FmcN@DtVB5#i<&;{TfNI$)px)Q&qI%VE zc{)juUjCHdSUtRY{Q(F=gAmVoO+pA>b3(lEp!B5S(wReMfDV1v55$=-cIP8};4QNf z$6q?R%lx=u-Fk%EhRP;@nQnb9al*Z=nSYfVX7n9Y8p7{p=SgI@^8eG=Q^wArB#6DxCxv z$w56_R|jyfz49D@r&FH`5b14BlMqJpY5WsKuHVk<0M>e%Hz1f=%enySPHPC;a)4;s zbbrg4Dd6*_K(=_^*c07pL1;aN?tTCzpSF2+PXYgY7~5(C0vqQ~bv^*H?rR8BV1w1{ zk*`Z`0iRrMe(Ms@Q*F#Tj$r@h?`^-LX#dpF@~0=ZN7T&Zv>oC+t_BX|4FHkap2r2qf`07*qoM6N<$g6J&O!2kdN diff --git a/tests/ref/math-mat-align-explicit-right.png b/tests/ref/math-mat-align-explicit-right.png index 3592c0cf53ae7b3568390f256dcbe34fc510b306..b537e6571847fc1fae250cc43fedc960ce557e18 100644 GIT binary patch delta 954 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 delta 987 zcmV<110?*52=NDyB!9R`L_t(|+U?l?Pt;`qz;XWte{E~J^+P|prB>jY|WW#}Y6%D>SAh@jS2piTp5Qc^!)LPd92)FG3>YjjPxFHV> zBPaS^0$4CQ4%V^_4bre0Vr!eR|oL9Su&i`gNC?q@0$SYt(SNB0Diyz zK^=gj<>CQCb$?t-ZvwEloHqj4UY852&jr!2jhh8iz+Z2866}Dwd$`gx3FMZ{rc8#O z^vjOo%hSLoRpW=mMBo`!D*Q(jS~8V)i+=;mXNGs>14}W=sjNSN)c*R|pZVWRta3c7 zAJ~7VI^F^h+a(qL!GIRkrj8&?53bDsto5!t5pK^-`hN-G>)jYf7?@X7fUFX!aCSC2 z%l}j=Y$!!%6=_o8eP(oaWuH_y#)8f)F;d~J1?a3`>r=2}`LM6!J_!B2ASmr#gw=M1 z@aAp7#|R&NkU0p@V6uD*;4;o6Bc#n`*a^p)Yl{N;@a7ys^_Wjq0z7CkB>?zaYj=17 zy3Cn%gnzTstsFtf*(?>_ZX!f**WKFk2_WzL)i^PBCvC(J1O6AC6{d+W-IZu+E-3(} z?T>KO-o5tR^jkp2&HBA&py{)|=&yj~SM8_Y?|-kVQCNcJAR2T**Y<^sa=ZmBT4#j;>q^&S^MU;W7nfZ0Y<0>D4XoNGqIMoaZJ zgn#bZ3=6Fl3{lY(EgEie|7+TaRXl*=>k5yR{hEq!1?vB(U;I*KHC{p0Jsuu zA7=~#hR({kt3c9{WSG-EfKS1LFcx$pOjNCU5ZudN4_Z9a0WUys3^#(ZwgPmXMK-)- zL`MhbrNcDWpqp=6kPlPUhEAGF0r@a9=wv4H&lL$r!jZ7v{sWMZ&8QxdpnU)U002ov JPDHLkV1j|p*Ny-H diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ index 9ddee5975..c3c119e33 100644 --- a/tests/suite/foundations/content.typ +++ b/tests/suite/foundations/content.typ @@ -85,7 +85,7 @@ [With ] vars .pairs() - .map(((name, value)) => $name = value$) + .map(((name, value)) => $#symbol(name) = value$) .join(", ", last: " and ") [ we have:] $ equation = result $ diff --git a/tests/suite/math/alignment.typ b/tests/suite/math/alignment.typ index 63033ef5c..941c20556 100644 --- a/tests/suite/math/alignment.typ +++ b/tests/suite/math/alignment.typ @@ -4,10 +4,10 @@ // Test alignment step functions. #set page(width: 225pt) $ -"a" &= c \ -&= c + 1 & "By definition" \ -&= d + 100 + 1000 \ -&= x && "Even longer" \ +a &= c \ + &= c + 1 & "By definition" \ + &= d + 100 + 1000 \ + &= x && "Even longer" \ $ --- math-align-post-fix --- diff --git a/tests/suite/math/delimited.typ b/tests/suite/math/delimited.typ index ca82427dd..794ffd8aa 100644 --- a/tests/suite/math/delimited.typ +++ b/tests/suite/math/delimited.typ @@ -41,8 +41,8 @@ $floor(x/2), ceil(x/2), abs(x), norm(x)$ --- math-lr-color --- // Test colored delimiters $ lr( - text("(", fill: #green) a/b - text(")", fill: #blue) + text(\(, fill: #green) a/b + text(\), fill: #blue) ) $ --- math-lr-mid --- diff --git a/tests/suite/math/stretch.typ b/tests/suite/math/stretch.typ index 1377f4d21..d145f72a1 100644 --- a/tests/suite/math/stretch.typ +++ b/tests/suite/math/stretch.typ @@ -63,8 +63,8 @@ $ ext(bar.v) quad ext(bar.v.double) quad // Test stretch when base is given with shorthand. $stretch(||, size: #2em)$ $stretch(\(, size: #2em)$ -$stretch("⟧", size: #2em)$ -$stretch("|", size: #2em)$ +$stretch(⟧, size: #2em)$ +$stretch(|, size: #2em)$ $stretch(->, size: #2em)$ $stretch(↣, size: #2em)$ @@ -87,7 +87,7 @@ $ body^"text" $ #{ let body = $stretch(=)$ for i in range(24) { - body = $body$ + body = $body$ } $body^"long text"$ } From cd044825fcb1651781f1dbcafac4dec8b216e370 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 23 Jan 2025 23:18:02 +0100 Subject: [PATCH 39/44] Handle boxes and blocks a bit better in HTML export (#5744) Co-authored-by: Martin Haug <3874949+reknih@users.noreply.github.com> --- crates/typst-html/src/lib.rs | 29 +++++++++++++++++++++---- crates/typst-library/src/html/dom.rs | 15 ++++++++++--- crates/typst-library/src/model/enum.rs | 6 ++--- crates/typst-library/src/model/quote.rs | 7 ++---- crates/typst-library/src/model/table.rs | 6 ++--- tests/ref/html/block-html.html | 15 +++++++++++++ tests/ref/html/box-html.html | 12 ++++++++++ tests/suite/layout/container.typ | 7 ++++++ 8 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 tests/ref/html/block-html.html create mode 100644 tests/ref/html/box-html.html diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index ffd8e2505..1fa6aa216 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -14,7 +14,7 @@ use typst_library::html::{ use typst_library::introspection::{ Introspector, Locator, LocatorLink, SplitLocator, TagElem, }; -use typst_library::layout::{Abs, Axes, BoxElem, Region, Size}; +use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; use typst_library::model::{DocumentInfo, ParElem}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; @@ -197,13 +197,34 @@ fn handle( .into(), ); } else if let Some(elem) = child.to_packed::() { - // FIXME: Very incomplete and hacky, but makes boxes kind fulfill their - // purpose for now. + // TODO: This is rather incomplete. if let Some(body) = elem.body(styles) { let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; - output.extend(children); + output.push( + HtmlElement::new(tag::span) + .with_attr(attr::style, "display: inline-block;") + .with_children(children) + .spanned(elem.span()) + .into(), + ) } + } else if let Some((elem, body)) = + child + .to_packed::() + .and_then(|elem| match elem.body(styles) { + Some(BlockBody::Content(body)) => Some((elem, body)), + _ => None, + }) + { + // TODO: This is rather incomplete. + let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::div) + .with_children(children) + .spanned(elem.span()) + .into(), + ); } else if child.is::() { output.push(HtmlNode::text(' ', child.span())); } else if let Some(elem) = child.to_packed::() { diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 5b6eab4d6..2acd839dd 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -210,7 +210,10 @@ impl HtmlAttr { /// Creates a compile-time constant `HtmlAttr`. /// - /// Should only be used in const contexts because it can panic. + /// Must only be used in const contexts (in a constant definition or + /// explicit `const { .. }` block) because otherwise a panic for a malformed + /// attribute or not auto-internible constant will only be caught at + /// runtime. #[track_caller] pub const fn constant(string: &'static str) -> Self { if string.is_empty() { @@ -605,6 +608,7 @@ pub mod tag { /// Predefined constants for HTML attributes. /// /// Note: These are very incomplete. +#[allow(non_upper_case_globals)] pub mod attr { use super::HtmlAttr; @@ -619,13 +623,18 @@ pub mod attr { attrs! { charset + cite + colspan content href name - value + reversed role + rowspan + start + style + value } - #[allow(non_upper_case_globals)] pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); } diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index 2d774cbbb..4dc834ab7 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -9,7 +9,7 @@ use crate::foundations::{ cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, Styles, TargetElem, }; -use crate::html::{attr, tag, HtmlAttr, HtmlElem}; +use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; @@ -229,10 +229,10 @@ impl Show for Packed { if TargetElem::target_in(styles).is_html() { let mut elem = HtmlElem::new(tag::ol); if self.reversed(styles) { - elem = elem.with_attr(HtmlAttr::constant("reversed"), "reversed"); + elem = elem.with_attr(attr::reversed, "reversed"); } if let Some(n) = self.start(styles).custom() { - elem = elem.with_attr(HtmlAttr::constant("start"), eco_format!("{n}")); + elem = elem.with_attr(attr::start, eco_format!("{n}")); } let body = Content::sequence(self.children.iter().map(|item| { let mut li = HtmlElem::new(tag::li); diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 774384acb..79e9b4e36 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -4,7 +4,7 @@ use crate::foundations::{ cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, TargetElem, }; -use crate::html::{tag, HtmlAttr, HtmlElem}; +use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Locatable; use crate::layout::{ Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, @@ -194,10 +194,7 @@ impl Show for Packed { if let Some(Attribution::Content(attribution)) = attribution { if let Some(link) = attribution.to_packed::() { if let LinkTarget::Dest(Destination::Url(url)) = &link.dest { - elem = elem.with_attr( - HtmlAttr::constant("cite"), - url.clone().into_inner(), - ); + elem = elem.with_attr(attr::cite, url.clone().into_inner()); } } } diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index ba7924422..82c1cc08b 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -9,7 +9,7 @@ use crate::foundations::{ cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, }; -use crate::html::{tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; +use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; use crate::introspection::Locator; use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use crate::layout::{ @@ -268,10 +268,10 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { let mut attrs = HtmlAttrs::default(); let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string()); if let Some(colspan) = span(cell.colspan(styles)) { - attrs.push(HtmlAttr::constant("colspan"), colspan); + attrs.push(attr::colspan, colspan); } if let Some(rowspan) = span(cell.rowspan(styles)) { - attrs.push(HtmlAttr::constant("rowspan"), rowspan); + attrs.push(attr::rowspan, rowspan); } HtmlElem::new(tag) .with_body(Some(cell.body.clone())) diff --git a/tests/ref/html/block-html.html b/tests/ref/html/block-html.html new file mode 100644 index 000000000..98d971b88 --- /dev/null +++ b/tests/ref/html/block-html.html @@ -0,0 +1,15 @@ + + + + + + + +

+ Paragraph +

+
+ Div +
+ + diff --git a/tests/ref/html/box-html.html b/tests/ref/html/box-html.html new file mode 100644 index 000000000..5c970a6bc --- /dev/null +++ b/tests/ref/html/box-html.html @@ -0,0 +1,12 @@ + + + + + + + +

+ Text Span. +

+ + diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ index bb53a0411..f15ddfe4a 100644 --- a/tests/suite/layout/container.typ +++ b/tests/suite/layout/container.typ @@ -264,6 +264,13 @@ First! image("/assets/images/rhino.png", width: 30pt) ) +--- box-html html --- +Text #box[Span]. + +--- block-html html --- +Paragraph +#block[Div] + --- container-layoutable-child --- // Test box/block sizing with directly layoutable child. // From 467968af0788a3059e1bed47f9daee846f5b3904 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 24 Jan 2025 12:15:09 +0100 Subject: [PATCH 40/44] Tweak HTML pretty printing (#5745) --- crates/typst-html/src/encode.rs | 52 ++++++++---- crates/typst-library/src/html/dom.rs | 94 ++++++++++++++-------- tests/ref/html/basic-table.html | 22 +++-- tests/ref/html/block-html.html | 8 +- tests/ref/html/box-html.html | 4 +- tests/ref/html/enum-start.html | 3 +- tests/ref/html/heading-html-basic.html | 28 ++----- tests/ref/html/link-basic.html | 16 +--- tests/ref/html/quote-attribution-link.html | 8 +- tests/ref/html/quote-nesting-html.html | 4 +- tests/ref/html/quote-plato.html | 16 +--- 11 files changed, 135 insertions(+), 120 deletions(-) diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 71422a0fc..612f923fc 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::foundations::Repr; -use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode}; +use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag}; use typst_library::layout::Frame; use typst_syntax::Span; @@ -20,10 +20,11 @@ pub fn html(document: &HtmlDocument) -> SourceResult { #[derive(Default)] struct Writer { + /// The output buffer. buf: String, - /// current indentation level + /// The current indentation level level: usize, - /// pretty printing enabled? + /// Whether pretty printing is enabled. pretty: bool, } @@ -88,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { let pretty = w.pretty; if !element.children.is_empty() { - w.pretty &= is_pretty(element); + let pretty_inside = allows_pretty_inside(element.tag) + && element.children.iter().any(|node| match node { + HtmlNode::Element(child) => wants_pretty_around(child.tag), + _ => false, + }); + + w.pretty &= pretty_inside; let mut indent = w.pretty; w.level += 1; for c in &element.children { - let pretty_child = match c { + let pretty_around = match c { HtmlNode::Tag(_) => continue, - HtmlNode::Element(element) => is_pretty(element), + HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag), HtmlNode::Text(..) | HtmlNode::Frame(_) => false, }; - if core::mem::take(&mut indent) || pretty_child { + if core::mem::take(&mut indent) || pretty_around { write_indent(w); } write_node(w, c)?; - indent = pretty_child; + indent = pretty_around; } w.level -= 1; - write_indent(w) + write_indent(w); } w.pretty = pretty; @@ -118,12 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { Ok(()) } -/// Whether the element should be pretty-printed. -fn is_pretty(element: &HtmlElement) -> bool { - matches!( - element.tag, - tag::meta | tag::table | tag::thead | tag::tbody | tag::tfoot | tag::tr - ) || tag::is_block_by_default(element.tag) +/// Whether we are allowed to add an extra newline at the start and end of the +/// element's contents. +/// +/// Technically, users can change CSS `display` properties such that the +/// insertion of whitespace may actually impact the visual output. For example, +/// shows how adding CSS +/// rules to `

` can make it sensitive to whitespace. For this reason, we +/// should also respect the `style` tag in the future. +fn allows_pretty_inside(tag: HtmlTag) -> bool { + (tag::is_block_by_default(tag) && tag != tag::pre) + || tag::is_tabular_by_default(tag) + || tag == tag::li +} + +/// Whether newlines should be added before and after the element if the parent +/// allows it. +/// +/// In contrast to `allows_pretty_inside`, which is purely spec-driven, this is +/// more subjective and depends on preference. +fn wants_pretty_around(tag: HtmlTag) -> bool { + allows_pretty_inside(tag) || tag::is_metadata(tag) || tag == tag::pre } /// Escape a character. diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 2acd839dd..1b725d543 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -475,17 +475,55 @@ pub mod tag { wbr } + /// Whether this is a void tag whose associated element may not have a + /// children. + pub fn is_void(tag: HtmlTag) -> bool { + matches!( + tag, + self::area + | self::base + | self::br + | self::col + | self::embed + | self::hr + | self::img + | self::input + | self::link + | self::meta + | self::param + | self::source + | self::track + | self::wbr + ) + } + + /// Whether this is a tag containing raw text. + pub fn is_raw(tag: HtmlTag) -> bool { + matches!(tag, self::script | self::style) + } + + /// Whether this is a tag containing escapable raw text. + pub fn is_escapable_raw(tag: HtmlTag) -> bool { + matches!(tag, self::textarea | self::title) + } + + /// Whether an element is considered metadata. + pub fn is_metadata(tag: HtmlTag) -> bool { + matches!( + tag, + self::base + | self::link + | self::meta + | self::noscript + | self::script + | self::style + | self::template + | self::title + ) + } + /// Whether nodes with the tag have the CSS property `display: block` by /// default. - /// - /// If this is true, then pretty-printing can insert spaces around such - /// nodes and around the contents of such nodes. - /// - /// However, when users change the properties of such tags via CSS, the - /// insertion of whitespace may actually impact the visual output; for - /// example, shows how - /// adding CSS rules to `

` can make it sensitive to whitespace. In such - /// cases, users should disable pretty-printing. pub fn is_block_by_default(tag: HtmlTag) -> bool { matches!( tag, @@ -572,37 +610,23 @@ pub mod tag { ) } - /// Whether this is a void tag whose associated element may not have a - /// children. - pub fn is_void(tag: HtmlTag) -> bool { + /// Whether nodes with the tag have the CSS property `display: table(-.*)?` + /// by default. + pub fn is_tabular_by_default(tag: HtmlTag) -> bool { matches!( tag, - self::area - | self::base - | self::br + self::table + | self::thead + | self::tbody + | self::tfoot + | self::tr + | self::th + | self::td + | self::caption | self::col - | self::embed - | self::hr - | self::img - | self::input - | self::link - | self::meta - | self::param - | self::source - | self::track - | self::wbr + | self::colgroup ) } - - /// Whether this is a tag containing raw text. - pub fn is_raw(tag: HtmlTag) -> bool { - matches!(tag, self::script | self::style) - } - - /// Whether this is a tag containing escapable raw text. - pub fn is_escapable_raw(tag: HtmlTag) -> bool { - matches!(tag, self::textarea | self::title) - } } /// Predefined constants for HTML attributes. diff --git a/tests/ref/html/basic-table.html b/tests/ref/html/basic-table.html index 6ba1864ef..189a5b314 100644 --- a/tests/ref/html/basic-table.html +++ b/tests/ref/html/basic-table.html @@ -8,26 +8,36 @@ - + + + - + + + - + + + - + + - + + - + + +
ThefirstandThefirstand
thesecondrowthesecondrow
FooBazBarFooBazBar
1212
3434
ThelastrowThelastrow
diff --git a/tests/ref/html/block-html.html b/tests/ref/html/block-html.html index 98d971b88..d1716c6d7 100644 --- a/tests/ref/html/block-html.html +++ b/tests/ref/html/block-html.html @@ -5,11 +5,7 @@ -

- Paragraph -

-
- Div -
+

Paragraph

+
Div
diff --git a/tests/ref/html/box-html.html b/tests/ref/html/box-html.html index 5c970a6bc..b2a26533b 100644 --- a/tests/ref/html/box-html.html +++ b/tests/ref/html/box-html.html @@ -5,8 +5,6 @@ -

- Text Span. -

+

Text Span.

diff --git a/tests/ref/html/enum-start.html b/tests/ref/html/enum-start.html index 8a4ff37f9..fc9b3c061 100644 --- a/tests/ref/html/enum-start.html +++ b/tests/ref/html/enum-start.html @@ -6,7 +6,8 @@
    -
  1. Skipping
  2. Ahead
  3. +
  4. Skipping
  5. +
  6. Ahead
diff --git a/tests/ref/html/heading-html-basic.html b/tests/ref/html/heading-html-basic.html index 56b1e32b7..54a22faf4 100644 --- a/tests/ref/html/heading-html-basic.html +++ b/tests/ref/html/heading-html-basic.html @@ -5,26 +5,12 @@ -

- Level 1 -

-

- Level 2 -

-

- Level 3 -

-
- Level 4 -
-
- Level 5 -
-
- Level 6 -
-
- Level 7 -
+

Level 1

+

Level 2

+

Level 3

+
Level 4
+
Level 5
+
Level 6
+
Level 7
diff --git a/tests/ref/html/link-basic.html b/tests/ref/html/link-basic.html index 5d998667e..89cb54db5 100644 --- a/tests/ref/html/link-basic.html +++ b/tests/ref/html/link-basic.html @@ -5,17 +5,9 @@ -

- https://example.com/ -

-

- Some text text text -

-

- This link appears in the middle of a paragraph. -

-

- Contact hi@typst.app or call 123 for more information. -

+

https://example.com/

+

Some text text text

+

This link appears in the middle of a paragraph.

+

Contact hi@typst.app or call 123 for more information.

diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html index 4da8b47f5..753807db2 100644 --- a/tests/ref/html/quote-attribution-link.html +++ b/tests/ref/html/quote-attribution-link.html @@ -5,11 +5,7 @@ -
- Compose papers faster -
-

- — typst.com -

+
Compose papers faster
+

typst.com

diff --git a/tests/ref/html/quote-nesting-html.html b/tests/ref/html/quote-nesting-html.html index c652bd97b..6b05a94a0 100644 --- a/tests/ref/html/quote-nesting-html.html +++ b/tests/ref/html/quote-nesting-html.html @@ -5,8 +5,6 @@ -

- When you said that “he surely meant that ‘she intended to say “I'm sorry”’”, I was quite confused. -

+

When you said that “he surely meant that ‘she intended to say “I'm sorry”’”, I was quite confused.

diff --git a/tests/ref/html/quote-plato.html b/tests/ref/html/quote-plato.html index fc052d10c..f516adc29 100644 --- a/tests/ref/html/quote-plato.html +++ b/tests/ref/html/quote-plato.html @@ -5,17 +5,9 @@ -
- … ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. -
-

- — Plato -

-
- … I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either. -
-

- — from the Henry Cary literal translation of 1897 -

+
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
+

— Plato

+
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.
+

— from the Henry Cary literal translation of 1897

From 26e65bfef5b1da7f6c72e1409237cf03fb5d6069 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 24 Jan 2025 13:11:26 +0100 Subject: [PATCH 41/44] Semantic paragraphs (#5746) --- crates/typst-html/src/lib.rs | 9 +- crates/typst-layout/src/flow/collect.rs | 85 ++++++++-- crates/typst-layout/src/flow/compose.rs | 6 +- crates/typst-layout/src/flow/mod.rs | 45 +++-- crates/typst-layout/src/inline/box.rs | 2 +- crates/typst-layout/src/inline/collect.rs | 57 ++++--- crates/typst-layout/src/inline/finalize.rs | 2 +- crates/typst-layout/src/inline/line.rs | 14 +- crates/typst-layout/src/inline/linebreak.rs | 27 ++- crates/typst-layout/src/inline/mod.rs | 77 ++++++--- crates/typst-layout/src/inline/prepare.rs | 48 ++++-- crates/typst-layout/src/inline/shaping.rs | 10 +- crates/typst-layout/src/lib.rs | 1 - crates/typst-layout/src/lists.rs | 24 ++- crates/typst-layout/src/math/lr.rs | 11 +- crates/typst-layout/src/math/mod.rs | 7 +- crates/typst-layout/src/math/text.rs | 13 +- crates/typst-layout/src/pages/collect.rs | 2 +- crates/typst-layout/src/pages/mod.rs | 4 +- crates/typst-layout/src/pages/run.rs | 4 +- .../typst-library/src/foundations/styles.rs | 101 ------------ crates/typst-library/src/layout/container.rs | 10 +- crates/typst-library/src/math/equation.rs | 4 +- .../typst-library/src/model/bibliography.rs | 44 ++--- crates/typst-library/src/model/enum.rs | 15 +- crates/typst-library/src/model/figure.rs | 33 ++-- crates/typst-library/src/model/footnote.rs | 6 +- crates/typst-library/src/model/list.rs | 13 +- crates/typst-library/src/model/outline.rs | 1 - crates/typst-library/src/model/par.rs | 110 ++++++++----- crates/typst-library/src/model/quote.rs | 19 ++- crates/typst-library/src/model/terms.rs | 22 ++- crates/typst-library/src/routines.rs | 76 ++++++--- crates/typst-realize/src/lib.rs | 155 +++++++++++++----- crates/typst-utils/src/lib.rs | 27 +++ crates/typst/src/lib.rs | 2 - tests/ref/bibliography-grid-par.png | Bin 0 -> 8757 bytes tests/ref/bibliography-indent-par.png | Bin 0 -> 9087 bytes tests/ref/enum-par.png | Bin 0 -> 3521 bytes tests/ref/figure-par.png | Bin 0 -> 1701 bytes tests/ref/heading-par.png | Bin 0 -> 555 bytes tests/ref/html/enum-par.html | 36 ++++ tests/ref/html/list-par.html | 36 ++++ tests/ref/html/par-semantic-html.html | 16 ++ tests/ref/html/quote-attribution-link.html | 2 +- tests/ref/html/quote-plato.html | 4 +- tests/ref/html/terms-par.html | 42 +++++ tests/ref/issue-5503-enum-in-align.png | Bin 0 -> 421 bytes ...sue-5503-enum-interrupted-by-par-align.png | Bin 1004 -> 0 bytes ...align.png => issue-5503-list-in-align.png} | Bin ...lign.png => issue-5503-terms-in-align.png} | Bin tests/ref/list-par.png | Bin 0 -> 3319 bytes tests/ref/math-par.png | Bin 0 -> 387 bytes tests/ref/outline-par.png | Bin 0 -> 2911 bytes tests/ref/par-contains-block.png | Bin 0 -> 426 bytes tests/ref/par-contains-parbreak.png | Bin 0 -> 426 bytes tests/ref/par-hanging-indent-semantic.png | Bin 0 -> 1594 bytes tests/ref/par-semantic-align.png | Bin 0 -> 3082 bytes tests/ref/par-semantic-tag.png | Bin 0 -> 278 bytes tests/ref/par-semantic.png | Bin 0 -> 3485 bytes tests/ref/par-show.png | Bin 0 -> 932 bytes tests/ref/quote-par.png | Bin 0 -> 2792 bytes tests/ref/table-cell-par.png | Bin 0 -> 645 bytes tests/ref/terms-par.png | Bin 0 -> 3892 bytes tests/suite/layout/table.typ | 11 ++ tests/suite/math/text.typ | 5 + tests/suite/model/bibliography.typ | 18 ++ tests/suite/model/enum.typ | 38 ++++- tests/suite/model/figure.typ | 11 ++ tests/suite/model/heading.typ | 5 + tests/suite/model/list.typ | 38 ++++- tests/suite/model/outline.typ | 9 + tests/suite/model/par.typ | 141 ++++++++++++++++ tests/suite/model/quote.typ | 11 ++ tests/suite/model/terms.typ | 40 +++-- 75 files changed, 1098 insertions(+), 451 deletions(-) create mode 100644 tests/ref/bibliography-grid-par.png create mode 100644 tests/ref/bibliography-indent-par.png create mode 100644 tests/ref/enum-par.png create mode 100644 tests/ref/figure-par.png create mode 100644 tests/ref/heading-par.png create mode 100644 tests/ref/html/enum-par.html create mode 100644 tests/ref/html/list-par.html create mode 100644 tests/ref/html/par-semantic-html.html create mode 100644 tests/ref/html/terms-par.html create mode 100644 tests/ref/issue-5503-enum-in-align.png delete mode 100644 tests/ref/issue-5503-enum-interrupted-by-par-align.png rename tests/ref/{issue-5503-list-interrupted-by-par-align.png => issue-5503-list-in-align.png} (100%) rename tests/ref/{issue-5503-terms-interrupted-by-par-align.png => issue-5503-terms-in-align.png} (100%) create mode 100644 tests/ref/list-par.png create mode 100644 tests/ref/math-par.png create mode 100644 tests/ref/outline-par.png create mode 100644 tests/ref/par-contains-block.png create mode 100644 tests/ref/par-contains-parbreak.png create mode 100644 tests/ref/par-hanging-indent-semantic.png create mode 100644 tests/ref/par-semantic-align.png create mode 100644 tests/ref/par-semantic-tag.png create mode 100644 tests/ref/par-semantic.png create mode 100644 tests/ref/par-show.png create mode 100644 tests/ref/quote-par.png create mode 100644 tests/ref/table-cell-par.png create mode 100644 tests/ref/terms-par.png diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 1fa6aa216..25d0cd5d8 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -16,7 +16,7 @@ use typst_library::introspection::{ }; use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; use typst_library::model::{DocumentInfo, ParElem}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_library::World; use typst_syntax::Span; @@ -139,7 +139,9 @@ fn html_fragment_impl( let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::HtmlFragment, + // No need to know about the `FragmentKind` because we handle both + // uniformly. + RealizationKind::HtmlFragment(&mut FragmentKind::Block), &mut engine, &mut locator, &arenas, @@ -189,7 +191,8 @@ fn handle( }; output.push(element.into()); } else if let Some(elem) = child.to_packed::() { - let children = handle_list(engine, locator, elem.children.iter(&styles))?; + let children = + html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; output.push( HtmlElement::new(tag::p) .with_children(children) diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 76d7b7433..f2c7ebd1e 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -20,13 +20,15 @@ use typst_library::model::ParElem; use typst_library::routines::{Pair, Routines}; use typst_library::text::TextElem; use typst_library::World; +use typst_utils::SliceExt; -use super::{layout_multi_block, layout_single_block}; +use super::{layout_multi_block, layout_single_block, FlowMode}; use crate::modifiers::layout_and_modify; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. #[typst_macros::time] +#[allow(clippy::too_many_arguments)] pub fn collect<'a>( engine: &mut Engine, bump: &'a Bump, @@ -34,6 +36,7 @@ pub fn collect<'a>( locator: Locator<'a>, base: Size, expand: bool, + mode: FlowMode, ) -> SourceResult>> { Collector { engine, @@ -45,7 +48,7 @@ pub fn collect<'a>( output: Vec::with_capacity(children.len()), last_was_par: false, } - .run() + .run(mode) } /// State for collection. @@ -62,7 +65,15 @@ struct Collector<'a, 'x, 'y> { impl<'a> Collector<'a, '_, '_> { /// Perform the collection. - fn run(mut self) -> SourceResult>> { + fn run(self, mode: FlowMode) -> SourceResult>> { + match mode { + FlowMode::Root | FlowMode::Block => self.run_block(), + FlowMode::Inline => self.run_inline(), + } + } + + /// Perform collection for block-level children. + fn run_block(mut self) -> SourceResult>> { for &(child, styles) in self.children { if let Some(elem) = child.to_packed::() { self.output.push(Child::Tag(&elem.tag)); @@ -95,6 +106,43 @@ impl<'a> Collector<'a, '_, '_> { Ok(self.output) } + /// Perform collection for inline-level children. + fn run_inline(mut self) -> SourceResult>> { + // Extract leading and trailing tags. + let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::()); + let inner = &self.children[start..end]; + + // Compute the shared styles, ignoring tags. + let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default(); + + // Layout the lines. + let lines = crate::inline::layout_inline( + self.engine, + inner, + &mut self.locator, + styles, + self.base, + self.expand, + false, + false, + )? + .into_frames(); + + for (c, _) in &self.children[..start] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + self.lines(lines, styles); + + for (c, _) in &self.children[end..] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + Ok(self.output) + } + /// Collect vertical spacing into a relative or fractional child. fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { self.output.push(match elem.amount { @@ -110,24 +158,34 @@ impl<'a> Collector<'a, '_, '_> { elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { - let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); - let spacing = ParElem::spacing_in(styles); - let costs = TextElem::costs_in(styles); - - let lines = crate::layout_inline( + let lines = crate::inline::layout_par( + elem, self.engine, - &elem.children, self.locator.next(&elem.span()), styles, - self.last_was_par, self.base, self.expand, + self.last_was_par, )? .into_frames(); + let spacing = ParElem::spacing_in(styles); self.output.push(Child::Rel(spacing.into(), 4)); + self.lines(lines, styles); + + self.output.push(Child::Rel(spacing.into(), 4)); + self.last_was_par = true; + + Ok(()) + } + + /// Collect laid-out lines. + fn lines(&mut self, lines: Vec, styles: StyleChain<'a>) { + let align = AlignElem::alignment_in(styles).resolve(styles); + let leading = ParElem::leading_in(styles); + let costs = TextElem::costs_in(styles); + // Determine whether to prevent widow and orphans. let len = lines.len(); let prevent_orphans = @@ -166,11 +224,6 @@ impl<'a> Collector<'a, '_, '_> { self.output .push(Child::Line(self.boxed(LineChild { frame, align, need }))); } - - self.output.push(Child::Rel(spacing.into(), 4)); - self.last_was_par = true; - - Ok(()) } /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 3cf66f9e3..76af8f650 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -17,7 +17,9 @@ use typst_library::model::{ use typst_syntax::Span; use typst_utils::{NonZeroExt, Numeric}; -use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; +use super::{ + distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work, +}; /// Composes the contents of a single page/region. A region can have multiple /// columns/subregions. @@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { migratable: bool, ) -> FlowResult<()> { // Footnotes are only supported at the root level. - if !self.config.root { + if self.config.mode != FlowMode::Root { return Ok(()); } diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2f0ec39a9..2acbbcef3 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -25,7 +25,7 @@ use typst_library::layout::{ Regions, Rel, Size, }; use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::TextElem; use typst_library::World; use typst_utils::{NonZeroExt, Numeric}; @@ -140,9 +140,10 @@ fn layout_fragment_impl( engine.route.check_layout_depth().at(content.span())?; + let mut kind = FragmentKind::Block; let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::LayoutFragment, + RealizationKind::LayoutFragment(&mut kind), &mut engine, &mut locator, &arenas, @@ -158,25 +159,46 @@ fn layout_fragment_impl( regions, columns, column_gutter, - false, + kind.into(), ) } +/// The mode a flow can be laid out in. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum FlowMode { + /// A root flow with block-level elements. Like `FlowMode::Block`, but can + /// additionally host footnotes and line numbers. + Root, + /// A flow whose children are block-level elements. + Block, + /// A flow whose children are inline-level elements. + Inline, +} + +impl From for FlowMode { + fn from(value: FragmentKind) -> Self { + match value { + FragmentKind::Inline => Self::Inline, + FragmentKind::Block => Self::Block, + } + } +} + /// Lays out realized content into regions, potentially with columns. #[allow(clippy::too_many_arguments)] -pub(crate) fn layout_flow( +pub fn layout_flow<'a>( engine: &mut Engine, - children: &[Pair], - locator: &mut SplitLocator, - shared: StyleChain, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, mut regions: Regions, columns: NonZeroUsize, column_gutter: Rel, - root: bool, + mode: FlowMode, ) -> SourceResult { // Prepare configuration that is shared across the whole flow. let config = Config { - root, + mode, shared, columns: { let mut count = columns.get(); @@ -195,7 +217,7 @@ pub(crate) fn layout_flow( gap: FootnoteEntry::gap_in(shared), expand: regions.expand.x, }, - line_numbers: root.then(|| LineNumberConfig { + line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig { scope: ParLine::numbering_scope_in(shared), default_clearance: { let width = if PageElem::flipped_in(shared) { @@ -225,6 +247,7 @@ pub(crate) fn layout_flow( locator.next(&()), Size::new(config.columns.width, regions.full), regions.expand.x, + mode, )?; let mut work = Work::new(&children); @@ -318,7 +341,7 @@ impl<'a, 'b> Work<'a, 'b> { struct Config<'x> { /// Whether this is the root flow, which can host footnotes and line /// numbers. - root: bool, + mode: FlowMode, /// The styles shared by the whole flow. This is used for footnotes and line /// numbers. shared: StyleChain<'x>, diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs index 6dfbc9696..e21928d3c 100644 --- a/crates/typst-layout/src/inline/box.rs +++ b/crates/typst-layout/src/inline/box.rs @@ -11,7 +11,7 @@ use typst_utils::Numeric; use crate::flow::unbreakable_pod; use crate::shapes::{clip_rect, fill_and_stroke}; -/// Lay out a box as part of a paragraph. +/// Lay out a box as part of inline layout. #[typst_macros::time(name = "box", span = elem.span())] pub fn layout_box( elem: &Packed, diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 6023f5c63..cbc490ba1 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -1,10 +1,11 @@ -use typst_library::diag::bail; +use typst_library::diag::warning; use typst_library::foundations::{Packed, Resolve}; use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::layout::{ Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; +use typst_library::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, @@ -16,7 +17,7 @@ use super::*; use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify}; // The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. +// full text. const SPACING_REPLACE: &str = " "; // Space const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character @@ -27,7 +28,7 @@ const POP_EMBEDDING: &str = "\u{202C}"; const LTR_ISOLATE: &str = "\u{2066}"; const POP_ISOLATE: &str = "\u{2069}"; -/// A prepared item in a paragraph layout. +/// A prepared item in a inline layout. #[derive(Debug)] pub enum Item<'a> { /// A shaped text run with consistent style and direction. @@ -113,38 +114,44 @@ impl Segment<'_> { } } -/// Collects all text of the paragraph into one string and a collection of -/// segments that correspond to pieces of that string. This also performs -/// string-level preprocessing like case transformations. +/// Collects all text into one string and a collection of segments that +/// correspond to pieces of that string. This also performs string-level +/// preprocessing like case transformations. #[typst_macros::time] pub fn collect<'a>( - children: &'a StyleVec, + children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: &'a StyleChain<'a>, + styles: StyleChain<'a>, region: Size, consecutive: bool, + paragraph: bool, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); let mut quoter = SmartQuoter::new(); - let outer_dir = TextElem::dir_in(*styles); - let first_line_indent = ParElem::first_line_indent_in(*styles); - if !first_line_indent.is_zero() - && consecutive - && AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into() - { - collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false)); - collector.spans.push(1, Span::detached()); + let outer_dir = TextElem::dir_in(styles); + + if paragraph && consecutive { + let first_line_indent = ParElem::first_line_indent_in(styles); + if !first_line_indent.is_zero() + && AlignElem::alignment_in(styles).resolve(styles).x + == outer_dir.start().into() + { + collector.push_item(Item::Absolute(first_line_indent.resolve(styles), false)); + collector.spans.push(1, Span::detached()); + } } - let hang = ParElem::hanging_indent_in(*styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); - collector.spans.push(1, Span::detached()); + if paragraph { + let hang = ParElem::hanging_indent_in(styles); + if !hang.is_zero() { + collector.push_item(Item::Absolute(-hang, false)); + collector.spans.push(1, Span::detached()); + } } - for (child, styles) in children.iter(styles) { + for &(child, styles) in children { let prev_len = collector.full.len(); if child.is::() { @@ -234,7 +241,13 @@ pub fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.push_item(Item::Tag(&elem.tag)); } else { - bail!(child.span(), "unexpected paragraph child"); + // Non-paragraph inline layout should never trigger this since it + // only won't be triggered if we see any non-inline content. + engine.sink.warn(warning!( + child.span(), + "{} may not occur inside of a paragraph and was ignored", + child.func().name() + )); }; let len = collector.full.len() - prev_len; diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 57044f0ec..7ad287c45 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -14,7 +14,7 @@ pub fn finalize( expand: bool, locator: &mut SplitLocator<'_>, ) -> SourceResult { - // Determine the paragraph's width: Full width of the region if we should + // Determine the resulting width: Full width of the region if we should // expand or there's fractional spacing, fit-to-width otherwise. let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index fba4bef80..9f6973807 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -18,12 +18,12 @@ const EN_DASH: char = '–'; const EM_DASH: char = '—'; const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks. -/// A layouted line, consisting of a sequence of layouted paragraph items that -/// are mostly borrowed from the preparation phase. This type enables you to -/// measure the size of a line in a range before committing to building the -/// line's frame. +/// A layouted line, consisting of a sequence of layouted inline items that are +/// mostly borrowed from the preparation phase. This type enables you to measure +/// the size of a line in a range before committing to building the line's +/// frame. /// -/// At most two paragraph items must be created individually for this line: The +/// At most two inline items must be created individually for this line: The /// first and last one since they may be broken apart by the start or end of the /// line, respectively. But even those can partially reuse previous results when /// the break index is safe-to-break per rustybuzz. @@ -430,7 +430,7 @@ pub fn commit( let mut offset = Abs::zero(); // We always build the line from left to right. In an LTR paragraph, we must - // thus add the hanging indent to the offset. When the paragraph is RTL, the + // thus add the hanging indent to the offset. In an RTL paragraph, the // hanging indent arises naturally due to the line width. if p.dir == Dir::LTR { offset += p.hang; @@ -631,7 +631,7 @@ fn overhang(c: char) -> f64 { } } -/// A collection of owned or borrowed paragraph items. +/// A collection of owned or borrowed inline items. pub struct Items<'a>(Vec>); impl<'a> Items<'a> { diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 7b66fcdb4..87113c689 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::*; -/// The cost of a line or paragraph layout. +/// The cost of a line or inline layout. type Cost = f64; // Cost parameters. @@ -104,7 +104,7 @@ impl Breakpoint { } } -/// Breaks the paragraph into lines. +/// Breaks the text into lines. pub fn linebreak<'a>( engine: &Engine, p: &'a Preparation<'a>, @@ -181,13 +181,12 @@ fn linebreak_simple<'a>( /// lines with hyphens even more. /// /// To find the layout with the minimal total cost the algorithm uses dynamic -/// programming: For each possible breakpoint it determines the optimal -/// paragraph layout _up to that point_. It walks over all possible start points -/// for a line ending at that point and finds the one for which the cost of the -/// line plus the cost of the optimal paragraph up to the start point (already -/// computed and stored in dynamic programming table) is minimal. The final -/// result is simply the layout determined for the last breakpoint at the end of -/// text. +/// programming: For each possible breakpoint, it determines the optimal layout +/// _up to that point_. It walks over all possible start points for a line +/// ending at that point and finds the one for which the cost of the line plus +/// the cost of the optimal layout up to the start point (already computed and +/// stored in dynamic programming table) is minimal. The final result is simply +/// the layout determined for the last breakpoint at the end of text. #[typst_macros::time] fn linebreak_optimized<'a>( engine: &Engine, @@ -215,7 +214,7 @@ fn linebreak_optimized_bounded<'a>( metrics: &CostMetrics, upper_bound: Cost, ) -> Vec> { - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry<'a> { pred: usize, total: Cost, @@ -321,7 +320,7 @@ fn linebreak_optimized_bounded<'a>( // This should only happen if our bound was faulty. Which shouldn't happen! if table[idx].end != p.text.len() { #[cfg(debug_assertions)] - panic!("bounded paragraph layout is incomplete"); + panic!("bounded inline layout is incomplete"); #[cfg(not(debug_assertions))] return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY); @@ -342,7 +341,7 @@ fn linebreak_optimized_bounded<'a>( /// (which is costly) to determine costs, it determines approximate costs using /// cumulative arrays. /// -/// This results in a likely good paragraph layouts, for which we then compute +/// This results in a likely good inline layouts, for which we then compute /// the exact cost. This cost is an upper bound for proper optimized /// linebreaking. We can use it to heavily prune the search space. #[typst_macros::time] @@ -355,7 +354,7 @@ fn linebreak_optimized_approximate( // Determine the cumulative estimation metrics. let estimates = Estimates::compute(p); - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry { pred: usize, total: Cost, @@ -862,7 +861,7 @@ struct CostMetrics { } impl CostMetrics { - /// Compute shared metrics for paragraph optimization. + /// Compute shared metrics for inline layout optimization. fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index bedc54d63..83ca82bf2 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,11 +13,11 @@ pub use self::box_::layout_box; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{StyleChain, StyleVec}; -use typst_library::introspection::{Introspector, Locator, LocatorLink}; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; use typst_library::layout::{Fragment, Size}; use typst_library::model::ParElem; -use typst_library::routines::Routines; +use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::World; use self::collect::{collect, Item, Segment, SpanMapper}; @@ -34,18 +34,18 @@ use self::shaping::{ /// Range of a substring of text. type Range = std::ops::Range; -/// Layouts content inline. -pub fn layout_inline( +/// Layouts the paragraph. +pub fn layout_par( + elem: &Packed, engine: &mut Engine, - children: &StyleVec, locator: Locator, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + consecutive: bool, ) -> SourceResult { - layout_inline_impl( - children, + layout_par_impl( + elem, engine.routines, engine.world, engine.introspector, @@ -54,17 +54,17 @@ pub fn layout_inline( engine.route.track(), locator.track(), styles, - consecutive, region, expand, + consecutive, ) } -/// The internal, memoized implementation of `layout_inline`. +/// The internal, memoized implementation of `layout_par`. #[comemo::memoize] #[allow(clippy::too_many_arguments)] -fn layout_inline_impl( - children: &StyleVec, +fn layout_par_impl( + elem: &Packed, routines: &Routines, world: Tracked, introspector: Tracked, @@ -73,12 +73,12 @@ fn layout_inline_impl( route: Tracked, locator: Tracked, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + consecutive: bool, ) -> SourceResult { let link = LocatorLink::new(locator); - let locator = Locator::link(&link); + let mut locator = Locator::link(&link).split(); let mut engine = Engine { routines, world, @@ -88,18 +88,51 @@ fn layout_inline_impl( route: Route::extend(route), }; - let mut locator = locator.split(); + let arenas = Arenas::default(); + let children = (engine.routines.realize)( + RealizationKind::LayoutPar, + &mut engine, + &mut locator, + &arenas, + &elem.body, + styles, + )?; + layout_inline( + &mut engine, + &children, + &mut locator, + styles, + region, + expand, + true, + consecutive, + ) +} + +/// Lays out realized content with inline layout. +#[allow(clippy::too_many_arguments)] +pub fn layout_inline<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + styles: StyleChain<'a>, + region: Size, + expand: bool, + paragraph: bool, + consecutive: bool, +) -> SourceResult { // Collect all text into one string for BiDi analysis. let (text, segments, spans) = - collect(children, &mut engine, &mut locator, &styles, region, consecutive)?; + collect(children, engine, locator, styles, region, consecutive, paragraph)?; - // Perform BiDi analysis and then prepares paragraph layout. - let p = prepare(&mut engine, children, &text, segments, spans, styles)?; + // Perform BiDi analysis and performs some preparation steps before we + // proceed to line breaking. + let p = prepare(engine, children, &text, segments, spans, styles, paragraph)?; - // Break the paragraph into lines. - let lines = linebreak(&engine, &p, region.x - p.hang); + // Break the text into lines. + let lines = linebreak(engine, &p, region.x - p.hang); // Turn the selected lines into frames. - finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator) + finalize(engine, &p, &lines, styles, region, expand, locator) } diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 2dd79aecf..e26c9b147 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,23 +1,26 @@ use typst_library::foundations::{Resolve, Smart}; use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; use typst_library::model::Linebreaks; +use typst_library::routines::Pair; use typst_library::text::{Costs, Lang, TextElem}; +use typst_utils::SliceExt; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; -/// A paragraph representation in which children are already layouted and text -/// is already preshaped. +/// A representation in which children are already layouted and text is already +/// preshaped. /// /// In many cases, we can directly reuse these results when constructing a line. /// Only when a line break falls onto a text index that is not safe-to-break per /// rustybuzz, we have to reshape that portion. pub struct Preparation<'a> { - /// The paragraph's full text. + /// The full text. pub text: &'a str, - /// Bidirectional text embedding levels for the paragraph. + /// Bidirectional text embedding levels. /// - /// This is `None` if the paragraph is BiDi-uniform (all the base direction). + /// This is `None` if all text directions are uniform (all the base + /// direction). pub bidi: Option>, /// Text runs, spacing and layouted elements. pub items: Vec<(Range, Item<'a>)>, @@ -33,15 +36,15 @@ pub struct Preparation<'a> { pub dir: Dir, /// The text language if it's the same for all children. pub lang: Option, - /// The paragraph's resolved horizontal alignment. + /// The resolved horizontal alignment. pub align: FixedAlignment, - /// Whether to justify the paragraph. + /// Whether to justify text. pub justify: bool, - /// The paragraph's hanging indent. + /// Hanging indent to apply. pub hang: Abs, /// Whether to add spacing between CJK and Latin characters. pub cjk_latin_spacing: bool, - /// Whether font fallback is enabled for this paragraph. + /// Whether font fallback is enabled. pub fallback: bool, /// How to determine line breaks. pub linebreaks: Smart, @@ -71,17 +74,18 @@ impl<'a> Preparation<'a> { } } -/// Performs BiDi analysis and then prepares paragraph layout by building a +/// Performs BiDi analysis and then prepares further layout by building a /// representation on which we can do line breaking without layouting each and /// every line from scratch. #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &'a StyleVec, + children: &[Pair<'a>], text: &'a str, segments: Vec>, spans: SpanMapper, styles: StyleChain<'a>, + paragraph: bool, ) -> SourceResult> { let dir = TextElem::dir_in(styles); let default_level = match dir { @@ -125,19 +129,22 @@ pub fn prepare<'a>( add_cjk_latin_spacing(&mut items); } + // Only apply hanging indent to real paragraphs. + let hang = if paragraph { ParElem::hanging_indent_in(styles) } else { Abs::zero() }; + Ok(Preparation { text, bidi: is_bidi.then_some(bidi), items, indices, spans, - hyphenate: children.shared_get(styles, TextElem::hyphenate_in), + hyphenate: shared_get(children, styles, TextElem::hyphenate_in), costs: TextElem::costs_in(styles), dir, - lang: children.shared_get(styles, TextElem::lang_in), + lang: shared_get(children, styles, TextElem::lang_in), align: AlignElem::alignment_in(styles).resolve(styles).x, justify: ParElem::justify_in(styles), - hang: ParElem::hanging_indent_in(styles), + hang, cjk_latin_spacing, fallback: TextElem::fallback_in(styles), linebreaks: ParElem::linebreaks_in(styles), @@ -145,6 +152,19 @@ pub fn prepare<'a>( }) } +/// Get a style property, but only if it is the same for all of the children. +fn shared_get( + children: &[Pair], + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, +) -> Option { + let value = getter(styles); + children + .group_by_key(|&(_, s)| s) + .all(|(s, _)| getter(s) == value) + .then_some(value) +} + /// Add some spacing between Han characters and western characters. See /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// in Horizontal Written Mode diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 2ed95f14f..b688981ae 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -29,7 +29,7 @@ use crate::modifiers::{FrameModifiers, FrameModify}; /// frame. #[derive(Clone)] pub struct ShapedText<'a> { - /// The start of the text in the full paragraph. + /// The start of the text in the full text. pub base: usize, /// The text that was shaped. pub text: &'a str, @@ -66,9 +66,9 @@ pub struct ShapedGlyph { pub y_offset: Em, /// The adjustability of the glyph. pub adjustability: Adjustability, - /// The byte range of this glyph's cluster in the full paragraph. A cluster - /// is a sequence of one or multiple glyphs that cannot be separated and - /// must always be treated as a union. + /// The byte range of this glyph's cluster in the full inline layout. A + /// cluster is a sequence of one or multiple glyphs that cannot be separated + /// and must always be treated as a union. /// /// The range values of the glyphs in a [`ShapedText`] should not overlap /// with each other, and they should be monotonically increasing (for @@ -405,7 +405,7 @@ impl<'a> ShapedText<'a> { /// Reshape a range of the shaped text, reusing information from this /// shaping process if possible. /// - /// The text `range` is relative to the whole paragraph. + /// The text `range` is relative to the whole inline layout. pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> { let text = &self.text[text_range.start - self.base..text_range.end - self.base]; if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 56d7afe11..443e90d61 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -17,7 +17,6 @@ mod transforms; pub use self::flow::{layout_columns, layout_fragment, layout_frame}; pub use self::grid::{layout_grid, layout_table}; pub use self::image::layout_image; -pub use self::inline::{layout_box, layout_inline}; pub use self::lists::{layout_enum, layout_list}; pub use self::math::{layout_equation_block, layout_equation_inline}; pub use self::pad::layout_pad; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 63127474b..f8d910abf 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -6,7 +6,7 @@ use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::grid::resolve::{Cell, CellGrid}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; -use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; +use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem}; use typst_library::text::TextElem; use crate::grid::GridLayouter; @@ -22,8 +22,9 @@ pub fn layout_list( ) -> SourceResult { let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -41,11 +42,17 @@ pub fn layout_list( let mut locator = locator.split(); for item in &elem.children { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(ListElem::set_depth(Depth(1))), + body.styled(ListElem::set_depth(Depth(1))), locator.next(&item.body.span()), )); } @@ -78,8 +85,9 @@ pub fn layout_enum( let reversed = elem.reversed(styles); let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -124,11 +132,17 @@ pub fn layout_enum( let resolved = resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(EnumElem::set_parents(smallvec![number])), + body.styled(EnumElem::set_parents(smallvec![number])), locator.next(&item.body.span()), )); number = diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index 19176ee88..bf8235411 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -2,6 +2,7 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Abs, Axis, Rel}; use typst_library::math::{EquationElem, LrElem, MidElem}; +use typst_utils::SliceExt; use unicode_math_class::MathClass; use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; @@ -29,15 +30,7 @@ pub fn layout_lr( let mut fragments = ctx.layout_into_fragments(body, styles)?; // Ignore leading and trailing ignorant fragments. - let start_idx = fragments - .iter() - .position(|f| !f.is_ignorant()) - .unwrap_or(fragments.len()); - let end_idx = fragments - .iter() - .skip(start_idx) - .rposition(|f| !f.is_ignorant()) - .map_or(start_idx, |i| start_idx + i + 1); + let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant()); let inner_fragments = &mut fragments[start_idx..end_idx]; let axis = scaled!(ctx, styles, axis_height); diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 702816ee6..e5a3d94c9 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -202,8 +202,7 @@ pub fn layout_equation_block( let counter = Counter::of(EquationElem::elem()) .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .spanned(span); - let number = - (engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?; + let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?; static NUMBER_GUTTER: Em = Em::new(0.5); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); @@ -619,7 +618,7 @@ fn layout_box( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let frame = (ctx.engine.routines.layout_box)( + let frame = crate::inline::layout_box( elem, ctx.engine, ctx.locator.next(&elem.span()), @@ -692,7 +691,7 @@ fn layout_external( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { - (ctx.engine.routines.layout_frame)( + crate::layout_frame( ctx.engine, content, ctx.locator.next(&content.span()), diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 6b9703aa2..5897c3c0c 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -1,8 +1,8 @@ use std::f64::consts::SQRT_2; -use ecow::{eco_vec, EcoString}; +use ecow::EcoString; use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, StyleVec, SymbolElem}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -100,14 +100,15 @@ fn layout_inline_text( // because it will be placed somewhere probably not at the left margin // it will overflow. So emulate an `hbox` instead and allow the // paragraph to extend as far as needed. - let frame = (ctx.engine.routines.layout_inline)( + let frame = crate::inline::layout_inline( ctx.engine, - &StyleVec::wrap(eco_vec![elem]), - ctx.locator.next(&span), + &[(&elem, styles)], + &mut ctx.locator.next(&span).split(), styles, - false, Size::splat(Abs::inf()), false, + false, + false, )? .into_frame(); diff --git a/crates/typst-layout/src/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs index 0bbae9f4c..8eab18a62 100644 --- a/crates/typst-layout/src/pages/collect.rs +++ b/crates/typst-layout/src/pages/collect.rs @@ -23,7 +23,7 @@ pub enum Item<'a> { /// things like tags and weak pagebreaks. pub fn collect<'a>( mut children: &'a mut [Pair<'a>], - mut locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, mut initial: StyleChain<'a>, ) -> Vec> { // The collected page-level items. diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 27002a6c9..14dc0f3fb 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -83,7 +83,7 @@ fn layout_document_impl( styles, )?; - let pages = layout_pages(&mut engine, &mut children, locator, styles)?; + let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?; let introspector = Introspector::paged(&pages); Ok(PagedDocument { pages, info, introspector }) @@ -93,7 +93,7 @@ fn layout_document_impl( fn layout_pages<'a>( engine: &mut Engine, children: &'a mut [Pair<'a>], - locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, styles: StyleChain<'a>, ) -> SourceResult> { // Slice up the children into logical parts. diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs index 79ff5ab05..6d2d29da5 100644 --- a/crates/typst-layout/src/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -19,7 +19,7 @@ use typst_library::visualize::Paint; use typst_library::World; use typst_utils::Numeric; -use crate::flow::layout_flow; +use crate::flow::{layout_flow, FlowMode}; /// A mostly finished layout for one page. Needs only knowledge of its exact /// page number to be finalized into a `Page`. (Because the margins can depend @@ -181,7 +181,7 @@ fn layout_page_run_impl( Regions::repeat(area, area.map(Abs::is_finite)), PageElem::columns_in(styles), ColumnsElem::gutter_in(styles), - true, + FlowMode::Root, )?; // Layouts a single marginal. diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 37094dcd8..983803300 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -776,107 +776,6 @@ impl<'a> Iterator for Links<'a> { } } -/// A sequence of elements with associated styles. -#[derive(Clone, PartialEq, Hash)] -pub struct StyleVec { - /// The elements themselves. - elements: EcoVec, - /// A run-length encoded list of style lists. - /// - /// Each element is a (styles, count) pair. Any elements whose - /// style falls after the end of this list is considered to - /// have an empty style list. - styles: EcoVec<(Styles, usize)>, -} - -impl StyleVec { - /// Create a style vector from an unstyled vector content. - pub fn wrap(elements: EcoVec) -> Self { - Self { elements, styles: EcoVec::new() } - } - - /// Create a `StyleVec` from a list of content with style chains. - pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { - let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); - let depth = trunk.links().count(); - - let mut elements = EcoVec::with_capacity(buf.len()); - let mut styles = EcoVec::<(Styles, usize)>::new(); - let mut last: Option<(StyleChain<'a>, usize)> = None; - - for &(element, chain) in buf { - elements.push(element.clone()); - - if let Some((prev, run)) = &mut last { - if chain == *prev { - *run += 1; - } else { - styles.push((prev.suffix(depth), *run)); - last = Some((chain, 1)); - } - } else { - last = Some((chain, 1)); - } - } - - if let Some((last, run)) = last { - let skippable = styles.is_empty() && last == trunk; - if !skippable { - styles.push((last.suffix(depth), run)); - } - } - - (StyleVec { elements, styles }, trunk) - } - - /// Whether there are no elements. - pub fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - /// The number of elements. - pub fn len(&self) -> usize { - self.elements.len() - } - - /// Iterate over the contained content and style chains. - pub fn iter<'a>( - &'a self, - outer: &'a StyleChain<'_>, - ) -> impl Iterator)> { - static EMPTY: Styles = Styles::new(); - self.elements - .iter() - .zip( - self.styles - .iter() - .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) - .chain(std::iter::repeat(&EMPTY)), - ) - .map(|(element, local)| (element, outer.chain(local))) - } - - /// Get a style property, but only if it is the same for all children of the - /// style vector. - pub fn shared_get( - &self, - styles: StyleChain<'_>, - getter: fn(StyleChain) -> T, - ) -> Option { - let value = getter(styles); - self.styles - .iter() - .all(|(local, _)| getter(styles.chain(local)) == value) - .then_some(value) - } -} - -impl Debug for StyleVec { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - f.debug_list().entries(&self.elements).finish() - } -} - /// A property that is resolved with other properties from the style chain. pub trait Resolve { /// The type of the resolved output. diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index c8c74269b..725f177b7 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst-library/src/layout/container.rs @@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke}; /// An inline-level container that sizes content. /// /// All elements except inline math, text, and boxes are block-level and cannot -/// occur inside of a paragraph. The box function can be used to integrate such -/// elements into a paragraph. Boxes take the size of their contents by default -/// but can also be sized explicitly. +/// occur inside of a [paragraph]($par). The box function can be used to +/// integrate such elements into a paragraph. Boxes take the size of their +/// contents by default but can also be sized explicitly. /// /// # Example /// ```example @@ -184,6 +184,10 @@ pub enum InlineItem { /// Such a container can be used to separate content, size it, and give it a /// background or border. /// +/// Blocks are also the primary way to control whether text becomes part of a +/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph) +/// for more details. +/// /// # Examples /// With a block, you can give a background to content while still allowing it /// to break across multiple pages. diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index 1e346280a..32be216a4 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; /// A mathematical equation. /// -/// Can be displayed inline with text or as a separate block. +/// Can be displayed inline with text or as a separate block. An equation +/// becomes block-level through the presence of at least one space after the +/// opening dollar sign and one space before the closing dollar sign. /// /// # Example /// ```example diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 762a97fd9..a391e5804 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -17,7 +17,7 @@ use hayagriva::{ use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; use typst_syntax::{Span, Spanned}; -use typst_utils::{ManuallyHash, NonZeroExt, PicoStr}; +use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; @@ -29,7 +29,7 @@ use crate::foundations::{ use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, - Sizing, TrackSizings, VElem, + Sides, Sizing, TrackSizings, }; use crate::loading::{DataSource, Load}; use crate::model::{ @@ -206,19 +206,20 @@ impl Show for Packed { const COLUMN_GUTTER: Em = Em::new(0.65); const INDENT: Em = Em::new(1.5); + let span = self.span(); + let mut seq = vec![]; if let Some(title) = self.title(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) }) { seq.push( HeadingElem::new(title) .with_depth(NonZeroUsize::ONE) .pack() - .spanned(self.span()), + .spanned(span), ); } - let span = self.span(); let works = Works::generate(engine).at(span)?; let references = works .references @@ -226,10 +227,9 @@ impl Show for Packed { .ok_or("CSL style is not suitable for bibliographies") .at(span)?; - let row_gutter = ParElem::spacing_in(styles); - let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack(); - if references.iter().any(|(prefix, _)| prefix.is_some()) { + let row_gutter = ParElem::spacing_in(styles); + let mut cells = vec![]; for (prefix, reference) in references { cells.push(GridChild::Item(GridItem::Cell( @@ -246,23 +246,27 @@ impl Show for Packed { .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) .pack() - .spanned(self.span()), + .spanned(span), ); } else { - for (i, (_, reference)) in references.iter().enumerate() { - if i > 0 { - seq.push(row_gutter_elem.clone()); - } - seq.push(reference.clone()); + for (_, reference) in references { + let realized = reference.clone(); + let block = if works.hanging_indent { + let body = HElem::new((-INDENT).into()).pack() + realized; + let inset = Sides::default() + .with(TextElem::dir_in(styles).start(), Some(INDENT.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + + seq.push(block.pack().spanned(span)); } } - let mut content = Content::sequence(seq); - if works.hanging_indent { - content = content.styled(ParElem::set_hanging_indent(INDENT.into())); - } - - Ok(content) + Ok(Content::sequence(seq)) } } diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index 4dc834ab7..a4126e72c 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -11,7 +11,9 @@ use crate::foundations::{ }; use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; -use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; +use crate::model::{ + ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem, +}; /// A numbered list. /// @@ -226,6 +228,8 @@ impl EnumElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { let mut elem = HtmlElem::new(tag::ol); if self.reversed(styles) { @@ -239,7 +243,12 @@ impl Show for Packed { if let Some(nr) = item.number(styles) { li = li.with_attr(attr::value, eco_format!("{nr}")); } - li.with_body(Some(item.body.clone())).pack().spanned(item.span()) + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + li.with_body(Some(body)).pack().spanned(item.span()) })); return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); } @@ -249,7 +258,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index ce7460c9b..78a79a8e2 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -19,7 +19,9 @@ use crate::layout::{ AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem, PlacementScope, VAlignment, VElem, }; -use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; +use crate::model::{ + Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement, +}; use crate::text::{Lang, Region, TextElem}; use crate::visualize::ImageElem; @@ -328,6 +330,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "figure", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); let target = TargetElem::target_in(styles); let mut realized = self.body.clone(); @@ -341,24 +344,27 @@ impl Show for Packed { seq.push(first); if !target.is_html() { let v = VElem::new(self.gap(styles).into()).with_weak(true); - seq.push(v.pack().spanned(self.span())) + seq.push(v.pack().spanned(span)) } seq.push(second); realized = Content::sequence(seq) } + // Ensure that the body is considered a paragraph. + realized += ParbreakElem::shared().clone().spanned(span); + if target.is_html() { return Ok(HtmlElem::new(tag::figure) .with_body(Some(realized)) .pack() - .spanned(self.span())); + .spanned(span)); } // Wrap the contents in a block. realized = BlockElem::new() .with_body(Some(BlockBody::Content(realized))) .pack() - .spanned(self.span()); + .spanned(span); // Wrap in a float. if let Some(align) = self.placement(styles) { @@ -367,10 +373,10 @@ impl Show for Packed { .with_scope(self.scope(styles)) .with_float(true) .pack() - .spanned(self.span()); + .spanned(span); } else if self.scope(styles) == PlacementScope::Parent { bail!( - self.span(), + span, "parent-scoped placement is only available for floating figures"; hint: "you can enable floating placement with `figure(placement: auto, ..)`" ); @@ -604,14 +610,17 @@ impl Show for Packed { realized = supplement + numbers + self.get_separator(styles) + realized; } - if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::figcaption) + Ok(if TargetElem::target_in(styles).is_html() { + HtmlElem::new(tag::figcaption) .with_body(Some(realized)) .pack() - .spanned(self.span())); - } - - Ok(realized) + .spanned(self.span()) + } else { + BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()) + }) } } diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index f3b2a19eb..dfa3933bb 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -310,11 +310,9 @@ impl Show for Packed { impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { - let text_size = Em::new(0.85); - let leading = Em::new(0.5); let mut out = Styles::new(); - out.set(ParElem::set_leading(leading.into())); - out.set(TextElem::set_size(TextSize(text_size.into()))); + out.set(ParElem::set_leading(Em::new(0.5).into())); + out.set(TextElem::set_size(TextSize(Em::new(0.85).into()))); out } } diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index 1e369d541..d93ec9172 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -8,7 +8,7 @@ use crate::foundations::{ }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockElem, Em, Length, VElem}; -use crate::model::ParElem; +use crate::model::{ParElem, ParbreakElem}; use crate::text::TextElem; /// A bullet list. @@ -141,11 +141,18 @@ impl ListElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { return Ok(HtmlElem::new(tag::ul) .with_body(Some(Content::sequence(self.children.iter().map(|item| { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } HtmlElem::new(tag::li) - .with_body(Some(item.body.clone())) + .with_body(Some(body)) .pack() .spanned(item.span()) })))) @@ -158,7 +165,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 0db056e40..1214f2b0e 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -297,7 +297,6 @@ impl ShowSet for Packed { let mut out = Styles::new(); out.set(HeadingElem::set_outlined(false)); out.set(HeadingElem::set_numbering(None)); - out.set(ParElem::set_first_line_indent(Em::new(0.0).into())); out.set(ParElem::set_justify(false)); out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into()))); // Makes the outline itself available to its entries. Should be diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs index 8b82abdf7..0bdbe4ea6 100644 --- a/crates/typst-library/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -1,22 +1,78 @@ -use std::fmt::{self, Debug, Formatter}; - use typst_utils::singleton; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, - StyleVec, Unlabellable, + elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Smart, + Unlabellable, }; use crate::introspection::{Count, CounterUpdate, Locatable}; use crate::layout::{Em, HAlignment, Length, OuterHAlignment}; use crate::model::Numbering; -/// Arranges text, spacing and inline-level elements into a paragraph. +/// A logical subdivison of textual content. /// -/// Although this function is primarily used in set rules to affect paragraph -/// properties, it can also be used to explicitly render its argument onto a -/// paragraph of its own. +/// Typst automatically collects _inline-level_ elements into paragraphs. +/// Inline-level elements include [text], [horizontal spacing]($h), +/// [boxes]($box), and [inline equations]($math.equation). +/// +/// To separate paragraphs, use a blank line (or an explicit [`parbreak`]). +/// Paragraphs are also automatically interrupted by any block-level element +/// (like [`block`], [`place`], or anything that shows itself as one of these). +/// +/// The `par` element is primarily used in set rules to affect paragraph +/// properties, but it can also be used to explicitly display its argument as a +/// paragraph of its own. Then, the paragraph's body may not contain any +/// block-level content. +/// +/// # Boxes and blocks +/// As explained above, usually paragraphs only contain inline-level content. +/// However, you can integrate any kind of block-level content into a paragraph +/// by wrapping it in a [`box`]. +/// +/// Conversely, you can separate inline-level content from a paragraph by +/// wrapping it in a [`block`]. In this case, it will not become part of any +/// paragraph at all. Read the following section for an explanation of why that +/// matters and how it differs from just adding paragraph breaks around the +/// content. +/// +/// # What becomes a paragraph? +/// When you add inline-level content to your document, Typst will automatically +/// wrap it in paragraphs. However, a typical document also contains some text +/// that is not semantically part of a paragraph, for example in a heading or +/// caption. +/// +/// The rules for when Typst wraps inline-level content in a paragraph are as +/// follows: +/// +/// - All text at the root of a document is wrapped in paragraphs. +/// +/// - Text in a container (like a `block`) is only wrapped in a paragraph if the +/// container holds any block-level content. If all of the contents are +/// inline-level, no paragraph is created. +/// +/// In the laid-out document, it's not immediately visible whether text became +/// part of a paragraph. However, it is still important for various reasons: +/// +/// - Certain paragraph styling like `first-line-indent` will only apply to +/// proper paragraphs, not any text. Similarly, `par` show rules of course +/// only trigger on paragraphs. +/// +/// - A proper distinction between paragraphs and other text helps people who +/// rely on assistive technologies (such as screen readers) navigate and +/// understand the document properly. Currently, this only applies to HTML +/// export since Typst does not yet output accessible PDFs, but support for +/// this is planned for the near future. +/// +/// - HTML export will generate a `

` tag only for paragraphs. +/// +/// When creating custom reusable components, you can and should take charge +/// over whether Typst creates paragraphs. By wrapping text in a [`block`] +/// instead of just adding paragraph breaks around it, you can force the absence +/// of a paragraph. Conversely, by adding a [`parbreak`] after some content in a +/// container, you can force it to become a paragraph even if it's just one +/// word. This is, for example, what [non-`tight`]($list.tight) lists do to +/// force their items to become paragraphs. /// /// # Example /// ```example @@ -37,7 +93,7 @@ use crate::model::Numbering; /// let $a$ be the smallest of the /// three integers. Then, we ... /// ``` -#[elem(scope, title = "Paragraph", Debug, Construct)] +#[elem(scope, title = "Paragraph")] pub struct ParElem { /// The spacing between lines. /// @@ -53,7 +109,6 @@ pub struct ParElem { /// distribution of the top- and bottom-edge values affects the bounds of /// the first and last line. #[resolve] - #[ghost] #[default(Em::new(0.65).into())] pub leading: Length, @@ -68,7 +123,6 @@ pub struct ParElem { /// takes precedence over the paragraph spacing. Headings, for instance, /// reduce the spacing below them by default for a better look. #[resolve] - #[ghost] #[default(Em::new(1.2).into())] pub spacing: Length, @@ -81,7 +135,6 @@ pub struct ParElem { /// Note that the current [alignment]($align.alignment) still has an effect /// on the placement of the last line except if it ends with a /// [justified line break]($linebreak.justify). - #[ghost] #[default(false)] pub justify: bool, @@ -106,7 +159,6 @@ pub struct ParElem { /// challenging to break in a visually /// pleasing way. /// ``` - #[ghost] pub linebreaks: Smart, /// The indent the first line of a paragraph should have. @@ -118,23 +170,15 @@ pub struct ParElem { /// space between paragraphs or by indented first lines. Consider reducing /// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading) /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`). - #[ghost] pub first_line_indent: Length, - /// The indent all but the first line of a paragraph should have. - #[ghost] + /// The indent that all but the first line of a paragraph should have. #[resolve] pub hanging_indent: Length, /// The contents of the paragraph. - #[external] #[required] pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: StyleVec, } #[scope] @@ -143,28 +187,6 @@ impl ParElem { type ParLine; } -impl Construct for ParElem { - fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult { - // The paragraph constructor is special: It doesn't create a paragraph - // element. Instead, it just ensures that the passed content lives in a - // separate paragraph and styles it. - let styles = Self::set(engine, args)?; - let body = args.expect::("body")?; - Ok(Content::sequence([ - ParbreakElem::shared().clone(), - body.styled_with_map(styles), - ParbreakElem::shared().clone(), - ])) - } -} - -impl Debug for ParElem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Par ")?; - self.children.fmt(f) - } -} - /// How to determine line breaks in a paragraph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum Linebreaks { diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 79e9b4e36..919ab12c7 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -212,17 +212,24 @@ impl Show for Packed { .pack() .spanned(self.span()), }; - let attribution = - [TextElem::packed('—'), SpaceElem::shared().clone(), attribution]; + let attribution = Content::sequence([ + TextElem::packed('—'), + SpaceElem::shared().clone(), + attribution, + ]); - if !html { - // Use v(0.9em, weak: true) to bring the attribution closer - // to the quote. + if html { + realized += attribution; + } else { + // Bring the attribution a bit closer to the quote. let gap = Spacing::Rel(Em::new(0.9).into()); let v = VElem::new(gap).with_weak(true).pack(); realized += v; + realized += BlockElem::new() + .with_body(Some(BlockBody::Content(attribution))) + .pack() + .aligned(Alignment::END); } - realized += Content::sequence(attribution).aligned(Alignment::END); } if !html { diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index c91eeb17a..9a2ed6aad 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -8,7 +8,7 @@ use crate::foundations::{ }; use crate::html::{tag, HtmlElem}; use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::{ListItemLike, ListLike, ParElem}; +use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; use crate::text::TextElem; /// A list of terms and their descriptions. @@ -116,17 +116,25 @@ impl TermsElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let span = self.span(); + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { return Ok(HtmlElem::new(tag::dl) .with_body(Some(Content::sequence(self.children.iter().flat_map( |item| { + // Text in wide term lists shall always turn into paragraphs. + let mut description = item.description.clone(); + if !tight { + description += ParbreakElem::shared(); + } + [ HtmlElem::new(tag::dt) .with_body(Some(item.term.clone())) .pack() .spanned(item.term.span()), HtmlElem::new(tag::dd) - .with_body(Some(item.description.clone())) + .with_body(Some(description)) .pack() .spanned(item.description.span()), ] @@ -139,7 +147,7 @@ impl Show for Packed { let indent = self.indent(styles); let hanging_indent = self.hanging_indent(styles); let gutter = self.spacing(styles).unwrap_or_else(|| { - if self.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -157,6 +165,12 @@ impl Show for Packed { seq.push(child.term.clone().strong()); seq.push((*separator).clone()); seq.push(child.description.clone()); + + // Text in wide term lists shall always turn into paragraphs. + if !tight { + seq.push(ParbreakElem::shared().clone()); + } + children.push(StackChild::Block(Content::sequence(seq))); } @@ -168,7 +182,7 @@ impl Show for Packed { .spanned(span) .padded(padding); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()) .with_weak(true) diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index a11268604..b283052a4 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -10,8 +10,7 @@ use typst_utils::LazyHash; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, StyleVec, - Styles, Value, + Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::layout::{ @@ -104,26 +103,6 @@ routines! { region: Region, ) -> SourceResult - /// Lays out inline content. - fn layout_inline( - engine: &mut Engine, - children: &StyleVec, - locator: Locator, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult - - /// Lays out a [`BoxElem`]. - fn layout_box( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult - /// Lays out a [`ListElem`]. fn layout_list( elem: &Packed, @@ -348,17 +327,62 @@ pub enum RealizationKind<'a> { /// This the root realization for layout. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. LayoutDocument(&'a mut DocumentInfo), - /// A nested realization in a container (e.g. a `block`). - LayoutFragment, + /// A nested realization in a container (e.g. a `block`). Requires a mutable + /// reference to an enum that will be set to `FragmentKind::Inline` if the + /// fragment's content was fully inline. + LayoutFragment(&'a mut FragmentKind), + /// A nested realization in a paragraph (i.e. a `par`) + LayoutPar, /// This the root realization for HTML. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. HtmlDocument(&'a mut DocumentInfo), - /// A nested realization in a container (e.g. a `block`). - HtmlFragment, + /// A nested realization in a container (e.g. a `block`). Requires a mutable + /// reference to an enum that will be set to `FragmentKind::Inline` if the + /// fragment's content was fully inline. + HtmlFragment(&'a mut FragmentKind), /// A realization within math. Math, } +impl RealizationKind<'_> { + /// It this a realization for HTML export? + pub fn is_html(&self) -> bool { + matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_)) + } + + /// It this a realization for a container? + pub fn is_fragment(&self) -> bool { + matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_)) + } + + /// If this is a document-level realization, accesses the document info. + pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> { + match self { + Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info), + _ => None, + } + } + + /// If this is a container-level realization, accesses the fragment kind. + pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> { + match self { + Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind), + _ => None, + } + } +} + +/// The kind of fragment output that realization produced. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum FragmentKind { + /// The fragment's contents were fully inline, and as a result, the output + /// elements are too. + Inline, + /// The fragment contained non-inline content, so inline content was forced + /// into paragraphs, and as a result, the output elements are not inline. + Block, +} + /// Temporary storage arenas for lifetime extension during realization. /// /// Must be kept live while the content returned from realization is processed. diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index ff42c3e95..754e89aac 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -15,8 +15,8 @@ use typst_library::diag::{bail, At, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{ Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, - SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles, - SymbolElem, Synthesize, Transformation, + SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, + Synthesize, Transformation, }; use typst_library::html::{tag, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; @@ -28,7 +28,7 @@ use typst_library::model::{ CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike, ParElem, ParbreakElem, TermsElem, }; -use typst_library::routines::{Arenas, Pair, RealizationKind}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_syntax::Span; use typst_utils::{SliceExt, SmallBitSet}; @@ -48,17 +48,18 @@ pub fn realize<'a>( locator, arenas, rules: match kind { - RealizationKind::LayoutDocument(_) | RealizationKind::LayoutFragment => { - LAYOUT_RULES - } + RealizationKind::LayoutDocument(_) => LAYOUT_RULES, + RealizationKind::LayoutFragment(_) => LAYOUT_RULES, + RealizationKind::LayoutPar => LAYOUT_PAR_RULES, RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES, - RealizationKind::HtmlFragment => HTML_FRAGMENT_RULES, + RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES, RealizationKind::Math => MATH_RULES, }, sink: vec![], groupings: ArrayVec::new(), outside: matches!(kind, RealizationKind::LayoutDocument(_)), may_attach: false, + saw_parbreak: false, kind, }; @@ -98,6 +99,8 @@ struct State<'a, 'x, 'y, 'z> { outside: bool, /// Whether now following attach spacing can survive. may_attach: bool, + /// Whether we visited any paragraph breaks. + saw_parbreak: bool, } /// Defines a rule for how certain elements shall be grouped during realization. @@ -125,6 +128,10 @@ struct GroupingRule { struct Grouping<'a> { /// The position in `s.sink` where the group starts. start: usize, + /// Only applies to `PAR` grouping: Whether this paragraph group is + /// interrupted, but not yet finished because it may be ignored due to being + /// fully inline. + interrupted: bool, /// The rule used for this grouping. rule: &'a GroupingRule, } @@ -575,19 +582,21 @@ fn visit_styled<'a>( for style in local.iter() { let Some(elem) = style.element() else { continue }; if elem == DocumentElem::elem() { - match &mut s.kind { - RealizationKind::LayoutDocument(info) - | RealizationKind::HtmlDocument(info) => info.populate(&local), - _ => bail!( + if let Some(info) = s.kind.as_document_mut() { + info.populate(&local) + } else { + bail!( style.span(), "document set rules are not allowed inside of containers" - ), + ); } } else if elem == PageElem::elem() { - let RealizationKind::LayoutDocument(_) = s.kind else { - let span = style.span(); - bail!(span, "page configuration is not allowed inside of containers"); - }; + if !matches!(s.kind, RealizationKind::LayoutDocument(_)) { + bail!( + style.span(), + "page configuration is not allowed inside of containers" + ); + } // When there are page styles, we "break free" from our show rule cage. pagebreak = true; @@ -650,7 +659,9 @@ fn visit_grouping_rules<'a>( } // If the element can be added to the active grouping, do it. - if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) { + if !active.interrupted + && ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content)) + { s.sink.push((content, styles)); return Ok(true); } @@ -661,7 +672,7 @@ fn visit_grouping_rules<'a>( // Start a new grouping. if let Some(rule) = matching { let start = s.sink.len(); - s.groupings.push(Grouping { start, rule }); + s.groupings.push(Grouping { start, rule, interrupted: false }); s.sink.push((content, styles)); return Ok(true); } @@ -676,22 +687,24 @@ fn visit_filter_rules<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult { - if content.is::() - && !matches!(s.kind, RealizationKind::Math | RealizationKind::HtmlFragment) - { - // Outside of maths, spaces that were not collected by the paragraph - // grouper don't interest us. + if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) { + return Ok(false); + } + + if content.is::() { + // Outside of maths and paragraph realization, spaces that were not + // collected by the paragraph grouper don't interest us. return Ok(true); } else if content.is::() { // Paragraph breaks are only a boundary for paragraph grouping, we don't // need to store them. s.may_attach = false; + s.saw_parbreak = true; return Ok(true); } else if !s.may_attach && content.to_packed::().is_some_and(|elem| elem.attach(styles)) { - // Delete attach spacing collapses if not immediately following a - // paragraph. + // Attach spacing collapses if not immediately following a paragraph. return Ok(true); } @@ -703,7 +716,18 @@ fn visit_filter_rules<'a>( /// Finishes all grouping. fn finish(s: &mut State) -> SourceResult<()> { - finish_grouping_while(s, |s| !s.groupings.is_empty())?; + finish_grouping_while(s, |s| { + // If this is a fragment realization and all we've got is inline + // content, don't turn it into a paragraph. + if is_fully_inline(s) { + *s.kind.as_fragment_mut().unwrap() = FragmentKind::Inline; + s.groupings.pop(); + collapse_spaces(&mut s.sink, 0); + false + } else { + !s.groupings.is_empty() + } + })?; // In math, spaces are top-level. if let RealizationKind::Math = s.kind { @@ -722,6 +746,12 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } finish_grouping_while(s, |s| { s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem)) + && if is_fully_inline(s) { + s.groupings[0].interrupted = true; + false + } else { + true + } })?; last = Some(elem); } @@ -729,9 +759,9 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } /// Finishes groupings while `f` returns `true`. -fn finish_grouping_while(s: &mut State, f: F) -> SourceResult<()> +fn finish_grouping_while(s: &mut State, mut f: F) -> SourceResult<()> where - F: Fn(&State) -> bool, + F: FnMut(&mut State) -> bool, { // Finishing of a group may result in new content and new grouping. This // can, in theory, go on for a bit. To prevent it from becoming an infinite @@ -750,7 +780,7 @@ where /// Finishes the currently innermost grouping. fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> { // The grouping we are interrupting. - let Grouping { start, rule } = s.groupings.pop().unwrap(); + let Grouping { start, rule, .. } = s.groupings.pop().unwrap(); // Trim trailing non-trigger elements. let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind)); @@ -794,12 +824,16 @@ const MAX_GROUP_NESTING: usize = 3; /// Grouping rules used in layout realization. static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; +/// Grouping rules used in paragraph layout realization. +static LAYOUT_PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; + /// Grouping rules used in HTML root realization. static HTML_DOCUMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; /// Grouping rules used in HTML fragment realization. -static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; +static HTML_FRAGMENT_RULES: &[&GroupingRule] = + &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; /// Grouping rules used in math realization. static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; @@ -836,12 +870,10 @@ static PAR: GroupingRule = GroupingRule { || elem == SmartQuoteElem::elem() || elem == InlineElem::elem() || elem == BoxElem::elem() - || (matches!( - kind, - RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment - ) && content - .to_packed::() - .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) + || (kind.is_html() + && content + .to_packed::() + .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) }, inner: |content| content.elem() == SpaceElem::elem(), interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(), @@ -914,17 +946,31 @@ fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> { // transparently become part of it. // 2. There is no group at all. In this case, we create one. if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) { - s.groupings.push(Grouping { start, rule: &PAR }); + s.groupings.push(Grouping { start, rule: &PAR, interrupted: false }); } Ok(()) } /// Whether there is an active grouping, but it is not a `PAR` grouping. -fn in_non_par_grouping(s: &State) -> bool { - s.groupings - .last() - .is_some_and(|grouping| !std::ptr::eq(grouping.rule, &PAR)) +fn in_non_par_grouping(s: &mut State) -> bool { + s.groupings.last().is_some_and(|grouping| { + !std::ptr::eq(grouping.rule, &PAR) || grouping.interrupted + }) +} + +/// Whether there is exactly one active grouping, it is a `PAR` grouping, and it +/// spans the whole sink (with the exception of leading tags). +fn is_fully_inline(s: &State) -> bool { + s.kind.is_fragment() + && !s.saw_parbreak + && match s.groupings.as_slice() { + [grouping] => { + std::ptr::eq(grouping.rule, &PAR) + && s.sink[..grouping.start].iter().all(|(c, _)| c.is::()) + } + _ => false, + } } /// Builds the `ParElem` from inline-level elements. @@ -936,11 +982,11 @@ fn finish_par(mut grouped: Grouped) -> SourceResult<()> { // Collect the children. let elems = grouped.get(); let span = select_span(elems); - let (children, trunk) = StyleVec::create(elems); + let (body, trunk) = repack(elems); // Create and visit the paragraph. let s = grouped.end(); - let elem = ParElem::new(children).pack().spanned(span); + let elem = ParElem::new(body).pack().spanned(span); visit(s, s.store(elem), trunk) } @@ -1277,3 +1323,26 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) { fn select_span(children: &[Pair]) -> Span { Span::find(children.iter().map(|(c, _)| c.span())) } + +/// Turn realized content with styles back into owned content and a trunk style +/// chain. +fn repack<'a>(buf: &[Pair<'a>]) -> (Content, StyleChain<'a>) { + let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); + let depth = trunk.links().count(); + + let mut seq = Vec::with_capacity(buf.len()); + + for (chain, group) in buf.group_by_key(|&(_, s)| s) { + let iter = group.iter().map(|&(c, _)| c.clone()); + let suffix = chain.suffix(depth); + if suffix.is_empty() { + seq.extend(iter); + } else if let &[(element, _)] = group { + seq.push(element.clone().styled_with_map(suffix)); + } else { + seq.push(Content::sequence(iter).styled_with_map(suffix)); + } + } + + (Content::sequence(seq), trunk) +} diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index f3fe79d2c..b59fe2f73 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -128,6 +128,20 @@ pub trait SliceExt { where F: FnMut(&T) -> K, K: PartialEq; + + /// Computes two indices which split a slice into three parts. + /// + /// - A prefix which matches `f` + /// - An inner portion + /// - A suffix which matches `f` and does not overlap with the prefix + /// + /// If all elements match `f`, the prefix becomes `self` and the suffix + /// will be empty. + /// + /// Returns the indices at which the inner portion and the suffix start. + fn split_prefix_suffix(&self, f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool; } impl SliceExt for [T] { @@ -157,6 +171,19 @@ impl SliceExt for [T] { fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> { GroupByKey { slice: self, f } } + + fn split_prefix_suffix(&self, mut f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool, + { + let start = self.iter().position(|v| !f(v)).unwrap_or(self.len()); + let end = self + .iter() + .skip(start) + .rposition(|v| !f(v)) + .map_or(start, |i| start + i + 1); + (start, end) + } } /// This struct is created by [`SliceExt::group_by_key`]. diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 7d02aa426..580ba9e80 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -333,8 +333,6 @@ pub static ROUTINES: Routines = Routines { realize: typst_realize::realize, layout_fragment: typst_layout::layout_fragment, layout_frame: typst_layout::layout_frame, - layout_inline: typst_layout::layout_inline, - layout_box: typst_layout::layout_box, layout_list: typst_layout::layout_list, layout_enum: typst_layout::layout_enum, layout_grid: typst_layout::layout_grid, diff --git a/tests/ref/bibliography-grid-par.png b/tests/ref/bibliography-grid-par.png new file mode 100644 index 0000000000000000000000000000000000000000..5befbcc54160ec62b898eeaafc5649464f416d46 GIT binary patch literal 8757 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 literal 0 HcmV?d00001 diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png new file mode 100644 index 0000000000000000000000000000000000000000..98a3c4d049b1759d06ca051b14ec394f6a6e7692 GIT binary patch literal 9087 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 literal 0 HcmV?d00001 diff --git a/tests/ref/enum-par.png b/tests/ref/enum-par.png new file mode 100644 index 0000000000000000000000000000000000000000..ca923a52623a0bbc725b1b61526478894637f8a7 GIT binary patch literal 3521 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 literal 0 HcmV?d00001 diff --git a/tests/ref/figure-par.png b/tests/ref/figure-par.png new file mode 100644 index 0000000000000000000000000000000000000000..d70bbcb12970dcf6e0b3f228544b2bbdd99cb3fb GIT binary patch literal 1701 zcmV;W23q-vP)66u|orXqvvLCDznXL?}q0$XXEOfdY!qS}Y2ddQnIa2_!^R1j?3}Qe;z>7a&lV zL>3DqMG&woLRl4|3KYs#R@uP?u)jFT4Z+k0q(`JeZ6itGC9S+IhdJkMv@@`h!7 zK#R~ZHtM0H^AU>j$5Od>uNv+wLBEj*FAfeiz>&EpBg($tJ$fX% zK;u;%G@_}g$#Z#md2nzLWqf>GF4KH zSXe0D`}px=kxBEOK7Fcm^TowQ+DRi-0`!g@J2EpfeSCZ#K72^$&CSipEGQ^QPfw>a zv9YmzeSHoN4p*;U9T*tczJ2@9&=9(TfdLS4TWBUEB%C>O27Q&~OP4NTg!tg-=(wur z!i5VwO-xL{x-y@Zq^72NdwVkyd3kwPu3Yi-^lWHoV2W(rx-}*y=G?h+j3N*Z95}#? zX=`hv8UFtM{5L*6J|!h3GBT1Uqhe@iNae7wFbfL{Rq<%NucD#?j(G8&J$uM>adBZv zR##V_J$qJg$;rto zO2QidU0GymYAPHV$L#O#=a0d`!4VM=WC9KkMbXpK!-Y?uK25|UdFRfZz`#Hzfti_^ zFtTmiHhRPWl$Mr~w6U?FR8>NUa;gGOcovv0U%uSi+bcldxN$=XT2xc^>eabP6cXIU z-&aX^csPzMh+}Z&+tt+6l*z2DthBT=;?Yc_7&$lMf36b%gxngt8onXL%UOFm)2!^4AkLT6Yl$yZlb7tzz# z*OxxCbzyZE0v|LuCC|LpC@h*$DEv; zm}iVPnT)Q!zW)9D_w)1fiE`9r4iP$9TU+r4UdIe@b8|~hPNvx>Po5-pGieB1j40e| zYHHpwXu6F@)YjIDWOH*fN!*a|nvs#glim^sDQaqJVnaqipi7O7jZ_ocK@oO9ztSuQ ze}5^MQm$h{s7ZYhOo&d{SqYj^ffPW|8OZYTa%Mes2pWVATo{`aiB*v#ZzqQeyh1dR zzl*h+Ooo4CWCVq7s^|v`ag9Epu&}WDy-)$s($d0D-?uMAmXWx)I94iRiz-lN9-WPSYF^~>!jiypWGttI)4^&N|>kWVLo8IavTVVgGJga5vIDL zd4X?BClC=QKr#lv=U-HI+^r%o69;{YY#4y?$=>ZRje*<4;jn)m)=#zW*fovcb=}en zoIm8XOJr(5UR{!5jf(?in$c6&+d59~z6I$gH8R24N*3S&<%8ABVUPMkT_gLgX@gbp3 z>jzsU6j#d@f!|sWZhV9xuuo0^qp|{a%C)Z}1Df?H`(%t-@u#<_0C&|Tu-jm(3Uat7 z2Zq2eoZ{#w)Fg6_V4YZ+6Eu`{9FzJ9|Ei^J+mQO tiEHTonGp&O6MH=LK{x-g37hcmegYM2&@e%Fp(OwS002ovPDHLkV1m2N1W*6~ literal 0 HcmV?d00001 diff --git a/tests/ref/html/enum-par.html b/tests/ref/html/enum-par.html new file mode 100644 index 000000000..60d4592b7 --- /dev/null +++ b/tests/ref/html/enum-par.html @@ -0,0 +1,36 @@ + + + + + + + +

+
    +
  1. Hello
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +

    The

    +
  2. +
  3. +

    World

    +
  4. +
+
+ + diff --git a/tests/ref/html/list-par.html b/tests/ref/html/list-par.html new file mode 100644 index 000000000..7c747ff44 --- /dev/null +++ b/tests/ref/html/list-par.html @@ -0,0 +1,36 @@ + + + + + + + +
+
    +
  • Hello
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +

    The

    +
  • +
  • +

    World

    +
  • +
+
+ + diff --git a/tests/ref/html/par-semantic-html.html b/tests/ref/html/par-semantic-html.html new file mode 100644 index 000000000..09c7d2fd0 --- /dev/null +++ b/tests/ref/html/par-semantic-html.html @@ -0,0 +1,16 @@ + + + + + + + +

Heading is no paragraph

+

I'm a paragraph.

+
I'm not.
+
+

We are two.

+

So we are paragraphs.

+
+ + diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html index 753807db2..c12d2ae2d 100644 --- a/tests/ref/html/quote-attribution-link.html +++ b/tests/ref/html/quote-attribution-link.html @@ -5,7 +5,7 @@ -
Compose papers faster
+
Compose papers faster

typst.com

diff --git a/tests/ref/html/quote-plato.html b/tests/ref/html/quote-plato.html index f516adc29..039835082 100644 --- a/tests/ref/html/quote-plato.html +++ b/tests/ref/html/quote-plato.html @@ -5,9 +5,9 @@ -
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
+
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.

— Plato

-
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.
+
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.

— from the Henry Cary literal translation of 1897

diff --git a/tests/ref/html/terms-par.html b/tests/ref/html/terms-par.html new file mode 100644 index 000000000..78bc5df16 --- /dev/null +++ b/tests/ref/html/terms-par.html @@ -0,0 +1,42 @@ + + + + + + + +
+
+
Hello
+
A
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+

The

+
+
World
+
+

B

+
+
+
+ + diff --git a/tests/ref/issue-5503-enum-in-align.png b/tests/ref/issue-5503-enum-in-align.png new file mode 100644 index 0000000000000000000000000000000000000000..4857e731bbcc4cc780e07103c6bb34d9dc7ef13a GIT binary patch literal 421 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 BaoPX? literal 0 HcmV?d00001 diff --git a/tests/ref/math-par.png b/tests/ref/math-par.png new file mode 100644 index 0000000000000000000000000000000000000000..30d64794cb9fbcf1d65e0b3f04b47c5e3ec2e776 GIT binary patch literal 387 zcmV-}0et?6P)Nkl%xiej^)6-?EkuAO!@pBuh#ebKqyGMe>-Z%fa1F27KZAH;-ZFhi7$AABemuYsH@T~t=K^C8CYjbaF>-qQpf9?MNAtYFA_5WDqWfUJf z{`nty$mQAp2gp8t>G&TcfbjA3j{lcXeQdgFXD103Putp4a16!blEsso|6k79x&tMS zr*&+-ifr-El1)tpw~#H4IrjfHS*iNv2^i}ign)pJK#a`4au>?@3l>7CcwW15(`d;x hYVoMWqZSVnive`)&pxRUt8f4S002ovPDHLkV1kYxxsw0@ literal 0 HcmV?d00001 diff --git a/tests/ref/outline-par.png b/tests/ref/outline-par.png new file mode 100644 index 0000000000000000000000000000000000000000..04c63f62c4827dd51999a285df1715a094520101 GIT binary patch literal 2911 zcmV-l3!wCgP)Gs000XnNkl^m+(@f)kYS(Vk{C-XO8`EAFyY;QT*0<+-_q*Si)jgOB{N=j;JY01ye4+{(H?Ci97$>-DY!m32nHhU~`-+MR8ChCd0(Z;S8y_Ecc6J^c8?&rDHa0dUCdLMVHW&>4{{BGo z^73LDZ)+5Tr*VosJiwhaAudlbWvzwThfY&le<|jp>MDmy4 zAbgh4YPAUo2_(zl+1c66&CRa|dTniuZ;OkI`&&YPe}7O=P)$uuczAeDPR`@wBf)}# z0w*V@#Kgqr=4SSZH#9Xh5ucr%jf{*0XjG`EsEEk@{e5zBa(Q`qL_|bMNeS>REGz^E z2UF7B-JPDEjuRLdcy)C}a&d7nwcg&|$k)`=6v?Tnsl11U_yNBg=&r6VJ~b;V%k1pt z<|e9AS67F>xVQ)kd3kyGmzS4R1q1}Zbf9-}aREC~LNaOd^Yh{{H#avlG!&<`wUzkj z=qP?^X(@hVVkDe4-XG_cXy%Qr&*ANQyhPM zsO|3V4!I-{W{qu-U>zJBKvJ}&y}ex~e6to~SGKmc3JVKq7#y9Up&@GqTKo01hld9@ zIUHmc5fH(RjSYCj2WMtxA`$SKAe2N&)6&vF9I6rlM3MjS@W36MR)+83;J`>GBiBSP zFRzuAm64GV`XN`5<>h7KvRu?MvX^3CdAX>?s;jHnegp(I&`UH{ZXe&Uo}M0#Fbo3j z?CflMm%M}w4-cac85tSqcuGpj`T02qTy!n^AzC~zFu-Q;D0FFu6@)7Q{6I|e6F~r2j2#Q&J(+e4baD04Bn!LkN$Rqho!ESs;;_^O6T=?)E{rSVb zxwEtt#$mimi_{J+2o_vQ{Qxd5;^5|{vv%mxFJLKHOBcmm1qBgw5k*k3f`wABR1r)h zc#XMuX>3gDC6yR!MOzU1g9l#1>4lPGw*McI$8(aClgH$}{NCr~l(7ygGun(cqpi$n zGurCErr@W!?-D}Nx2*nN5ANS9dbckg{|vX zoCgv+LKp;TES{jg)NF<~cK~{dXudh!B!Bw-fv(lut;Xl$! z9D?b2X#j{XsI9w^DiIbD;&dw+4-Q5I#dLE!BN@Ox8iNt5=?d{!cHs^V1 zZf0X+Q~L1mm|K#1OiJOvgsBQs-dC5 z%*`*th?SLf?3$rPXB~VgTw+Gl!YnI_(QF-L&t-Raw<0;@cigF!l@*6t z=n!B}4i8C?=z#=Dvvt|+a|YJ%Ezvej7>z?hY`2#l9UbN6Tr$XAZDL}=p(}WkbZ>7j zJVlEg0|Ns(2&-aWqV&kf2v-x;aaUszV{&rRU5%zz8WarG($&~qse_?V2(ILCmG(=k zcq8;-kTSm;I)zR^DpOO_(P$j)ev-f1J7X3#f-np>BKRk)#llj15wRC-w6L+XQ%fs< zfQ^4au$8;S>JUxf^h~fkQeBerkn|QI4;fhGERw)Z6P~wN#w`YUSccti-g&>T-;+Cs z!|{Cn5ylS>n=p8NJwH8tN(xL#S!%P{hWZi9(suhH+89!f>YY!sMoXFDE0hcT6=>)Dc`()Dc5&tJm$@$o=iEP+7b>Hhxx-Q7DXYksi! zWhiJRXhADM3tG@h(1KQi7PO$1parc2EoedCioL65v$@~zr4|=7z618v2_}_VM9^5| z2ZKQ_mm{(n1HIs4fhK2{onxYQ@!k=SBDg4^NxdMrm;v?yK$9;oxPR4OSwnF+@Hmt9 zgGYj&Jkgxe(G!z$P&X?wttQBQ`T1K!z4K7xDfJ;udFkK)+I6>^^?D8HYPBl6G?xwn zqDG?;FB>6uKMCo_Wy7&^hTacCYm9nIUmo zA%1c&(OQnwBsqm&@tyljCl;<60~hwF|`cb%YMHEYp&T z3%Xn`bEQKbt?YyqH;%2->99XH_f*WtV#8Hoi(K0ZWCKY+ehr^ zr_)JxQN9czOQ;~-H|ferTS~$C2`*?sD?tld30lyCR)YR-^b6Y|-l4spLNWjV002ov JPDHLkV1nq{iBA9k literal 0 HcmV?d00001 diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bd071f62fe2e7ee6eee07dff9f8d71301dbbc1 GIT binary patch literal 426 zcmV;b0agBqP)8FMq|`en-UFzBm_1^p%g^~1`R<$MM~rgVVFjm*_(z*feB$5Mz|QX38iL& zc~A<4$S|@X(2`RguS7Y4ON$4BI{)>0;0Moe*Ws6{V__C%;lB%Kj;W{*KlX8U2f#0D#Aa!ofh*Q$0Tu;95WkLp0of0$|2iJB;NGAku?!ykcUoY9t#!p|T^p z20D!gz`ltA*ksD(f?|+ntG@!@>P$h<4SS*LhT|c-U~|H%9SxRXx4nxISYo&9#89uh zS}U-CBAZEiJsrs#IaxiV7Z9IcbKjxy#3!-K2)qRlVhAo%Pi45+1PGJ#T4>o$s|`+c zB%$4jOHhIxbIg3T#sK&`F}sR;aNeRK@WbF0KsP>yoBBLdD;SDh^#St3P&+j*s~t`$ zlxb>dD)1`T6X6NBO`3RKw8ZgUBnBJ-lx5Qk^EC0I&|**NGUV9H#lkGi!v6#Q0=gDM UEs$${_5c6?07*qoM6N<$f~DBB3jhEB literal 0 HcmV?d00001 diff --git a/tests/ref/par-contains-parbreak.png b/tests/ref/par-contains-parbreak.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bd071f62fe2e7ee6eee07dff9f8d71301dbbc1 GIT binary patch literal 426 zcmV;b0agBqP)8FMq|`en-UFzBm_1^p%g^~1`R<$MM~rgVVFjm*_(z*feB$5Mz|QX38iL& zc~A<4$S|@X(2`RguS7Y4ON$4BI{)>0;0Moe*Ws6{V__C%;lB%Kj;W{*KlX8U2f#0D#Aa!ofh*Q$0Tu;95WkLp0of0$|2iJB;NGAku?!ykcUoY9t#!p|T^p z20D!gz`ltA*ksD(f?|+ntG@!@>P$h<4SS*LhT|c-U~|H%9SxRXx4nxISYo&9#89uh zS}U-CBAZEiJsrs#IaxiV7Z9IcbKjxy#3!-K2)qRlVhAo%Pi45+1PGJ#T4>o$s|`+c zB%$4jOHhIxbIg3T#sK&`F}sR;aNeRK@WbF0KsP>yoBBLdD;SDh^#St3P&+j*s~t`$ zlxb>dD)1`T6X6NBO`3RKw8ZgUBnBJ-lx5Qk^EC0I&|**NGUV9H#lkGi!v6#Q0=gDM UEs$${_5c6?07*qoM6N<$f~DBB3jhEB literal 0 HcmV?d00001 diff --git a/tests/ref/par-hanging-indent-semantic.png b/tests/ref/par-hanging-indent-semantic.png new file mode 100644 index 0000000000000000000000000000000000000000..e05795c7f2f128ec72ac6e728b58dd09c86120df GIT binary patch literal 1594 zcmV-A2F3Y_P)xYL>H!bzfj#ig33fPe*zX7Y=~(gWG`>&9E?Hm_#d5w+Cx;%B2i_&ZxLt3R4RE$% zZ325euA1TY+Efb=D>Oz~VM|cr6EiG?4;vFZT6!mXoa-S{eFmuf#Y_i=CC|JwYA4wf z#dFJ@a>7R*G4BwkaSr=ctI6A|)j8VB34p&i$!t-Rg}k-vcn zgCJ1q2H-EEmA{Kz#!7aS;!PsthO7J{4v56N!{bE$R=V+-C=S ztsTJB;rS#yum}FJ!WAE88Y&~>^>Ft}G9ugLhSMJ|n)Lf27k&Nsb~gH@9V4n`hFgZN zqH^RAnkwjD{nT*!rw}#r!8QQ3lmb5E0B{0;12`w*=zkO}ADp@Rg)x<6O_C-n6aybp zoTPjH;wgKeP7yMG-_04~@wv5Zekovu2~uBqbGU46-KL zCA{oQme>SoQsY@EmOTT%Vy+|?Oo#papg;D58>C5VgT@vYi49mg(TD^H@LC%>b^{Y1 zhG6;Ntusyxf1)3(5vu^w#cF_KVk3Bs05D13a>4~t2Ht;wjncod4}KJ?20l_l-i=ae zR0o&Qh<|{;bdbiA_)2&Ue?Ge)L4VmOJ`z2M~AIU0ClRUb`^H3k-1{WEaSvW|d8N)agUlpC z^uXV=%Wm9;=U#%cK=>d0;uX-&`UZ~5W{vX1J;T?%s-b(j_PKp!<9fHOv^<{)Z2`pr zhc5V;lb2c!&$)cgK7H?rmfc~+%9o1HTl}&jC)R`J)zTF
FfXSk?93zX}=((4Ku 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 LNkvXXu0mjfO9jX3 literal 0 HcmV?d00001 diff --git a/tests/ref/par-show.png b/tests/ref/par-show.png new file mode 100644 index 0000000000000000000000000000000000000000..1ceb26f71142ca0b0edd35a4da93a41fb252e271 GIT binary patch literal 932 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 uG6l2(S^=$00j+>mrhryJE1;GAFZ&xQWdS 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ð3J!Q`ss(-k-mX;;$ zv2{brkd`B)S(g=Qm`x_p^xJf~q8wBXjjg5<$!(lR7Z%vHbRa+jVZIhc^TogS_0cvs@#ZjVpN*Ig!k_e=?3}O2k$!sn!oR-U0 zVUBh4=6L2If~bcGT{g)*cN&xP*#Pj=7~xy!?&ug2jnNGK90;3x3*2`JBYWgdHy|Dx zCex#1TU#fPY5dhP&p1_PQ{>*g(DplMrcU1_1W+^c?^3OIXDZtH11}2v?Y02PY(}x% z!~;pHrCV4s!A?uCxOtQ}iA}Ptq<5wU*c!(ZX);wDG32T=Dl`PfsZE|OD$kg=LfRSK zt9%L}tDiD^>mgdTJn^MjCW$}Cc(gV8pd4gvfgXN^qZIzCsve*+C&O#w+JC*T`5UX)+3_AbwP0;;IyyR5x8333eSExO zwieEF2i$MX_UQ|cZmbU5j5vZqyI?4LxRf!zxyrulAX~(O+&}Yb40X)9-EGx;y;A{R zu5?A_$=(b~LBGVC)1%&Tl+J}fD*;iURg4a!DdWEok=R?~m={FvQZHSS7gOqIy}@X(Jth(T6jOJT-XK zg!Sbbc#7Y!vcquSIHt7BG`JVoDgv-AK!qc!GymAvD+Uk!bRpLYnUk{PAP&Expd&1k zV_%B?H2H+V7Y?$#c5R5|>6;y!W{D8U@>?+5tX z73-1&$P%6}9mRkQ=S#d-eea9A>`uM~h*Li5z({GfvX@zqH~u3}|E9se-c$f}*{OaS zZ?Qo7uk*{F{94{(YOl{JMsWqT!UVq+6?IH`F-ctF%f!u1{#N z)t^r;*tAVSGZJZSFZTBGw1TTXy~{MJDpZ*Ls;}*1lttt9E=tjhnEjW08*b88dS&EJ zBvE$xc__>e2#VKP#X9t2Kon^wtaPU#?T`+(TR5vc^%Weh=)tGO%wSIGYKh&Pk&Z02 z&6?*6O;tP-!N+U&;lZcuw>X|bAME;fl$lsC<`V$B!dS{G(n?;sh`arabrem&wH;S zOcg|i(^RGH);&Bt;wxMdfpDU7g13~=O-f30Du|hvr>B|A!&3@>n}Q-=5lW6qC1cl? zNPDKD1b-|EZN&MpDb7t&HOh9rRJ!e6lTzcLMTsV6sFP%l3_Xf++#Ug(U~W!>POF!E zS;4;d`EE@dJo&(TlLa6K1OHFc2_zc4kGvAH(6Jo`a$6#G!(`g(`f4?*k7ND=UUYA0 literal 0 HcmV?d00001 diff --git a/tests/suite/layout/table.typ b/tests/suite/layout/table.typ index f59d8b424..5c2b07492 100644 --- a/tests/suite/layout/table.typ +++ b/tests/suite/layout/table.typ @@ -310,6 +310,17 @@ ) } +--- table-cell-par --- +// Ensure that table cells aren't considered paragraphs by default. +#show par: highlight + +#table( + columns: 3, + [A], + block[B], + par[C], +) + --- grid-cell-in-table --- // Error: 8-19 cannot use `grid.cell` as a table cell // Hint: 8-19 use `table.cell` instead diff --git a/tests/suite/math/text.typ b/tests/suite/math/text.typ index 760910f4d..8c7611114 100644 --- a/tests/suite/math/text.typ +++ b/tests/suite/math/text.typ @@ -43,3 +43,8 @@ $sum_(k in NN)^prime 1/k^2$ // Test script-script in a fraction. $ 1/(x^A) $ #[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$] + +--- math-par --- +// Ensure that math does not produce paragraphs. +#show par: highlight +$ a + "bc" + #[c] + #box[d] + #block[e] $ diff --git a/tests/suite/model/bibliography.typ b/tests/suite/model/bibliography.typ index 20eb8acd9..6de44e240 100644 --- a/tests/suite/model/bibliography.typ +++ b/tests/suite/model/bibliography.typ @@ -53,6 +53,24 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read @Zee04 #bibliography("/assets/bib/works_too.bib", style: "mla") +--- bibliography-grid-par --- +// Ensure that a grid-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib") + +--- bibliography-indent-par --- +// Ensure that an indent-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib", style: "mla") + --- issue-4618-bibliography-set-heading-level --- // Test that the bibliography block's heading is set to 2 by the show rule, // and therefore should be rendered like a level-2 heading. Notably, this diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index 288392d45..7176b04e2 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -183,22 +183,44 @@ a + 0. #set enum(number-align: horizon) #set enum(number-align: bottom) +--- enum-par render html --- +// Check whether the contents of enum items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + + Hello + + World +] + +#block[ + + Hello // Paragraphs + + From + + World // No paragraph because it's a tight enum +] + +#block[ + + Hello // Paragraphs + + From + + The + + + World // Paragraph because it's a wide enum +] + --- issue-2530-enum-item-panic --- // Enum item (pre-emptive) #enum.item(none)[Hello] #enum.item(17)[Hello] ---- issue-5503-enum-interrupted-by-par-align --- -// `align` is block-level and should interrupt an enum -// but not a `par` +--- issue-5503-enum-in-align --- +// `align` is block-level and should interrupt an enum. + a + b -#par(leading: 5em)[+ par] +#align(right)[+ c] + d -#par[+ par] -+ f -#align(right)[+ align] -+ h --- issue-5719-enum-nested --- // Enums can be immediately nested. diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index 58ba2b2a4..37fb4ecda 100644 --- a/tests/suite/model/figure.typ +++ b/tests/suite/model/figure.typ @@ -180,6 +180,17 @@ We can clearly see that @fig-cylinder and caption: [Underlined], ) +--- figure-par --- +// Ensure that a figure body is considered a paragraph. +#show par: highlight + +#figure[Text] + +#figure( + [Text], + caption: [A caption] +) + --- figure-and-caption-show --- // Test creating custom figure and custom caption diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ index 4e529fdf6..4e04e5c56 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -128,6 +128,11 @@ Not in heading // Hint: 1:19-1:25 you can enable heading numbering with `#set heading(numbering: "1.")` Cannot be used as @intro +--- heading-par --- +// Ensure that heading text isn't considered a paragraph. +#show par: highlight += Heading + --- heading-html-basic html --- // level 1 => h2 // ... diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 96ddf3c18..9bed930bb 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -238,6 +238,33 @@ World #text(red)[- World] #text(green)[- What up?] +--- list-par render html --- +// Check whether the contents of list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +#block[ + // No paragraphs. + - Hello + - World +] + +#block[ + - Hello // Paragraphs + + From + - World // No paragraph because it's a tight list. +] + +#block[ + - Hello // Paragraphs either way + + From + + The + + - World // Paragraph because it's a wide list. +] + --- issue-2530-list-item-panic --- // List item (pre-emptive) #list.item[Hello] @@ -262,18 +289,11 @@ World part($ x $ + parbreak() + parbreak() + list[A]) } ---- issue-5503-list-interrupted-by-par-align --- -// `align` is block-level and should interrupt a list -// but not a `par` +--- issue-5503-list-in-align --- +// `align` is block-level and should interrupt a list. #show list: [List] - a - b -#par(leading: 5em)[- c] -- d -- e -#par[- f] -- g -- h #align(right)[- i] - j diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index a755151d6..49fd7d7cb 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -242,6 +242,15 @@ A #outline(target: metadata) #metadata("hello") +--- outline-par --- +// Ensure that an outline does not produce paragraphs. +#show par: highlight + +#outline() + += A += B += C --- issue-2048-outline-multiline --- // Without the word joiner between the dots and the page number, diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index 0c2b5cb54..84f2ec152 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -19,6 +19,105 @@ heaven Would through the airy region stream so bright That birds would sing and think it were not night. See, how she leans her cheek upon her hand! O, that I were a glove upon that hand, That I might touch that cheek! +--- par-semantic --- +#show par: highlight + +I'm a paragraph. + +#align(center, table( + columns: 3, + + // No paragraphs. + [A], + block[B], + block[C *D*], + + // Paragraphs. + par[E], + [ + + F + ], + [ + G + + ], + + // Paragraphs. + parbreak() + [H], + [I] + parbreak(), + parbreak() + [J] + parbreak(), + + // Paragraphs. + [K #v(10pt)], + [#v(10pt) L], + [#place[] M], + + // Paragraphs. + [ + N + + O + ], + [#par[P]#par[Q]], + // No paragraphs. + [#block[R]#block[S]], +)) + +--- par-semantic-html html --- += Heading is no paragraph + +I'm a paragraph. + +#html.elem("div")[I'm not.] + +#html.elem("div")[ + We are two. + + So we are paragraphs. +] + +--- par-semantic-tag --- +#show par: highlight +#block[ + #metadata(none) + A + #metadata(none) +] + +#block(width: 100%, metadata(none) + align(center)[A]) +#block(width: 100%, align(center)[A] + metadata(none)) + +--- par-semantic-align --- +#show par: highlight +#show bibliography: none +#set block(width: 100%, stroke: 1pt, inset: 5pt) + +#bibliography("/assets/bib/works.bib") + +#block[ + #set align(right) + Hello +] + +#block[ + #set align(right) + Hello + @netwok +] + +#block[ + Hello + #align(right)[World] + You +] + +#block[ + Hello + #align(right)[@netwok] + You +] + --- par-leading-and-spacing --- // Test changing leading and spacing. #set par(spacing: 1em, leading: 2pt) @@ -69,6 +168,12 @@ Why would anybody ever ... #set par(hanging-indent: 15pt, justify: true) #lorem(10) +--- par-hanging-indent-semantic --- +#set par(hanging-indent: 15pt) += I am not affected + +I am affected by hanging indent. + --- par-hanging-indent-manual-linebreak --- #set par(hanging-indent: 1em) Welcome \ here. Does this work well? @@ -83,6 +188,22 @@ Welcome \ here. Does this work well? // Ensure that trailing whitespace layouts as intended. #box(fill: aqua, " ") +--- par-contains-parbreak --- +#par[ + Hello + // Warning: 4-14 parbreak may not occur inside of a paragraph and was ignored + #parbreak() + World +] + +--- par-contains-block --- +#par[ + Hello + // Warning: 4-11 block may not occur inside of a paragraph and was ignored + #block[] + World +] + --- par-empty-metadata --- // Check that metadata still works in a zero length paragraph. #block(height: 0pt)[#""#metadata(false)] @@ -94,6 +215,26 @@ Welcome \ here. Does this work well? #set text(hyphenate: false) Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. +--- par-show --- +// This is only slightly cursed. +#let revoke = metadata("revoke") +#show par: it => { + if bibliography.title == revoke { return it } + set bibliography(title: revoke) + let p = counter("p") + par[#p.step() §#context p.display() #it.body] +} + += A + +B + +C #parbreak() D + +#block[E] + +#block[F #parbreak() G] + --- issue-4278-par-trim-before-equation --- #set par(justify: true) #lorem(6) aa $a = c + b$ diff --git a/tests/suite/model/quote.typ b/tests/suite/model/quote.typ index d0dcc55dd..51c4bba59 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -107,3 +107,14 @@ When you said that #quote[he surely meant that #quote[she intended to say #quote )[ Compose papers faster ] + +--- quote-par --- +// Ensure that an inline quote is part of a paragraph, but a block quote +// does not result in paragraphs. +#show par: highlight + +An inline #quote[quote.] + +#quote(block: true, attribution: [The Test Author])[ + A block-level quote. +] diff --git a/tests/suite/model/terms.typ b/tests/suite/model/terms.typ index 23ac6e513..103a8033e 100644 --- a/tests/suite/model/terms.typ +++ b/tests/suite/model/terms.typ @@ -59,6 +59,34 @@ Not in list // Error: 8 expected colon / Hello +--- terms-par render html --- +// Check whether the contents of term list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + / Hello: A + / World: B +] + +#block[ + / Hello: A // Paragraphs + + From + / World: B // No paragraphs because it's a tight term list. +] + +#block[ + / Hello: A // Paragraphs + + From + + The + + / World: B // Paragraph because it's a wide term list. +] + + --- issue-1050-terms-indent --- #set page(width: 110pt) #set par(first-line-indent: 0.5cm) @@ -76,18 +104,10 @@ Not in list // Term item (pre-emptive) #terms.item[Hello][World!] ---- issue-5503-terms-interrupted-by-par-align --- -// `align` is block-level and should interrupt a `terms` -// but not a `par` +--- issue-5503-terms-in-align --- +// `align` is block-level and should interrupt a `terms`. #show terms: [Terms] / a: a -/ b: b -#par(leading: 5em)[/ c: c] -/ d: d -/ e: e -#par[/ f: f] -/ g: g -/ h: h #align(right)[/ i: i] / j: j From 176b070c779ef8aa4515c8ff062b17ca9114fd3f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 24 Jan 2025 13:31:03 +0100 Subject: [PATCH 42/44] Fix space collapsing for explicit paragraphs (#5749) --- crates/typst-realize/src/lib.rs | 4 +-- tests/ref/par-contains-block.png | Bin 426 -> 423 bytes tests/ref/par-contains-parbreak.png | Bin 426 -> 423 bytes tests/ref/par-explicit-trim-space.png | Bin 0 -> 215 bytes tests/ref/par-show-children.png | Bin 0 -> 920 bytes tests/ref/par-show-styles.png | Bin 0 -> 471 bytes tests/ref/par-show.png | Bin 932 -> 0 bytes tests/suite/model/par.typ | 37 +++++++++++++++++++++----- 8 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 tests/ref/par-explicit-trim-space.png create mode 100644 tests/ref/par-show-children.png create mode 100644 tests/ref/par-show-styles.png delete mode 100644 tests/ref/par-show.png diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 754e89aac..50685a962 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -729,8 +729,8 @@ fn finish(s: &mut State) -> SourceResult<()> { } })?; - // In math, spaces are top-level. - if let RealizationKind::Math = s.kind { + // In paragraph and math realization, spaces are top-level. + if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) { collapse_spaces(&mut s.sink, 0); } diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png index f4bd071f62fe2e7ee6eee07dff9f8d71301dbbc1..27ca0cf6b0cb79a4f2fc573d20054596bd009117 100644 GIT binary patch delta 397 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 N002ovPDHLkV1lSL(j@=@ literal 0 HcmV?d00001 diff --git a/tests/ref/par-show.png b/tests/ref/par-show.png deleted file mode 100644 index 1ceb26f71142ca0b0edd35a4da93a41fb252e271..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 932 zcmV;V16%xwP)Bb(Si*|f=W>9Q=hWGlQ@2_q?IkRcgr*pehq95_)yp&#O8 z!X-}Bew`Ny6;v84#T4~KoW!ODq z>=TEFrvsl_Y~rwO6L7+kCJmQOUf?EPVzIpKlZ8W(PyzxIkzhhU356BDV>se$Pes>v z_Lgxmc#U_9k2BVOAZcyz&h$vZ+rCIb_e%o+D;-#SrQogWW?zA~ZotJKWhbTJOpX?l zf*q8vmV!5C`Abvq$3Npvmx5`2!cF&-DqwK-@|IGE6#Vb--e#`$dw@W{=PF3SuC2QQ zTA2zftgynj1^?6dd4!1{E&{}>cCTCJ7lQK!&y2jwoQ(=V_R`}Mi zeOaZg-VE4y6n2^cGZk0d6!@{a7sDo<*q1N`eiHsXhdf-LR=s0xObbL|g%wu#Zij=3 zR5D`mcvK8-IK1Qv#A*T8^xVoB2`TuKjv%l-1E9c3&x91b>-jQ+U)wSOR@8Rbk%IH% zMRlV!V>jUHmc3$d&HSXr|WT$>*O8vx7- zLE@AUyt(EXH%p7Q0G@Qo9WUfWAu#s>3_Jyata(yb{j#e9c)c5Vt`!az3&H#~#S^gB ztpV`8W81gtCn30f(0-6)2mpwLK?okGwMVov6;@c`+nnp3yU8IFH8&&x0000 { - if bibliography.title == revoke { return it } - set bibliography(title: revoke) - let p = counter("p") - par[#p.step() §#context p.display() #it.body] + if it.body.at("children", default: ()).at(0, default: none) == step { + return it + } + par(step + [§#nr ] + it.body) } = A @@ -235,6 +237,27 @@ C #parbreak() D #block[F #parbreak() G] +--- par-show-styles --- +// Variant 2: Prevent recursion by observing a style. +#let revoke = metadata("revoke") +#show par: it => { + if bibliography.title == revoke { return it } + set bibliography(title: revoke) + let p = counter("p") + par[#p.step()§#context p.display() #it.body] +} + += A + +B + +C + +--- par-explicit-trim-space --- +A + +#par[ B ] + --- issue-4278-par-trim-before-equation --- #set par(justify: true) #lorem(6) aa $a = c + b$ From 85d177897468165b93056947a80086b2f84d815d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 27 Jan 2025 14:15:20 +0100 Subject: [PATCH 43/44] Support first-line-indent for every paragraph (#5768) --- crates/typst-layout/src/flow/collect.rs | 14 +-- crates/typst-layout/src/inline/collect.rs | 32 +++++-- crates/typst-layout/src/inline/mod.rs | 30 ++++-- crates/typst-layout/src/inline/prepare.rs | 8 +- crates/typst-layout/src/math/text.rs | 3 +- crates/typst-library/src/model/par.rs | 86 ++++++++++++++++-- crates/typst-library/src/model/terms.rs | 8 +- tests/ref/par-first-line-indent-all-enum.png | Bin 0 -> 425 bytes tests/ref/par-first-line-indent-all-list.png | Bin 0 -> 383 bytes tests/ref/par-first-line-indent-all-terms.png | Bin 0 -> 755 bytes tests/ref/par-first-line-indent-all.png | Bin 0 -> 1335 bytes tests/suite/model/par.typ | 51 +++++++++++ 12 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 tests/ref/par-first-line-indent-all-enum.png create mode 100644 tests/ref/par-first-line-indent-all-list.png create mode 100644 tests/ref/par-first-line-indent-all-terms.png create mode 100644 tests/ref/par-first-line-indent-all.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index f2c7ebd1e..34362a6c5 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -23,6 +23,7 @@ use typst_library::World; use typst_utils::SliceExt; use super::{layout_multi_block, layout_single_block, FlowMode}; +use crate::inline::ParSituation; use crate::modifiers::layout_and_modify; /// Collects all elements of the flow into prepared children. These are much @@ -46,7 +47,7 @@ pub fn collect<'a>( base, expand, output: Vec::with_capacity(children.len()), - last_was_par: false, + par_situation: ParSituation::First, } .run(mode) } @@ -60,7 +61,7 @@ struct Collector<'a, 'x, 'y> { expand: bool, locator: SplitLocator<'a>, output: Vec>, - last_was_par: bool, + par_situation: ParSituation, } impl<'a> Collector<'a, '_, '_> { @@ -123,8 +124,7 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - false, - false, + None, )? .into_frames(); @@ -165,7 +165,7 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - self.last_was_par, + self.par_situation, )? .into_frames(); @@ -175,7 +175,7 @@ impl<'a> Collector<'a, '_, '_> { self.lines(lines, styles); self.output.push(Child::Rel(spacing.into(), 4)); - self.last_was_par = true; + self.par_situation = ParSituation::Consecutive; Ok(()) } @@ -272,7 +272,7 @@ impl<'a> Collector<'a, '_, '_> { }; self.output.push(spacing(elem.below(styles))); - self.last_was_par = false; + self.par_situation = ParSituation::Other; } /// Collects a placed element into a [`PlacedChild`]. diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index cbc490ba1..14cf2e3b8 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -5,6 +5,7 @@ use typst_library::layout::{ Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; +use typst_library::model::{EnumElem, ListElem, TermsElem}; use typst_library::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, @@ -124,26 +125,33 @@ pub fn collect<'a>( locator: &mut SplitLocator<'a>, styles: StyleChain<'a>, region: Size, - consecutive: bool, - paragraph: bool, + situation: Option, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); let mut quoter = SmartQuoter::new(); let outer_dir = TextElem::dir_in(styles); - if paragraph && consecutive { + if let Some(situation) = situation { let first_line_indent = ParElem::first_line_indent_in(styles); - if !first_line_indent.is_zero() + if !first_line_indent.amount.is_zero() + && match situation { + // First-line indent for the first paragraph after a list bullet + // just looks bad. + ParSituation::First => first_line_indent.all && !in_list(styles), + ParSituation::Consecutive => true, + ParSituation::Other => first_line_indent.all, + } && AlignElem::alignment_in(styles).resolve(styles).x == outer_dir.start().into() { - collector.push_item(Item::Absolute(first_line_indent.resolve(styles), false)); + collector.push_item(Item::Absolute( + first_line_indent.amount.resolve(styles), + false, + )); collector.spans.push(1, Span::detached()); } - } - if paragraph { let hang = ParElem::hanging_indent_in(styles); if !hang.is_zero() { collector.push_item(Item::Absolute(-hang, false)); @@ -257,6 +265,16 @@ pub fn collect<'a>( Ok((collector.full, collector.segments, collector.spans)) } +/// Whether we have a list ancestor. +/// +/// When we support some kind of more general ancestry mechanism, this can +/// become more elegant. +fn in_list(styles: StyleChain) -> bool { + ListElem::depth_in(styles).0 > 0 + || !EnumElem::parents_in(styles).is_empty() + || TermsElem::within_in(styles) +} + /// Collects segments. struct Collector<'a> { full: String, diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 83ca82bf2..f8a36368d 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -42,7 +42,7 @@ pub fn layout_par( styles: StyleChain, region: Size, expand: bool, - consecutive: bool, + situation: ParSituation, ) -> SourceResult { layout_par_impl( elem, @@ -56,7 +56,7 @@ pub fn layout_par( styles, region, expand, - consecutive, + situation, ) } @@ -75,7 +75,7 @@ fn layout_par_impl( styles: StyleChain, region: Size, expand: bool, - consecutive: bool, + situation: ParSituation, ) -> SourceResult { let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); @@ -105,8 +105,7 @@ fn layout_par_impl( styles, region, expand, - true, - consecutive, + Some(situation), ) } @@ -119,16 +118,15 @@ pub fn layout_inline<'a>( styles: StyleChain<'a>, region: Size, expand: bool, - paragraph: bool, - consecutive: bool, + par: Option, ) -> SourceResult { // Collect all text into one string for BiDi analysis. let (text, segments, spans) = - collect(children, engine, locator, styles, region, consecutive, paragraph)?; + collect(children, engine, locator, styles, region, par)?; // Perform BiDi analysis and performs some preparation steps before we // proceed to line breaking. - let p = prepare(engine, children, &text, segments, spans, styles, paragraph)?; + let p = prepare(engine, children, &text, segments, spans, styles, par)?; // Break the text into lines. let lines = linebreak(engine, &p, region.x - p.hang); @@ -136,3 +134,17 @@ pub fn layout_inline<'a>( // Turn the selected lines into frames. finalize(engine, &p, &lines, styles, region, expand, locator) } + +/// Distinguishes between a few different kinds of paragraphs. +/// +/// In the form `Option`, `None` implies that we are creating an +/// inline layout that isn't a semantic paragraph. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ParSituation { + /// The paragraph is the first thing in the flow. + First, + /// The paragraph follows another paragraph. + Consecutive, + /// Any other kind of paragraph. + Other, +} diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index e26c9b147..0344d4331 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -85,7 +85,7 @@ pub fn prepare<'a>( segments: Vec>, spans: SpanMapper, styles: StyleChain<'a>, - paragraph: bool, + situation: Option, ) -> SourceResult> { let dir = TextElem::dir_in(styles); let default_level = match dir { @@ -130,7 +130,11 @@ pub fn prepare<'a>( } // Only apply hanging indent to real paragraphs. - let hang = if paragraph { ParElem::hanging_indent_in(styles) } else { Abs::zero() }; + let hang = if situation.is_some() { + ParElem::hanging_indent_in(styles) + } else { + Abs::zero() + }; Ok(Preparation { text, diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 5897c3c0c..9a64992aa 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -107,8 +107,7 @@ fn layout_inline_text( styles, Size::splat(Abs::inf()), false, - false, - false, + None, )? .into_frame(); diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs index 0bdbe4ea6..cf31b5195 100644 --- a/crates/typst-library/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -3,8 +3,8 @@ use typst_utils::singleton; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Smart, - Unlabellable, + cast, dict, elem, scope, Args, Cast, Construct, Content, Dict, NativeElement, Packed, + Smart, Unlabellable, Value, }; use crate::introspection::{Count, CounterUpdate, Locatable}; use crate::layout::{Em, HAlignment, Length, OuterHAlignment}; @@ -163,16 +163,56 @@ pub struct ParElem { /// The indent the first line of a paragraph should have. /// - /// Only the first line of a consecutive paragraph will be indented (not - /// the first one in a block or on the page). + /// By default, only the first line of a consecutive paragraph will be + /// indented (not the first one in the document or container, and not + /// paragraphs immediately following other block-level elements). + /// + /// If you want to indent all paragraphs instead, you can pass a dictionary + /// containing the `amount` of indent as a length and the pair + /// `{all: true}`. When `all` is omitted from the dictionary, it defaults to + /// `{false}`. /// /// By typographic convention, paragraph breaks are indicated either by some - /// space between paragraphs or by indented first lines. Consider reducing - /// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading) - /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`). - pub first_line_indent: Length, + /// space between paragraphs or by indented first lines. Consider + /// - reducing the [paragraph `spacing`]($par.spacing) to the + /// [`leading`]($par.leading) using `{set par(spacing: 0.65em)}` + /// - increasing the [block `spacing`]($block.spacing) (which inherits the + /// paragraph spacing by default) to the original paragraph spacing using + /// `{set block(spacing: 1.2em)}` + /// + /// ```example + /// #set block(spacing: 1.2em) + /// #set par( + /// first-line-indent: 1.5em, + /// spacing: 0.65em, + /// ) + /// + /// The first paragraph is not affected + /// by the indent. + /// + /// But the second paragraph is. + /// + /// #line(length: 100%) + /// + /// #set par(first-line-indent: ( + /// amount: 1.5em, + /// all: true, + /// )) + /// + /// Now all paragraphs are affected + /// by the first line indent. + /// + /// Even the first one. + /// ``` + pub first_line_indent: FirstLineIndent, /// The indent that all but the first line of a paragraph should have. + /// + /// ```example + /// #set par(hanging-indent: 1em) + /// + /// #lorem(15) + /// ``` #[resolve] pub hanging_indent: Length, @@ -199,6 +239,36 @@ pub enum Linebreaks { Optimized, } +/// Configuration for first line indent. +#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] +pub struct FirstLineIndent { + /// The amount of indent. + pub amount: Length, + /// Whether to indent all paragraphs, not just consecutive ones. + pub all: bool, +} + +cast! { + FirstLineIndent, + self => Value::Dict(self.into()), + amount: Length => Self { amount, all: false }, + mut dict: Dict => { + let amount = dict.take("amount")?.cast()?; + let all = dict.take("all").ok().map(|v| v.cast()).transpose()?.unwrap_or(false); + dict.finish(&["amount", "all"])?; + Self { amount, all } + }, +} + +impl From for Dict { + fn from(indent: FirstLineIndent) -> Self { + dict! { + "amount" => indent.amount, + "all" => indent.all, + } + } +} + /// A paragraph break. /// /// This starts a new paragraph. Especially useful when used within code like diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 9a2ed6aad..e197ff318 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -105,6 +105,11 @@ pub struct TermsElem { /// ``` #[variadic] pub children: Vec>, + + /// Whether we are currently within a term list. + #[internal] + #[ghost] + pub within: bool, } #[scope] @@ -180,7 +185,8 @@ impl Show for Packed { .with_spacing(Some(gutter.into())) .pack() .spanned(span) - .padded(padding); + .padded(padding) + .styled(TermsElem::set_within(true)); if tight { let leading = ParElem::leading_in(styles); diff --git a/tests/ref/par-first-line-indent-all-enum.png b/tests/ref/par-first-line-indent-all-enum.png new file mode 100644 index 0000000000000000000000000000000000000000..38cdea7926d11bcd7f1071bb100779305541f432 GIT binary patch literal 425 zcmV;a0apHrP)YUQM5#m4D+#4`&(xX%!KXnuc;8zncdO-lY+aeyRNC3y4gQF~f~XIF_gV1n|qq556n}@XRTI4+41Tp6JF?+eC1f$d*ZqkT{Z%nAQ0}X>tO>yIl_=Qu&;Uzb zL9p{NC)dLRMKN&5yg&ZEmQi}RGyiVifEf7M=Q~8Gpmd*iFOcHpVwhoun-l&8!d`<7 TAPREd00000NkvXXu0mjfQTn#b literal 0 HcmV?d00001 diff --git a/tests/ref/par-first-line-indent-all-list.png b/tests/ref/par-first-line-indent-all-list.png new file mode 100644 index 0000000000000000000000000000000000000000..cf731e79fc8a549a581f4b3bc9d6675fa3f1837d GIT binary patch literal 383 zcmV-_0f7FAP)SsPZ8rVm_(t*i|35p*?vEBAqZZTD;+I>u9Qra47XQhuedKa( zAcA`G)c@tX2E^jzTJJvtf3#Q&jZn2~!5Z;?LKv-TpUPat%9+udJXQ zz-#g9aN2>)m?~2E^hIVF}x+o9JpWqEGyPViO%L{()ri zFID0008INkl^KoV!5~XFNjB_v#$Z10T){|8wy~fKm|* zW-x;pECYN=l@@pRvK}G(cw$95)W>UUjdH?9=!kQ33L#U)R6r^o&f{{zfS$O4DrKp^ z`cy~y(Ag(hse>802)0p~mk6n&g7&HWur~Qa_%00bjYhtun^yz2;s&@M@y!r3@J;$zHb5Bs zukf61WP=8*>hZizp#Yoi05DR3OM582<9p4N;NkNCTmcFnS53}j&j2XE_&MullZ(OO zhPy7dR2l*lU>pJTnE?uLB7o~=N^rwNN^tQY1vnO1zpz8$t3KD%Q7f6o7HMvqXAFs>+j45==`0mRN%wL91j&Z!O`la^{8EI6ks9RZoer|fX90K z`uf@AVlaal%-~%DKQ6ytSeg4a#G>!zg*8>R(pjIb%pfbQL@QaH??i~6k+om>fLOsK zcG7)e*lC+qzr$GFm-dCm&xWq;_7MQ+Aihm6#1_*4*gf*YmLz-B!gH>ya3zv&<7(7< zwne!5%p%0<>H)Xllpns4tSS0x=A7bA8!;`Dr6A1trUi&;3IOZLHhJL}WCK|VTR#L- z5Z>SIB5yvCc>rz1Ev>N~TLrGYmld{MRXo5-QcSitU!lQ(;sS6oVCIrN}B@%mLqWzxd&GYJ$JbBLl z#WM?Usaa0g2|M9;2_Cn9_GQDgtR<-cMKfGC+pSyYL!@AO37??#Q<`CX84Cyq1E$A! z1;#Lyp~Of7yl|UTnm|xEePrWfV`P*skQ?Xeio>6e@a86YZGw8s5jUZh4mdAG{SYBi676T*jV_&_17-qeU8vlTdIwAtlS@cAACKGNYlL1) zNCt@(^^#*dXt~v{+krHf-urWQ71R|hYjVk!d?48KVb4XPzn8e8AVf{R(oR}#H7BK0 zo5taziCvE2l)E{lkYu!+mS=G)%Q=;GoXnQx8|E!Ezl8x3numTj+gpIe+q1}L&ROkv zcocxJfJ&9&^^%=%5WKh(c;JV)a=c!)ZyW>HauR?6eJK-g)dM?=`T+E* zBz_>{(X+1D`^L=R<9djHn}QRoke?zuS@SdEMKBy{TmjL}95JNPKx)N~7G((Z46J~C z>0`VADC2Lf1uqvhkuyE&r@sQW1)7H*VCBLNXOC`C?v|7VnD!Lo!V%?y(Hw;asM!i? z^@w+kaxF5s<6l^*1>N4;fT?4(VPa#0730nqf%gDzt;e*am4l9j`Sa3#KZsWw zfXE*&5~dEV$7Gb&0WiJ-uH%*JnNS7cMccZj-SfD5N5G48#pd3i3f+!2SwW(<=x}0a zJAe~DH7ehvuvT0bBpQIOrD^2%+RA+~LP`EEgLiDJ1K{~pKN*ppZOg-&OOZ)E3}Zz) z05J-prlI&yKkw+EG`7$RDB}c=nPp`M tn~=l5g!{3*=h~f<%LzMSC;Se>{{X?#gsHQ~(a!(?002ovPDHLkV1nKZaaI5T literal 0 HcmV?d00001 diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index fa230451d..e76690064 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -156,6 +156,57 @@ starts a paragraph, also with indent. ثم يصبح النص رطبًا وقابل للطرق ويبدو المستند رائعًا. +--- par-first-line-indent-all --- +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) +#set block(spacing: 1.2em) +#show heading: set text(size: 10pt) + += Heading +All paragraphs are indented. + +Even the first. + +--- par-first-line-indent-all-list --- +#show list.where(tight: false): set list(spacing: 1.2em) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + +- A #parbreak() B #line(length: 100%) C + +- D + +--- par-first-line-indent-all-enum --- +#show enum.where(tight: false): set enum(spacing: 1.2em) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + ++ A #parbreak() B #line(length: 100%) C + ++ D + +--- par-first-line-indent-all-terms --- +#show terms.where(tight: false): set terms(spacing: 1.2em) +#set terms(hanging-indent: 10pt) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + +/ Term A: B \ C #parbreak() D #line(length: 100%) E + +/ Term F: G + --- par-spacing-and-first-line-indent --- // This is madness. #set par(first-line-indent: 12pt) From 9665eecdb62ee94cd9fcf4dfc61e2c70ba9391fb Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:08:12 +0300 Subject: [PATCH 44/44] Fixed typo in the new outline docs (#5772) --- crates/typst-library/src/model/outline.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 1214f2b0e..f413189ba 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -445,9 +445,9 @@ impl OutlineEntry { /// /// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the /// inner content of all entries at level `N` is aligned with the prefix of - /// all entries at with level `N + 1`, leaving at least `gap` space between - /// the prefix and inner parts. Furthermore, the `inner` contents of all - /// entries at the same level are aligned. + /// all entries at level `N + 1`, leaving at least `gap` space between the + /// prefix and inner parts. Furthermore, the `inner` contents of all entries + /// 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 @@ -461,13 +461,13 @@ impl OutlineEntry { /// The `prefix` is aligned with the `inner` content of entries that /// have level one less. /// - /// In the default show rule, this is just to `it.prefix()`, but it can - /// be freely customized. + /// In the default show rule, this is just `it.prefix()`, but it can be + /// freely customized. prefix: Option, /// The formatted inner content of the entry. /// - /// In the default show rule, this is just to `it.inner()`, but it can - /// be freely customized. + /// In the default show rule, this is just `it.inner()`, but it can be + /// freely customized. inner: Content, /// The gap between the prefix and the inner content. #[named]