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 001/101] 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 002/101] 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 003/101] 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 004/101] 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 005/101] 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 006/101] 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 007/101] 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 008/101] 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 009/101] 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 010/101] 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 011/101] 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 012/101] 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 013/101] 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 014/101] 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 015/101] 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 z4u~g8EGA0^uDaAQ#@|jGxIju5#}UUhE5*kGQGyXS$caAQ`PTO=m5Cw4VeDJhI9YAkVpF%<)J9p&8-yK+^;E$nh@Lhdp;`Eq9^U{*Q- zuHVxYZy}|12XhK-I{xY!=8h#}U+y5Dbi$uM@XC<$2ZtWv~M zxSse;vP$fb2WH6l2#?h=kjn~)*oHfsV?!}Z2h?swY>oMfcX<-3h#~p4jyR?n#XPAG z5nOp`4640UEH9^>v`|usK@9te^l?8v^b(-r*4;Ydd7u_oKmI*Pp{yUKeJ9{gv_@|K zLN=w9wDu@ai?e&W>2aV8p0T?%oU+b6Cn_*oTkKgv4^2_n+u}>(MYh|n-sv7(&AdYD z(F#&4(}5(T9M7E)N;4@q)NirCYEt1sDHZYu|5>0WA2@Izt znAG4FFdS3PM2cTcZt)ou_W`D>hg07uf%t=p*ZlLShx~wP^Kx?Z15eKg6`#=*UrQj> zo)wGz&APOGjEZZoMgu0TnAH1Gq;9=OdDBfiC)mPnGdg77Rf>!9QYSeimTQWe!_)(0 z^%n^8QYvieoP&$+VvFgDnba%`sAlziXj;KA-d}i-kD*{H4u7R7_Rd#}o#2^?_%ZbX z1L;};6(6rAwd$}+3_sy^G-5-HgTdFQp;WwRKvN8JxR@uY9-;5b0et*ECXReh>e~pl z82n*wq^^W&)vShjpe;^pREuvkxP26gJTSGJfd8O1g3M}hG5)F@R*7Y^tCNDa5E|xE zdY;2(^|Lq~ar<6UUu?m)ZqMai!<5##ok9F9%_t(LF0$rA9Uam9JxV=oIYFU21j<4I zv$RVo``UHHuu<`}S@LmKmkWZGk>gHg8=uvA`5gee@w538O=fj|0nTbc5FX)oUTOC5 zS)FH*K-va+;(99>%8Lw6S)agA%=Nlr$m{?E`4u8^6cdKQK%S(4zF5wdVEhQqF?LxD n{dp?~{*wrZ1LA;q(u@BB_|mxg8^q3M00000NkvXXu0mjfI)~vcUop8U~xBK)t-#PP&GN=~3Vkj^~ z8=?&i(S~S4w1FYo5N%+Ht^q{H#>QU1eqH&GO`A3uqWvH`JUsmJ<;%){%%4Bs5FH57 zrKP3&_wV1ibLXQ+k5mP`c<~}GE^gnxeTL{@iJmoUR(yOsg!bvvM^(VBTemiB*zon& zUmK!>CAv|gM!KhS=gzi+`sU4>hUiKl`i(c<&^@q8ry;tkh<@v>w{*`}UwwrgVu-FP zqCflWv!bFR8ZBG4%*)FQfatq-@2b!TXHm68U%GT@?b@|bQBgnt{IjZn#KgogW5#^& z!3P)~-iThga;37Mq@+Xz2Mi$=XXPFImt$0LC@wDcIEyN`qYDZO?%lhm>`-1_URGAd zUrx*o(c89d!&p>sQCb$;Z$(U(1rIi!SH6(4{A76JN$K0eXzOdgc=2N89rNbRi;Rq< zQJ9n&-DzHAn_0duUS45T`#BsOKXe1%jhAI?O#Y12jz#`GOwiL+o_ zg*6&qUhHG^m1N~T*l;d+$ua3^+qNxN*V}mi;)^eIyS%Wttl;^j24j^kyeKIv%YRNI zZouly!!ddc@hKrm^pht~x^(HH+jquo<9xDykGjk-Y5q^*+m=-AwbB=ovF+ z96fqe+2O!}19Rrg35@8Z#YdAC9+9U52M)y03O%B;va)1Er>y)vb^6!24t1sumg$qZH9H}bc`Gf4^f9u6zJf571({r?Z z+v*@12BAPKWe*G+HmtC)@ZrOUuf6t~8=^OF-hA-jL3vvGG+)}BKjwsYd##LzDu1EB z+kyoPly}UYJv%x&TDN(Xe$WQJdiAomh^kQFGsbq*YL>m~Q4bg^ZLxCaM5N%+HHbfhu z4GhtLU7~R+`RgG6oK3v(MckkW^v8;DP z|Ni|A(Z2EBci)L18e!3d2@^DZi2mIW4LWx0hz!9H?F3|ipMLtOJYBzj{iI2gw8p1l z!-g)2M*4+T+z{;qG-s11PnM@gjvPVvMFZ&<8E`I%M&t3xC!ZLioiKg+bZb60Gr%|B zd_#kjh~IwuP1yl?jfe^b=si3v6Ot8FD1)S_y?ggo8qu(J?%X*K#qps-hscmsc7Q&v z4d(Oh+qabk@4WMlb(&taYLzwpX5+?U%0lAG*@r*r9v6ckj+vmIKC)9a}j>Bfv#`uYyL;o;@We?&QgnEUGj_q^ynf;*6ZA zcC%?b+8c52m;K5YM7}TUhl7lyRjXFkoz$)28--|}$d6A$k*?dTSFhf_eY^F`vyc0e z5{IOpKa!Rz>lqU6WA2U`m_L*-8$AAR)E{rmSlycvwa z8#iv)W=~SkgLS!Cvt}Y8lPQu>Y`YjD?Bi9sO_E{!u#Mz*iudZh_uivno6p0VrXRY8 zjsD}uj~zAyVs)ghKCNH)H0+MnB9r5;?10Ikk0fbKsts^qDt|E~HR{ZnGtOHQ0YXSU zG%)`9_U)@Is9U!#ta&rC$7!Aj-F9%^Kwhs^$dnBL?^(;5t~MDc97YLkIp^)ic3YQ`H(&2T=8j z@m0B>f``k2N1Thqw&f1SwU02t_B#-4!q0BX=$jsb!Y+wMna}tLkZ6MQ z9!jvGLx&Q<6g35M0Q4?_5$&KX{qoB%acT!ZG*yTZk`<+9Iebck1`W6X9-cDf4D$5Y zv16R3jw^lq@y9NSM%O{j&H#xfXzkInbm>yj_eK17I*JI?5vYnJD*$b-OQOlWP+mav zo```rqL~nF)jUixF`egghgrkb6sPt$jI~{651BU@kJhyg{fSO&Ks4);Xi&R$?Ldge z(tzlSk$*~$>UQ?|c({xB&-C1w`?{xHyLKl|oKSW^4RfZ}APS755!JUa1>KYvV9ZJt z;zzr~!<$aI@Q;yqr|uS!iDbuELGj^>^+v%p4@jry`0?WsOA}B4PNn-m}%$xG9 ze1h>G}4H1Q`{plcGanmlS!Q*<&tJzWJ2=z=suv?2O0O|;BqH4dyj9;v1E>eUnL zLTU-48k9~CV9c;_Lx}FNCP5dQ9lwdjc0qTmY8>XhgtDM(*RJ9n6)TpDVO#Yc{iZfd zQDJ-hGQj7bf6f*1n`jpIH{X0y)i_{TB34%x;Jv|h=W|OU>Irr?+L~qut1Ni;-FIY-=2_n#prf7T^v=67P~|0lRiKHOhkb-+w>AEjWt?yJ}Ul039AX5&SvisryAV znt7Ry#a+b!4~S^VJ*q(4Q>PpfjhrhWQQ`rj0w-Er+T=;_Mxky#>)4P3@QzA; zl}qT~?1rMs(RGlDBCgR81$&T6JWK#BkYkieMiOR-HZVjRq74kuhG;{yfgw7i@jvI% 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 016/101] 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 017/101] 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 018/101] 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 019/101] 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 020/101] 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 021/101] 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 022/101] 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 023/101] 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 024/101] 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 025/101] 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 zcmV+o4f67dP)Onvs!_ot<4% zQ*-Ce9rWJb-s$P-?d|R0xVX5aq@-iVj)7NvEsc$h;5l>V(96&!GBT3pg$ozJAt50t zDJeH^-UN$Wy?WKx*SDde;r8v@ckkZy^76{f%`GS>IC}J`7=(w1FI~E{v$GT2+SL4Er489aFKrAwEF4f5h=EEG#%H8-Uh^F2=^jg@uLUzjk(ZI5k*Fd3pK#`SWdUZSUW|&*FXh zwM0Zj^y}B}?Afz7ZroVEem!eeQBi?GKtRBZ88a|o#~2wIEnd91w-W8>=$Mw4CYb#T z(e?H9;A_{eIXE~dGZv$=vQn^=)fK0cnHAy|70 zcn8Q6Jvo3(Tq~K?V#MP+VMWXi2!(ym|8jN<*8htSkU;O9%o?ojR3pWN0~Q(xkw^ zK&|H9y?d+^FGgvao0}8OnI7Y)_vGx^vwJJi#6g~f4~doE2(Ll)v6VuexL}XytDO08pLozcn4ZBR7IFSW7apHu!FJBjgU}Cfw3>h+nbqx&-WgH3j zh=+$qZzWp2_qC!tSH9bnJ}4*%jiJQG#%gT{`mn>^Cq585zO@UH-P6-k)&uF^zyHXQ zBV{*;3>`X@_p@A8`svfBM@2>TN}_qK-!c%C%7B2Y^z3fK5e#kE?|kq*U}G4LU0q#O zN5heMi*kNoKI{NJGyHDC8Vj6TqEbY1fQCp)`zeZUPH zvuf&^84!H9{p1HJ8@2j|y)EGS#)D}Yp90GFG!&IoG&Q$z(CiY?t!*c^7wokEy<^WL zx~28lSo@~|2M>K=`M9+xw{9ze6lGITw0r07T0m~zj@tVDkg{!i>HdR9&{WklZOP8x zx4&I1n-6^ct0!zt9<}6ncU98frqybSzTij^FhXs zaV%UE(p8Vm`)xH2x0GwVS|0Dcw_7!Eje__!?R-{-ysV4Z?O zmdFH9;^VmdkDO@Rk>g+%r=tmXF0Ssvi_Mc`;q*DD7lkh&ae2(UfLCAlX3OucsCRh! z`IHYgX$OpcJ6=Fs{8|B#%VQZWK4G0Sab%^J?ZP?hlkFQfebNm?J5GF|#}YlpZh|&s zGgV<0J&wlS*Lo`+4e)qvPGNC5aih9+pX&6w)oVT!%-&=k@|^1Z$!yqt%3uEXQHEeo zFF$suM8;XK{6&-*n{&hdyMH~cS* z9-w~iT)%!jAt8aEJ~Xv@hUMS9kf0|l-5^4=o*8~0#qo<5FX}|=wGLe#2GO0JoxHnr zLLYijslK6UW$Y@zPxk1Bgr%ag@w0u0B_kk5pxPpNDhEIvNgr(Bl3)PY7ococ4PBHC zg#v9C`2T_PrO)@&| zRk8%h9+|T;)80h@U*bwhvQxUV8nCBouT)ODfoQ4$QfJ}&f1r^mhsChpkb0n$l7b{D zlv*gSpyV^2Lf2#FR%?<{$(_cHe^$+J!ZS{!KO7jdP9(~VU^ng=8RxY>`$iYpXHh`tlowwn*O=+%$sj7nVFZdkBp|HK z=&=qonH!dpF3D9ANQQlW(lh{iDktcFY4xNwi+O6ob4&v>y36FL6q37>E8$!3tauN7 zU{FLi5H0>IxfWFgu#{99KRX1c+Ju;^tn>Ug|0zkMxS`etKdShuUAueHs*1t@{x3vV zWp{NE-IWB<7iG|e1!YB4A{8NCbP*vEAt53}AJPYXP(fi9A_XELIt|)5(>TpE77SxU zGET`jjfD(pLtS*&j~-YYoGudIF4Fz7*sQbPv-jEi*=wzH*0cB61POfuf;Eq>!xh}K z>R~!-f>+W@KBPAp+Vd~G=28}i9-_Ro^-c93@7T5BnN8M!Z7SKXl@38s7N?2zMlkKz zxf@@Rkz?aLNImN@{!N=-{9xNo&t1RaIhUD&J}sV3N{lK!Wbkji^?n>Z)CbT#t~WqS zLp>(C)RQ>}wQ}U{ot7j)1LCN)N@s}~27S427Nfj!*X};bp1ofP6y1e^1;zRy14M~R zxS!p)MUC}HrVds}RYU&AqtkPjIkFf4lDnHux}{Uzd2f3~!ZMDMvGGY|ET9HYVgzA4 zdINM;V23(ENUq%RqufgI<{Sw z)@K@_pG*&TbJ?;hSFc&?vP4KN#DT~RdF8dsZ@S^e6_#iD^|v5`)ztm$&SmI)5DSi> zItgc2UVY7VI3TgLxUEDaJ1_B!&-U!!w*8Z?sj^}*SG>wNEfrf`0^?xit0ZBnujiZHIuMw{)(^FMn*~3`AvN=QIFIggPOY@!`pt%@?{qkVP zBH)NrK^{mTaRag*M3TgW*@bnH+DKs@eCRPbH$<&qk9s1~ieR{Kke(%8-+b$xs4k4Q8(w|=U2Bk; zvUo+ibGjy4T~z@KL=FM~09cAJInCGG>vb2QA-6sfYeq|PDa~n z+iX5Tr;bFEH_$-)YjEOrCj!+A|ppTJumj6 zx|(J3n3@JwEzehlC(@jOv8-{G6Y6 z(=BRjXz1`bd(4LtbLw9|Ke)29f8jFfxISHOtgCa_8%thtR)WDu#XLAfGtbvM7rT8u z-TaJu12`q4qoWfN5`=5?n`3Sxu_z*XtbR~?)4$n?o+^}*5*Ifth+W+tj0_GKio(lV zSzg9NKA6rQ87ePs016c0%y{ptHl?;5Dn(W-9uTafz zsIT|B+8JJLb^L17IU*JtA3tpNnvcm$?$Ka83wwM2>%-Y;(Di(kmcO50+Vnk&b?I!> z%dhXbOrE0kQpl@1cVf^>9Zc?}-W# zkWepENMBqJrl}-+1c*I7JsaYWTr)ZJT%PZ*xZ?;|R2f~;B|nTM_4W0oq~Jp_MdZjl zPnW*ApDd`N3$z8*JQEDT;d~q%Q}d~A&QG^z3kHL)NfiT6WdOn&oVFqGH2KN@E!@-}cPTTem z4lHtuTl=c)G)x6l7&(EiKE$J=Z(j-*wdfkZn)DVZ$Fsm|4O)3E zMtI1Fiq#4wr+yq)FJ*EX{My@_XVn7E`7}nG5(4q-mdMu2IkO&lHuNr=Uku?_@G_-+ z=v+p&yhy-oVN49DoA23b08_EMG;_Vv8)2A)|IN`Q2ON6>^_&b$OK?vsr*`a z+U)XQmkXY^9=FW}>V6kw@uUYwNqBsXP$^kO7ⅈ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 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 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 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 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 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

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 026/101] 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 027/101] 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 028/101] 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 029/101] 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 030/101] 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 031/101] 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 032/101] 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 033/101] 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 034/101] 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 035/101] 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 036/101] 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 037/101] 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 038/101] 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 039/101] 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 040/101] 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 041/101] 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 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&&x0000P)Zz|vAK5kVBOA{dNE;uWp8CZ6%O8jTw3edDe1 zzOX7PN2?%Ep&pUIbrM)uJBlB`&QqGS0Q*cX5;?tf+q&^I*G|Fuk zbYP%;aZy~;Gci`kH*NId+Hc>I`T6yL@>m^y<+7xiTxQQ6_~dcKlEp(>3e-|PrMxVj zYlr`GdH;hd(XpfcHVPVml9F7uZ1(=;Bx`FDxkhA=NYs<*b+w7j}X zs+FFnPX%)A`oh@z_d>5;k(8Cj+bHPu>xLW3Ckcgi?Do(>(b6SDH>@8X8{6-tcC2+K zsskhD%^PI;G8s`p*36wd2ugj>3l@0n-{<#6H!dW^@!D1C)Tu7pwhCZJm+jjG${oU| zPa+y+hXS%|r;kE0`0ydoi|3J{p^h*@U$}5E6kYx}z}>vb`@#KCb5%lkc%Nz0T-A@l zt4s+PcBj$@dMl{WAB*D>6J4ukc9B+IGAKbAa* zpt}_`(v!89NE}!V@%FaE@BH~5pp5u=d3EDbMx{nXp&)n&Fcc4u&dd#|)By?x2j3dY z6j#@d@Ct7~du9~uixv$*Akhn+w*t^WgG|ZF9E0qD>#W!X2ls}8Ee?$8uT2Je?Hc}x zP95-zL6Ff<3JYQzJ+urP))fhR z{+yVB_VMXn@@eATJ0Y-Fn-c;8>`{DS7YOW7h1RU*BR24S&YXeRSJ<$yW2doQ&|9|% zPzPB?;9!K>w{Nc>D!{B+?(iN@k!v_MG4JsRb|ewHAgPN!Ou)%u&u(8#8_{}}7O{L8 z4|Ns|(Cp-i0K_0UdBClkGK_%Yj-|sYKKBA6EQRUQT>+YTg3}0!58j^pItH5?;O^Ki z#EB43wVEij#1e!Z4c4vYYqFw{*Y!_C^oOOX3mJ{lPk&hc`h`6?D98at&rlM!9_<$x5z7@P}tT$t5m9#l$7M;Z|$4cOI=wM z#{Y}tF(IUog!F_I@)FWtLP9zrg;dK}!7lb*#|jqI5$xEoBX&i^-ay5Ih`k~zA}aG^ z7929ySTaLMa@p(Qu+BdFoO|!L*IxUqZ-4g>o%ZY3udl4EjE#-S>9c3g1Zct>;5Iim zr5%UU)zww}J9q8?^!WJr;Nale+M4#Lfq{WnuU?5iIXNkVF*GzpI}Q_NWMo8!T$?nv zZrxg1TGA_^eSLj7Uc7jb_|*YAF)`86(J>$(Km&SeYU;y>4|R2Q&!0c<>+54AK7anq zY`A&zCWs5r&d$#M{{BF}73i3lnDq2?YinzUmOh|KqnDRgR#q0t%HG~SCnv|m#6%B) zuC1+o|NebMM8xgexAXJ!b$|x?_4V}=Cr)TU`}z4X131md$gs4uoa#h0m>xKAKyr?l zo0}VBTv}Qx&#$Se!O_Fik(-BCA$%;L<>&VK#oN6W&(0)A&_=hts9)6>(M z&)9I4+Zk8-vs$WxMHMW{6wnH2WeR8ov;z7E8g=vX^0ET@KN>Hs94P_;QQoN+7Z-&R zk~e57Iw6lJD0tQ7J(^Q`(yoBslTj=f8ymypz=&2=RV61U-@kvKc6cITnp|C7;gxJ` zY_hYnDIL$8IRh6#Ns^kH8W$I5VPR2GQK5j|-5@TY&2X%&tN)I$B%13lT{8L zJg9Q?cNmlf`&Dp5FJHa{Xp--#Q>WmXVVvQYp(el`&@(eLy}iBZ|McloG6t#R&6_vO z2KXa5KPV^oAO-X;#&!`T*~gC`0a_MD2t1A&?*pk1XCyvpT6Py%2FuIK87F*jhlEo= z@5$KIfS*(^%Rd%q)Lxxup3-sWyWpFQjEs;Yy?gf##SFzHCFpm3E@jbPq>+f((3a=}(C9Qc78Dc! z84i*cBIxejyF1?n>jd43vv1!%j?K-@4Gj(3br$wY$DkF_;=pZv^A_kZm_-~!CTPj@ zCv{MB6QSZmE#13!ue!S0-Q69AS~3Z&2s;B?Z$zr9`E-C{h2xe7GBco7;7kr3I)twy z{-cIMat5h_k7|<11dj+q5FH&2IsW$TTf}ULn|O|cg9D$ATut^R(4WwUaiSaI5VU>h zbzo_JKG3AOr7gBI(8rD)BRLRO$hv5G{rWYu3H&vZrR{)*QziOfNRV1HCt$LW-N0CJ z>GI{vI(NYxa*3~j#7UE=F+%8d&YwRonrp~$`C_n)U0q$k&9tM>s@i19v_(U`;eYEF$czEDw zYDye7B^VWk|Mcn80<_3b88%rANj}5}Uy1J~2?S*!4)M-j((eH@#3`FzEo~BMcRpofO2M2biO(U{M8& 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 042/101] 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 043/101] 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 044/101] 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] From 1b2719c94c6422112508cfad24bdd9504541c363 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 29 Jan 2025 15:20:30 +0100 Subject: [PATCH 045/101] Resolve bound name of bare import statically (#5773) --- crates/typst-eval/src/import.rs | 49 ++++++++--- crates/typst-ide/src/matchers.rs | 88 +++++++++++-------- crates/typst-library/src/foundations/mod.rs | 4 +- .../typst-library/src/foundations/module.rs | 28 ++++-- crates/typst-library/src/foundations/scope.rs | 9 +- crates/typst-library/src/foundations/value.rs | 15 ++-- crates/typst-library/src/lib.rs | 4 +- crates/typst-library/src/pdf/mod.rs | 2 +- crates/typst-syntax/src/ast.rs | 52 ++++++++++- tests/suite/scripting/import.typ | 66 +++++++++++++- tests/suite/scripting/modules/with space.typ | 1 + 11 files changed, 234 insertions(+), 84 deletions(-) create mode 100644 tests/suite/scripting/modules/with space.typ diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 2060d25f1..2bbc7e41c 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -6,7 +6,7 @@ use typst_library::diag::{ use typst_library::engine::Engine; use typst_library::foundations::{Content, Module, Value}; use typst_library::World; -use typst_syntax::ast::{self, AstNode}; +use typst_syntax::ast::{self, AstNode, BareImportError}; use typst_syntax::package::{PackageManifest, PackageSpec}; use typst_syntax::{FileId, Span, VirtualPath}; @@ -16,11 +16,11 @@ impl Eval for ast::ModuleImport<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - let source = self.source(); - let source_span = source.span(); - let mut source = source.eval(vm)?; - let new_name = self.new_name(); - let imports = self.imports(); + let source_expr = self.source(); + let source_span = source_expr.span(); + + let mut source = source_expr.eval(vm)?; + let mut is_str = false; match &source { Value::Func(func) => { @@ -32,6 +32,7 @@ impl Eval for ast::ModuleImport<'_> { Value::Module(_) => {} Value::Str(path) => { source = Value::Module(import(&mut vm.engine, path, source_span)?); + is_str = true; } v => { bail!( @@ -42,9 +43,12 @@ impl Eval for ast::ModuleImport<'_> { } } + // Source itself is imported if there is no import list or a rename. + let bare_name = self.bare_name(); + let new_name = self.new_name(); if let Some(new_name) = new_name { - if let ast::Expr::Ident(ident) = self.source() { - if ident.as_str() == new_name.as_str() { + if let Ok(source_name) = &bare_name { + if source_name == new_name.as_str() { // Warn on `import x as x` vm.engine.sink.warn(warning!( new_name.span(), @@ -58,12 +62,33 @@ impl Eval for ast::ModuleImport<'_> { } let scope = source.scope().unwrap(); - match imports { + match self.imports() { None => { - // Only import here if there is no rename. if new_name.is_none() { - let name: EcoString = source.name().unwrap().into(); - vm.scopes.top.define(name, source); + match self.bare_name() { + // Bare dynamic string imports are not allowed. + Ok(name) + if !is_str || matches!(source_expr, ast::Expr::Str(_)) => + { + if matches!(source_expr, ast::Expr::Ident(_)) { + vm.engine.sink.warn(warning!( + source_expr.span(), + "this import has no effect", + )); + } + vm.scopes.top.define_spanned(name, source, source_span); + } + Ok(_) | Err(BareImportError::Dynamic) => bail!( + source_span, "dynamic import requires an explicit name"; + hint: "you can name the import with `as`" + ), + Err(BareImportError::PathInvalid) => bail!( + source_span, "module name would not be a valid identifier"; + hint: "you can rename the import with `as`", + ), + // Bad package spec would have failed the import already. + Err(BareImportError::PackageInvalid) => unreachable!(), + } } } Some(ast::Imports::Wildcard) => { diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index b92cbf557..ef8288f2a 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use typst::foundations::{Module, Value}; use typst::syntax::ast::AstNode; -use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode}; +use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use crate::{analyze_import, IdeWorld}; @@ -30,38 +30,38 @@ pub fn named_items( if let Some(v) = node.cast::() { let imports = v.imports(); - let source = node - .children() - .find(|child| child.is::()) - .and_then(|source: LinkedNode| { - Some((analyze_import(world, &source)?, source)) - }); - let source = source.as_ref(); + let source = v.source(); + + let source_value = node + .find(source.span()) + .and_then(|source| analyze_import(world, &source)); + let source_value = source_value.as_ref(); + + let module = source_value.and_then(|value| match value { + Value::Module(module) => Some(module), + _ => None, + }); + + let name_and_span = match (imports, v.new_name()) { + // ```plain + // import "foo" as name + // import "foo" as name: .. + // ``` + (_, Some(name)) => Some((name.get().clone(), name.span())), + // ```plain + // import "foo" + // ``` + (None, None) => v.bare_name().ok().map(|name| (name, source.span())), + // ```plain + // import "foo": .. + // ``` + (Some(..), None) => None, + }; // Seeing the module itself. - if let Some((value, source)) = source { - let site = match (imports, v.new_name()) { - // ```plain - // import "foo" as name; - // import "foo" as name: ..; - // ``` - (_, Some(name)) => Some(name.to_untyped()), - // ```plain - // import "foo"; - // ``` - (None, None) => Some(source.get()), - // ```plain - // import "foo": ..; - // ``` - (Some(..), None) => None, - }; - - if let Some((site, value)) = - site.zip(value.clone().cast::().ok()) - { - if let Some(res) = recv(NamedItem::Module(&value, site)) { - return Some(res); - } + if let Some((name, span)) = name_and_span { + if let Some(res) = recv(NamedItem::Module(&name, span, module)) { + return Some(res); } } @@ -75,7 +75,7 @@ pub fn named_items( // import "foo": *; // ``` Some(ast::Imports::Wildcard) => { - if let Some(scope) = source.and_then(|(value, _)| value.scope()) { + if let Some(scope) = source_value.and_then(Value::scope) { for (name, value, span) in scope.iter() { let item = NamedItem::Import(name, span, Some(value)); if let Some(res) = recv(item) { @@ -92,7 +92,7 @@ pub fn named_items( let bound = item.bound_name(); let (span, value) = item.path().iter().fold( - (bound.span(), source.map(|(value, _)| value)), + (bound.span(), source_value), |(span, value), path_ident| { let scope = value.and_then(|v| v.scope()); let span = scope @@ -175,8 +175,8 @@ pub enum NamedItem<'a> { Var(ast::Ident<'a>), /// A function item. Fn(ast::Ident<'a>), - /// A (imported) module item. - Module(&'a Module, &'a SyntaxNode), + /// A (imported) module. + Module(&'a EcoString, Span, Option<&'a Module>), /// An imported item. Import(&'a EcoString, Span, Option<&'a Value>), } @@ -186,7 +186,7 @@ impl<'a> NamedItem<'a> { match self { NamedItem::Var(ident) => ident.get(), NamedItem::Fn(ident) => ident.get(), - NamedItem::Module(value, _) => value.name(), + NamedItem::Module(name, _, _) => name, NamedItem::Import(name, _, _) => name, } } @@ -194,7 +194,7 @@ impl<'a> NamedItem<'a> { pub(crate) fn value(&self) -> Option { match self { NamedItem::Var(..) | NamedItem::Fn(..) => None, - NamedItem::Module(value, _) => Some(Value::Module((*value).clone())), + NamedItem::Module(_, _, value) => value.cloned().map(Value::Module), NamedItem::Import(_, _, value) => value.cloned(), } } @@ -202,7 +202,7 @@ impl<'a> NamedItem<'a> { pub(crate) fn span(&self) -> Span { match *self { NamedItem::Var(name) | NamedItem::Fn(name) => name.span(), - NamedItem::Module(_, site) => site.span(), + NamedItem::Module(_, span, _) => span, NamedItem::Import(_, span, _) => span, } } @@ -356,7 +356,17 @@ mod tests { #[test] fn test_named_items_import() { - test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]); + test("#import \"foo.typ\"", 2).must_include(["foo"]); + test("#import \"foo.typ\" as bar", 2) + .must_include(["bar"]) + .must_exclude(["foo"]); + } + + #[test] + fn test_named_items_import_items() { + test("#import \"foo.typ\": a; #(a);", 2) + .must_include(["a"]) + .must_exclude(["foo"]); let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);") .with_source("foo.typ", "#import \"a.typ\"") diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 2c3730d53..2921481bc 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -122,8 +122,8 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { if features.is_enabled(Feature::Html) { global.define_func::(); } - global.define_module(calc::module()); - global.define_module(sys::module(inputs)); + global.define("calc", calc::module()); + global.define("sys", sys::module(inputs)); } /// Fails with an error. diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index a476d6af1..2001aca16 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -32,7 +32,7 @@ use crate::foundations::{repr, ty, Content, Scope, Value}; #[allow(clippy::derived_hash_with_manual_eq)] pub struct Module { /// The module's name. - name: EcoString, + name: Option, /// The reference-counted inner fields. inner: Arc, } @@ -52,14 +52,22 @@ impl Module { /// Create a new module. pub fn new(name: impl Into, scope: Scope) -> Self { Self { - name: name.into(), + name: Some(name.into()), + inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), + } + } + + /// Create a new anonymous module without a name. + pub fn anonymous(scope: Scope) -> Self { + Self { + name: None, inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), } } /// Update the module's name. pub fn with_name(mut self, name: impl Into) -> Self { - self.name = name.into(); + self.name = Some(name.into()); self } @@ -82,8 +90,8 @@ impl Module { } /// Get the module's name. - pub fn name(&self) -> &EcoString { - &self.name + pub fn name(&self) -> Option<&EcoString> { + self.name.as_ref() } /// Access the module's scope. @@ -105,8 +113,9 @@ impl Module { /// Try to access a definition in the module. pub fn field(&self, name: &str) -> StrResult<&Value> { - self.scope().get(name).ok_or_else(|| { - eco_format!("module `{}` does not contain `{name}`", self.name()) + self.scope().get(name).ok_or_else(|| match &self.name { + Some(module) => eco_format!("module `{module}` does not contain `{name}`"), + None => eco_format!("module does not contain `{name}`"), }) } @@ -131,7 +140,10 @@ impl Debug for Module { impl repr::Repr for Module { fn repr(&self) -> EcoString { - eco_format!("", self.name()) + match &self.name { + Some(module) => eco_format!(""), + None => "".into(), + } } } diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index b51f8caaf..99c9a37e6 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -12,8 +12,8 @@ use typst_utils::Static; use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ - Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, - NativeType, Type, Value, + Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, + Type, Value, }; use crate::Library; @@ -252,11 +252,6 @@ impl Scope { self.define(data.name, Element::from(data)); } - /// Define a module. - pub fn define_module(&mut self, module: Module) { - self.define(module.name().clone(), module); - } - /// Try to access a variable immutably. pub fn get(&self, var: &str) -> Option<&Value> { self.map.get(var).map(Slot::read) diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 8d9f59332..d99027728 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -181,16 +181,6 @@ impl Value { } } - /// The name, if this is a function, type, or module. - pub fn name(&self) -> Option<&str> { - match self { - Self::Func(func) => func.name(), - Self::Type(ty) => Some(ty.short_name()), - Self::Module(module) => Some(module.name()), - _ => None, - } - } - /// Try to extract documentation for the value. pub fn docs(&self) -> Option<&'static str> { match self { @@ -730,6 +720,11 @@ mod tests { assert_eq!(value.into_value().repr(), exp); } + #[test] + fn test_value_size() { + assert!(std::mem::size_of::() <= 32); + } + #[test] fn test_value_debug() { // Primitives. diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 2ea77eaa5..22f3a62a3 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -244,7 +244,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { self::model::define(&mut global); self::text::define(&mut global); global.reset_category(); - global.define_module(math); + global.define("math", math); self::layout::define(&mut global); self::visualize::define(&mut global); self::introspection::define(&mut global); @@ -253,7 +253,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { self::pdf::define(&mut global); global.reset_category(); if features.is_enabled(Feature::Html) { - global.define_module(self::html::module()); + global.define("html", self::html::module()); } prelude(&mut global); Module::new("global", global) diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs index 669835d4c..ec0754631 100644 --- a/crates/typst-library/src/pdf/mod.rs +++ b/crates/typst-library/src/pdf/mod.rs @@ -13,7 +13,7 @@ pub static PDF: Category; /// Hook up the `pdf` module. pub(super) fn define(global: &mut Scope) { global.category(PDF); - global.define_module(module()); + global.define("pdf", module()); } /// Hook up all `pdf` definitions. diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 014e8392e..640138e77 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -4,11 +4,14 @@ use std::num::NonZeroUsize; use std::ops::Deref; +use std::path::Path; +use std::str::FromStr; use ecow::EcoString; use unscanny::Scanner; -use crate::{is_newline, Span, SyntaxKind, SyntaxNode}; +use crate::package::PackageSpec; +use crate::{is_ident, is_newline, Span, SyntaxKind, SyntaxNode}; /// A typed AST node. pub trait AstNode<'a>: Sized { @@ -2064,6 +2067,41 @@ impl<'a> ModuleImport<'a> { }) } + /// The name that will be bound for a bare import. This name must be + /// statically known. It can come from: + /// - an identifier + /// - a field access + /// - a string that is a valid file path where the file stem is a valid + /// identifier + /// - a string that is a valid package spec + pub fn bare_name(self) -> Result { + match self.source() { + Expr::Ident(ident) => Ok(ident.get().clone()), + Expr::FieldAccess(access) => Ok(access.field().get().clone()), + Expr::Str(string) => { + let string = string.get(); + let name = if string.starts_with('@') { + PackageSpec::from_str(&string) + .map_err(|_| BareImportError::PackageInvalid)? + .name + } else { + Path::new(string.as_str()) + .file_stem() + .and_then(|path| path.to_str()) + .ok_or(BareImportError::PathInvalid)? + .into() + }; + + if !is_ident(&name) { + return Err(BareImportError::PathInvalid); + } + + Ok(name) + } + _ => Err(BareImportError::Dynamic), + } + } + /// The name this module was assigned to, if it was renamed with `as` /// (`renamed` in `import "..." as renamed`). pub fn new_name(self) -> Option> { @@ -2074,6 +2112,18 @@ impl<'a> ModuleImport<'a> { } } +/// Reasons why a bare name cannot be determined for an import source. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BareImportError { + /// There is no statically resolvable binding name. + Dynamic, + /// The import source is not a valid path or the path stem not a valid + /// identifier. + PathInvalid, + /// The import source is not a valid package spec. + PackageInvalid, +} + /// The items that ought to be imported from a file. #[derive(Debug, Copy, Clone, Hash)] pub enum Imports<'a> { diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ index 95214db76..03e2efc6b 100644 --- a/tests/suite/scripting/import.typ +++ b/tests/suite/scripting/import.typ @@ -145,6 +145,34 @@ #test(module.item(1, 2), 3) #test(module.push(2), 3) +--- import-from-file-bare-invalid --- +// Error: 9-33 module name would not be a valid identifier +// Hint: 9-33 you can rename the import with `as` +#import "modules/with space.typ" + +--- import-from-file-bare-dynamic --- +// Error: 9-26 dynamic import requires an explicit name +// Hint: 9-26 you can name the import with `as` +#import "mod" + "ule.typ" + +--- import-from-var-bare --- +#let p = "module.typ" +// Error: 9-10 dynamic import requires an explicit name +// Hint: 9-10 you can name the import with `as` +#import p +#test(p.b, 1) + +--- import-from-dict-field-bare --- +#let d = (p: "module.typ") +// Error: 9-12 dynamic import requires an explicit name +// Hint: 9-12 you can name the import with `as` +#import d.p +#test(p.b, 1) + +--- import-from-file-renamed-dynamic --- +#import "mod" + "ule.typ" as mod +#test(mod.b, 1) + --- import-from-file-renamed --- // A renamed module import without items. #import "module.typ" as other @@ -160,6 +188,10 @@ #test(item(1, 2), 3) #test(newname.item(1, 2), 3) +--- import-from-function-scope-bare --- +// Warning: 9-13 this import has no effect +#import enum + --- import-from-function-scope-renamed --- // Renamed module import with function scopes. #import enum as othernum @@ -171,6 +203,23 @@ #import asrt: ne as asne #asne(1, 2) +--- import-from-module-bare --- +#import "modules/chap1.typ" as mymod +// Warning: 9-14 this import has no effect +#import mymod +// The name `chap1` is not bound. +// Error: 2-7 unknown variable: chap1 +#chap1 + +--- import-module-nested --- +#import std.calc: pi +#test(pi, calc.pi) + +--- import-module-nested-bare --- +#import "module.typ" +#import module.chap2 +#test(chap2.name, "Peter") + --- import-module-item-name-mutating --- // Edge case for module access that isn't fixed. #import "module.typ" @@ -214,10 +263,14 @@ // Warning: 31-35 unnecessary import rename to same name #import enum as enum: item as item ---- import-item-rename-unnecessary-but-ok --- -// No warning on a case that isn't obviously pathological +--- import-item-rename-unnecessary-string --- +// Warning: 25-31 unnecessary import rename to same name #import "module.typ" as module +--- import-item-rename-unnecessary-but-ok --- +#import "modul" + "e.typ" as module +#test(module.b, 1) + --- import-from-closure-invalid --- // Can't import from closures. #let f(x) = x @@ -359,6 +412,15 @@ This is never reached. #import "@test/adder:0.1.0" #test(adder.add(2, 8), 10) +--- import-from-package-dynamic --- +// Error: 9-33 dynamic import requires an explicit name +// Hint: 9-33 you can name the import with `as` +#import "@test/" + "adder:0.1.0" + +--- import-from-package-renamed-dynamic --- +#import "@test/" + "adder:0.1.0" as adder +#test(adder.add(2, 8), 10) + --- import-from-package-items --- // Test import with items. #import "@test/adder:0.1.0": add diff --git a/tests/suite/scripting/modules/with space.typ b/tests/suite/scripting/modules/with space.typ new file mode 100644 index 000000000..9138f3c3f --- /dev/null +++ b/tests/suite/scripting/modules/with space.typ @@ -0,0 +1 @@ +// SKIP From 7a0d7092bc00ee4f5c0d4887ea3ccf3fbceb2426 Mon Sep 17 00:00:00 2001 From: TwoF1nger <140991913+TwoF1nger@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:11:03 +0000 Subject: [PATCH 046/101] Fix typo in scripting.md (#5783) --- docs/reference/scripting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/scripting.md b/docs/reference/scripting.md index 6c7a7b338..5e0f1555e 100644 --- a/docs/reference/scripting.md +++ b/docs/reference/scripting.md @@ -363,7 +363,7 @@ and can be achieved using functions from the | `{not in}` | Check if not in collection | Binary | 4 | | `{not}` | Logical "not" | Unary | 3 | | `{and}` | Short-circuiting logical "and" | Binary | 3 | -| `{or}` | Short-circuiting logical "or | Binary | 2 | +| `{or}` | Short-circuiting logical "or" | Binary | 2 | | `{=}` | Assignment | Binary | 1 | | `{+=}` | Add-Assignment | Binary | 1 | | `{-=}` | Subtraction-Assignment | Binary | 1 | From be1fa91a00a9bff6c5eb9744266f252b8cc23fe4 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 30 Jan 2025 14:36:15 +0100 Subject: [PATCH 047/101] Modular, multi-threaded, transitioning plugins (#5779) --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/typst-eval/src/call.rs | 12 +- crates/typst-ide/src/complete.rs | 10 - crates/typst-library/src/foundations/func.rs | 59 +- crates/typst-library/src/foundations/mod.rs | 7 +- .../typst-library/src/foundations/module.rs | 20 +- crates/typst-library/src/foundations/ops.rs | 1 - .../typst-library/src/foundations/plugin.rs | 562 +++++++++++++----- crates/typst-library/src/foundations/scope.rs | 8 + crates/typst-library/src/foundations/value.rs | 11 +- tests/suite/foundations/plugin.typ | 31 + tools/test-helper/package.json | 209 +++---- 13 files changed, 618 insertions(+), 316 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8aa7c0ec1..3343c246b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2766,7 +2766,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.12.0" -source = "git+https://github.com/typst/typst-dev-assets?rev=b07d156#b07d1560143d6883887358d30edb25cb12fcf5b9" +source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c" [[package]] name = "typst-docs" diff --git a/Cargo.toml b/Cargo.toml index 1be7816a7..6b592cd39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } typst-timing = { path = "crates/typst-timing", version = "0.12.0" } typst-utils = { path = "crates/typst-utils", version = "0.12.0" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index f59235c78..2a2223e15 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -6,8 +6,8 @@ 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, SymbolElem, Value, + Arg, Args, Capturer, Closure, Content, Context, Func, NativeElement, Scope, Scopes, + SymbolElem, Value, }; use typst_library::introspection::Introspector; use typst_library::math::LrElem; @@ -315,13 +315,7 @@ fn eval_field_call( (target, args) }; - if let Value::Plugin(plugin) = &target { - // Call plugins by converting args to bytes. - let bytes = args.all::()?; - args.finish()?; - let value = plugin.call(&field, bytes).at(span)?.into_value(); - Ok(FieldCall::Resolved(value)) - } else if let Some(callee) = target.ty().scope().get(&field) { + 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 { diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 0f8abddb7..24b76537a 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -452,16 +452,6 @@ fn field_access_completions( } } } - Value::Plugin(plugin) => { - for name in plugin.iter() { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: name.clone(), - apply: None, - detail: None, - }) - } - } _ => {} } } diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index cb3eba161..a05deb1f3 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -9,11 +9,11 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{ast, Span, SyntaxNode}; use typst_utils::{singleton, LazyHash, Static}; -use crate::diag::{bail, SourceResult, StrResult}; +use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, - Selector, Type, Value, + cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs, + PluginFunc, Scope, Selector, Type, Value, }; /// A mapping from argument values to a return value. @@ -151,6 +151,8 @@ enum Repr { Element(Element), /// A user-defined closure. Closure(Arc>), + /// A plugin WebAssembly function. + Plugin(Arc), /// A nested function with pre-applied arguments. With(Arc<(Func, Args)>), } @@ -164,6 +166,7 @@ impl Func { Repr::Native(native) => Some(native.name), Repr::Element(elem) => Some(elem.name()), Repr::Closure(closure) => closure.name(), + Repr::Plugin(func) => Some(func.name()), Repr::With(with) => with.0.name(), } } @@ -176,6 +179,7 @@ impl Func { Repr::Native(native) => Some(native.title), Repr::Element(elem) => Some(elem.title()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.title(), } } @@ -186,6 +190,7 @@ impl Func { Repr::Native(native) => Some(native.docs), Repr::Element(elem) => Some(elem.docs()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.docs(), } } @@ -204,6 +209,7 @@ impl Func { Repr::Native(native) => Some(&native.0.params), Repr::Element(elem) => Some(elem.params()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.params(), } } @@ -221,6 +227,7 @@ impl Func { Some(singleton!(CastInfo, CastInfo::Type(Type::of::()))) } Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.returns(), } } @@ -231,6 +238,7 @@ impl Func { Repr::Native(native) => native.keywords, Repr::Element(elem) => elem.keywords(), Repr::Closure(_) => &[], + Repr::Plugin(_) => &[], Repr::With(with) => with.0.keywords(), } } @@ -241,6 +249,7 @@ impl Func { Repr::Native(native) => Some(&native.0.scope), Repr::Element(elem) => Some(elem.scope()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.scope(), } } @@ -266,6 +275,14 @@ impl Func { } } + /// Extract the plugin function, if it is one. + pub fn to_plugin(&self) -> Option<&PluginFunc> { + match &self.repr { + Repr::Plugin(func) => Some(func), + _ => None, + } + } + /// Call the function with the given context and arguments. pub fn call( &self, @@ -307,6 +324,12 @@ impl Func { context, args, ), + Repr::Plugin(func) => { + let inputs = args.all::()?; + let output = func.call(inputs).at(args.span)?; + args.finish()?; + Ok(Value::Bytes(output)) + } Repr::With(with) => { args.items = with.1.items.iter().cloned().chain(args.items).collect(); with.0.call(engine, context, args) @@ -425,12 +448,30 @@ impl From for Func { } } +impl From<&'static NativeFuncData> for Func { + fn from(data: &'static NativeFuncData) -> Self { + Repr::Native(Static(data)).into() + } +} + impl From for Func { fn from(func: Element) -> Self { Repr::Element(func).into() } } +impl From for Func { + fn from(closure: Closure) -> Self { + Repr::Closure(Arc::new(LazyHash::new(closure))).into() + } +} + +impl From for Func { + fn from(func: PluginFunc) -> Self { + Repr::Plugin(Arc::new(func)).into() + } +} + /// A Typst function that is defined by a native Rust type that shadows a /// native Rust function. pub trait NativeFunc { @@ -466,12 +507,6 @@ pub struct NativeFuncData { pub returns: LazyLock, } -impl From<&'static NativeFuncData> for Func { - fn from(data: &'static NativeFuncData) -> Self { - Repr::Native(Static(data)).into() - } -} - cast! { &'static NativeFuncData, self => Func::from(self).into_value(), @@ -525,12 +560,6 @@ impl Closure { } } -impl From for Func { - fn from(closure: Closure) -> Self { - Repr::Closure(Arc::new(LazyHash::new(closure))).into() - } -} - cast! { Closure, self => Value::Func(self.into()), diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 2921481bc..a790da4f4 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -25,7 +25,8 @@ mod int; mod label; mod module; mod none; -mod plugin; +#[path = "plugin.rs"] +mod plugin_; mod scope; mod selector; mod str; @@ -56,7 +57,7 @@ pub use self::int::*; pub use self::label::*; pub use self::module::*; pub use self::none::*; -pub use self::plugin::*; +pub use self::plugin_::*; pub use self::repr::Repr; pub use self::scope::*; pub use self::selector::*; @@ -114,11 +115,11 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { global.define_type::(); global.define_type::(); global.define_type::(); - global.define_type::(); global.define_func::(); global.define_func::(); global.define_func::(); global.define_func::(); + global.define_func::(); if features.is_enabled(Feature::Html) { global.define_func::(); } diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 2001aca16..3ee59c106 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -7,14 +7,20 @@ use typst_syntax::FileId; use crate::diag::StrResult; use crate::foundations::{repr, ty, Content, Scope, Value}; -/// An evaluated module, either built-in or resulting from a file. +/// An module of definitions. /// -/// You can access definitions from the module using -/// [field access notation]($scripting/#fields) and interact with it using the -/// [import and include syntaxes]($scripting/#modules). Alternatively, it is -/// possible to convert a module to a dictionary, and therefore access its -/// contents dynamically, using the -/// [dictionary constructor]($dictionary/#constructor). +/// A module +/// - be built-in +/// - stem from a [file import]($scripting/#modules) +/// - stem from a [package import]($scripting/#packages) (and thus indirectly +/// its entrypoint file) +/// - result from a call to the [plugin]($plugin) function +/// +/// You can access definitions from the module using [field access +/// notation]($scripting/#fields) and interact with it using the [import and +/// include syntaxes]($scripting/#modules). Alternatively, it is possible to +/// convert a module to a dictionary, and therefore access its contents +/// dynamically, using the [dictionary constructor]($dictionary/#constructor). /// /// # Example /// ```example diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 7dbdde8ff..6c2408446 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -447,7 +447,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { (Args(a), Args(b)) => a == b, (Type(a), Type(b)) => a == b, (Module(a), Module(b)) => a == b, - (Plugin(a), Plugin(b)) => a == b, (Datetime(a), Datetime(b)) => a == b, (Duration(a), Duration(b)) => a == b, (Dyn(a), Dyn(b)) => a == b, diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index d41261edc..cbc0f52de 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -4,43 +4,27 @@ use std::sync::{Arc, Mutex}; use ecow::{eco_format, EcoString}; use typst_syntax::Spanned; -use wasmi::{AsContext, AsContextMut}; +use wasmi::Memory; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; -use crate::foundations::{func, repr, scope, ty, Bytes}; +use crate::foundations::{cast, func, scope, Bytes, Func, Module, Scope, Value}; use crate::loading::{DataSource, Load}; -/// A WebAssembly plugin. +/// Loads a WebAssembly module. /// -/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin -/// functions may accept multiple [byte buffers]($bytes) as arguments and return -/// a single byte buffer. They should typically be wrapped in idiomatic Typst -/// functions that perform the necessary conversions between native Typst types -/// and bytes. +/// The resulting [module] will contain one Typst [function] for each function +/// export of the loaded WebAssembly module. /// -/// Plugins run in isolation from your system, which means that printing, -/// reading files, or anything like that will not be supported for security -/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit -/// shared WebAssembly library. Many compilers will use the -/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. -/// emscripten), which allows printing, reading files, etc. This ABI will not -/// directly work with Typst. You will either need to compile to a different -/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub). +/// Typst WebAssembly plugins need to follow a specific +/// [protocol]($plugin/#protocol). To run as a plugin, a program needs to be +/// compiled to a 32-bit shared WebAssembly library. Plugin functions may accept +/// multiple [byte buffers]($bytes) as arguments and return a single byte +/// buffer. They should typically be wrapped in idiomatic Typst functions that +/// perform the necessary conversions between native Typst types and bytes. /// -/// # Plugins and Packages -/// Plugins are distributed as packages. A package can make use of a plugin -/// simply by including a WebAssembly file and loading it. Because the -/// byte-based plugin interface is quite low-level, plugins are typically -/// exposed through wrapper functions, that also live in the same package. -/// -/// # Purity -/// Plugin functions must be pure: Given the same arguments, they must always -/// return the same value. The reason for this is that Typst functions must be -/// pure (which is quite fundamental to the language design) and, since Typst -/// function can call plugin functions, this requirement is inherited. In -/// particular, if a plugin function is called twice with the same arguments, -/// Typst might cache the results and call your function only once. +/// For security reasons, plugins run in isolation from your system. This means +/// that printing, reading files, or similar things are not supported. /// /// # Example /// ```example @@ -55,6 +39,50 @@ use crate::loading::{DataSource, Load}; /// #concat("hello", "world") /// ``` /// +/// Since the plugin function returns a module, it can be used with import +/// syntax: +/// ```typ +/// #import plugin("hello.wasm"): concatenate +/// ``` +/// +/// # Purity +/// Plugin functions **must be pure:** A plugin function call most not have any +/// observable side effects on future plugin calls and given the same arguments, +/// it must always return the same value. +/// +/// The reason for this is that Typst functions must be pure (which is quite +/// fundamental to the language design) and, since Typst function can call +/// plugin functions, this requirement is inherited. In particular, if a plugin +/// function is called twice with the same arguments, Typst might cache the +/// results and call your function only once. Moreover, Typst may run multiple +/// instances of your plugin in multiple threads, with no state shared between +/// them. +/// +/// Typst does not enforce plugin function purity (for efficiency reasons), but +/// calling an impure function will lead to unpredictable and irreproducible +/// results and must be avoided. +/// +/// That said, mutable operations _can be_ useful for plugins that require +/// costly runtime initialization. Due to the purity requirement, such +/// initialization cannot be performed through a normal function call. Instead, +/// Typst exposes a [plugin transition API]($plugin.transition), which executes +/// a function call and then creates a derived module with new functions which +/// will observe the side effects produced by the transition call. The original +/// plugin remains unaffected. +/// +/// # Plugins and Packages +/// Any Typst code can make use of a plugin simply by including a WebAssembly +/// file and loading it. However, because the byte-based plugin interface is +/// quite low-level, plugins are typically exposed through a package containing +/// the plugin and idiomatic wrapper functions. +/// +/// # WASI +/// Many compilers will use the [WASI ABI](https://wasi.dev/) by default or as +/// their only option (e.g. emscripten), which allows printing, reading files, +/// etc. This ABI will not directly work with Typst. You will either need to +/// compile to a different target or [stub all +/// functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub). +/// /// # Protocol /// To be used as a plugin, a WebAssembly module must conform to the following /// protocol: @@ -67,8 +95,8 @@ use crate::loading::{DataSource, Load}; /// lengths, so `usize/size_t` may be preferable), and return one 32-bit /// integer. /// -/// - The function should first allocate a buffer `buf` of length -/// `a_1 + a_2 + ... + a_n`, and then call +/// - The function should first allocate a buffer `buf` of length `a_1 + a_2 + +/// ... + a_n`, and then call /// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. /// /// - The `a_1` first bytes of the buffer now constitute the first argument, the @@ -85,19 +113,21 @@ use crate::loading::{DataSource, Load}; /// then interpreted as an UTF-8 encoded error message. /// /// ## Imports -/// Plugin modules need to import two functions that are provided by the runtime. -/// (Types and functions are described using WAT syntax.) +/// Plugin modules need to import two functions that are provided by the +/// runtime. (Types and functions are described using WAT syntax.) /// -/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` +/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func +/// (param i32)))` /// /// Writes the arguments for the current function into a plugin-allocated -/// buffer. When a plugin function is called, it -/// [receives the lengths](#exports) of its input buffers as arguments. It -/// should then allocate a buffer whose capacity is at least the sum of these -/// lengths. It should then call this function with a `ptr` to the buffer to -/// fill it with the arguments, one after another. +/// buffer. When a plugin function is called, it [receives the +/// lengths](#exports) of its input buffers as arguments. It should then +/// allocate a buffer whose capacity is at least the sum of these lengths. It +/// should then call this function with a `ptr` to the buffer to fill it with +/// the arguments, one after another. /// -/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` +/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func +/// (param i32 i32)))` /// /// Sends the output of the current function to the host (Typst). The first /// parameter shall be a pointer to a buffer (`ptr`), while the second is the @@ -106,72 +136,147 @@ use crate::loading::{DataSource, Load}; /// interpreted as an error message, it should be encoded as UTF-8. /// /// # Resources -/// For more resources, check out the -/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). -/// It contains: +/// For more resources, check out the [wasm-minimal-protocol +/// repository](https://github.com/astrale-sharp/wasm-minimal-protocol). It +/// contains: /// /// - A list of example plugin implementations and a test runner for these /// examples /// - Wrappers to help you write your plugin in Rust (Zig wrapper in /// development) /// - A stubber for WASI -#[ty(scope, cast)] -#[derive(Clone)] -pub struct Plugin(Arc); - -/// The internal representation of a plugin. -struct Repr { - /// The raw WebAssembly bytes. - bytes: Bytes, - /// The function defined by the WebAssembly module. - functions: Vec<(EcoString, wasmi::Func)>, - /// Owns all data associated with the WebAssembly module. - store: Mutex, -} - -/// Owns all data associated with the WebAssembly module. -type Store = wasmi::Store; - -/// If there was an error reading/writing memory, keep the offset + length to -/// display an error message. -struct MemoryError { - offset: u32, - length: u32, - write: bool, -} -/// The persistent store data used for communication between store and host. -#[derive(Default)] -struct StoreData { - args: Vec, - output: Vec, - memory_error: Option, +#[func(scope)] +pub fn plugin( + engine: &mut Engine, + /// A path to a WebAssembly file or raw WebAssembly bytes. + /// + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, +) -> SourceResult { + let data = source.load(engine.world)?; + Plugin::module(data).at(source.span) } #[scope] -impl Plugin { - /// Creates a new plugin from a WebAssembly file. - #[func(constructor)] - pub fn construct( - engine: &mut Engine, - /// A path to a WebAssembly file or raw WebAssembly bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). - source: Spanned, - ) -> SourceResult { - let data = source.load(engine.world)?; - Plugin::new(data).at(source.span) +impl plugin { + /// Calls a plugin function that has side effects and returns a new module + /// with plugin functions that are guaranteed to have observed the results + /// of the mutable call. + /// + /// Note that calling an impure function through a normal function call + /// (without use of the transition API) is forbidden and leads to + /// unpredictable behaviour. Read the [section on purity]($plugin/#purity) + /// for more details. + /// + /// In the example below, we load the plugin `hello-mut.wasm` which exports + /// two functions: The `get()` function retrieves a global array as a + /// string. The `add(value)` function adds a value to the global array. + /// + /// We call `add` via the transition API. The call `mutated.get()` on the + /// derived module will observe the addition. Meanwhile the original module + /// remains untouched as demonstrated by the `base.get()` call. + /// + /// _Note:_ Due to limitations in the internal WebAssembly implementation, + /// the transition API can only guarantee to reflect changes in the plugin's + /// memory, not in WebAssembly globals. If your plugin relies on changes to + /// globals being visible after transition, you might want to avoid use of + /// the transition API for now. We hope to lift this limitation in the + /// future. + /// + /// ```typ + /// #let base = plugin("hello-mut.wasm") + /// #assert.eq(base.get(), "[]") + /// + /// #let mutated = plugin.transition(base.add, "hello") + /// #assert.eq(base.get(), "[]") + /// #assert.eq(mutated.get(), "[hello]") + /// ``` + #[func] + pub fn transition( + /// The plugin function to call. + func: PluginFunc, + /// The byte buffers to call the function with. + #[variadic] + arguments: Vec, + ) -> StrResult { + func.transition(arguments) } } +/// A function loaded from a WebAssembly plugin. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct PluginFunc { + /// The underlying plugin, shared by this and the other functions. + plugin: Arc, + /// The name of the plugin function. + name: EcoString, +} + +impl PluginFunc { + /// The name of the plugin function. + pub fn name(&self) -> &str { + &self.name + } + + /// Call the WebAssembly function with the given arguments. + #[comemo::memoize] + #[typst_macros::time(name = "call plugin")] + pub fn call(&self, args: Vec) -> StrResult { + self.plugin.call(&self.name, args) + } + + /// Transition a plugin and turn the result into a module. + #[comemo::memoize] + #[typst_macros::time(name = "transition plugin")] + pub fn transition(&self, args: Vec) -> StrResult { + self.plugin.transition(&self.name, args).map(Plugin::into_module) + } +} + +cast! { + PluginFunc, + self => Value::Func(self.into()), + v: Func => v.to_plugin().ok_or("expected plugin function")?.clone(), +} + +/// A plugin with potentially multiple instances for multi-threaded +/// execution. +struct Plugin { + /// Shared by all variants of the plugin. + base: Arc, + /// A pool of plugin instances. + /// + /// When multiple plugin calls run concurrently due to multi-threading, we + /// create new instances whenever we run out of ones. + pool: Mutex>, + /// A snapshot that new instances should be restored to. + snapshot: Option, + /// A combined hash that incorporates all function names and arguments used + /// in transitions of this plugin, such that this plugin has a deterministic + /// hash and equality check that can differentiate it from "siblings" (same + /// base, different transitions). + fingerprint: u128, +} + impl Plugin { - /// Create a new plugin from raw WebAssembly bytes. + /// Create a plugin and turn it into a module. #[comemo::memoize] #[typst_macros::time(name = "load plugin")] - pub fn new(bytes: Bytes) -> StrResult { + fn module(bytes: Bytes) -> StrResult { + Self::new(bytes).map(Self::into_module) + } + + /// Create a new plugin from raw WebAssembly bytes. + fn new(bytes: Bytes) -> StrResult { let engine = wasmi::Engine::default(); let module = wasmi::Module::new(&engine, bytes.as_slice()) .map_err(|err| format!("failed to load WebAssembly module ({err})"))?; + // Ensure that the plugin exports its memory. + if !matches!(module.get_export("memory"), Some(wasmi::ExternType::Memory(_))) { + bail!("plugin does not export its memory"); + } + let mut linker = wasmi::Linker::new(&engine); linker .func_wrap( @@ -188,58 +293,174 @@ impl Plugin { ) .unwrap(); - let mut store = Store::new(&engine, StoreData::default()); - let instance = linker - .instantiate(&mut store, &module) + let base = Arc::new(PluginBase { bytes, linker, module }); + let instance = PluginInstance::new(&base, None)?; + + Ok(Self { + base, + snapshot: None, + fingerprint: 0, + pool: Mutex::new(vec![instance]), + }) + } + + /// Execute a function with access to an instsance. + fn call(&self, func: &str, args: Vec) -> StrResult { + // Acquire an instance from the pool (potentially creating a new one). + let mut instance = self.acquire()?; + + // Execute the call on an instance from the pool. If the call fails, we + // return early and _don't_ return the instance to the pool as it might + // be irrecoverably damaged. + let output = instance.call(func, args)?; + + // Return the instance to the pool. + self.pool.lock().unwrap().push(instance); + + Ok(output) + } + + /// Call a mutable plugin function, producing a new mutable whose functions + /// are guaranteed to be able to observe the mutation. + fn transition(&self, func: &str, args: Vec) -> StrResult { + // Derive a new transition hash from the old one and the function and arguments. + let fingerprint = typst_utils::hash128(&(self.fingerprint, func, &args)); + + // Execute the mutable call on an instance. + let mut instance = self.acquire()?; + + // Call the function. If the call fails, we return early and _don't_ + // return the instance to the pool as it might be irrecoverably damaged. + instance.call(func, args)?; + + // Snapshot the instance after the mutable call. + let snapshot = instance.snapshot(); + + // Create a new plugin and move (this is important!) the used instance + // into it, so that the old plugin won't observe the mutation. Also + // save the snapshot so that instances that are initialized for the + // transitioned plugin's pool observe the mutation. + Ok(Self { + base: self.base.clone(), + snapshot: Some(snapshot), + fingerprint, + pool: Mutex::new(vec![instance]), + }) + } + + /// Acquire an instance from the pool (or create a new one). + fn acquire(&self) -> StrResult { + // Don't use match to ensure that the lock is released before we create + // a new instance. + if let Some(instance) = self.pool.lock().unwrap().pop() { + return Ok(instance); + } + + PluginInstance::new(&self.base, self.snapshot.as_ref()) + } + + /// Turn a plugin into a Typst module containing plugin functions. + fn into_module(self) -> Module { + let shared = Arc::new(self); + + // Build a scope from the collected functions. + let mut scope = Scope::new(); + for export in shared.base.module.exports() { + if matches!(export.ty(), wasmi::ExternType::Func(_)) { + let name = EcoString::from(export.name()); + let func = PluginFunc { plugin: shared.clone(), name: name.clone() }; + scope.define(name, Func::from(func)); + } + } + + Module::anonymous(scope) + } +} + +impl Debug for Plugin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Plugin(..)") + } +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.base.bytes == other.base.bytes && self.fingerprint == other.fingerprint + } +} + +impl Hash for Plugin { + fn hash(&self, state: &mut H) { + self.base.bytes.hash(state); + self.fingerprint.hash(state); + } +} + +/// Shared by all pooled & transitioned variants of the plugin. +struct PluginBase { + /// The raw WebAssembly bytes. + bytes: Bytes, + /// The compiled WebAssembly module. + module: wasmi::Module, + /// A linker used to create a `Store` for execution. + linker: wasmi::Linker, +} + +/// An single plugin instance for single-threaded execution. +struct PluginInstance { + /// The underlying wasmi instance. + instance: wasmi::Instance, + /// The execution store of this concrete plugin instance. + store: wasmi::Store, +} + +/// A snapshot of a plugin instance. +struct Snapshot { + /// The number of pages in the main memory. + mem_pages: u32, + /// The data in the main memory. + mem_data: Vec, +} + +impl PluginInstance { + /// Create a new execution instance of a plugin, potentially restoring + /// a snapshot. + #[typst_macros::time(name = "create plugin instance")] + fn new(base: &PluginBase, snapshot: Option<&Snapshot>) -> StrResult { + let mut store = wasmi::Store::new(base.linker.engine(), CallData::default()); + let instance = base + .linker + .instantiate(&mut store, &base.module) .and_then(|pre_instance| pre_instance.start(&mut store)) .map_err(|e| eco_format!("{e}"))?; - // Ensure that the plugin exports its memory. - if !matches!( - instance.get_export(&store, "memory"), - Some(wasmi::Extern::Memory(_)) - ) { - bail!("plugin does not export its memory"); + let mut instance = PluginInstance { instance, store }; + if let Some(snapshot) = snapshot { + instance.restore(snapshot); } - - // Collect exported functions. - let functions = instance - .exports(&store) - .filter_map(|export| { - let name = export.name().into(); - export.into_func().map(|func| (name, func)) - }) - .collect(); - - Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) }))) + Ok(instance) } - /// Call the plugin function with the given `name`. - #[comemo::memoize] - #[typst_macros::time(name = "call plugin")] - pub fn call(&self, name: &str, args: Vec) -> StrResult { - // Find the function with the given name. - let func = self - .0 - .functions - .iter() - .find(|(v, _)| v == name) - .map(|&(_, func)| func) - .ok_or_else(|| { - eco_format!("plugin does not contain a function called {name}") - })?; + /// Call a plugin function with byte arguments. + fn call(&mut self, func: &str, args: Vec) -> StrResult { + let handle = self + .instance + .get_export(&self.store, func) + .unwrap() + .into_func() + .unwrap(); + let ty = handle.ty(&self.store); - let mut store = self.0.store.lock().unwrap(); - let ty = func.ty(store.as_context()); - - // Check function signature. + // Check function signature. Do this lazily only when a function is called + // because there might be exported functions like `_initialize` that don't + // match the schema. if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) { bail!( - "plugin function `{name}` has a parameter that is not a 32-bit integer" + "plugin function `{func}` has a parameter that is not a 32-bit integer" ); } if ty.results() != [wasmi::core::ValType::I32] { - bail!("plugin function `{name}` does not return exactly one 32-bit integer"); + bail!("plugin function `{func}` does not return exactly one 32-bit integer"); } // Check inputs. @@ -260,23 +481,26 @@ impl Plugin { .collect::>(); // Store the input data. - store.data_mut().args = args; + self.store.data_mut().args = args; // Call the function. let mut code = wasmi::Val::I32(-1); - func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) + handle + .call(&mut self.store, &lengths, std::slice::from_mut(&mut code)) .map_err(|err| eco_format!("plugin panicked: {err}"))?; + if let Some(MemoryError { offset, length, write }) = - store.data_mut().memory_error.take() + self.store.data_mut().memory_error.take() { return Err(eco_format!( - "plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}", + "plugin tried to {kind} out of bounds: \ + pointer {offset:#x} is out of bounds for {kind} of length {length}", kind = if write { "write" } else { "read" } )); } // Extract the returned data. - let output = std::mem::take(&mut store.data_mut().output); + let output = std::mem::take(&mut self.store.data_mut().output); // Parse the functions return value. match code { @@ -293,39 +517,63 @@ impl Plugin { Ok(Bytes::new(output)) } - /// An iterator over all the function names defined by the plugin. - pub fn iter(&self) -> impl Iterator { - self.0.functions.as_slice().iter().map(|(func_name, _)| func_name) + /// Creates a snapshot of this instance from which another one can be + /// initialized. + #[typst_macros::time(name = "save snapshot")] + fn snapshot(&self) -> Snapshot { + let memory = self.memory(); + let mem_pages = memory.size(&self.store); + let mem_data = memory.data(&self.store).to_vec(); + Snapshot { mem_pages, mem_data } + } + + /// Restores the instance to a snapshot. + #[typst_macros::time(name = "restore snapshot")] + fn restore(&mut self, snapshot: &Snapshot) { + let memory = self.memory(); + let current_size = memory.size(&self.store); + if current_size < snapshot.mem_pages { + memory + .grow(&mut self.store, snapshot.mem_pages - current_size) + .unwrap(); + } + + memory.data_mut(&mut self.store)[..snapshot.mem_data.len()] + .copy_from_slice(&snapshot.mem_data); + } + + /// Retrieves a handle to the plugin's main memory. + fn memory(&self) -> Memory { + self.instance + .get_export(&self.store, "memory") + .unwrap() + .into_memory() + .unwrap() } } -impl Debug for Plugin { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("Plugin(..)") - } +/// The persistent store data used for communication between store and host. +#[derive(Default)] +struct CallData { + /// Arguments for a current call. + args: Vec, + /// The results of the current call. + output: Vec, + /// A memory error that occured during execution of the current call. + memory_error: Option, } -impl repr::Repr for Plugin { - fn repr(&self) -> EcoString { - "plugin(..)".into() - } -} - -impl PartialEq for Plugin { - fn eq(&self, other: &Self) -> bool { - self.0.bytes == other.0.bytes - } -} - -impl Hash for Plugin { - fn hash(&self, state: &mut H) { - self.0.bytes.hash(state); - } +/// If there was an error reading/writing memory, keep the offset + length to +/// display an error message. +struct MemoryError { + offset: u32, + length: u32, + write: bool, } /// Write the arguments to the plugin function into the plugin's memory. fn wasm_minimal_protocol_write_args_to_buffer( - mut caller: wasmi::Caller, + mut caller: wasmi::Caller, ptr: u32, ) { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); @@ -346,7 +594,7 @@ fn wasm_minimal_protocol_write_args_to_buffer( /// Extracts the output of the plugin function from the plugin's memory. fn wasm_minimal_protocol_send_result_to_host( - mut caller: wasmi::Caller, + mut caller: wasmi::Caller, ptr: u32, len: u32, ) { diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index 99c9a37e6..b7b4a6d9d 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -167,6 +167,14 @@ impl Scope { Default::default() } + /// Create a new scope with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { + map: IndexMap::with_capacity(capacity), + ..Default::default() + } + } + /// Create a new scope with duplication prevention. pub fn deduplicating() -> Self { Self { deduplicate: true, ..Default::default() } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index d99027728..4fa380b46 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -15,8 +15,8 @@ use crate::diag::{HintedStrResult, HintedString, StrResult}; 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, SymbolElem, Type, Version, + NativeElement, NativeType, NoneValue, Reflect, Repr, Resolve, Scope, Str, Styles, + Symbol, SymbolElem, Type, Version, }; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; use crate::text::{RawContent, RawElem, TextElem}; @@ -84,8 +84,6 @@ pub enum Value { Type(Type), /// A module. Module(Module), - /// A WebAssembly plugin. - Plugin(Plugin), /// A dynamic value. Dyn(Dynamic), } @@ -147,7 +145,6 @@ impl Value { Self::Args(_) => Type::of::(), Self::Type(_) => Type::of::(), Self::Module(_) => Type::of::(), - Self::Plugin(_) => Type::of::(), Self::Dyn(v) => v.ty(), } } @@ -251,7 +248,6 @@ impl Debug for Value { Self::Args(v) => Debug::fmt(v, f), Self::Type(v) => Debug::fmt(v, f), Self::Module(v) => Debug::fmt(v, f), - Self::Plugin(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f), } } @@ -289,7 +285,6 @@ impl Repr for Value { Self::Args(v) => v.repr(), Self::Type(v) => v.repr(), Self::Module(v) => v.repr(), - Self::Plugin(v) => v.repr(), Self::Dyn(v) => v.repr(), } } @@ -340,7 +335,6 @@ impl Hash for Value { Self::Args(v) => v.hash(state), Self::Type(v) => v.hash(state), Self::Module(v) => v.hash(state), - Self::Plugin(v) => v.hash(state), Self::Dyn(v) => v.hash(state), } } @@ -661,7 +655,6 @@ primitive! { primitive! { Args: "arguments", Args } primitive! { Type: "type", Type } primitive! { Module: "module", Module } -primitive! { Plugin: "plugin", Plugin } impl Reflect for Arc { fn input() -> CastInfo { diff --git a/tests/suite/foundations/plugin.typ b/tests/suite/foundations/plugin.typ index 0842980ec..9feacc030 100644 --- a/tests/suite/foundations/plugin.typ +++ b/tests/suite/foundations/plugin.typ @@ -9,6 +9,37 @@ bytes("value3-value1-value2"), ) +--- plugin-func --- +#let p = plugin("/assets/plugins/hello.wasm") +#test(type(p.hello), function) +#test(("a", "b").map(bytes).map(p.double_it), ("a.a", "b.b").map(bytes)) + +--- plugin-import --- +#import plugin("/assets/plugins/hello.wasm"): hello, double_it + +#test(hello(), bytes("Hello from wasm!!!")) +#test(double_it(bytes("hey!")), bytes("hey!.hey!")) + +--- plugin-transition --- +#let empty = plugin("/assets/plugins/hello-mut.wasm") +#test(str(empty.get()), "[]") + +#let hello = plugin.transition(empty.add, bytes("hello")) +#test(str(empty.get()), "[]") +#test(str(hello.get()), "[hello]") + +#let world = plugin.transition(empty.add, bytes("world")) +#let hello_you = plugin.transition(hello.add, bytes("you")) + +#test(str(empty.get()), "[]") +#test(str(hello.get()), "[hello]") +#test(str(world.get()), "[world]") +#test(str(hello_you.get()), "[hello, you]") + +#let hello2 = plugin.transition(empty.add, bytes("hello")) +#test(hello == world, false) +#test(hello == hello2, true) + --- plugin-wrong-number-of-arguments --- #let p = plugin("/assets/plugins/hello.wasm") diff --git a/tools/test-helper/package.json b/tools/test-helper/package.json index d34213fb0..08a60fa31 100644 --- a/tools/test-helper/package.json +++ b/tools/test-helper/package.json @@ -1,104 +1,107 @@ { - "name": "typst-test-helper", - "publisher": "typst", - "displayName": "Typst Test Helper", - "description": "Helps to run, compare and update Typst tests.", - "version": "0.0.1", - "categories": [ - "Other" - ], - "activationEvents": [ - "workspaceContains:tests/suite/playground.typ" - ], - "main": "./dist/extension.js", - "contributes": { - "commands": [ - { - "command": "typst-test-helper.refreshFromPreview", - "title": "Refresh preview", - "category": "Typst Test Helper", - "icon": "$(refresh)" - }, - { - "command": "typst-test-helper.runFromPreview", - "title": "Run test", - "category": "Typst Test Helper", - "icon": "$(debug-start)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.saveFromPreview", - "title": "Run and save reference output", - "category": "Typst Test Helper", - "icon": "$(save)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.copyImageFilePathFromPreviewContext", - "title": "Copy image file path", - "category": "Typst Test Helper" - }, - { - "command": "typst-test-helper.increaseResolution", - "title": "Render at higher resolution", - "category": "Typst Test Helper", - "icon": "$(zoom-in)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.decreaseResolution", - "title": "Render at lower resolution", - "category": "Typst Test Helper", - "icon": "$(zoom-out)", - "enablement": "typst-test-helper.runButtonEnabled" - } - ], - "menus": { - "editor/title": [ - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.refreshFromPreview", - "group": "navigation@1" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.runFromPreview", - "group": "navigation@2" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.saveFromPreview", - "group": "navigation@3" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.increaseResolution", - "group": "navigation@4" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.decreaseResolution", - "group": "navigation@4" - } - ], - "webview/context": [ - { - "command": "typst-test-helper.copyImageFilePathFromPreviewContext", - "when": "webviewId == typst-test-helper.preview && (webviewSection == png || webviewSection == ref)" - } - ] - } - }, - "scripts": { - "build": "tsc -p ./", - "watch": "tsc -watch -p ./" - }, - "devDependencies": { - "@types/node": "18.x", - "@types/vscode": "^1.88.0", - "typescript": "^5.3.3" - }, - "engines": { - "vscode": "^1.88.0" - } -} + "name": "typst-test-helper", + "publisher": "typst", + "displayName": "Typst Test Helper", + "description": "Helps to run, compare and update Typst tests.", + "version": "0.0.1", + "categories": [ + "Other" + ], + "activationEvents": [ + "workspaceContains:tests/suite/playground.typ" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "typst-test-helper.refreshFromPreview", + "title": "Refresh preview", + "category": "Typst Test Helper", + "icon": "$(refresh)" + }, + { + "command": "typst-test-helper.runFromPreview", + "title": "Run test", + "category": "Typst Test Helper", + "icon": "$(debug-start)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.saveFromPreview", + "title": "Run and save reference output", + "category": "Typst Test Helper", + "icon": "$(save)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.copyImageFilePathFromPreviewContext", + "title": "Copy image file path", + "category": "Typst Test Helper" + }, + { + "command": "typst-test-helper.increaseResolution", + "title": "Render at higher resolution", + "category": "Typst Test Helper", + "icon": "$(zoom-in)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.decreaseResolution", + "title": "Render at lower resolution", + "category": "Typst Test Helper", + "icon": "$(zoom-out)", + "enablement": "typst-test-helper.runButtonEnabled" + } + ], + "menus": { + "editor/title": [ + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.refreshFromPreview", + "group": "navigation@1" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.runFromPreview", + "group": "navigation@2" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.saveFromPreview", + "group": "navigation@3" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.increaseResolution", + "group": "navigation@4" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.decreaseResolution", + "group": "navigation@4" + } + ], + "webview/context": [ + { + "command": "typst-test-helper.copyImageFilePathFromPreviewContext", + "when": "webviewId == typst-test-helper.preview && (webviewSection == png || webviewSection == ref)" + } + ] + } + }, + "scripts": { + "build": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "18.x", + "@types/vscode": "^1.88.0", + "typescript": "^5.3.3" + }, + "engines": { + "vscode": "^1.88.0" + }, + "__metadata": { + "size": 35098973 + } +} \ No newline at end of file From 3eb6e87af1d8870a38cc5914e345d07373e1e8c1 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:56:25 +0100 Subject: [PATCH 048/101] Include images from raw pixmaps and more (#5632) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: Laurenz --- Cargo.lock | 5 +- Cargo.toml | 2 +- crates/typst-layout/src/image.rs | 41 ++- crates/typst-library/src/text/font/color.rs | 22 +- .../typst-library/src/visualize/image/mod.rs | 216 ++++++++++----- .../src/visualize/image/raster.rs | 261 ++++++++++++++---- .../typst-library/src/visualize/image/svg.rs | 2 + crates/typst-pdf/src/image.rs | 163 ++++++----- crates/typst-render/src/image.rs | 40 ++- crates/typst-svg/Cargo.toml | 1 + crates/typst-svg/src/image.rs | 48 +++- crates/typst-svg/src/text.rs | 10 +- tests/ref/baseline-box.png | Bin 3896 -> 4021 bytes tests/ref/box-clip-outset.png | Bin 1442 -> 1492 bytes tests/ref/box-clip-radius-without-stroke.png | Bin 1225 -> 1255 bytes tests/ref/box-clip-radius.png | Bin 1245 -> 1250 bytes .../closure-path-resolve-in-layout-phase.png | Bin 2193 -> 2256 bytes tests/ref/coma.png | Bin 28740 -> 28615 bytes tests/ref/footnote-in-caption.png | Bin 6111 -> 6154 bytes tests/ref/footnote-in-table.png | Bin 12380 -> 12727 bytes tests/ref/image-baseline-with-box.png | Bin 6375 -> 6375 bytes tests/ref/image-decode-detect-format.png | Bin 10648 -> 11032 bytes tests/ref/image-decode-specify-format.png | Bin 10648 -> 11032 bytes tests/ref/image-fit.png | Bin 10302 -> 10390 bytes tests/ref/image-pixmap-luma8.png | Bin 0 -> 321 bytes tests/ref/image-pixmap-lumaa8.png | Bin 0 -> 299 bytes tests/ref/image-pixmap-rgb8.png | Bin 0 -> 1220 bytes tests/ref/image-pixmap-rgba8.png | Bin 0 -> 854 bytes tests/ref/image-scaling-methods.png | Bin 0 -> 1539 bytes tests/ref/image-sizing.png | Bin 8662 -> 8925 bytes tests/ref/issue-4361-transparency-leak.png | Bin 3515 -> 3738 bytes tests/ref/pad-followed-by-content.png | Bin 11897 -> 12071 bytes tests/suite/visualize/image.typ | 128 +++++++++ 33 files changed, 689 insertions(+), 250 deletions(-) create mode 100644 tests/ref/image-pixmap-luma8.png create mode 100644 tests/ref/image-pixmap-lumaa8.png create mode 100644 tests/ref/image-pixmap-rgb8.png create mode 100644 tests/ref/image-pixmap-rgba8.png create mode 100644 tests/ref/image-scaling-methods.png diff --git a/Cargo.lock b/Cargo.lock index 3343c246b..ada3a3d4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1122,9 +1122,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -3036,6 +3036,7 @@ dependencies = [ "comemo", "ecow", "flate2", + "image", "ttf-parser", "typst-library", "typst-macros", diff --git a/Cargo.toml b/Cargo.toml index 6b592cd39..d03bfa6d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ icu_provider_adapters = "1.4" icu_provider_blob = "1.4" icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" -image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] } +image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.5" kurbo = "0.11" diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index e521b993f..503c30820 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -10,7 +10,8 @@ use typst_library::layout::{ use typst_library::loading::DataSource; use typst_library::text::families; use typst_library::visualize::{ - Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat, + Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind, + RasterImage, SvgImage, VectorFormat, }; /// Layout the image. @@ -49,15 +50,27 @@ pub fn layout_image( } // Construct the image itself. - let image = Image::with_fonts( - data.clone(), - format, - elem.alt(styles), - engine.world, - &families(styles).map(|f| f.as_str()).collect::>(), - elem.flatten_text(styles), - ) - .at(span)?; + let kind = match format { + ImageFormat::Raster(format) => ImageKind::Raster( + RasterImage::new( + data.clone(), + format, + elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), + ) + .at(span)?, + ), + ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( + SvgImage::with_fonts( + data.clone(), + engine.world, + elem.flatten_text(styles), + &families(styles).map(|f| f.as_str()).collect::>(), + ) + .at(span)?, + ), + }; + + let image = Image::new(kind, elem.alt(styles), elem.scaling(styles)); // Determine the image's pixel aspect ratio. let pxw = image.width(); @@ -129,10 +142,10 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult .to_lowercase(); 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)), + "png" => return Ok(ExchangeFormat::Png.into()), + "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), + "gif" => return Ok(ExchangeFormat::Gif.into()), + "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), _ => {} } } diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index e3183e885..0a7b13c97 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -10,7 +10,9 @@ 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}; +use crate::visualize::{ + ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, SvgImage, +}; /// Whether this glyph should be rendered via simple outlining instead of via /// `glyph_frame`. @@ -102,12 +104,8 @@ fn draw_raster_glyph( upem: Abs, raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { - let image = Image::new( - Bytes::new(raster_image.data.to_vec()), - RasterFormat::Png.into(), - None, - ) - .ok()?; + let data = Bytes::new(raster_image.data.to_vec()); + let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?); // Apple Color emoji doesn't provide offset information (or at least // not in a way ttf-parser understands), so we artificially shift their @@ -178,9 +176,8 @@ fn draw_colr_glyph( ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?; svg.end_element(); - let data = svg.end_document().into_bytes(); - - let image = Image::new(Bytes::new(data), VectorFormat::Svg.into(), None).ok()?; + let data = Bytes::from_string(svg.end_document()); + let image = Image::plain(SvgImage::new(data).ok()?); let y_shift = Abs::pt(upem.to_pt() - y_max); let position = Point::new(Abs::pt(x_min), y_shift); @@ -255,9 +252,8 @@ fn draw_svg_glyph( ty = -top, ); - let image = - Image::new(Bytes::new(wrapper_svg.into_bytes()), VectorFormat::Svg.into(), None) - .ok()?; + let data = Bytes::from_string(wrapper_svg); + let image = Image::plain(SvgImage::new(data).ok()?); let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); let size = Size::new(Abs::pt(width), Abs::pt(height)); diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 77f8426e4..0e5c9e329 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -3,13 +3,14 @@ mod raster; mod svg; -pub use self::raster::{RasterFormat, RasterImage}; +pub use self::raster::{ + ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage, +}; pub use self::svg::SvgImage; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; -use comemo::Tracked; use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; @@ -24,7 +25,6 @@ use crate::layout::{BlockElem, Length, Rel, Sizing}; use crate::loading::{DataSource, Load, Readable}; use crate::model::Figurable; use crate::text::LocalName; -use crate::World; /// A raster or vector graphic. /// @@ -46,7 +46,8 @@ use crate::World; /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { - /// A path to an image file or raw bytes making up an encoded image. + /// A path to an image file or raw bytes making up an image in one of the + /// supported [formats]($image.format). /// /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] @@ -57,10 +58,50 @@ pub struct ImageElem { )] pub source: Derived, - /// The image's format. Detected automatically by default. + /// The image's format. /// - /// Supported formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image - /// is [not currently supported](https://github.com/typst/typst/issues/145). + /// By default, the format is detected automatically. Typically, you thus + /// only need to specify this when providing raw bytes as the + /// [`source`]($image.source) (even then, Typst will try to figure out the + /// format automatically, but that's not always possible). + /// + /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well + /// as raw pixel data. Embedding PDFs as images is + /// [not currently supported](https://github.com/typst/typst/issues/145). + /// + /// When providing raw pixel data as the `source`, you must specify a + /// dictionary with the following keys as the `format`: + /// - `encoding` ([str]): The encoding of the pixel data. One of: + /// - `{"rgb8"}` (three 8-bit channels: red, green, blue) + /// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha) + /// - `{"luma8"}` (one 8-bit channel) + /// - `{"lumaa8"}` (two 8-bit channels: luma and alpha) + /// - `width` ([int]): The pixel width of the image. + /// - `height` ([int]): The pixel height of the image. + /// + /// The pixel width multiplied by the height multiplied by the channel count + /// for the specified encoding must then match the `source` data. + /// + /// ```example + /// #image( + /// read( + /// "tetrahedron.svg", + /// encoding: none, + /// ), + /// format: "svg", + /// width: 2cm, + /// ) + /// + /// #image( + /// bytes(range(16).map(x => x * 16)), + /// format: ( + /// encoding: "luma8", + /// width: 4, + /// height: 4, + /// ), + /// width: 2cm, + /// ) + /// ``` pub format: Smart, /// The width of the image. @@ -86,6 +127,30 @@ pub struct ImageElem { #[default(ImageFit::Cover)] pub fit: ImageFit, + /// A hint to viewers how they should scale the image. + /// + /// When set to `{auto}`, the default is left up to the viewer. For PNG + /// export, Typst will default to smooth scaling, like most PDF and SVG + /// viewers. + /// + /// _Note:_ The exact look may differ across PDF viewers. + pub scaling: Smart, + + /// An ICC profile for the image. + /// + /// ICC profiles define how to interpret the colors in an image. When set + /// to `{auto}`, Typst will try to extract an ICC profile from the image. + #[parse(match args.named::>>("icc")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({ + let data = Spanned::new(&source, span).load(engine.world)?; + Derived::new(source, data) + })), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub icc: Smart>, + /// Whether text in SVG images should be converted into curves before /// embedding. This will result in the text becoming unselectable in the /// output. @@ -94,6 +159,7 @@ pub struct ImageElem { } #[scope] +#[allow(clippy::too_many_arguments)] impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. /// @@ -130,6 +196,13 @@ impl ImageElem { /// How the image should adjust itself to a given area. #[named] fit: Option, + /// A hint to viewers how they should scale the image. + #[named] + scaling: Option>, + /// Whether text in SVG images should be converted into curves before + /// embedding. + #[named] + flatten_text: Option, ) -> StrResult { let bytes = data.into_bytes(); let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); @@ -149,6 +222,12 @@ impl ImageElem { if let Some(fit) = fit { elem.push_fit(fit); } + if let Some(scaling) = scaling { + elem.push_scaling(scaling); + } + if let Some(flatten_text) = flatten_text { + elem.push_flatten_text(flatten_text); + } Ok(elem.pack().spanned(span)) } } @@ -199,15 +278,8 @@ struct Repr { kind: ImageKind, /// A text describing the image. alt: Option, -} - -/// A kind of image. -#[derive(Hash)] -pub enum ImageKind { - /// A raster image. - Raster(RasterImage), - /// An SVG image. - Svg(SvgImage), + /// The scaling algorithm to use. + scaling: Smart, } impl Image { @@ -218,55 +290,29 @@ impl Image { /// Should always be the same as the default DPI used by usvg. pub const USVG_DEFAULT_DPI: f64 = 96.0; - /// Create an image from a buffer and a format. - #[comemo::memoize] - #[typst_macros::time(name = "load image")] + /// Create an image from a `RasterImage` or `SvgImage`. pub fn new( - data: Bytes, - format: ImageFormat, + kind: impl Into, alt: Option, - ) -> StrResult { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::new(data)?) - } - }; - - Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) + scaling: Smart, + ) -> Self { + Self::new_impl(kind.into(), alt, scaling) } - /// Create a possibly font-dependent image from a buffer and a format. + /// Create an image with optional properties set to the default. + pub fn plain(kind: impl Into) -> Self { + Self::new(kind, None, Smart::Auto) + } + + /// The internal, non-generic implementation. This is memoized to reuse + /// the `Arc` and `LazyHash`. #[comemo::memoize] - #[typst_macros::time(name = "load image")] - pub fn with_fonts( - data: Bytes, - format: ImageFormat, + fn new_impl( + kind: ImageKind, alt: Option, - world: Tracked, - families: &[&str], - flatten_text: bool, - ) -> StrResult { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::with_fonts(data, world, flatten_text, families)?) - } - }; - - Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) - } - - /// The raw image data. - pub fn data(&self) -> &Bytes { - match &self.0.kind { - ImageKind::Raster(raster) => raster.data(), - ImageKind::Svg(svg) => svg.data(), - } + scaling: Smart, + ) -> Image { + Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling }))) } /// The format of the image. @@ -306,6 +352,11 @@ impl Image { self.0.alt.as_deref() } + /// The image scaling algorithm to use for this image. + pub fn scaling(&self) -> Smart { + self.0.scaling + } + /// The decoded image. pub fn kind(&self) -> &ImageKind { &self.0.kind @@ -319,10 +370,32 @@ impl Debug for Image { .field("width", &self.width()) .field("height", &self.height()) .field("alt", &self.alt()) + .field("scaling", &self.scaling()) .finish() } } +/// A kind of image. +#[derive(Clone, Hash)] +pub enum ImageKind { + /// A raster image. + Raster(RasterImage), + /// An SVG image. + Svg(SvgImage), +} + +impl From for ImageKind { + fn from(image: RasterImage) -> Self { + Self::Raster(image) + } +} + +impl From for ImageKind { + fn from(image: SvgImage) -> Self { + Self::Svg(image) + } +} + /// A raster or vector image format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ImageFormat { @@ -335,8 +408,8 @@ pub enum ImageFormat { 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)); + if let Some(format) = ExchangeFormat::detect(data) { + return Some(Self::Raster(RasterFormat::Exchange(format))); } // SVG or compressed SVG. @@ -355,9 +428,12 @@ pub enum VectorFormat { Svg, } -impl From for ImageFormat { - fn from(format: RasterFormat) -> Self { - Self::Raster(format) +impl From for ImageFormat +where + R: Into, +{ + fn from(format: R) -> Self { + Self::Raster(format.into()) } } @@ -371,8 +447,18 @@ cast! { ImageFormat, self => match self { Self::Raster(v) => v.into_value(), - Self::Vector(v) => v.into_value() + Self::Vector(v) => v.into_value(), }, v: RasterFormat => Self::Raster(v), v: VectorFormat => Self::Vector(v), } + +/// The image scaling algorithm a viewer should use. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum ImageScaling { + /// Scale with a smoothing algorithm such as bilinear interpolation. + Smooth, + /// Scale with nearest neighbor or a similar algorithm to preserve the + /// pixelated look of the image. + Pixelated, +} diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 098843a25..d43b15486 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -7,10 +7,12 @@ use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; -use image::{guess_format, DynamicImage, ImageDecoder, ImageResult, Limits}; +use image::{ + guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, +}; use crate::diag::{bail, StrResult}; -use crate::foundations::{Bytes, Cast}; +use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; /// A decoded raster image. #[derive(Clone, Hash)] @@ -21,43 +23,118 @@ struct Repr { data: Bytes, format: RasterFormat, dynamic: image::DynamicImage, - icc: Option>, + icc: Option, dpi: Option, } impl RasterImage { /// Decode a raster image. + pub fn new( + data: Bytes, + format: impl Into, + icc: Smart, + ) -> StrResult { + Self::new_impl(data, format.into(), icc) + } + + /// Create a raster image with optional properties set to the default. + pub fn plain(data: Bytes, format: impl Into) -> StrResult { + Self::new(data, format, Smart::Auto) + } + + /// The internal, non-generic implementation. #[comemo::memoize] - pub fn new(data: Bytes, format: RasterFormat) -> StrResult { - fn decode_with( - decoder: ImageResult, - ) -> ImageResult<(image::DynamicImage, Option>)> { - let mut decoder = decoder?; - let icc = decoder.icc_profile().ok().flatten().filter(|icc| !icc.is_empty()); - decoder.set_limits(Limits::default())?; - let dynamic = image::DynamicImage::from_decoder(decoder)?; - Ok((dynamic, icc)) - } + #[typst_macros::time(name = "load raster image")] + fn new_impl( + data: Bytes, + format: RasterFormat, + icc: Smart, + ) -> StrResult { + let (dynamic, icc, dpi) = match format { + RasterFormat::Exchange(format) => { + fn decode( + decoder: ImageResult, + icc: Smart, + ) -> ImageResult<(image::DynamicImage, Option)> { + let mut decoder = decoder?; + let icc = icc.custom().or_else(|| { + decoder + .icc_profile() + .ok() + .flatten() + .filter(|icc| !icc.is_empty()) + .map(Bytes::new) + }); + decoder.set_limits(Limits::default())?; + let dynamic = image::DynamicImage::from_decoder(decoder)?; + Ok((dynamic, icc)) + } - let cursor = io::Cursor::new(&data); - let (mut dynamic, icc) = match format { - RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)), - RasterFormat::Png => decode_with(PngDecoder::new(cursor)), - RasterFormat::Gif => decode_with(GifDecoder::new(cursor)), - } - .map_err(format_image_error)?; + let cursor = io::Cursor::new(&data); + let (mut dynamic, icc) = match format { + ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc), + ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc), + ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc), + } + .map_err(format_image_error)?; - let exif = exif::Reader::new() - .read_from_container(&mut std::io::Cursor::new(&data)) - .ok(); + let exif = exif::Reader::new() + .read_from_container(&mut std::io::Cursor::new(&data)) + .ok(); - // Apply rotation from EXIF metadata. - if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { - apply_rotation(&mut dynamic, rotation); - } + // Apply rotation from EXIF metadata. + if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { + apply_rotation(&mut dynamic, rotation); + } - // Extract pixel density. - let dpi = determine_dpi(&data, exif.as_ref()); + // Extract pixel density. + let dpi = determine_dpi(&data, exif.as_ref()); + + (dynamic, icc, dpi) + } + + RasterFormat::Pixel(format) => { + if format.width == 0 || format.height == 0 { + bail!("zero-sized images are not allowed"); + } + + let channels = match format.encoding { + PixelEncoding::Rgb8 => 3, + PixelEncoding::Rgba8 => 4, + PixelEncoding::Luma8 => 1, + PixelEncoding::Lumaa8 => 2, + }; + + let Some(expected_size) = format + .width + .checked_mul(format.height) + .and_then(|size| size.checked_mul(channels)) + else { + bail!("pixel dimensions are too large"); + }; + + if expected_size as usize != data.len() { + bail!("pixel dimensions and pixel data do not match"); + } + + fn to>( + data: &Bytes, + format: PixelFormat, + ) -> ImageBuffer> { + ImageBuffer::from_raw(format.width, format.height, data.to_vec()) + .unwrap() + } + + let dynamic = match format.encoding { + PixelEncoding::Rgb8 => to::>(&data, format).into(), + PixelEncoding::Rgba8 => to::>(&data, format).into(), + PixelEncoding::Luma8 => to::>(&data, format).into(), + PixelEncoding::Lumaa8 => to::>(&data, format).into(), + }; + + (dynamic, icc.custom(), None) + } + }; Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi }))) } @@ -93,60 +170,141 @@ impl RasterImage { } /// Access the ICC profile, if any. - pub fn icc(&self) -> Option<&[u8]> { - self.0.icc.as_deref() + pub fn icc(&self) -> Option<&Bytes> { + self.0.icc.as_ref() } } impl Hash for Repr { fn hash(&self, state: &mut H) { - // The image is fully defined by data and format. + // The image is fully defined by data, format, and ICC profile. self.data.hash(state); self.format.hash(state); + self.icc.hash(state); } } /// A raster graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum RasterFormat { + /// A format typically used in image exchange. + Exchange(ExchangeFormat), + /// A format of raw pixel data. + Pixel(PixelFormat), +} + +impl From for RasterFormat { + fn from(format: ExchangeFormat) -> Self { + Self::Exchange(format) + } +} + +impl From for RasterFormat { + fn from(format: PixelFormat) -> Self { + Self::Pixel(format) + } +} + +cast! { + RasterFormat, + self => match self { + Self::Exchange(v) => v.into_value(), + Self::Pixel(v) => v.into_value(), + }, + v: ExchangeFormat => Self::Exchange(v), + v: PixelFormat => Self::Pixel(v), +} + +/// A raster format typically used in image exchange, with efficient encoding. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum ExchangeFormat { /// Raster format for illustrations and transparent graphics. Png, /// Lossy raster format suitable for photos. Jpg, - /// Raster format that is typically used for short animated clips. + /// Raster format that is typically used for short animated clips. Typst can + /// load GIFs, but they will become static. Gif, } -impl RasterFormat { +impl ExchangeFormat { /// Try to detect the format of data in a buffer. pub fn detect(data: &[u8]) -> Option { guess_format(data).ok().and_then(|format| format.try_into().ok()) } } -impl From for image::ImageFormat { - fn from(format: RasterFormat) -> Self { +impl From for image::ImageFormat { + fn from(format: ExchangeFormat) -> Self { match format { - RasterFormat::Png => image::ImageFormat::Png, - RasterFormat::Jpg => image::ImageFormat::Jpeg, - RasterFormat::Gif => image::ImageFormat::Gif, + ExchangeFormat::Png => image::ImageFormat::Png, + ExchangeFormat::Jpg => image::ImageFormat::Jpeg, + ExchangeFormat::Gif => image::ImageFormat::Gif, } } } -impl TryFrom for RasterFormat { +impl TryFrom for ExchangeFormat { type Error = EcoString; fn try_from(format: image::ImageFormat) -> StrResult { Ok(match format { - image::ImageFormat::Png => RasterFormat::Png, - image::ImageFormat::Jpeg => RasterFormat::Jpg, - image::ImageFormat::Gif => RasterFormat::Gif, - _ => bail!("Format not yet supported."), + image::ImageFormat::Png => ExchangeFormat::Png, + image::ImageFormat::Jpeg => ExchangeFormat::Jpg, + image::ImageFormat::Gif => ExchangeFormat::Gif, + _ => bail!("format not yet supported"), }) } } +/// Information that is needed to understand a pixmap buffer. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct PixelFormat { + /// The channel encoding. + encoding: PixelEncoding, + /// The pixel width. + width: u32, + /// The pixel height. + height: u32, +} + +/// Determines the channel encoding of raw pixel data. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum PixelEncoding { + /// Three 8-bit channels: Red, green, blue. + Rgb8, + /// Four 8-bit channels: Red, green, blue, alpha. + Rgba8, + /// One 8-bit channel. + Luma8, + /// Two 8-bit channels: Luma and alpha. + Lumaa8, +} + +cast! { + PixelFormat, + self => Value::Dict(self.into()), + mut dict: Dict => { + let format = Self { + encoding: dict.take("encoding")?.cast()?, + width: dict.take("width")?.cast()?, + height: dict.take("height")?.cast()?, + }; + dict.finish(&["encoding", "width", "height"])?; + format + } +} + +impl From for Dict { + fn from(format: PixelFormat) -> Self { + dict! { + "encoding" => format.encoding, + "width" => format.width, + "height" => format.height, + } + } +} + /// Try to get the rotation from the EXIF metadata. fn exif_rotation(exif: &exif::Exif) -> Option { exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)? @@ -266,21 +424,20 @@ fn format_image_error(error: image::ImageError) -> EcoString { #[cfg(test)] mod tests { - use super::{RasterFormat, RasterImage}; - use crate::foundations::Bytes; + use super::*; #[test] fn test_image_dpi() { #[track_caller] - fn test(path: &str, format: RasterFormat, dpi: f64) { + fn test(path: &str, format: ExchangeFormat, dpi: f64) { let data = typst_dev_assets::get(path).unwrap(); let bytes = Bytes::new(data); - let image = RasterImage::new(bytes, format).unwrap(); + let image = RasterImage::plain(bytes, format).unwrap(); assert_eq!(image.dpi().map(f64::round), Some(dpi)); } - test("images/f2t.jpg", RasterFormat::Jpg, 220.0); - test("images/tiger.jpg", RasterFormat::Jpg, 72.0); - test("images/graph.png", RasterFormat::Png, 144.0); + test("images/f2t.jpg", ExchangeFormat::Jpg, 220.0); + test("images/tiger.jpg", ExchangeFormat::Jpg, 72.0); + test("images/graph.png", ExchangeFormat::Png, 144.0); } } diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index 089f05430..dcc55077b 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -30,6 +30,7 @@ struct Repr { impl SvgImage { /// Decode an SVG image without fonts. #[comemo::memoize] + #[typst_macros::time(name = "load svg")] pub fn new(data: Bytes) -> StrResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; @@ -44,6 +45,7 @@ impl SvgImage { /// Decode an SVG image with access to fonts. #[comemo::memoize] + #[typst_macros::time(name = "load svg")] pub fn with_fonts( data: Bytes, world: Tracked, diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index bff7bfefa..550f60a4b 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -5,8 +5,10 @@ use ecow::eco_format; use image::{DynamicImage, GenericImageView, Rgba}; use pdf_writer::{Chunk, Filter, Finish, Ref}; use typst_library::diag::{At, SourceResult, StrResult}; +use typst_library::foundations::Smart; use typst_library::visualize::{ - ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage, + ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, + RasterImage, SvgImage, }; use typst_utils::Deferred; @@ -32,11 +34,13 @@ pub fn write_images( EncodedImage::Raster { data, filter, - has_color, + color_space, + bits_per_component, width, height, - icc, + compressed_icc, alpha, + interpolate, } => { let image_ref = chunk.alloc(); out.insert(image.clone(), image_ref); @@ -45,23 +49,18 @@ pub fn write_images( image.filter(*filter); image.width(*width as i32); image.height(*height as i32); - image.bits_per_component(8); + image.bits_per_component(i32::from(*bits_per_component)); + image.interpolate(*interpolate); let mut icc_ref = None; let space = image.color_space(); - if icc.is_some() { + if compressed_icc.is_some() { let id = chunk.alloc.bump(); space.icc_based(id); icc_ref = Some(id); - } else if *has_color { - color::write( - ColorSpace::Srgb, - space, - &context.globals.color_functions, - ); } else { color::write( - ColorSpace::D65Gray, + *color_space, space, &context.globals.color_functions, ); @@ -79,20 +78,27 @@ pub fn write_images( mask.width(*width as i32); mask.height(*height as i32); mask.color_space().device_gray(); - mask.bits_per_component(8); + mask.bits_per_component(i32::from(*bits_per_component)); + mask.interpolate(*interpolate); } else { image.finish(); } - if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) { - let mut stream = chunk.icc_profile(icc_ref, icc); + if let (Some(compressed_icc), Some(icc_ref)) = + (compressed_icc, icc_ref) + { + let mut stream = chunk.icc_profile(icc_ref, compressed_icc); stream.filter(Filter::FlateDecode); - if *has_color { - stream.n(3); - stream.alternate().srgb(); - } else { - stream.n(1); - stream.alternate().d65_gray(); + match color_space { + ColorSpace::Srgb => { + stream.n(3); + stream.alternate().srgb(); + } + ColorSpace::D65Gray => { + stream.n(1); + stream.alternate().d65_gray(); + } + _ => unimplemented!(), } } } @@ -122,35 +128,17 @@ pub fn deferred_image( ) -> (Deferred>, Option) { let color_space = match image.kind() { ImageKind::Raster(raster) if raster.icc().is_none() => { - if raster.dynamic().color().channel_count() > 2 { - Some(ColorSpace::Srgb) - } else { - Some(ColorSpace::D65Gray) - } + Some(to_color_space(raster.dynamic().color())) } _ => None, }; + // PDF/A does not appear to allow interpolation. + // See https://github.com/typst/typst/issues/2942. + let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); + let deferred = Deferred::new(move || match image.kind() { - ImageKind::Raster(raster) => { - let raster = raster.clone(); - let (width, height) = (raster.width(), raster.height()); - let (data, filter, has_color) = encode_raster_image(&raster); - let icc = raster.icc().map(deflate); - - let alpha = - raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster)); - - Ok(EncodedImage::Raster { - data, - filter, - has_color, - width, - height, - icc, - alpha, - }) - } + ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)), ImageKind::Svg(svg) => { let (chunk, id) = encode_svg(svg, pdfa) .map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?; @@ -161,42 +149,51 @@ pub fn deferred_image( (deferred, color_space) } -/// Encode an image with a suitable filter and return the data, filter and -/// whether the image has color. -/// -/// Skips the alpha channel as that's encoded separately. +/// Encode an image with a suitable filter. #[typst_macros::time(name = "encode raster image")] -fn encode_raster_image(image: &RasterImage) -> (Vec, Filter, bool) { +fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage { let dynamic = image.dynamic(); - let channel_count = dynamic.color().channel_count(); - let has_color = channel_count > 2; + let color_space = to_color_space(dynamic.color()); - if image.format() == RasterFormat::Jpg { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner(), Filter::DctDecode, has_color) - } else { - // TODO: Encode flate streams with PNG-predictor? - let data = match (dynamic, channel_count) { - (DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()), - (DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()), - // Grayscale image - (_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()), - // Anything else - _ => deflate(dynamic.to_rgb8().as_raw()), + let (filter, data, bits_per_component) = + if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (Filter::DctDecode, data.into_inner(), 8) + } else { + // TODO: Encode flate streams with PNG-predictor? + let (data, bits_per_component) = match (dynamic, color_space) { + // RGB image. + (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8), + // Grayscale image + (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8), + (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8), + // Anything else + _ => (deflate(dynamic.to_rgb8().as_raw()), 8), + }; + (Filter::FlateDecode, data, bits_per_component) }; - (data, Filter::FlateDecode, has_color) + + let compressed_icc = image.icc().map(|data| deflate(data)); + let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic)); + + EncodedImage::Raster { + data, + filter, + color_space, + bits_per_component, + width: image.width(), + height: image.height(), + compressed_icc, + alpha, + interpolate, } } /// Encode an image's alpha channel if present. #[typst_macros::time(name = "encode alpha")] -fn encode_alpha(raster: &RasterImage) -> (Vec, Filter) { - let pixels: Vec<_> = raster - .dynamic() - .pixels() - .map(|(_, _, Rgba([_, _, _, a]))| a) - .collect(); +fn encode_alpha(image: &DynamicImage) -> (Vec, Filter) { + let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); (deflate(&pixels), Filter::FlateDecode) } @@ -224,19 +221,33 @@ pub enum EncodedImage { data: Vec, /// The filter to use for the image. filter: Filter, - /// Whether the image has color. - has_color: bool, + /// Which color space this image is encoded in. + color_space: ColorSpace, + /// How many bits of each color component are stored. + bits_per_component: u8, /// The image's width. width: u32, /// The image's height. height: u32, - /// The image's ICC profile, pre-deflated, if any. - icc: Option>, + /// The image's ICC profile, deflated, if any. + compressed_icc: Option>, /// The alpha channel of the image, pre-deflated, if any. alpha: Option<(Vec, Filter)>, + /// Whether image interpolation should be enabled. + interpolate: bool, }, /// A vector graphic. /// /// The chunk is the SVG converted to PDF objects. Svg(Chunk, Ref), } + +/// Matches an [`image::ColorType`] to [`ColorSpace`]. +fn to_color_space(color: image::ColorType) -> ColorSpace { + use image::ColorType::*; + match color { + L8 | La8 | L16 | La16 => ColorSpace::D65Gray, + Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, + _ => unimplemented!(), + } +} diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 27b039113..7425bdd2f 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; use tiny_skia as sk; +use typst_library::foundations::Smart; use typst_library::layout::Size; -use typst_library::visualize::{Image, ImageKind}; +use typst_library::visualize::{Image, ImageKind, ImageScaling}; use crate::{AbsExt, State}; @@ -34,7 +35,7 @@ pub fn render_image( let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32; let h = ((w as f32) / aspect).ceil() as u32; - let pixmap = scaled_texture(image, w, h)?; + let pixmap = build_texture(image, w, h)?; let paint_scale_x = view_width / pixmap.width() as f32; let paint_scale_y = view_height / pixmap.height() as f32; @@ -57,29 +58,42 @@ pub fn render_image( /// Prepare a texture for an image at a scaled size. #[comemo::memoize] -fn scaled_texture(image: &Image, w: u32, h: u32) -> Option> { - let mut pixmap = sk::Pixmap::new(w, h)?; +fn build_texture(image: &Image, w: u32, h: u32) -> Option> { + let mut texture = sk::Pixmap::new(w, h)?; match image.kind() { ImageKind::Raster(raster) => { - let downscale = w < raster.width(); - let filter = - if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; - let buf = raster.dynamic().resize(w, h, filter); - for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { + let w = texture.width(); + let h = texture.height(); + + let buf; + let dynamic = raster.dynamic(); + let resized = if (w, h) == (dynamic.width(), dynamic.height()) { + // Small optimization to not allocate in case image is not resized. + dynamic + } else { + let upscale = w > dynamic.width(); + let filter = match image.scaling() { + Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest, + _ if upscale => FilterType::CatmullRom, + _ => FilterType::Lanczos3, // downscale + }; + buf = dynamic.resize_exact(w, h, filter); + &buf + }; + + for ((_, _, src), dest) in resized.pixels().zip(texture.pixels_mut()) { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } } - // Safety: We do not keep any references to tree nodes beyond the scope - // of `with`. ImageKind::Svg(svg) => { let tree = svg.tree(); let ts = tiny_skia::Transform::from_scale( w as f32 / tree.size().width(), h as f32 / tree.size().height(), ); - resvg::render(tree, ts, &mut pixmap.as_mut()) + resvg::render(tree, ts, &mut texture.as_mut()); } } - Some(Arc::new(pixmap)) + Some(Arc::new(texture)) } diff --git a/crates/typst-svg/Cargo.toml b/crates/typst-svg/Cargo.toml index 41d355659..5416621e5 100644 --- a/crates/typst-svg/Cargo.toml +++ b/crates/typst-svg/Cargo.toml @@ -21,6 +21,7 @@ base64 = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } flate2 = { workspace = true } +image = { workspace = true } ttf-parser = { workspace = true } xmlparser = { workspace = true } xmlwriter = { workspace = true } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index ede4e76e3..d74432026 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,7 +1,11 @@ use base64::Engine; use ecow::{eco_format, EcoString}; +use image::{codecs::png::PngEncoder, ImageEncoder}; +use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; -use typst_library::visualize::{Image, ImageFormat, RasterFormat, VectorFormat}; +use typst_library::visualize::{ + ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, +}; use crate::SVGRenderer; @@ -14,6 +18,17 @@ impl SVGRenderer { self.xml.write_attribute("width", &size.x.to_pt()); self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("preserveAspectRatio", "none"); + match image.scaling() { + Smart::Auto => {} + Smart::Custom(ImageScaling::Smooth) => { + // This is still experimental and not implemented in all major browsers. + // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility + self.xml.write_attribute("style", "image-rendering: smooth") + } + Smart::Custom(ImageScaling::Pixelated) => { + self.xml.write_attribute("style", "image-rendering: pixelated") + } + } self.xml.end_element(); } } @@ -22,19 +37,32 @@ impl SVGRenderer { /// `data:image/{format};base64,`. #[comemo::memoize] pub fn convert_image_to_base64_url(image: &Image) -> EcoString { - let format = match image.format() { - ImageFormat::Raster(f) => match f { - RasterFormat::Png => "png", - RasterFormat::Jpg => "jpeg", - RasterFormat::Gif => "gif", - }, - ImageFormat::Vector(f) => match f { - VectorFormat::Svg => "svg+xml", + let mut buf; + let (format, data): (&str, &[u8]) = match image.kind() { + ImageKind::Raster(raster) => match raster.format() { + RasterFormat::Exchange(format) => ( + match format { + ExchangeFormat::Png => "png", + ExchangeFormat::Jpg => "jpeg", + ExchangeFormat::Gif => "gif", + }, + raster.data(), + ), + RasterFormat::Pixel(_) => ("png", { + buf = vec![]; + let mut encoder = PngEncoder::new(&mut buf); + if let Some(icc_profile) = raster.icc() { + encoder.set_icc_profile(icc_profile.to_vec()).ok(); + } + raster.dynamic().write_with_encoder(encoder).unwrap(); + buf.as_slice() + }), }, + ImageKind::Svg(svg) => ("svg+xml", svg.data()), }; let mut url = eco_format!("data:image/{format};base64,"); - let data = base64::engine::general_purpose::STANDARD.encode(image.data()); + let data = base64::engine::general_purpose::STANDARD.encode(data); url.push_str(&data); url } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index fa471b2ae..e6620a59e 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -6,7 +6,9 @@ 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}; +use typst_library::visualize::{ + ExchangeFormat, FillRule, Image, Paint, RasterImage, RelativeTo, +}; use typst_utils::hash128; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -244,9 +246,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(Bytes::new(raster.data.to_vec()), RasterFormat::Png.into(), None) - .ok()?; + let image = Image::plain( + RasterImage::plain(Bytes::new(raster.data.to_vec()), ExchangeFormat::Png).ok()?, + ); Some((image, raster.x as f64, raster.y as f64)) } diff --git a/tests/ref/baseline-box.png b/tests/ref/baseline-box.png index 2a9e517580cc73174735d95d87244c7dc8196845..e07e22ea75b069488b4f62d1df1ab6aeabb4d349 100644 GIT binary patch literal 4021 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 bo$KELgpN8z--biX00000NkvXXu0mjf7N69W literal 3896 zcmV-856AF{P)KbB&5$k?pDUbmgYNji4|$5S1DU{363P0Y=eUr?N9*mn+?_*1 zm!ti`!E4$*rQKTCGqDXLK)Yk%2+lH8$5|?tRsn(+Dy_;Y+&n(&123D{@YB2h1zb&B zDbdY(MU3$@hu|caBG6=f#B`e=C>mCy?uFH|p5?gw#O@tqCtPSQDGI4{MS&8_ z>P3{sWEF3=DbF0d{wF{Ar<4CYCV8X0zCvA8QFyWT*#tk{ZLgYfC&9%sSrG$vx92N? z)YUqSSWEGkp^J@%md|jCR?kZ^$NHzw$!gc)DQ#)lPsD47_JVaG8Vlo!QVdjxvR)#_ z?vzG72nmd-b~?D9{G(0MhNR_&sq_D)o&5Pb6F6R-T6Rf{WQ37XX~ZVBZNKJ3(fs9w z1)hQ&*K=HGJDwl5X$l`4AmeP{*~;{68wTYt^hK83vN2UGnx%%Ndu0s8;VJpA?^qwA zWmhwZNP?#;H6KmUUfT8nP5=TGVl<&TCJen?n#;|76r!&^!-z6o=&z+Y3D}a6D&j*<%S02?Ic&L_m42*HU2RWDB|>FH)*bsG5^Z zAONfn(F{csG^CM~l*rk>N3j_)fGuS?E|3l`U<88_HbC-)+3MGY`oTj-8_SnC((wVl z?eNXj>NN7B8;3K>&7(ztx+F0f_?iR#ZqJ6c*O3k1Q&5bxOxQKNPDktMK0zVA4^R{& z1tjp8ojaxVB6?-XJhRZ1;)d%ZFt90dm1GzX${2|;1kQ1^i0AAw|H=C^5CJ|jY>^5Q z&g{JQRwlpq+_|X}|M6e@Z@FG~`JQbtbQi~vWGpQRIDz{NMPk4qQ5xC~8nTXS>n@a) z3K?=eUuiZ95EA2qeAUKxKW$Ao% z*()^EYd4S7stdT&-m_)6Q!jA1mmQykbgC-14oqBs^4$}X&&ayo4&6Y2s;6)%Z6L>Z z4iG$lmRa>NANN@tK`=WzJ2W&T2m<(hfP(**H{N*T$dMy& zzx{U9K7IQ1Ew|kAhvu$;5Ww@R#*+C7nspIJE`(8*FP2!5_ktxhgW=66qPD zddl}_5cF)AZ*El@$fZ@eP?w>_s+PY}QyL9sr3k@g)<&x+%6<3UH$OiQen(z@`Q;Bk z{P2or^j&w|_2{FI-f_nrQG5UX{V0mwcH3;8BWHQ-puKJ;=`80uhx(9Hc zcT5L&QL?6Zpaw>_YBI5rpWME;;R0)OR@#WCnw>fdaA1RPx%JjtUwiE}Ns{*L*|RqC z--&9q%5mHk&*+HX{2S^UK;&_1>iF^F$BrE{3?o{JS6+D~3cmQ_i*LR47FYvg445jn zJzf|Z9UE~VQt!4kMOj)>P)D%s_*AJ@E}S9tqGQ&S#BP7!031AsZu#+i(}~gH*5MTI zYx?QqDO&An>RjCb1U+4B7TR{nBI^#yZvIml8*1sgYy05q4U6ZE55M^MjW^#|oW9^0 z7BoZf4aH({d3pKu*I%zxDp9vwEeGW7Z;$*=o@x)6ny%b&3>2#DS z(O9E6Ns_nUe*5K`h`7*lA@FRgru%Wu$fli~nBD9mEQ9=D-_Xp0B3wXLOBG&Vb4eN# zlDj8&RTihU+Ql95;eGH(v7B7 zabzJbHldw}59m5NTj-`m9ssbWX<%UB-h1y|OP7U(g=e05=DO>yi?qJ?-g_%6D>vPA zQj4B zHGpm=hB;`6^0W*6Oga`PQD1lG0+k~JqwP>5o526Y(W6JB5ys>3NbCFWzaMoqO^ePU z06;XO!-o$?TB8KYWHPcWM{yj-CnhGM;1w?&VHoy$J#ZP@wk^xrzI}VN)O+{tJ$UdS z_6>L5dFS1C-yI#5;4&0W20DbQ6F7S4geXF*8n5=~OBe0c0$7Ta)aeZwvD6KY9Qt8l zwRv`~(p9a&(arM}9eO_HwX0JfL%Z9xf}V>z5J^hn=jY}TI?hQ$LEwid$?>eNs}zo? zJ*_0`B1~lhfgwzh3)4~(Tn0f9*LBx6;%_!4Cnux&8%-fnxfXOBCu&6TLZJ}FqiIIn zR4Vm-Jat5+>cD{m@4WNQ+EWM5^HKeZG9ubyk390otFOLFlH}Tf6BQ7eromt z!&3~0v6yDJW-E(DrC76TMW zCQ5?mo_p^6`SU>#L`O@hREmz-s5nFqAyLVG_St98o;@4Y5Ddejsjbb0VVI|%etK>D zo;Y#h)TvY8JA;H_2);AXxTCy?3L?17U;N$QOfAhgs)ggE=bEPGScnTiz+eFgJjccU z;m-~U2zPE_RZL{LSbY1Cuqo-KaQW-$3zdefdXX`7OO`&RjJlZk;a9UizWcBK^@C3! zA=0SJ+GQHGRHaR$UVz5(xgnMjHV%-Dwsq24)zRvaz1KeXx9@_h2^4&1qJ#U)nKNsa zzok)cR|+Ln>omKqwx$HMPhcQMGoj=6WQ}vIVU|j8*p{TfFIm4XIgi#){`*JA=cZ5P zIJkR*L=s#FQmWN!x zVmL}x6=t!F{QQwYjO58Oz^x3qm%)*=Zq=pAZh)Rt^`D8v(TJoz4OE(_gOXI74PA{2_1rwbuf*>#D zH|)t#+|2YTh3Xxf{j#GfHtA$y2@>EQ^w-NP_?DfB>T}a`wxRV1l0b06hgbjulICz6 zmDOHjY%&}cEfmq`KMsQ796m6zU2L`*Q(A9Dalh2uLT~N^4MIZjiUo#@>5FoySy4@k zO^O~y5;VGS@lp(r1CnkTrfYV2CP6^Fp}Lq)L!Say7i%^83h<9V|7oFo+Ec}aLN}IU zdp%oGY?9~NEvs497z7TDjayx$uv*T=z?cXIV(6Y5?rVp_TOa={KuwY!avUwdJcjm2 z1eg{G34y_g&~r`8Q+r;B1PsR!1PUR*2@r(PhaMY~8l5`y!oAyn@WIcHuZxUkh!}~B zI<0Nk6;iC@dVX7WFv1+)NVs}3nP3&uU7FF8p|g4SH7sYBs^xC&RUe=`u94cUfl5;) zD1?p05Z7(C^Z+8+Vb(FafS^QLObrm031hryS%%!xjlk-uJ;c*sAd>*IT|0Em^-?-~ z1ojLUK+X@tMpq$2U0_or%{SWZB#CKi-Gc1I7BQO@SuVgZk`hEkky(~fjfNnF2tp%F zR!9NhV7YOoSJw^0v2+)wU_j#}K>`veM%lDoA0i!DMSPp3FvllD82B#g8TNV+jo~nz zC!yo`ggs7Aa7?ae)HJJ;O>YYw(t~cdr*#y~b1WOzbgjm5ETTsWDVmd9EgZ~q!XPaq zLPhE1as=XKi;G67$Pp}0iYx(CT_s!~hzw9Pn`u{ z1&%A1S&~I0A$DNjgxqoMz&3R+m*x@7>~sK&lY6&FD|L1Ej>&Y~LPD!&5GleA&`dh6+Aau+x)(6mU{`796Pcb`K^Zq52MJEJpq-G!MoTN#nu!!6nrJSQ2d+su zT$tI+k~~e)m8RV8Dx*8&$*j?=Q92Ws@@h)RKr=^RBwo0f7u}~Xx-Ysfy04%wx-Ysvm>RY50{$?Asy z^akj4W!H6kvT%Pp_%U?8d7rK?dRLBlg(i`;BwQ6K z(==8u0O$?Si+{ypH4sR~HRVgI>*!KXW(|q7wECd0T%K#TR=%HNIXdH>|We#2zWAD)9A&sW1 z#W#7Ly1#y!Rrs^GqUPB)Tj9ZP!kInh3)% zfd3PoJZX$kgHbdZTrdKWKoFEg5v!mSDg`=PTd;PsmT5;TLuV)sQ0O9*E`TjiXiH_S zKv|R^2ofP)yb(+|5mFKpCwvLJ|5%4Tb1eWK6magX4li>widM4QHpZW{cK8E>3pmA+3Vem zzKN)|k_rby7}Z#xmGqenW}TE$R@iG*EwsK4mk~;_wO&tZs{C$Cja&@S+z&$+qia%E z@NQhYdhR4H=F=K+176*r6*Jn$>{q3{?0;GnHs0?Hby@ASmey9$dWBLVq+hAr7P{Bf z>ao2Q3+@3lH*`4Qx9KEOetu%Y+39H1NN9w2v-r2tOK+vqnx~h~oyxzNPOGtpH@~$T_rh3$vC^9$oVnIgIoqzNz zH&YT4&m4at$bDXtn~@lQ_~3y{XXCQcl8f%<)e~}DR%vI*V6O+DIiS%3&tlZ&q+Doe zZbXgS<%~OeC@JAIu9DfzOr3!sPe~d&-hL_@!0t$a#R+I4TD-I|O)JPu-CzG#loT42n6J~d|7T1@M%_l)qko*_k>Md! zqRpRDCKVOm254^Rot+(z)A71Sg9O>^?(Rx3VcCN_)xyHn)o-YL?`~(O2QK2ZG@8)N z**~4B6chvWzaC}%-Jt8%)>bSQ`@I*o@wD4)^vhaBIrT=57TNnFesgmZpy6;9paB}7 z0UBO%1ZaSU7tsM40?+^r(35Tj93KRr0U83(0R2Db57Ho{X;PcStpET307*qoM6N<$ Eg2srySpWb4 delta 1393 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 diff --git a/tests/ref/closure-path-resolve-in-layout-phase.png b/tests/ref/closure-path-resolve-in-layout-phase.png index baaed3564387f4f7ccd5ee63d3f9f7e9055d6a94..a3d6999813b88ef0eb23b49054b8793d1dc6a4d2 100644 GIT binary patch delta 2244 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 diff --git a/tests/ref/coma.png b/tests/ref/coma.png index 2c59ae87039f7de3576f41b8c1e46b6b32145d41..a1d743a49f71209e12f4ee5be99b69bf86e9be55 100644 GIT binary patch delta 28237 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 E0PbK?-T(jq delta 28370 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 DeS~A_ diff --git a/tests/ref/footnote-in-caption.png b/tests/ref/footnote-in-caption.png index 12a5fde5e965edf996b1fdfd5bff04e160077c56..79b2b5d0f955479b46cdab66ffcfb4f936beb893 100644 GIT binary patch literal 6154 zcmV+l81?6gP)0ssI2phCI_000-!Nkl%51LX zHm>c^jG3b|#yB(Q*rz=;YqxgI)+XxzG@ah}%zK{K|95&{jo^vsRU{w?2xtPDh=3-b z31}h$nt&#H1E6V|o|>AXDC)QS48zRL&7Gg0n`5HW>1JkTZfFDVA&Q^!PV9;nZ2CbdLCxgkB*tZDXD2EuDmXZJVqyZ1pPZav!YGPbTU+bv>wE5i7l8Zk zY<2YX^kijaVNMa_NSMm|`}^|p^6KhpnM?+_ppSE6uCZrv{<5;N#>U3Bwl-`}lL=_% z=jR_B9F&xl;9CrPK|uj(K(nr{P9zdx-J6QxiPcn7Q-faE;J5?Oc+FC&lrKP|!ootl z47};m(o)P527;z_SQ_{|JUpUuZ*PzMU?D$PhzMu`nuvfVpb2Oq0-As(BA^Lq0-A_` zCZJzSMvm1P9%<&`%f;8}!wr>6MNt&PFa-4fk%2X889n&wjSS0izo0i7X_{6lm1?zG zuh(-NM?n9ZxF_zB{m#A_t0oM@VZ6}d*s*J0#KqBf@EHVO!Od=cg1$hf&Yc{K(5XWO zwwXVU5r^&3PX3FenC-~9t$6mX+#nQ6sO;}nRKB*EiAkK zhvwKxfM!a=%b8x~Q`Pf4KFtBdG*UP?%ck%9c>I__Ge|@#HDzKbrqZ~_4B=1&1u<>P zM5}};*S3-fQm7s~F-#!P~>G59fnCX3&NC(;PerkW_$aQ-ws zFrBF>OgbKqgCHO=s>e6wG~$~t4F)|G{@u$Xxa&@4oleKL`f7>tBc0N^<1?w%ZntUS zG#IoW1fh%>lj~O5aV5R7T9M7h?81(9=L#Nhh%DC3rh+CVt+^7FGGz5RZYW1Olj3&@+WV2)QKu0L&ELOs`sbo(QGe4XOAu763N?Cdk_wvgfkiU~i>L!84 zjGOL&wb><}m^t~v`nEH3*?^U^K+xV6ZduBCks*x%jhp)Ye(41A@b>KRHMP~vGe^VW zkZQKsqRC`38jVW-#$YgTT$^66SL$D01OfK}is<_mFP_-fNeXIRUfo{bT$rsp(u}qh z^n#c`9B;m~TCE>;?ifXpCvWUSLnKA6tpo>@c8%23^7M=Jo6D>gsxru6q83Mo9J-3I!C*J9;)64Wvx6 zztw8JeS)N0S?LZ-YUor_L#KvL4V{`GHS`ligoz%z$+!w}pKoXpAjEYv8sXad{l2sg z8tzda{Flq+Bg3$?kJ=2}Y&L_zKqKVmuixLk|M>EC46J){zyfPUzX}aRgR9kwM|3)! zVzKykw_u?o5_w*P;eNlr>9-pT zDute-^+C8uf9QE|z{b%~A3@4A zdEdaH-Hz*7XRZ&qavazX7>?;iE!@3gGjeYpmAZ;`gw^zG#4v0(5ZrT0ahMI; z?e>4Le7D>6CX)T;LA6?iq%#dx`KIEBSLpRrDwXToWilCTx)bRZ{gkJ0yWRFC!WC4Z zP-MW0q@PO44!p$DVSWWlPp9xQqRoqC-K-ul%x0j!9#7gghB;9~p#)PKTwklzZX73g zf1(hFwK$ojQ9#0^@yi$s5r4d%;9Sxkl~i2RQwXi1Is>0o$HKdhk@F4`tCEXIiE~Ho%#444<7=oltAY0@O5=fIG zANgSrL&VA`%&^FKz4y%AbI+W)_y6bqyKk8Q3JYNlIgw8fnkK@+c#u^J0K?(Xe6SC$ z5(62J$I>)xS`Xz7`O5u?UYR^?F)QfLU@&mtY&MHc>LN~RI-N59`O6a}5Px3kztH<= z^NEod3|_dYoU=&8X0z1Y3vup|`#BTi6+f_;TMSQ8DRsDKIJ2V-|8BRdY4;K|Fpey3 zFk$xleM#n89i3SIzbo?j@swr03hu2wf>*4gmg?y0=<4WNs-vr;YpIUDI!D7TVPN~u z9WX6W@5xI;e?2@r-rc=a*@s7Hyh16@bB3CxI(UEoO68)pNiB(+&*$YUs0TIIV0iP-+TQ&KU9OY5qVJ+JS}2GCoue z+EucKvG4}=ylDMs1fT-gR7Yn(jjfSY4@X6e6cd3eq-;#H6dB=ECzA}A_v|Jg9p^dZV>Q>3+Igz!s#m9Dr{9i)l{Ta(7vbumo^P? zCDa&cFdB_~)hiiEmg82g*I~cn88lt`aJf0!9+)RSCABLRda35-=0^MqC0>YRF{Uaz zW9NJdM(h#z92tr?`j=%!#9c;=g%dm|5hYpuEgT;*Nkgcdg9Ece+C}E#C*h(I7uR_Z z=ym8v6UGj8CThZ0IX48AY#{UEp6N#mH2AL|!Uox)*Xv1|yWP$>kXsK7B0G45TP-Dk zQ*SmKq&O}E;l#c}M61Be{|b?kZ$c}>Xh*J8e!!N~l}B=$k{r3nKoZ(tzK%Zn9XJQy#6 z;zblBeU|LA1U{`|a+pX6F~)E+gviSF5{rzn)uYdP$j# z1`FLC9UmQ=MC{pn08kYB)8}t$$4(v%JP=xe&+^mo_8rK=>jQKG2?TaHimPxI zz6bsC)*prfe}P)T3|?C20==2gRmg4d71bl^eh*@CU3*5S)^5SU_U|>=8=s z+G@4xd#8{|n3+>5M{q7oT`(wY#r$aykOnjC?d>gQ3Np>O4=jdx!^q93UQD5=V^LxN zc%YXqOriK1K&Xyf2x|w_Nl)ZEqsoDn>chZa-1egV0f z4*|ddn<~LdbI{e*g~q!GBTSdOa{*H7pd)HgK%kj`5c){Dr$=AKj)WSTMc)ZFJ?g8R zAl)D2refIbF0Xkwg|3WFe!mNH)*cC8W*lQ>baj(i!U3-cs-CZN4K z3a6Cr?rsYgcN9aiUbzr>56HL5aS|f$LVYy?$s9U7@E;_v;;r$!Qzj-68&yV3584`S zo?7qG0u9F9H~=ie?(J!?fWx_h5bbsqb52Z=CBxC@+=+JUmkTiUUTVdivSY(sC-N@oq!@y{7`u(+qECkUnx1uiv3wE>rzuOm4fqunP}mk&A1 zm_!uUTrUk6FGP~~s}or}!f{f4?=-@-S&9KS+}GD<<`_7s1rvbL_{HTpWj0m^7Z`_J zuv0#7Zl#gj7=NSCU>s8kmNC#(GrPrd?dv7|G(6qK<;*B+7e@apX^lyfyIiX7pf5(MjuI7>TBphNKjonk8#d=oOWjW6pU9(n91F z5=oEch3OdX#ib7R>#g4X3f=W$CRMSEE5M#?dOS4WA9TiJ)l5_TGS+F{_ zKjTrfEc3L4#B3M)Ivfsi`okUM-R4J5#;BL<%S$ldZnrbLB8K3sU9jh&UX~Dq@vi%; zAdFX}gEjR#4f@QZ($NhaZXpLsSWZNRch10aL}N%Ojwj*1G*~rlNF(N#5K*TjvJrCR z#=90jE*(d^ybNv2nH_thN+zK+j-Fa{9OLxZs__CqI;>9DhVC-Ap@&Q8i0}$Ip4$Q+ z?SVe!yv>|20c+%S6jmhr@`g4yBuk{_RWY#Fxh*h+`2ECXaRs|aDx7sz^D{$?3aNGd9tkCIv(n3wSr9Akw4KF%e;GM4EH2h2BzBm-(0<4F+#BLf1Of34 z$F(oOLr4Sf7UVi`BqxNtF`ocMNK_fRzCxY#0yS3xKG(kpSBb&1xbY5e*wOCHY7ty} zsSoEd99Uh)Wl$Yi)W6!{lCu_(cz2| zw~gv*_hxA{BZ>)){6g-GtWX!t~1UF_sG5Qf&B|=nwWmf-`2h4obN-NO=Gk z4A_Yo>7f>86%5hYsj*q%JDg256++wc@=iZ$Sqog27Topt2eLPJfol3Mk(Xc6IO_{M z5wrd7{$&wVyHI$Y5`Kgksfi_VP4l~sh)ITUI*!kHKnWN&n9qi3yvrINs>_;l4(JG>czy z0SoT;d+v@fhK2Gcf`#soI`(w}c_S?!(l~nRN{-v@mL2r~cdH4@2NpvF-eBuyhRqDS zEWyxTPV)ca>d)VQwt2wQ-&Er12c?N$x1mo}SZ?p`BhHR5zKlN99IOD_gi2OXrYkIv z8T(2Z#hz(ya4Um?+gM+67)c&Ba9pGy>w5%NLuPOrq=mtYvLgQxXiB^ zTI2|Liq%kU)CNtLS{OQG3HW-!X%RO34UDz1*=&3wY3@Y#=(kf3@|jHUsHG%6EF6y* zCi2!cE)ykRjx)+9c{eGx-?+Skq1jj3vK$YA2%Q?$7EY4mAtRO@xfgs)ACK$cuK`;T z3@$}|k}htB%MJYovC*f|73MpgoWH{#`!nsj=BLx93n;An6y`L82&2z0_4(JvyA zM5q<+u)9b+8i}V^zBf{=8$%^X>YK`L(&KY^*|NkqX;7=c(R3`Y0W=Y4q)uL0$7F13 zuw#t2VL=8ZxcD!dGIVrOlg_LrN~lHX?JG4B1H#M6V8b?L&e_XEOuE|2DERS0G?I9i z<%n@^Heyh!Hd&UqM?%fIrZiS!V1!p+aT)i+k76laMtQ7p(*;*Y!S&Yqq{-H*=E=d0 ztVc7Iq@aWQr1r8Yk(2{T1tr^A#$3XNC8UK)%oDAaQ;30XgNO>kxr!cLX}h-56UnBG zg?w||CE1x>%}n$pS>m);hNeBsh2^PfweoJcX8NhFE%o11k^2^^#?*zP#Hy@IEG{VN zU>MG3XursSXdFGY2!n`8MeUZR|0?Qe!ty?a%hqRK#p#9xC)0-R(uQtBx1qbVq1(`1 c+R$^BCj%&Ky*)CX>Hq)$07*qoM6N<$g0cG69RL6T literal 6111 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 diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png index 062a6fc7104e0e4e89de9cb676bc0cd48b0860bb..e110eac6d4cbe9fb78c999cecc4b801cbe2e58ee 100644 GIT binary patch literal 12727 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¥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 Q*8r5fw6av?C)3dX4;RwKWB>pF diff --git a/tests/ref/image-baseline-with-box.png b/tests/ref/image-baseline-with-box.png index dc8e8bc5714b8850a9aae75adeef261f1c77718f..ade90e2f559608c317f3b99b442ebd6b63b12ae1 100644 GIT binary patch literal 6375 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 diff --git a/tests/ref/image-decode-detect-format.png b/tests/ref/image-decode-detect-format.png index 6ecb7dcdaeed44fe450d98c7f2efcefdd98f47ab..cee71bb934d4688812dd42e9bb6b0df697eea8ec 100644 GIT binary patch literal 11032 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 yJ?$y001amNklQzBWmxVT=b5VQX4< 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

    e{DNB=+`MAN;jyhK+PLE4Es72h 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 T?NNt1D{%MD6jvub#*(9r{Y#lSZ^pIU? zR{znuhgyr9eA&AHT)DB;c}H25%*+SB{pJ=0+iJyZEVi1+b+0BhtEzwL-6wkYuQILD vSfG`fr1c}LHdvf-N70sN|389U|9`Fc!u1hl7w(;j0EL35tDnm{r-UW|UEhBi literal 0 HcmV?d00001 diff --git a/tests/ref/image-pixmap-rgb8.png b/tests/ref/image-pixmap-rgb8.png new file mode 100644 index 0000000000000000000000000000000000000000..d905c1eee9a68a63c5f9908784f60cb451608c75 GIT binary patch literal 1220 zcmV;#1UvhQP)TWFhww$L^U{lA8W6Rz3)|_47n&ei<;y}VcrAoX6jDMWIiLYN z#CnFKjx>$8m->Rx3d;dy%{U>%Lh^`1*8&U0t%6U*#ejISL(4sD{pZjE4#vtXEW#QK z!4Vy@kSBD4f-onRhzkTyT#IJfZhH_5eSUkc4)kzfIFK0=b7b{6M_5QRGNMEKLYNXu zDB7>Y1)0zhn%+ZSNf!F+IvQ79>m()g7+E~)N|;F_c|dz82t#5DEDpa^Jyo~g=nh!u zGuv%+EtS#>Q)I58{Y)H@9xV>~OyB;4D>_W10h-(KNwm;k_vg?qHKW|tZ6J!C84fAQd8vzh6zwj$mZJOZiVCzr>?F3tDK&mVUJ@1kE%118U>Lf# zKMsfyX^6ink4^N2(8u znMdZsS}%hwIoD(^167y>!nF#T@Gkme`~eqSgM?>%Og?Enq}tvTtCn4|lkp*#Qa$84 z6(5S{LOOur4WSBv=PvqlBt(mVLIM*h%4kfPO{Ux68kQNcHc)B!T2Kw;>AlkQ|hMraHxGC*l4Ibr6FRL}udWTmK=L~dPX>w`6L4KzRxR6rB#X@ea-utT(gr~$D9e8*1iI5%jLph!sP*RM#? zXL<3-I`(J=e`%g2c+xyeRJ4j#(Ha%4qE)miP|+${^Y29i?E53ax0mqsHGFytAKt^e zkMQOTy!s9=es1{Z3;6yDzPy2t@8JChc>4)ne}(v?v0XoZR?!+2t)ew5`hOB_%v9i3 zB!g%O4MXd_cwPK zA13F0A!m%4mTW?{E_;)O@`5?R8K;a(CdtNUXLB%lp`07rmsv&MO)2|AE|>}}g{3%O zf+-7^1SR(sxj-ts6cdVfMYt?fGF4|>ujD|mreFz&jW;E_*G%g)G({Tcw(N~C$@uEuvi4-EcXl-;H14e7O|izD%{UvXwu%$cD`$D=CxFzb5Jx?)*5twvVw z*I?JkYsR&7Tvp60$JK_0#jJ_fI_;q|`Y_hcH5S++TO!M78LT;33mbzYJa3@iupEYY zU!Yzj24n;#V21q)X1HP4Fm*htqOTCWb(q9q^2F2`wE{6x!`LmW=qp4cPKEPd7;XPd z_=as@u8O`!^w4<1Tgx!xu?SB`uJz{zhXFY3$Rn_x=)>HrYV>bRE$c6pRJ4lLsAv_f gQPC<|MQfgt-{x1c2u5nq3IG5A07*qoM6N<$f){{|;s5{u literal 0 HcmV?d00001 diff --git a/tests/ref/image-scaling-methods.png b/tests/ref/image-scaling-methods.png new file mode 100644 index 0000000000000000000000000000000000000000..9d543e114fc5578525babe90f004745905aa5e6e GIT binary patch literal 1539 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 z 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 diff --git a/tests/ref/issue-4361-transparency-leak.png b/tests/ref/issue-4361-transparency-leak.png index 4060d43ac442e67e674245685f3fbfbe1d555157..660798166355dfb1e8afb091f76ea3dc0b585ff2 100644 GIT binary patch literal 3738 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 delta 3514 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 diff --git a/tests/ref/pad-followed-by-content.png b/tests/ref/pad-followed-by-content.png index 90b48232a9febcfe2c6920848fb21ba1ce8ff659..534a97870e937fef7b91e964c2bbe8588714ecdb 100644 GIT binary patch literal 12071 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 literal 11897 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 x * 16)), + format: ( + encoding: "luma8", + width: 4, + height: 4, + ), + width: 1cm, +) + +--- image-pixmap-lumaa8 --- +#image( + bytes(range(16).map(x => (0x80, x * 16)).flatten()), + format: ( + encoding: "lumaa8", + width: 4, + height: 4, + ), + width: 1cm, +) + +--- image-scaling-methods --- +#let img(scaling) = image( + bytes(( + 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80, + )), + format: ( + encoding: "rgb8", + width: 3, + height: 3, + ), + width: 1cm, + scaling: scaling, +) + +#stack( + dir: ltr, + spacing: 4pt, + img(auto), + img("smooth"), + img("pixelated"), +) + --- image-natural-dpi-sizing --- // Test that images aren't upscaled. // Image is just 48x80 at 220dpi. It should not be scaled to fit the page @@ -103,6 +179,58 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B // Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.) #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%) +--- image-pixmap-empty --- +// Error: 1:2-8:2 zero-sized images are not allowed +#image( + bytes(()), + format: ( + encoding: "rgb8", + width: 0, + height: 0, + ), +) + +--- image-pixmap-invalid-size --- +// Error: 1:2-8:2 pixel dimensions and pixel data do not match +#image( + bytes((0x00, 0x00, 0x00)), + format: ( + encoding: "rgb8", + width: 16, + height: 16, + ), +) + +--- image-pixmap-unknown-attribute --- +#image( + bytes((0x00, 0x00, 0x00)), + // Error: 1:11-6:4 unexpected key "stowaway", valid keys are "encoding", "width", and "height" + format: ( + encoding: "rgb8", + width: 1, + height: 1, + stowaway: "I do work here, promise", + ), +) + +--- image-pixmap-but-png-format --- +#image( + bytes((0x00, 0x00, 0x00)), + // Error: 1:11-5:4 expected "rgb8", "rgba8", "luma8", or "lumaa8" + format: ( + encoding: "png", + width: 1, + height: 1, + ), +) + +--- image-png-but-pixmap-format --- +#image( + read("/assets/images/tiger.jpg", encoding: none), + // Error: 11-18 expected "png", "jpg", "gif", dictionary, "svg", or auto + format: "rgba8", +) + --- issue-870-image-rotation --- // Ensure that EXIF rotation is applied. // https://github.com/image-rs/image/issues/1045 From a1f263862ca3c9594700f0c95a8e5798baf07ea9 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 31 Jan 2025 10:56:49 +0100 Subject: [PATCH 049/101] Change type repr to short name (#5788) --- crates/typst-library/src/foundations/ty.rs | 2 +- tests/suite/foundations/repr.typ | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index a2395f2a7..973c1cb61 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -136,7 +136,7 @@ impl Repr for Type { } else if *self == Type::of::() { "type(none)" } else { - self.long_name() + self.short_name() } .into() } diff --git a/tests/suite/foundations/repr.typ b/tests/suite/foundations/repr.typ index 36823e98a..2f2c055ad 100644 --- a/tests/suite/foundations/repr.typ +++ b/tests/suite/foundations/repr.typ @@ -37,8 +37,8 @@ #t(() => none, `(..) => ..`) // Types. -#t(int, `integer`) -#t(type("hi"), `string`) +#t(int, `int`) +#t(type("hi"), `str`) #t(type((a: 1)), `dictionary`) // Constants. From 46727878da083eb8186373434997f5f7403cbb66 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Fri, 31 Jan 2025 18:02:42 +0800 Subject: [PATCH 050/101] Disable cjk_latin_spacing in raw by default (#5753) --- crates/typst-library/src/text/raw.rs | 1 + ...sue-5760-disable-cjk-latin-spacing-in-raw.png | Bin 0 -> 2011 bytes tests/suite/text/raw.typ | 11 +++++++++++ 3 files changed, 12 insertions(+) create mode 100644 tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 01d6d8f01..5bb21e43a 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -475,6 +475,7 @@ impl ShowSet for Packed { out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); + out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); if self.block(styles) { out.set(ParElem::set_justify(false)); } diff --git a/tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png b/tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png new file mode 100644 index 0000000000000000000000000000000000000000..9624273329b4df3fcd229acdef4757934cbd02f9 GIT binary patch literal 2011 zcmV<12PF83P)6J^w;o7Q? z8Lh{=3e_Xmiqr>AbRBLVX&SuMxBib$e{I=Yr>mh&*^Q&)6LvAo#ix{*Po3*6^M$;k zveVqsW9#BC5{iU4{G=>JY0o{Y>OFa|d5-|Sp?U>hW83AgW(TzU>NVYmtIRw055BwH zeyl7Gw7uxKHSdt~ylQyU4nTWb+I1hLu@9OpiJ<4som*O3I)DEBa5x+c230E6f&~i_ zuNZN;Eqe=;EwKSC92542A(eo&>g-~YMu)i>WB}FO>iauUcnVwhG3bmA9Rqxe>dXGI z?(4?7&(6I0*CnD6yU&(Yy|TLhOh(nxQ<|dXr``~qzi{wTZe&%@3CF~(SkV5^#M{T7 z2lQ)2PYcjZ##+7x7o8o@!C|v${T6R)2fsOSL=S$j()P}m%hV-mErH<6p zRDz6*42?!3DiC6e0w2{JMl*GvzG_Y!B>n*KK{;cF9gDVf|m*(S2~Xc&A%l3(pr&` z06qITdaUp_{qir285=FR2LpNoHO})FT^Fv1vqLM^!Xo1FvWXfp_|!Q%Gc&WJqhkut z;cn+N57nx);h#6mGOh7Yo8Ozj{v==D<>%9%%i{MQ6v32J@;jUz!Up6#8u=(N| zF(h9vekL3Z6Qq{C8r!)t(z~r48>&Ae*j4w~V6 z3~gh^H)BUjtp^IlSpv(x0$+z_YS3!6Iy*ai&YU^y$Ht}}w$p>Qc`XF`@qR!Ph;;mW z=b@7~j_{lmZA4?z$cu%KkvEwerRb@9 z0{X9~TsR_ym5S6@v1j7LW0P(C-4OGUx=XnAyambqq^hGS{6JPR3&;&dlX-Q0Kk`op=GnAku z0ts4zmPLY=pe1No9zf6&J^f6<@uf4pjK|~Y?CgxIY-nhhX+Og6V1>{|id18&!jz=h zR3F<6F@)|waUh=9E@~^q1ahVYk;IfIu9BIH%`-}X_T$S4ZJjLWu)~R>LwW>9MnhJc zHNS8QA3c)2`|f>oZf@s|>%0PGE=Kxx(tHvd5y|fPT4`^m|Xf|Y!nBdK{ zU`^jEU65#{<78>FpeK5@=%mpf^Yov7iS<|PouV{H_vE9h{R4Vt;g-EWYhPQ!-rKBB z1dZljwrp8mULJ=omn%Izedo@dNrDCplQdtbE!wN=>?8;~?GZ301~;W9uOw)po3}XyHcqGWe5ZLEB4CvS@;g#@L}r<3Q_{tYrLJa`N$+(}G^TdiC9C zQiK%uRoSLVYtgVUb1)T}v{;=#>K+CYSXm8;)1(w3uP+T$VWm;hh-t<$=8yWNCte1k z(9^fI(mV!s)@BhLYRQRncuA)YO^m+?N zS8-HSE8E>@Qmj011!$*#oQxqJVytp;ClW!E#91z3vGJ{v=g8*tfC3~7y?S*1ZDnbC z#RuYX$Eu@Pqv0QlrAA`ZL zfB$}h{QP{aR@>OvNRD2#Xc2ph#ge3th9-Q;9!<01e6 literal 0 HcmV?d00001 diff --git a/tests/suite/text/raw.typ b/tests/suite/text/raw.typ index 1ba216302..a7f58a8d0 100644 --- a/tests/suite/text/raw.typ +++ b/tests/suite/text/raw.typ @@ -676,6 +676,17 @@ a b c -------------------- `code` ``` +--- issue-5760-disable-cjk-latin-spacing-in-raw --- + +```typ +#let hi = "你好world" +``` + +#show raw: set text(cjk-latin-spacing: auto) +```typ +#let hi = "你好world" +``` + --- raw-theme-set-to-auto --- ```typ #let hi = "Hello World" From f239b0a6a1e68a016cacf19eeef2df52e4affeb9 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:05:03 +0100 Subject: [PATCH 051/101] =?UTF-8?q?Change=20the=20default=20math=20class?= =?UTF-8?q?=20of=20U+22A5=20=E2=8A=A5=20UP=20TACK=20to=20Normal=20(#5714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/typst-layout/src/math/fragment.rs | 9 ++---- crates/typst-library/src/math/matrix.rs | 6 ++-- crates/typst-syntax/src/parser.rs | 3 +- crates/typst-utils/Cargo.toml | 1 + crates/typst-utils/src/lib.rs | 26 ++++++++++++++++++ ...985-up-tack-is-normal-perp-is-relation.png | Bin 0 -> 360 bytes tests/suite/math/class.typ | 5 ++++ 8 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png diff --git a/Cargo.lock b/Cargo.lock index ada3a3d4e..d2e410e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3106,6 +3106,7 @@ dependencies = [ "rayon", "siphasher 1.0.1", "thin-vec", + "unicode-math-class", ] [[package]] diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 81b726bad..1b508a349 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -13,6 +13,7 @@ use typst_library::math::{EquationElem, MathSize}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::visualize::Paint; use typst_syntax::Span; +use typst_utils::default_math_class; use unicode_math_class::MathClass; use super::{stretch_glyph, MathContext, Scaled}; @@ -275,11 +276,7 @@ impl GlyphFragment { span: Span, ) -> Self { let class = EquationElem::class_in(styles) - .or_else(|| match c { - ':' => Some(MathClass::Relation), - '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), - _ => unicode_math_class::class(c), - }) + .or_else(|| default_math_class(c)) .unwrap_or(MathClass::Normal); let mut fragment = Self { @@ -629,7 +626,7 @@ pub enum Limits { impl Limits { /// The default limit configuration if the given character is the base. pub fn for_char(c: char) -> Self { - match unicode_math_class::class(c) { + match default_math_class(c) { Some(MathClass::Large) => { if is_integral_char(c) { Limits::Never diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs index c74eb8fad..b6c4654ed 100644 --- a/crates/typst-library/src/math/matrix.rs +++ b/crates/typst-library/src/math/matrix.rs @@ -1,6 +1,6 @@ use smallvec::{smallvec, SmallVec}; use typst_syntax::Spanned; -use typst_utils::Numeric; +use typst_utils::{default_math_class, Numeric}; use unicode_math_class::MathClass; use crate::diag::{bail, At, HintedStrResult, StrResult}; @@ -292,7 +292,7 @@ impl Delimiter { pub fn char(c: char) -> StrResult { if !matches!( - unicode_math_class::class(c), + default_math_class(c), Some(MathClass::Opening | MathClass::Closing | MathClass::Fence), ) { bail!("invalid delimiter: \"{}\"", c) @@ -311,7 +311,7 @@ impl Delimiter { Some(']') => Self(Some('[')), Some('{') => Self(Some('}')), Some('}') => Self(Some('{')), - Some(c) => match unicode_math_class::class(c) { + Some(c) => match default_math_class(c) { Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)), Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)), _ => Self(Some(c)), diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 55d5550b6..e187212da 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -3,6 +3,7 @@ use std::mem; use std::ops::{Index, IndexMut, Range}; use ecow::{eco_format, EcoString}; +use typst_utils::default_math_class; use unicode_math_class::MathClass; use crate::set::{syntax_set, SyntaxSet}; @@ -468,7 +469,7 @@ fn math_class(text: &str) -> Option { chars .next() .filter(|_| chars.next().is_none()) - .and_then(unicode_math_class::class) + .and_then(default_math_class) } /// Parse an argument list in math: `(a, b; c, d; size: #50%)`. diff --git a/crates/typst-utils/Cargo.toml b/crates/typst-utils/Cargo.toml index 5f828cff9..360e07d89 100644 --- a/crates/typst-utils/Cargo.toml +++ b/crates/typst-utils/Cargo.toml @@ -18,6 +18,7 @@ portable-atomic = { workspace = true } rayon = { workspace = true } siphasher = { workspace = true } thin-vec = { workspace = true } +unicode-math-class = { workspace = true } [lints] workspace = true diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index b59fe2f73..34d6a9432 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -31,6 +31,7 @@ use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher13}; +use unicode_math_class::MathClass; /// Turn a closure into a struct implementing [`Debug`]. pub fn debug(f: F) -> impl Debug @@ -337,3 +338,28 @@ pub trait Numeric: /// Whether `self` consists only of finite parts. fn is_finite(self) -> bool; } + +/// Returns the default math class of a character in Typst, if it has one. +/// +/// This is determined by the Unicode math class, with some manual overrides. +pub fn default_math_class(c: char) -> Option { + match c { + // Better spacing. + // https://github.com/typst/typst/commit/2e039cb052fcb768027053cbf02ce396f6d7a6be + ':' => Some(MathClass::Relation), + + // Better spacing when used alongside + PLUS SIGN. + // https://github.com/typst/typst/pull/1726 + '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), + + // Better spacing. + // https://github.com/typst/typst/pull/1855 + '.' | '/' => Some(MathClass::Normal), + + // ⊥ UP TACK should not be a relation, contrary to ⟂ PERPENDICULAR. + // https://github.com/typst/typst/pull/5714 + '\u{22A5}' => Some(MathClass::Normal), + + c => unicode_math_class::class(c), + } +} diff --git a/tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png b/tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png new file mode 100644 index 0000000000000000000000000000000000000000..acadc3be57b0eb584ab45622f4a1f116e7cc039b GIT binary patch literal 360 zcmV-u0hj)XP)goWYpqOi%HjV5;MjDv2vg z-a4Kg2#eR({%>xlpT!HhL0}hz<9ljg>f@EO{||_dPi^>5>o|V8XHUVpJ$wG32;Mb| zja))wi{G6Efpe&WuYp3JMoX?yi-(29-=OrVyL7g=9ZFX%qO-+qP&(xSJuLn`ZT?a; ziw$=zq=UuTC%(C$TKpvVf9@TcTl`b!|A7`%i`UNn@AQ!77Jm!>pTFZEipA@8+zX{c zP;c3=dd62Ey|}Zpq_cAi$So_Te;X~iMlBw-c+}z%U@-tBCHk)`{rg}50000)_a$ $limits(class("normal", ->))_a$ $ scripts(class("relation", x))_a $ + +--- issue-4985-up-tack-is-normal-perp-is-relation --- +$ top = 1 \ + bot = 2 \ + a perp b $ From 12dbb012b19a29612fc863c558901200b4013f5d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 2 Feb 2025 20:25:58 +0100 Subject: [PATCH 052/101] Revert adding `flatten-text` to `image` (#5789) --- crates/typst-layout/src/image.rs | 1 - .../typst-library/src/visualize/image/mod.rs | 13 ---------- .../typst-library/src/visualize/image/svg.rs | 24 ++----------------- crates/typst-pdf/src/image.rs | 6 +---- 4 files changed, 3 insertions(+), 41 deletions(-) diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 503c30820..d963ea50d 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -63,7 +63,6 @@ pub fn layout_image( SvgImage::with_fonts( data.clone(), engine.world, - elem.flatten_text(styles), &families(styles).map(|f| f.as_str()).collect::>(), ) .at(span)?, diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 0e5c9e329..07ebdabe2 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -150,12 +150,6 @@ pub struct ImageElem { })] #[borrowed] pub icc: Smart>, - - /// Whether text in SVG images should be converted into curves before - /// embedding. This will result in the text becoming unselectable in the - /// output. - #[default(false)] - pub flatten_text: bool, } #[scope] @@ -199,10 +193,6 @@ impl ImageElem { /// A hint to viewers how they should scale the image. #[named] scaling: Option>, - /// Whether text in SVG images should be converted into curves before - /// embedding. - #[named] - flatten_text: Option, ) -> StrResult { let bytes = data.into_bytes(); let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); @@ -225,9 +215,6 @@ impl ImageElem { if let Some(scaling) = scaling { elem.push_scaling(scaling); } - if let Some(flatten_text) = flatten_text { - elem.push_flatten_text(flatten_text); - } Ok(elem.pack().spanned(span)) } } diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index dcc55077b..9bf1ead0d 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -22,7 +22,6 @@ pub struct SvgImage(Arc); struct Repr { data: Bytes, size: Axes, - flatten_text: bool, font_hash: u128, tree: usvg::Tree, } @@ -34,13 +33,7 @@ impl SvgImage { pub fn new(data: Bytes) -> StrResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash: 0, - flatten_text: false, - tree, - }))) + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) } /// Decode an SVG image with access to fonts. @@ -49,7 +42,6 @@ impl SvgImage { pub fn with_fonts( data: Bytes, world: Tracked, - flatten_text: bool, families: &[&str], ) -> StrResult { let book = world.book(); @@ -70,13 +62,7 @@ impl SvgImage { ) .map_err(format_usvg_error)?; let font_hash = resolver.into_inner().unwrap().finish(); - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash, - flatten_text, - tree, - }))) + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree }))) } /// The raw image data. @@ -89,11 +75,6 @@ impl SvgImage { self.0.size.x } - /// Whether the SVG's text should be flattened. - pub fn flatten_text(&self) -> bool { - self.0.flatten_text - } - /// The SVG's height in pixels. pub fn height(&self) -> f64 { self.0.size.y @@ -112,7 +93,6 @@ impl Hash for Repr { // all used fonts gives us something similar. self.data.hash(state); self.font_hash.hash(state); - self.flatten_text.hash(state); } } diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index 550f60a4b..fa326e3e0 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -205,11 +205,7 @@ fn encode_svg( ) -> Result<(Chunk, Ref), svg2pdf::ConversionError> { svg2pdf::to_chunk( svg.tree(), - svg2pdf::ConversionOptions { - pdfa, - embed_text: !svg.flatten_text(), - ..Default::default() - }, + svg2pdf::ConversionOptions { pdfa, ..Default::default() }, ) } From eee903b0f8d5c0dfda3539888d7473c6163841b0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 3 Feb 2025 17:04:54 +0100 Subject: [PATCH 053/101] Refactor `Scope` (#5797) --- crates/typst-eval/src/access.rs | 10 +- crates/typst-eval/src/call.rs | 54 +-- crates/typst-eval/src/code.rs | 2 +- crates/typst-eval/src/import.rs | 18 +- crates/typst-eval/src/math.rs | 2 +- crates/typst-eval/src/vm.rs | 23 +- crates/typst-ide/src/complete.rs | 25 +- crates/typst-ide/src/definition.rs | 4 +- crates/typst-ide/src/matchers.rs | 42 +- crates/typst-ide/src/tooltip.rs | 16 +- crates/typst-ide/src/utils.rs | 2 +- crates/typst-library/src/foundations/dict.rs | 7 +- crates/typst-library/src/foundations/func.rs | 2 +- crates/typst-library/src/foundations/mod.rs | 4 +- .../typst-library/src/foundations/module.rs | 15 +- .../typst-library/src/foundations/plugin.rs | 4 +- crates/typst-library/src/foundations/scope.rs | 395 +++++++++--------- crates/typst-library/src/foundations/ty.rs | 9 +- crates/typst-library/src/html/mod.rs | 2 +- crates/typst-library/src/introspection/mod.rs | 2 +- crates/typst-library/src/layout/mod.rs | 2 +- crates/typst-library/src/lib.rs | 9 +- crates/typst-library/src/loading/mod.rs | 2 +- crates/typst-library/src/math/mod.rs | 2 +- crates/typst-library/src/model/mod.rs | 2 +- crates/typst-library/src/pdf/mod.rs | 2 +- crates/typst-library/src/symbols.rs | 2 +- crates/typst-library/src/text/mod.rs | 2 +- crates/typst-library/src/visualize/mod.rs | 2 +- docs/src/lib.rs | 25 +- docs/src/link.rs | 4 +- 31 files changed, 371 insertions(+), 321 deletions(-) diff --git a/crates/typst-eval/src/access.rs b/crates/typst-eval/src/access.rs index 9bcac4d68..22a6b7f3d 100644 --- a/crates/typst-eval/src/access.rs +++ b/crates/typst-eval/src/access.rs @@ -30,12 +30,14 @@ impl Access for ast::Ident<'_> { fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { let span = self.span(); if vm.inspected == Some(span) { - if let Ok(value) = vm.scopes.get(&self).cloned() { - vm.trace(value); + if let Ok(binding) = vm.scopes.get(&self) { + vm.trace(binding.read().clone()); } } - let value = vm.scopes.get_mut(&self).at(span)?; - Ok(value) + vm.scopes + .get_mut(&self) + .and_then(|b| b.write().map_err(Into::into)) + .at(span) } } diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 2a2223e15..6f0ec1fc9 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -6,8 +6,8 @@ use typst_library::diag::{ }; use typst_library::engine::{Engine, Sink, Traced}; use typst_library::foundations::{ - Arg, Args, Capturer, Closure, Content, Context, Func, NativeElement, Scope, Scopes, - SymbolElem, Value, + Arg, Args, Binding, Capturer, Closure, Content, Context, Func, NativeElement, Scope, + Scopes, SymbolElem, Value, }; use typst_library::introspection::Introspector; use typst_library::math::LrElem; @@ -196,7 +196,7 @@ pub fn eval_closure( // Provide the closure itself for recursive calls. if let Some(name) = name { - vm.define(name, Value::Func(func.clone())); + vm.define(name, func.clone()); } let num_pos_args = args.to_pos().len(); @@ -317,11 +317,11 @@ fn eval_field_call( if let Some(callee) = target.ty().scope().get(&field) { args.insert(0, target_expr.span(), target); - Ok(FieldCall::Normal(callee.clone(), args)) + Ok(FieldCall::Normal(callee.read().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)) + Ok(FieldCall::Normal(callee.read().clone(), args)) } else { bail!(missing_field_call_error(target, field)) } @@ -458,11 +458,9 @@ impl<'a> CapturesVisitor<'a> { // Identifiers that shouldn't count as captures because they // actually bind a new name are handled below (individually through // the expressions that contain them). - Some(ast::Expr::Ident(ident)) => { - self.capture(ident.get(), ident.span(), Scopes::get) - } + Some(ast::Expr::Ident(ident)) => self.capture(ident.get(), Scopes::get), Some(ast::Expr::MathIdent(ident)) => { - self.capture(ident.get(), ident.span(), Scopes::get_in_math) + self.capture(ident.get(), Scopes::get_in_math) } // Code and content blocks create a scope. @@ -570,32 +568,34 @@ impl<'a> CapturesVisitor<'a> { /// Bind a new internal variable. fn bind(&mut self, ident: ast::Ident) { - self.internal.top.define_ident(ident, Value::None); + // The concrete value does not matter as we only use the scoping + // mechanism of `Scopes`, not the values themselves. + self.internal + .top + .bind(ident.get().clone(), Binding::detached(Value::None)); } /// Capture a variable if it isn't internal. fn capture( &mut self, ident: &EcoString, - span: Span, - getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>, + getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Binding>, ) { - if self.internal.get(ident).is_err() { - let Some(value) = self - .external - .map(|external| getter(external, ident).ok()) - .unwrap_or(Some(&Value::None)) - else { - return; - }; - - self.captures.define_captured( - ident.clone(), - value.clone(), - self.capturer, - span, - ); + if self.internal.get(ident).is_ok() { + return; } + + let binding = match self.external { + Some(external) => match getter(external, ident) { + Ok(binding) => binding.capture(self.capturer), + Err(_) => return, + }, + // The external scopes are only `None` when we are doing IDE capture + // analysis, in which case the concrete value doesn't matter. + None => Binding::detached(Value::None), + }; + + self.captures.bind(ident.clone(), binding); } } diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index 2baf4ea9e..4ac481865 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -154,7 +154,7 @@ impl Eval for ast::Ident<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - vm.scopes.get(&self).cloned().at(self.span()) + Ok(vm.scopes.get(&self).at(self.span())?.read().clone()) } } diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 2bbc7e41c..27b06af41 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -4,7 +4,7 @@ use typst_library::diag::{ bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint, }; use typst_library::engine::Engine; -use typst_library::foundations::{Content, Module, Value}; +use typst_library::foundations::{Binding, Content, Module, Value}; use typst_library::World; use typst_syntax::ast::{self, AstNode, BareImportError}; use typst_syntax::package::{PackageManifest, PackageSpec}; @@ -43,7 +43,7 @@ impl Eval for ast::ModuleImport<'_> { } } - // Source itself is imported if there is no import list or a rename. + // If there is a rename, import the source itself under that name. let bare_name = self.bare_name(); let new_name = self.new_name(); if let Some(new_name) = new_name { @@ -57,8 +57,7 @@ impl Eval for ast::ModuleImport<'_> { } } - // Define renamed module on the scope. - vm.scopes.top.define_ident(new_name, source.clone()); + vm.define(new_name, source.clone()); } let scope = source.scope().unwrap(); @@ -76,7 +75,7 @@ impl Eval for ast::ModuleImport<'_> { "this import has no effect", )); } - vm.scopes.top.define_spanned(name, source, source_span); + vm.scopes.top.bind(name, Binding::new(source, source_span)); } Ok(_) | Err(BareImportError::Dynamic) => bail!( source_span, "dynamic import requires an explicit name"; @@ -92,8 +91,8 @@ impl Eval for ast::ModuleImport<'_> { } } Some(ast::Imports::Wildcard) => { - for (var, value, span) in scope.iter() { - vm.scopes.top.define_spanned(var.clone(), value.clone(), span); + for (var, binding) in scope.iter() { + vm.scopes.top.bind(var.clone(), binding.clone()); } } Some(ast::Imports::Items(items)) => { @@ -103,7 +102,7 @@ impl Eval for ast::ModuleImport<'_> { let mut scope = scope; while let Some(component) = &path.next() { - let Some(value) = scope.get(component) else { + let Some(binding) = scope.get(component) else { errors.push(error!(component.span(), "unresolved import")); break; }; @@ -111,6 +110,7 @@ impl Eval for ast::ModuleImport<'_> { if path.peek().is_some() { // Nested import, as this is not the last component. // This must be a submodule. + let value = binding.read(); let Some(submodule) = value.scope() else { let error = if matches!(value, Value::Func(function) if function.scope().is_none()) { @@ -153,7 +153,7 @@ impl Eval for ast::ModuleImport<'_> { } } - vm.define(item.bound_name(), value.clone()); + vm.bind(item.bound_name(), binding.clone()); } } } diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index bfb54aa87..23b293f26 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -35,7 +35,7 @@ impl Eval for ast::MathIdent<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - vm.scopes.get_in_math(&self).cloned().at(self.span()) + Ok(vm.scopes.get_in_math(&self).at(self.span())?.read().clone()) } } diff --git a/crates/typst-eval/src/vm.rs b/crates/typst-eval/src/vm.rs index a5cbb6fa0..52cfb4b5b 100644 --- a/crates/typst-eval/src/vm.rs +++ b/crates/typst-eval/src/vm.rs @@ -1,7 +1,7 @@ use comemo::Tracked; use typst_library::diag::warning; use typst_library::engine::Engine; -use typst_library::foundations::{Context, IntoValue, Scopes, Value}; +use typst_library::foundations::{Binding, Context, IntoValue, Scopes, Value}; use typst_library::World; use typst_syntax::ast::{self, AstNode}; use typst_syntax::Span; @@ -42,13 +42,23 @@ impl<'a> Vm<'a> { self.engine.world } - /// Define a variable in the current scope. + /// Bind a value to an identifier. + /// + /// This will create a [`Binding`] with the value and the identifier's span. pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { - let value = value.into_value(); + self.bind(var, Binding::new(value, var.span())); + } + + /// Insert a binding into the current scope. + /// + /// This will insert the value into the top-most scope and make it available + /// for dynamic tracing, assisting IDE functionality. + pub fn bind(&mut self, var: ast::Ident, binding: Binding) { if self.inspected == Some(var.span()) { - self.trace(value.clone()); + self.trace(binding.read().clone()); } - // This will become an error in the parser if 'is' becomes a keyword. + + // This will become an error in the parser if `is` becomes a keyword. if var.get() == "is" { self.engine.sink.warn(warning!( var.span(), @@ -58,7 +68,8 @@ impl<'a> Vm<'a> { hint: "try `is_` instead" )); } - self.scopes.top.define_ident(var, value); + + self.scopes.top.bind(var.get().clone(), binding); } /// Trace a value. diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 24b76537a..f68c925d4 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -398,13 +398,13 @@ fn field_access_completions( value: &Value, styles: &Option, ) { - for (name, value, _) in value.ty().scope().iter() { - ctx.call_completion(name.clone(), value); + for (name, binding) in value.ty().scope().iter() { + ctx.call_completion(name.clone(), binding.read()); } if let Some(scope) = value.scope() { - for (name, value, _) in scope.iter() { - ctx.call_completion(name.clone(), value); + for (name, binding) in scope.iter() { + ctx.call_completion(name.clone(), binding.read()); } } @@ -541,9 +541,9 @@ fn import_item_completions<'a>( ctx.snippet_completion("*", "*", "Import everything."); } - for (name, value, _) in scope.iter() { + for (name, binding) in scope.iter() { if existing.iter().all(|item| item.original_name().as_str() != name) { - ctx.value_completion(name.clone(), value); + ctx.value_completion(name.clone(), binding.read()); } } } @@ -846,13 +846,11 @@ fn resolve_global_callee<'a>( ) -> Option<&'a Func> { let globals = globals(ctx.world, ctx.leaf); let value = match callee { - ast::Expr::Ident(ident) => globals.get(&ident)?, + ast::Expr::Ident(ident) => globals.get(&ident)?.read(), ast::Expr::FieldAccess(access) => match access.target() { - ast::Expr::Ident(target) => match globals.get(&target)? { - Value::Module(module) => module.field(&access.field()).ok()?, - Value::Func(func) => func.field(&access.field()).ok()?, - _ => return None, - }, + ast::Expr::Ident(target) => { + globals.get(&target)?.read().scope()?.get(&access.field())?.read() + } _ => return None, }, _ => return None, @@ -1464,7 +1462,8 @@ impl<'a> CompletionContext<'a> { } } - for (name, value, _) in globals(self.world, self.leaf).iter() { + for (name, binding) in globals(self.world, self.leaf).iter() { + let value = binding.read(); if filter(value) && !defined.contains_key(name) { self.value_completion_full(Some(name.clone()), value, parens, None, None); } diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs index 31fb9e34e..69d702b3b 100644 --- a/crates/typst-ide/src/definition.rs +++ b/crates/typst-ide/src/definition.rs @@ -55,8 +55,8 @@ pub fn definition( } } - if let Some(value) = globals(world, &leaf).get(&name) { - return Some(Definition::Std(value.clone())); + if let Some(binding) = globals(world, &leaf).get(&name) { + return Some(Definition::Std(binding.read().clone())); } } diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index ef8288f2a..270d2f43c 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -76,8 +76,12 @@ pub fn named_items( // ``` Some(ast::Imports::Wildcard) => { if let Some(scope) = source_value.and_then(Value::scope) { - for (name, value, span) in scope.iter() { - let item = NamedItem::Import(name, span, Some(value)); + for (name, binding) in scope.iter() { + let item = NamedItem::Import( + name, + binding.span(), + Some(binding.read()), + ); if let Some(res) = recv(item) { return Some(res); } @@ -89,24 +93,26 @@ pub fn named_items( // ``` Some(ast::Imports::Items(items)) => { for item in items.iter() { + let mut iter = item.path().iter(); + let mut binding = source_value + .and_then(Value::scope) + .zip(iter.next()) + .and_then(|(scope, first)| scope.get(&first)); + + for ident in iter { + binding = binding.and_then(|binding| { + binding.read().scope()?.get(&ident) + }); + } + let bound = item.bound_name(); + let (span, value) = match binding { + Some(binding) => (binding.span(), Some(binding.read())), + None => (bound.span(), None), + }; - let (span, value) = item.path().iter().fold( - (bound.span(), source_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)) - { + let item = NamedItem::Import(bound.get(), span, value); + if let Some(res) = recv(item) { return Some(res); } } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 99ae0620b..cfb977733 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use typst::engine::Sink; -use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; +use typst::foundations::{repr, Binding, Capturer, CastInfo, Repr, Value}; use typst::layout::{Length, PagedDocument}; use typst::syntax::ast::AstNode; use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; @@ -206,7 +206,12 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { - self.find_iter(module.scope().iter().map(|(_, v, _)| v))?; + self.find_iter(module.scope().iter().map(|(_, b)| b.read()))?; } _ => {} } diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index e4ab54e72..c93670c1d 100644 --- a/crates/typst-library/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -261,7 +261,12 @@ pub struct ToDict(Dict); cast! { ToDict, - v: Module => Self(v.scope().iter().map(|(k, v, _)| (Str::from(k.clone()), v.clone())).collect()), + v: Module => Self(v + .scope() + .iter() + .map(|(k, b)| (Str::from(k.clone()), b.read().clone())) + .collect() + ), } impl Debug for Dict { diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index a05deb1f3..741b66331 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -259,7 +259,7 @@ impl Func { let scope = self.scope().ok_or("cannot access fields on user-defined functions")?; match scope.get(field) { - Some(field) => Ok(field), + Some(binding) => Ok(binding.read()), None => match self.name() { Some(name) => bail!("function `{name}` does not contain field `{field}`"), None => bail!("function does not contain field `{field}`"), diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index a790da4f4..c335484fa 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -94,7 +94,7 @@ pub static FOUNDATIONS: Category; /// Hook up all `foundations` definitions. pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { - global.category(FOUNDATIONS); + global.start_category(FOUNDATIONS); global.define_type::(); global.define_type::(); global.define_type::(); @@ -301,7 +301,7 @@ pub fn eval( let dict = scope; let mut scope = Scope::new(); for (key, value) in dict { - scope.define_spanned(key, value, span); + scope.bind(key.into(), Binding::new(value, span)); } (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) } diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 3ee59c106..3259c17e6 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use typst_syntax::FileId; -use crate::diag::StrResult; +use crate::diag::{bail, StrResult}; use crate::foundations::{repr, ty, Content, Scope, Value}; /// An module of definitions. @@ -118,11 +118,14 @@ impl Module { } /// Try to access a definition in the module. - pub fn field(&self, name: &str) -> StrResult<&Value> { - self.scope().get(name).ok_or_else(|| match &self.name { - Some(module) => eco_format!("module `{module}` does not contain `{name}`"), - None => eco_format!("module does not contain `{name}`"), - }) + pub fn field(&self, field: &str) -> StrResult<&Value> { + match self.scope().get(field) { + Some(binding) => Ok(binding.read()), + None => match &self.name { + Some(name) => bail!("module `{name}` does not contain `{field}`"), + None => bail!("module does not contain `{field}`"), + }, + } } /// Extract the module's content. diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index cbc0f52de..a33f1cb91 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -8,7 +8,7 @@ use wasmi::Memory; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; -use crate::foundations::{cast, func, scope, Bytes, Func, Module, Scope, Value}; +use crate::foundations::{cast, func, scope, Binding, Bytes, Func, Module, Scope, Value}; use crate::loading::{DataSource, Load}; /// Loads a WebAssembly module. @@ -369,7 +369,7 @@ impl Plugin { if matches!(export.ty(), wasmi::ExternType::Func(_)) { let name = EcoString::from(export.name()); let func = PluginFunc { plugin: shared.clone(), name: name.clone() }; - scope.define(name, Func::from(func)); + scope.bind(name, Binding::detached(Func::from(func))); } } diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index b7b4a6d9d..e73afeacd 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -5,8 +5,8 @@ use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use ecow::{eco_format, EcoString}; +use indexmap::map::Entry; use indexmap::IndexMap; -use typst_syntax::ast::{self, AstNode}; use typst_syntax::Span; use typst_utils::Static; @@ -46,14 +46,14 @@ impl<'a> Scopes<'a> { self.top = self.scopes.pop().expect("no pushed scope"); } - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> HintedStrResult<&Value> { + /// Try to access a binding immutably. + pub fn get(&self, var: &str) -> HintedStrResult<&Binding> { std::iter::once(&self.top) .chain(self.scopes.iter().rev()) .find_map(|scope| scope.get(var)) .or_else(|| { self.base.and_then(|base| match base.global.scope().get(var) { - Some(value) => Some(value), + Some(binding) => Some(binding), None if var == "std" => Some(&base.std), None => None, }) @@ -61,14 +61,28 @@ impl<'a> Scopes<'a> { .ok_or_else(|| unknown_variable(var)) } - /// Try to access a variable immutably in math. - pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> { + /// Try to access a binding mutably. + pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Binding> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => cannot_mutate_constant(var), + _ if var == "std" => cannot_mutate_constant(var), + _ => unknown_variable(var), + } + }) + } + + /// Try to access a binding immutably in math. + pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Binding> { std::iter::once(&self.top) .chain(self.scopes.iter().rev()) .find_map(|scope| scope.get(var)) .or_else(|| { self.base.and_then(|base| match base.math.scope().get(var) { - Some(value) => Some(value), + Some(binding) => Some(binding), None if var == "std" => Some(&base.std), None => None, }) @@ -81,20 +95,6 @@ impl<'a> Scopes<'a> { }) } - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Value> { - std::iter::once(&mut self.top) - .chain(&mut self.scopes.iter_mut().rev()) - .find_map(|scope| scope.get_mut(var)) - .ok_or_else(|| { - match self.base.and_then(|base| base.global.scope().get(var)) { - Some(_) => cannot_mutate_constant(var), - _ if var == "std" => cannot_mutate_constant(var), - _ => unknown_variable(var), - } - })? - } - /// Check if an std variable is shadowed. pub fn check_std_shadowed(&self, var: &str) -> bool { self.base.is_some_and(|base| base.global.scope().get(var).is_some()) @@ -104,84 +104,28 @@ impl<'a> Scopes<'a> { } } -#[cold] -fn cannot_mutate_constant(var: &str) -> HintedString { - eco_format!("cannot mutate a constant: {}", var).into() -} - -/// The error message when a variable is not found. -#[cold] -fn unknown_variable(var: &str) -> HintedString { - let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); - - if var.contains('-') { - res.hint(eco_format!( - "if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`", - if var.matches('-').count() > 1 { "s" } else { "" }, - var.replace('-', " - ") - )); - } - - res -} - -#[cold] -fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { - let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); - - if matches!(var, "none" | "auto" | "false" | "true") { - res.hint(eco_format!( - "if you meant to use a literal, try adding a hash before it: `#{var}`", - )); - } else if in_global { - res.hint(eco_format!( - "`{var}` is not available directly in math, try adding a hash before it: `#{var}`", - )); - } else { - res.hint(eco_format!( - "if you meant to display multiple letters as is, try adding spaces between each letter: `{}`", - var.chars() - .flat_map(|c| [' ', c]) - .skip(1) - .collect::() - )); - res.hint(eco_format!( - "or if you meant to display this as text, try placing it in quotes: `\"{var}\"`" - )); - } - - res -} - /// A map from binding names to values. #[derive(Default, Clone)] pub struct Scope { - map: IndexMap, + map: IndexMap, deduplicate: bool, category: Option, } +/// Scope construction. impl Scope { /// Create a new empty scope. pub fn new() -> Self { Default::default() } - /// Create a new scope with the given capacity. - pub fn with_capacity(capacity: usize) -> Self { - Self { - map: IndexMap::with_capacity(capacity), - ..Default::default() - } - } - /// Create a new scope with duplication prevention. pub fn deduplicating() -> Self { Self { deduplicate: true, ..Default::default() } } /// Enter a new category. - pub fn category(&mut self, category: Category) { + pub fn start_category(&mut self, category: Category) { self.category = Some(category); } @@ -190,102 +134,87 @@ impl Scope { self.category = None; } - /// Bind a value to a name. - #[track_caller] - pub fn define(&mut self, name: impl Into, value: impl IntoValue) { - self.define_spanned(name, value, Span::detached()) - } - - /// Bind a value to a name defined by an identifier. - #[track_caller] - pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) { - self.define_spanned(ident.get().clone(), value, ident.span()) - } - - /// Bind a value to a name. - #[track_caller] - pub fn define_spanned( - &mut self, - name: impl Into, - value: impl IntoValue, - span: Span, - ) { - let name = name.into(); - - #[cfg(debug_assertions)] - if self.deduplicate && self.map.contains_key(&name) { - panic!("duplicate definition: {name}"); - } - - self.map.insert( - name, - Slot::new(value.into_value(), span, Kind::Normal, self.category), - ); - } - - /// Define a captured, immutable binding. - pub fn define_captured( - &mut self, - name: EcoString, - value: Value, - capturer: Capturer, - span: Span, - ) { - self.map.insert( - name, - Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category), - ); - } - /// Define a native function through a Rust type that shadows the function. - pub fn define_func(&mut self) { + #[track_caller] + pub fn define_func(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Func::from(data)); + self.define(data.name, Func::from(data)) } /// Define a native function with raw function data. - pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { - self.define(data.name, Func::from(data)); + #[track_caller] + pub fn define_func_with_data( + &mut self, + data: &'static NativeFuncData, + ) -> &mut Binding { + self.define(data.name, Func::from(data)) } /// Define a native type. - pub fn define_type(&mut self) { + #[track_caller] + pub fn define_type(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Type::from(data)); + self.define(data.name, Type::from(data)) } /// Define a native element. - pub fn define_elem(&mut self) { + #[track_caller] + pub fn define_elem(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Element::from(data)); + self.define(data.name, Element::from(data)) } - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> Option<&Value> { - self.map.get(var).map(Slot::read) + /// Define a built-in with compile-time known name and returns a mutable + /// reference to it. + /// + /// When the name isn't compile-time known, you should instead use: + /// - `Vm::bind` if you already have [`Binding`] + /// - `Vm::define` if you only have a [`Value`] + /// - [`Scope::bind`](Self::bind) if you are not operating in the context of + /// a `Vm` or if you are binding to something that is not an AST + /// identifier (e.g. when constructing a dynamic + /// [`Module`](super::Module)) + #[track_caller] + pub fn define(&mut self, name: &'static str, value: impl IntoValue) -> &mut Binding { + #[cfg(debug_assertions)] + if self.deduplicate && self.map.contains_key(name) { + panic!("duplicate definition: {name}"); + } + + let mut binding = Binding::detached(value); + binding.category = self.category; + self.bind(name.into(), binding) + } +} + +/// Scope manipulation and access. +impl Scope { + /// Inserts a binding into this scope and returns a mutable reference to it. + /// + /// Prefer `Vm::bind` if you are operating in the context of a `Vm`. + pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding { + match self.map.entry(name) { + Entry::Occupied(mut entry) => { + entry.insert(binding); + entry.into_mut() + } + Entry::Vacant(entry) => entry.insert(binding), + } } - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> Option> { - self.map - .get_mut(var) - .map(Slot::write) - .map(|res| res.map_err(HintedString::from)) + /// Try to access a binding immutably. + pub fn get(&self, var: &str) -> Option<&Binding> { + self.map.get(var) } - /// Get the span of a definition. - pub fn get_span(&self, var: &str) -> Option { - Some(self.map.get(var)?.span) - } - - /// Get the category of a definition. - pub fn get_category(&self, var: &str) -> Option { - self.map.get(var)?.category + /// Try to access a binding mutably. + pub fn get_mut(&mut self, var: &str) -> Option<&mut Binding> { + self.map.get_mut(var) } /// Iterate over all definitions. - pub fn iter(&self) -> impl Iterator { - self.map.iter().map(|(k, v)| (k, v.read(), v.span)) + pub fn iter(&self) -> impl Iterator { + self.map.iter() } } @@ -318,28 +247,85 @@ pub trait NativeScope { fn scope() -> Scope; } -/// A slot where a value is stored. -#[derive(Clone, Hash)] -struct Slot { - /// The stored value. +/// A bound value with metadata. +#[derive(Debug, Clone, Hash)] +pub struct Binding { + /// The bound value. value: Value, - /// The kind of slot, determines how the value can be accessed. - kind: Kind, - /// A span associated with the stored value. + /// The kind of binding, determines how the value can be accessed. + kind: BindingKind, + /// A span associated with the binding. span: Span, - /// The category of the slot. + /// The category of the binding. category: Option, } /// The different kinds of slots. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum Kind { +enum BindingKind { /// A normal, mutable binding. Normal, /// A captured copy of another variable. Captured(Capturer), } +impl Binding { + /// Create a new binding with a span marking its definition site. + pub fn new(value: impl IntoValue, span: Span) -> Self { + Self { + value: value.into_value(), + span, + kind: BindingKind::Normal, + category: None, + } + } + + /// Create a binding without a span. + pub fn detached(value: impl IntoValue) -> Self { + Self::new(value, Span::detached()) + } + + /// Read the value. + pub fn read(&self) -> &Value { + &self.value + } + + /// Try to write to the value. + /// + /// This fails if the value is a read-only closure capture. + pub fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + BindingKind::Normal => Ok(&mut self.value), + BindingKind::Captured(capturer) => bail!( + "variables from outside the {} are \ + read-only and cannot be modified", + match capturer { + Capturer::Function => "function", + Capturer::Context => "context expression", + } + ), + } + } + + /// Create a copy of the binding for closure capturing. + pub fn capture(&self, capturer: Capturer) -> Self { + Self { + kind: BindingKind::Captured(capturer), + ..self.clone() + } + } + + /// A span associated with the stored value. + pub fn span(&self) -> Span { + self.span + } + + /// The category of the value, if any. + pub fn category(&self) -> Option { + self.category + } +} + /// What the variable was captured by. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Capturer { @@ -349,35 +335,6 @@ pub enum Capturer { Context, } -impl Slot { - /// Create a new slot. - fn new(value: Value, span: Span, kind: Kind, category: Option) -> Self { - Self { value, span, kind, category } - } - - /// Read the value. - fn read(&self) -> &Value { - &self.value - } - - /// Try to write to the value. - fn write(&mut self) -> StrResult<&mut Value> { - match self.kind { - Kind::Normal => Ok(&mut self.value), - Kind::Captured(capturer) => { - bail!( - "variables from outside the {} are \ - read-only and cannot be modified", - match capturer { - Capturer::Function => "function", - Capturer::Context => "context expression", - } - ) - } - } - } -} - /// A group of related definitions. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Category(Static); @@ -417,3 +374,57 @@ pub struct CategoryData { pub title: &'static str, pub docs: &'static str, } + +/// The error message when trying to mutate a variable from the standard +/// library. +#[cold] +fn cannot_mutate_constant(var: &str) -> HintedString { + eco_format!("cannot mutate a constant: {}", var).into() +} + +/// The error message when a variable wasn't found. +#[cold] +fn unknown_variable(var: &str) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if var.contains('-') { + res.hint(eco_format!( + "if you meant to use subtraction, \ + try adding spaces around the minus sign{}: `{}`", + if var.matches('-').count() > 1 { "s" } else { "" }, + var.replace('-', " - ") + )); + } + + res +} + +/// The error message when a variable wasn't found it math. +#[cold] +fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); + + if matches!(var, "none" | "auto" | "false" | "true") { + res.hint(eco_format!( + "if you meant to use a literal, \ + try adding a hash before it: `#{var}`", + )); + } else if in_global { + res.hint(eco_format!( + "`{var}` is not available directly in math, \ + try adding a hash before it: `#{var}`", + )); + } else { + res.hint(eco_format!( + "if you meant to display multiple letters as is, \ + try adding spaces between each letter: `{}`", + var.chars().flat_map(|c| [' ', c]).skip(1).collect::() + )); + res.hint(eco_format!( + "or if you meant to display this as text, \ + try placing it in quotes: `\"{var}\"`" + )); + } + + res +} diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 973c1cb61..09f5efa1e 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -8,7 +8,7 @@ use std::sync::LazyLock; use ecow::{eco_format, EcoString}; use typst_utils::Static; -use crate::diag::StrResult; +use crate::diag::{bail, StrResult}; use crate::foundations::{ cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, }; @@ -95,9 +95,10 @@ impl Type { /// Get a field from this type's scope, if possible. pub fn field(&self, field: &str) -> StrResult<&'static Value> { - self.scope() - .get(field) - .ok_or_else(|| eco_format!("type {self} does not contain field `{field}`")) + match self.scope().get(field) { + Some(binding) => Ok(binding.read()), + None => bail!("type {self} does not contain field `{field}`"), + } } } diff --git a/crates/typst-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs index ea248172a..c412b4607 100644 --- a/crates/typst-library/src/html/mod.rs +++ b/crates/typst-library/src/html/mod.rs @@ -15,7 +15,7 @@ pub static HTML: Category; /// Create a module with all HTML definitions. pub fn module() -> Module { let mut html = Scope::deduplicating(); - html.category(HTML); + html.start_category(HTML); html.define_elem::(); html.define_elem::(); Module::new("html", html) diff --git a/crates/typst-library/src/introspection/mod.rs b/crates/typst-library/src/introspection/mod.rs index b1ff2e080..d8184330d 100644 --- a/crates/typst-library/src/introspection/mod.rs +++ b/crates/typst-library/src/introspection/mod.rs @@ -42,7 +42,7 @@ pub static INTROSPECTION: Category; /// Hook up all `introspection` definitions. pub fn define(global: &mut Scope) { - global.category(INTROSPECTION); + global.start_category(INTROSPECTION); global.define_type::(); global.define_type::(); global.define_type::(); diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index 574a2830a..57518fe72 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -74,7 +74,7 @@ pub static LAYOUT: Category; /// Hook up all `layout` definitions. pub fn define(global: &mut Scope) { - global.category(LAYOUT); + global.start_category(LAYOUT); global.define_type::(); global.define_type::(); global.define_type::(); diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 22f3a62a3..460321aa3 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -33,7 +33,7 @@ use typst_syntax::{FileId, Source, Span}; use typst_utils::{LazyHash, SmallBitSet}; use crate::diag::FileResult; -use crate::foundations::{Array, Bytes, Datetime, Dict, Module, Scope, Styles, Value}; +use crate::foundations::{Array, Binding, Bytes, Datetime, Dict, Module, Scope, Styles}; use crate::layout::{Alignment, Dir}; use crate::text::{Font, FontBook}; use crate::visualize::Color; @@ -148,7 +148,7 @@ pub struct Library { /// everything else configurable via set and show rules). pub styles: Styles, /// The standard library as a value. Used to provide the `std` variable. - pub std: Value, + pub std: Binding, /// In-development features that were enabled. pub features: Features, } @@ -196,12 +196,11 @@ impl LibraryBuilder { let math = math::module(); let inputs = self.inputs.unwrap_or_default(); let global = global(math.clone(), inputs, &self.features); - let std = Value::Module(global.clone()); Library { - global, + global: global.clone(), math, styles: Styles::new(), - std, + std: Binding::detached(global), features: self.features, } } diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index 171ae651a..c645b691d 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -41,7 +41,7 @@ pub static DATA_LOADING: Category; /// Hook up all `data-loading` definitions. pub(super) fn define(global: &mut Scope) { - global.category(DATA_LOADING); + global.start_category(DATA_LOADING); global.define_func::(); global.define_func::(); global.define_func::(); diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 3b4b133d9..a97a19b09 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -150,7 +150,7 @@ pub const WIDE: Em = Em::new(2.0); /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); - math.category(MATH); + math.start_category(MATH); math.define_elem::(); math.define_elem::(); math.define_elem::(); diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 7dad51c39..586e10ec1 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -52,7 +52,7 @@ pub static MODEL: Category; /// Hook up all `model` definitions. pub fn define(global: &mut Scope) { - global.category(MODEL); + global.start_category(MODEL); global.define_elem::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs index ec0754631..3bd3b0c52 100644 --- a/crates/typst-library/src/pdf/mod.rs +++ b/crates/typst-library/src/pdf/mod.rs @@ -12,7 +12,7 @@ pub static PDF: Category; /// Hook up the `pdf` module. pub(super) fn define(global: &mut Scope) { - global.category(PDF); + global.start_category(PDF); global.define("pdf", module()); } diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index 1617d3aa8..aee7fb83e 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -39,7 +39,7 @@ fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { /// Hook up all `symbol` definitions. pub(super) fn define(global: &mut Scope) { - global.category(SYMBOLS); + global.start_category(SYMBOLS); extend_scope_from_codex_module(global, codex::ROOT); } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index edbd24139..f506397e1 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -63,7 +63,7 @@ pub static TEXT: Category; /// Hook up all `text` definitions. pub(super) fn define(global: &mut Scope) { - global.category(TEXT); + global.start_category(TEXT); global.define_elem::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index 431191491..b0e627af2 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -36,7 +36,7 @@ pub static VISUALIZE: Category; /// Hook up all visualize definitions. pub(super) fn define(global: &mut Scope) { - global.category(VISUALIZE); + global.start_category(VISUALIZE); global.define_type::(); global.define_type::(); global.define_type::(); diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 2751500e3..004c237c0 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -16,6 +16,7 @@ use serde::Deserialize; use serde_yaml as yaml; use std::sync::LazyLock; use typst::diag::{bail, StrResult}; +use typst::foundations::Binding; use typst::foundations::{ AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr, Scope, Smart, Type, Value, FOUNDATIONS, @@ -47,8 +48,8 @@ static GROUPS: LazyLock> = LazyLock::new(|| { .module() .scope() .iter() - .filter(|(_, v, _)| matches!(v, Value::Func(_))) - .map(|(k, _, _)| k.clone()) + .filter(|(_, b)| matches!(b.read(), Value::Func(_))) + .map(|(k, _)| k.clone()) .collect(); } } @@ -60,7 +61,7 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { let scope = lib.global.scope_mut(); // Add those types, so that they show up in the docs. - scope.category(FOUNDATIONS); + scope.start_category(FOUNDATIONS); scope.define_type::(); scope.define_type::(); @@ -270,8 +271,8 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { // Add values and types. let scope = module.scope(); - for (name, value, _) in scope.iter() { - if scope.get_category(name) != Some(category) { + for (name, binding) in scope.iter() { + if binding.category() != Some(category) { continue; } @@ -279,7 +280,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { continue; } - match value { + match binding.read() { Value::Func(func) => { let name = func.name().unwrap(); @@ -476,8 +477,8 @@ fn casts( fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec { scope .iter() - .filter_map(|(_, value, _)| { - let Value::Func(func) = value else { return None }; + .filter_map(|(_, binding)| { + let Value::Func(func) = binding.read() else { return None }; Some(func_model(resolver, func, &[name], true)) }) .collect() @@ -554,7 +555,7 @@ fn group_page( let mut outline_items = vec![]; for name in &group.filter { - let value = group.module().scope().get(name).unwrap(); + let value = group.module().scope().get(name).unwrap().read(); let Ok(ref func) = value.clone().cast::() else { panic!("not a function") }; let func = func_model(resolver, func, &path, true); let id_base = urlify(&eco_format!("functions-{}", func.name)); @@ -662,8 +663,8 @@ fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> Pag /// Produce a symbol list's model. fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { let mut list = vec![]; - for (name, value, _) in group.module().scope().iter() { - let Value::Symbol(symbol) = value else { continue }; + for (name, binding) in group.module().scope().iter() { + let Value::Symbol(symbol) = binding.read() else { continue }; let complete = |variant: &str| { if variant.is_empty() { name.clone() @@ -703,7 +704,7 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { /// Extract a module from another module. #[track_caller] fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> { - match parent.scope().get(name) { + match parent.scope().get(name).map(Binding::read) { Some(Value::Module(module)) => Ok(module), _ => bail!("module doesn't contain module `{name}`"), } diff --git a/docs/src/link.rs b/docs/src/link.rs index 375cc8c2b..c7222b8e1 100644 --- a/docs/src/link.rs +++ b/docs/src/link.rs @@ -1,5 +1,5 @@ use typst::diag::{bail, StrResult}; -use typst::foundations::Func; +use typst::foundations::{Binding, Func}; use crate::{get_module, GROUPS, LIBRARY}; @@ -59,7 +59,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { while let Some(name) = parts.peek() { if category.is_none() { - category = focus.scope().get_category(name); + category = focus.scope().get(name).and_then(Binding::category); } let Ok(module) = get_module(focus, name) else { break }; focus = module; From 5b3593e571826ae44a3aeb0e0f6f09face7291ac Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 3 Feb 2025 18:06:45 +0100 Subject: [PATCH 054/101] Enable HTML feature in docs generator (#5800) --- docs/src/lib.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 004c237c0..ff745c9c2 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -21,6 +21,7 @@ use typst::foundations::{ AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr, Scope, Smart, Type, Value, FOUNDATIONS, }; +use typst::html::HTML; use typst::introspection::INTROSPECTION; use typst::layout::{Abs, Margin, PageElem, PagedDocument, LAYOUT}; use typst::loading::DATA_LOADING; @@ -31,7 +32,7 @@ use typst::symbols::SYMBOLS; use typst::text::{Font, FontBook, TEXT}; use typst::utils::LazyHash; use typst::visualize::VISUALIZE; -use typst::Library; +use typst::{Feature, Library, LibraryBuilder}; macro_rules! load { ($path:literal) => { @@ -57,7 +58,9 @@ static GROUPS: LazyLock> = LazyLock::new(|| { }); static LIBRARY: LazyLock> = LazyLock::new(|| { - let mut lib = Library::default(); + let mut lib = LibraryBuilder::default() + .with_features([Feature::Html].into_iter().collect()) + .build(); let scope = lib.global.scope_mut(); // Add those types, so that they show up in the docs. @@ -166,6 +169,7 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { category_page(resolver, INTROSPECTION), category_page(resolver, DATA_LOADING), category_page(resolver, PDF), + category_page(resolver, HTML), ]; page } From 50ccd7d60f078f3617bfed5c4e8e1fd7d45ec340 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 4 Feb 2025 10:38:31 +0100 Subject: [PATCH 055/101] Scope deprecations (#5798) --- crates/typst-eval/src/call.rs | 10 +++-- crates/typst-eval/src/code.rs | 11 ++++- crates/typst-eval/src/math.rs | 8 +++- crates/typst-ide/src/complete.rs | 2 +- crates/typst-library/src/diag.rs | 18 ++++++++ crates/typst-library/src/foundations/func.rs | 10 +++-- .../typst-library/src/foundations/module.rs | 6 +-- crates/typst-library/src/foundations/scope.rs | 28 +++++++++++- crates/typst-library/src/foundations/ty.rs | 10 +++-- crates/typst-library/src/foundations/value.rs | 10 ++--- crates/typst-library/src/loading/cbor.rs | 1 + crates/typst-library/src/loading/csv.rs | 1 + crates/typst-library/src/loading/json.rs | 1 + crates/typst-library/src/loading/toml.rs | 1 + crates/typst-library/src/loading/xml.rs | 1 + crates/typst-library/src/loading/yaml.rs | 1 + .../typst-library/src/visualize/image/mod.rs | 1 + crates/typst-library/src/visualize/mod.rs | 12 +++--- crates/typst-library/src/visualize/path.rs | 2 +- crates/typst-macros/src/scope.rs | 43 +++++++++++++------ docs/src/link.rs | 4 +- tests/suite/loading/cbor.typ | 3 ++ tests/suite/loading/csv.typ | 4 ++ tests/suite/loading/json.typ | 4 ++ tests/suite/loading/toml.typ | 4 ++ tests/suite/loading/xml.typ | 4 ++ tests/suite/loading/yaml.typ | 4 ++ tests/suite/visualize/image.typ | 5 +++ tests/suite/visualize/path.typ | 11 +++++ tests/suite/visualize/tiling.typ | 2 + 30 files changed, 179 insertions(+), 43 deletions(-) create mode 100644 tests/suite/loading/cbor.typ diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 6f0ec1fc9..c68bef963 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -315,13 +315,15 @@ fn eval_field_call( (target, args) }; + let field_span = field.span(); + let sink = (&mut vm.engine, field_span); if let Some(callee) = target.ty().scope().get(&field) { args.insert(0, target_expr.span(), target); - Ok(FieldCall::Normal(callee.read().clone(), args)) + Ok(FieldCall::Normal(callee.read_checked(sink).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.read().clone(), args)) + Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args)) } else { bail!(missing_field_call_error(target, field)) } @@ -331,7 +333,7 @@ fn eval_field_call( ) { // Certain value types may have their own ways to access method fields. // e.g. `$arrow.r(v)$`, `table.cell[..]` - let value = target.field(&field).at(field.span())?; + let value = target.field(&field, sink).at(field_span)?; Ok(FieldCall::Normal(value, args)) } else { // Otherwise we cannot call this field. @@ -364,7 +366,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { field.as_str(), )); } - _ if target.field(&field).is_ok() => { + _ if target.field(&field, ()).is_ok() => { error.hint(eco_format!( "did you mean to access the field `{}`?", field.as_str(), diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index 4ac481865..a7b6b6f90 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -154,7 +154,13 @@ impl Eval for ast::Ident<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - Ok(vm.scopes.get(&self).at(self.span())?.read().clone()) + let span = self.span(); + Ok(vm + .scopes + .get(&self) + .at(span)? + .read_checked((&mut vm.engine, span)) + .clone()) } } @@ -310,8 +316,9 @@ impl Eval for ast::FieldAccess<'_> { fn eval(self, vm: &mut Vm) -> SourceResult { let value = self.target().eval(vm)?; let field = self.field(); + let field_span = field.span(); - let err = match value.field(&field).at(field.span()) { + let err = match value.field(&field, (&mut vm.engine, field_span)).at(field_span) { Ok(value) => return Ok(value), Err(err) => err, }; diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index 23b293f26..0e271a089 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -35,7 +35,13 @@ impl Eval for ast::MathIdent<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - Ok(vm.scopes.get_in_math(&self).at(self.span())?.read().clone()) + let span = self.span(); + Ok(vm + .scopes + .get_in_math(&self) + .at(span)? + .read_checked((&mut vm.engine, span)) + .clone()) } } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index f68c925d4..c1f08cf09 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -414,7 +414,7 @@ fn field_access_completions( // with method syntax; // 2. We can unwrap the field's value since it's a field belonging to // this value's type, so accessing it should not fail. - ctx.value_completion(field, &value.field(field).unwrap()); + ctx.value_completion(field, &value.field(field, ()).unwrap()); } match value { diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index bd4c90a15..49cbd02c6 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -11,6 +11,7 @@ use ecow::{eco_vec, EcoVec}; use typst_syntax::package::{PackageSpec, PackageVersion}; use typst_syntax::{Span, Spanned, SyntaxError}; +use crate::engine::Engine; use crate::{World, WorldExt}; /// Early-return with a [`StrResult`] or [`SourceResult`]. @@ -228,6 +229,23 @@ impl From for SourceDiagnostic { } } +/// Destination for a deprecation message when accessing a deprecated value. +pub trait DeprecationSink { + /// Emits the given deprecation message into this sink. + fn emit(self, message: &str); +} + +impl DeprecationSink for () { + fn emit(self, _: &str) {} +} + +impl DeprecationSink for (&mut Engine<'_>, Span) { + /// Emits the deprecation message as a warning. + fn emit(self, message: &str) { + self.0.sink.warn(SourceDiagnostic::warning(self.1, message)); + } +} + /// A part of a diagnostic's [trace](SourceDiagnostic::trace). #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Tracepoint { diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 741b66331..3ed1562f6 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -9,7 +9,7 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{ast, Span, SyntaxNode}; use typst_utils::{singleton, LazyHash, Static}; -use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::diag::{bail, At, DeprecationSink, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs, @@ -255,11 +255,15 @@ impl Func { } /// Get a field from this function's scope, if possible. - pub fn field(&self, field: &str) -> StrResult<&'static Value> { + pub fn field( + &self, + field: &str, + sink: impl DeprecationSink, + ) -> StrResult<&'static Value> { let scope = self.scope().ok_or("cannot access fields on user-defined functions")?; match scope.get(field) { - Some(binding) => Ok(binding.read()), + Some(binding) => Ok(binding.read_checked(sink)), None => match self.name() { Some(name) => bail!("function `{name}` does not contain field `{field}`"), None => bail!("function does not contain field `{field}`"), diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 3259c17e6..8d9626a1a 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use typst_syntax::FileId; -use crate::diag::{bail, StrResult}; +use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{repr, ty, Content, Scope, Value}; /// An module of definitions. @@ -118,9 +118,9 @@ impl Module { } /// Try to access a definition in the module. - pub fn field(&self, field: &str) -> StrResult<&Value> { + pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<&Value> { match self.scope().get(field) { - Some(binding) => Ok(binding.read()), + Some(binding) => Ok(binding.read_checked(sink)), None => match &self.name { Some(name) => bail!("module `{name}` does not contain `{field}`"), None => bail!("module does not contain `{field}`"), diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index e73afeacd..d6c5a8d05 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -10,7 +10,7 @@ use indexmap::IndexMap; use typst_syntax::Span; use typst_utils::Static; -use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; +use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, Type, Value, @@ -258,6 +258,8 @@ pub struct Binding { span: Span, /// The category of the binding. category: Option, + /// A deprecation message for the definition. + deprecation: Option<&'static str>, } /// The different kinds of slots. @@ -277,6 +279,7 @@ impl Binding { span, kind: BindingKind::Normal, category: None, + deprecation: None, } } @@ -285,11 +288,29 @@ impl Binding { Self::new(value, Span::detached()) } + /// Marks this binding as deprecated, with the given `message`. + pub fn deprecated(&mut self, message: &'static str) -> &mut Self { + self.deprecation = Some(message); + self + } + /// Read the value. pub fn read(&self) -> &Value { &self.value } + /// Read the value, checking for deprecation. + /// + /// As the `sink` + /// - pass `()` to ignore the message. + /// - pass `(&mut engine, span)` to emit a warning into the engine. + pub fn read_checked(&self, sink: impl DeprecationSink) -> &Value { + if let Some(message) = self.deprecation { + sink.emit(message); + } + &self.value + } + /// Try to write to the value. /// /// This fails if the value is a read-only closure capture. @@ -320,6 +341,11 @@ impl Binding { self.span } + /// A deprecation message for the value, if any. + pub fn deprecation(&self) -> Option<&'static str> { + self.deprecation + } + /// The category of the value, if any. pub fn category(&self) -> Option { self.category diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 09f5efa1e..40f7003c3 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -8,7 +8,7 @@ use std::sync::LazyLock; use ecow::{eco_format, EcoString}; use typst_utils::Static; -use crate::diag::{bail, StrResult}; +use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{ cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, }; @@ -94,9 +94,13 @@ impl Type { } /// Get a field from this type's scope, if possible. - pub fn field(&self, field: &str) -> StrResult<&'static Value> { + pub fn field( + &self, + field: &str, + sink: impl DeprecationSink, + ) -> StrResult<&'static Value> { match self.scope().get(field) { - Some(binding) => Ok(binding.read()), + Some(binding) => Ok(binding.read_checked(sink)), None => bail!("type {self} does not contain field `{field}`"), } } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 4fa380b46..854c2486e 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use typst_syntax::{ast, Span}; use typst_utils::ArcExt; -use crate::diag::{HintedStrResult, HintedString, StrResult}; +use crate::diag::{DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, @@ -155,15 +155,15 @@ impl Value { } /// Try to access a field on the value. - pub fn field(&self, field: &str) -> StrResult { + pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult { match self { Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), Self::Version(version) => version.component(field).map(Self::Int), Self::Dict(dict) => dict.get(field).cloned(), Self::Content(content) => content.field_by_name(field), - Self::Type(ty) => ty.field(field).cloned(), - Self::Func(func) => func.field(field).cloned(), - Self::Module(module) => module.field(field).cloned(), + Self::Type(ty) => ty.field(field, sink).cloned(), + Self::Func(func) => func.field(field, sink).cloned(), + Self::Module(module) => module.field(field, sink).cloned(), _ => fields::field(self, field), } } diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 2bdeb80ef..bd65e8442 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -38,6 +38,7 @@ impl cbor { /// This function is deprecated. The [`cbor`] function now accepts bytes /// directly. #[func(title = "Decode CBOR")] + #[deprecated = "`cbor.decode` is deprecated, directly pass bytes to `cbor` instead"] pub fn decode( engine: &mut Engine, /// CBOR data. diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 1cf656ae2..d01d687ba 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -100,6 +100,7 @@ impl csv { /// This function is deprecated. The [`csv`] function now accepts bytes /// directly. #[func(title = "Decode CSV")] + #[deprecated = "`csv.decode` is deprecated, directly pass bytes to `csv` instead"] pub fn decode( engine: &mut Engine, /// CSV data. diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 035c5e4a7..52c87371f 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -69,6 +69,7 @@ impl json { /// This function is deprecated. The [`json`] function now accepts bytes /// directly. #[func(title = "Decode JSON")] + #[deprecated = "`json.decode` is deprecated, directly pass bytes to `json` instead"] pub fn decode( engine: &mut Engine, /// JSON data. diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 402207b02..456112463 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -48,6 +48,7 @@ impl toml { /// This function is deprecated. The [`toml`] function now accepts bytes /// directly. #[func(title = "Decode TOML")] + #[deprecated = "`toml.decode` is deprecated, directly pass bytes to `toml` instead"] pub fn decode( engine: &mut Engine, /// TOML data. diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index ca467c238..0172071be 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -81,6 +81,7 @@ impl xml { /// This function is deprecated. The [`xml`] function now accepts bytes /// directly. #[func(title = "Decode XML")] + #[deprecated = "`xml.decode` is deprecated, directly pass bytes to `xml` instead"] pub fn decode( engine: &mut Engine, /// XML data. diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 5767cb640..511c676cb 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -59,6 +59,7 @@ impl yaml { /// This function is deprecated. The [`yaml`] function now accepts bytes /// directly. #[func(title = "Decode YAML")] + #[deprecated = "`yaml.decode` is deprecated, directly pass bytes to `yaml` instead"] pub fn decode( engine: &mut Engine, /// YAML data. diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 07ebdabe2..9306eb6f2 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -171,6 +171,7 @@ impl ImageElem { /// #image.decode(changed) /// ``` #[func(title = "Decode Image")] + #[deprecated = "`image.decode` is deprecated, directly pass bytes to `image` instead"] pub fn decode( span: Span, /// The data to decode as an image. Can be a string for SVGs. diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index b0e627af2..76849ac86 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -24,7 +24,7 @@ pub use self::shape::*; pub use self::stroke::*; pub use self::tiling::*; -use crate::foundations::{category, Category, Scope, Type}; +use crate::foundations::{category, Category, Element, Scope, Type}; /// Drawing and data visualization. /// @@ -49,8 +49,10 @@ pub(super) fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); - - // Compatibility. - global.define("pattern", Type::of::()); + global + .define("path", Element::of::()) + .deprecated("the `path` function is deprecated, use `curve` instead"); + global + .define("pattern", Type::of::()) + .deprecated("the name `pattern` is deprecated, use `tiling` instead"); } diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index 6aacb3198..5d3439c08 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -23,7 +23,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// ``` /// /// # Deprecation -/// This element is deprecated. The [`curve`] element should be used instead. +/// This function is deprecated. The [`curve`] function should be used instead. #[elem(Show)] pub struct PathElem { /// How to fill the path. diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs index 8a2f1ce61..392ab1a53 100644 --- a/crates/typst-macros/src/scope.rs +++ b/crates/typst-macros/src/scope.rs @@ -31,18 +31,37 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result { let mut definitions = vec![]; let mut constructor = quote! { None }; for child in &mut item.items { - let def = match child { - syn::ImplItem::Const(item) => handle_const(&self_ty_expr, item)?, - syn::ImplItem::Fn(item) => match handle_fn(self_ty, item)? { - FnKind::Member(tokens) => tokens, - FnKind::Constructor(tokens) => { - constructor = tokens; - continue; - } - }, - syn::ImplItem::Verbatim(item) => handle_type_or_elem(item)?, + let bare: BareType; + let (mut def, attrs) = match child { + syn::ImplItem::Const(item) => { + (handle_const(&self_ty_expr, item)?, &item.attrs) + } + syn::ImplItem::Fn(item) => ( + match handle_fn(self_ty, item)? { + FnKind::Member(tokens) => tokens, + FnKind::Constructor(tokens) => { + constructor = tokens; + continue; + } + }, + &item.attrs, + ), + syn::ImplItem::Verbatim(item) => { + bare = syn::parse2(item.clone())?; + (handle_type_or_elem(&bare)?, &bare.attrs) + } _ => bail!(child, "unexpected item in scope"), }; + + if let Some(message) = attrs.iter().find_map(|attr| match &attr.meta { + syn::Meta::NameValue(pair) if pair.path.is_ident("deprecated") => { + Some(&pair.value) + } + _ => None, + }) { + def = quote! { #def.deprecated(#message) } + } + definitions.push(def); } @@ -61,6 +80,7 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result { #constructor } + #[allow(deprecated)] fn scope() -> #foundations::Scope { let mut scope = #foundations::Scope::deduplicating(); #(#definitions;)* @@ -78,8 +98,7 @@ fn handle_const(self_ty: &TokenStream, item: &syn::ImplItemConst) -> Result Result { - let item: BareType = syn::parse2(item.clone())?; +fn handle_type_or_elem(item: &BareType) -> Result { let ident = &item.ident; let define = if item.attrs.iter().any(|attr| attr.path().is_ident("elem")) { quote! { define_elem } diff --git a/docs/src/link.rs b/docs/src/link.rs index c7222b8e1..c55261b84 100644 --- a/docs/src/link.rs +++ b/docs/src/link.rs @@ -69,7 +69,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { let Some(category) = category else { bail!("{head} has no category") }; let name = parts.next().ok_or("link is missing first part")?; - let value = focus.field(name)?; + let value = focus.field(name, ())?; // Handle grouped functions. if let Some(group) = GROUPS.iter().find(|group| { @@ -88,7 +88,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { let mut route = format!("{}reference/{}/{name}", base, category.name()); if let Some(next) = parts.next() { - if let Ok(field) = value.field(next) { + if let Ok(field) = value.field(next, ()) { route.push_str("/#definitions-"); route.push_str(next); if let Some(next) = parts.next() { diff --git a/tests/suite/loading/cbor.typ b/tests/suite/loading/cbor.typ new file mode 100644 index 000000000..4b50bb9c3 --- /dev/null +++ b/tests/suite/loading/cbor.typ @@ -0,0 +1,3 @@ +--- cbor-decode-deprecated --- +// Warning: 15-21 `cbor.decode` is deprecated, directly pass bytes to `cbor` instead +#let _ = cbor.decode diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ index 93545fc49..6f57ec458 100644 --- a/tests/suite/loading/csv.typ +++ b/tests/suite/loading/csv.typ @@ -29,3 +29,7 @@ --- csv-invalid-delimiter --- // Error: 41-51 delimiter must be an ASCII character #csv("/assets/data/zoo.csv", delimiter: "\u{2008}") + +--- csv-decode-deprecated --- +// Warning: 14-20 `csv.decode` is deprecated, directly pass bytes to `csv` instead +#let _ = csv.decode diff --git a/tests/suite/loading/json.typ b/tests/suite/loading/json.typ index 3ebeaf2f7..c8df1ff6e 100644 --- a/tests/suite/loading/json.typ +++ b/tests/suite/loading/json.typ @@ -9,6 +9,10 @@ // Error: 7-30 failed to parse JSON (expected value at line 3 column 14) #json("/assets/data/bad.json") +--- json-decode-deprecated --- +// Warning: 15-21 `json.decode` is deprecated, directly pass bytes to `json` instead +#let _ = json.decode + --- issue-3363-json-large-number --- // Big numbers (larger than what i64 can store) should just lose some precision // but not overflow diff --git a/tests/suite/loading/toml.typ b/tests/suite/loading/toml.typ index 855ca995d..a4318a015 100644 --- a/tests/suite/loading/toml.typ +++ b/tests/suite/loading/toml.typ @@ -39,3 +39,7 @@ --- toml-invalid --- // Error: 7-30 failed to parse TOML (expected `.`, `=` at line 1 column 16) #toml("/assets/data/bad.toml") + +--- toml-decode-deprecated --- +// Warning: 15-21 `toml.decode` is deprecated, directly pass bytes to `toml` instead +#let _ = toml.decode diff --git a/tests/suite/loading/xml.typ b/tests/suite/loading/xml.typ index 41cd20e74..933f3c480 100644 --- a/tests/suite/loading/xml.typ +++ b/tests/suite/loading/xml.typ @@ -26,3 +26,7 @@ --- xml-invalid --- // Error: 6-28 failed to parse XML (found closing tag 'data' instead of 'hello' in line 3) #xml("/assets/data/bad.xml") + +--- xml-decode-deprecated --- +// Warning: 14-20 `xml.decode` is deprecated, directly pass bytes to `xml` instead +#let _ = xml.decode diff --git a/tests/suite/loading/yaml.typ b/tests/suite/loading/yaml.typ index bbfea41cb..a8089052c 100644 --- a/tests/suite/loading/yaml.typ +++ b/tests/suite/loading/yaml.typ @@ -15,3 +15,7 @@ --- yaml-invalid --- // Error: 7-30 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18) #yaml("/assets/data/bad.yaml") + +--- yaml-decode-deprecated --- +// Warning: 15-21 `yaml.decode` is deprecated, directly pass bytes to `yaml` instead +#let _ = yaml.decode diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 6f6e1a157..e37932f28 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -161,22 +161,27 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-decode-svg --- // Test parsing from svg data +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") --- image-decode-bad-svg --- // Error: 2-168 failed to parse SVG (missing root node) +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") --- image-decode-detect-format --- // Test format auto detect +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), width: 80%) --- image-decode-specify-format --- // Test format manual +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "jpg", width: 80%) --- image-decode-specify-wrong-format --- // Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.) +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%) --- image-pixmap-empty --- diff --git a/tests/suite/visualize/path.typ b/tests/suite/visualize/path.typ index 55c0f5340..e44b2270e 100644 --- a/tests/suite/visualize/path.typ +++ b/tests/suite/visualize/path.typ @@ -6,6 +6,7 @@ columns: (1fr, 1fr), rows: (1fr, 1fr, 1fr), align: center + horizon, + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, closed: true, @@ -14,6 +15,7 @@ ((0%, 50%), (4%, 4%)), ((50%, 0%), (4%, 4%)), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: purple, stroke: 1pt, @@ -22,6 +24,7 @@ (0pt, 30pt), (30pt, 0pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: blue, stroke: 1pt, @@ -30,6 +33,7 @@ ((30%, 60%), (-20%, 0%), (0%, 0%)), ((50%, 30%), (60%, -30%), (60%, 0%)), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( stroke: 5pt, closed: true, @@ -37,6 +41,7 @@ (30pt, 30pt), (15pt, 0pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, fill-rule: "non-zero", @@ -47,6 +52,7 @@ (0pt, 20pt), (40pt, 50pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, fill-rule: "even-odd", @@ -61,18 +67,22 @@ --- path-bad-vertex --- // Error: 7-9 path vertex must have 1, 2, or 3 points +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(()) --- path-bad-point-count --- // Error: 7-47 path vertex must have 1, 2, or 3 points +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(((0%, 0%), (0%, 0%), (0%, 0%), (0%, 0%))) --- path-bad-point-array --- // Error: 7-31 point array must contain exactly two entries +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(((0%, 0%), (0%, 0%, 0%))) --- path-infinite-length --- // Error: 2-42 cannot create path with infinite length +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path((0pt, 0pt), (float.inf * 1pt, 0pt)) --- issue-path-in-sized-container --- @@ -82,6 +92,7 @@ fill: aqua, width: 20pt, height: 15pt, + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( (0pt, 0pt), (10pt, 10pt), diff --git a/tests/suite/visualize/tiling.typ b/tests/suite/visualize/tiling.typ index 5e61aa43a..904133411 100644 --- a/tests/suite/visualize/tiling.typ +++ b/tests/suite/visualize/tiling.typ @@ -159,5 +159,7 @@ --- tiling-pattern-compatibility --- #set page(width: auto, height: auto, margin: 0pt) + +// Warning: 10-17 the name `pattern` is deprecated, use `tiling` instead #let t = pattern(size: (10pt, 10pt), line(stroke: 4pt, start: (0%, 0%), end: (100%, 100%))) #rect(width: 50pt, height: 50pt, fill: t) From b25cf22018e849c7f52ee107789946f7c271e54e Mon Sep 17 00:00:00 2001 From: Ryan Chua <71936834+Toafu@users.noreply.github.com> Date: Tue, 4 Feb 2025 04:40:10 -0500 Subject: [PATCH 056/101] Fix typo in page documentation (#5804) --- crates/typst-library/src/layout/page.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 68fd89745..0964dccd2 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -270,7 +270,7 @@ pub struct PageElem { /// margin: (top: 32pt, bottom: 20pt), /// header: [ /// #set text(8pt) - /// #smallcaps[Typst Academcy] + /// #smallcaps[Typst Academy] /// #h(1fr) _Exercise Sheet 3_ /// ], /// ) From 73ffbdef2b3498307328da355b1d933b1ccf206a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:40:28 +0100 Subject: [PATCH 057/101] Bump openssl from 0.10.66 to 0.10.70 (#5802) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d2e410e14..44006cd14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,9 +1566,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -1607,9 +1607,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", From 0ea668077d6a47f64ee3875dbed31f9e8d832ae3 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 4 Feb 2025 11:08:43 +0100 Subject: [PATCH 058/101] Bump codex to 0.1.0 (#5805) --- Cargo.lock | 3 +- Cargo.toml | 2 +- crates/typst-library/src/symbols.rs | 46 +++++++++++++++------------ tests/ref/symbol-sect-deprecated.png | Bin 0 -> 391 bytes tests/suite/symbols/symbol.typ | 4 +++ 5 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 tests/ref/symbol-sect-deprecated.png diff --git a/Cargo.lock b/Cargo.lock index 44006cd14..215731282 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,7 +410,8 @@ dependencies = [ [[package]] name = "codex" version = "0.1.0" -source = "git+https://github.com/typst/codex?rev=343a9b1#343a9b199430681ba3ca0e2242097c6419492d55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0ee2092c3513f63588d51c3f81b98e6b1aa8ddcca3b5892b288f093516497d" [[package]] name = "color-print" diff --git a/Cargo.toml b/Cargo.toml index d03bfa6d1..3550963e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "343a9b1" } +codex = "0.1.0" color-print = "0.3.6" comemo = "0.4" csv = "1" diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index aee7fb83e..777f8172f 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -10,6 +10,31 @@ use crate::foundations::{category, Category, Module, Scope, Symbol, Value}; #[category] pub static SYMBOLS: Category; +/// Hook up all `symbol` definitions. +pub(super) fn define(global: &mut Scope) { + global.start_category(SYMBOLS); + extend_scope_from_codex_module(global, codex::ROOT); +} + +/// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. +pub(super) fn define_math(math: &mut Scope) { + extend_scope_from_codex_module(math, codex::SYM); +} + +fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { + for (name, binding) in module.iter() { + let value = match binding.def { + codex::Def::Symbol(s) => Value::Symbol(s.into()), + codex::Def::Module(m) => Value::Module(Module::new(name, m.into())), + }; + + let scope_binding = scope.define(name, value); + if let Some(message) = binding.deprecation { + scope_binding.deprecated(message); + } + } +} + impl From for Scope { fn from(module: codex::Module) -> Scope { let mut scope = Self::new(); @@ -26,24 +51,3 @@ impl From for Symbol { } } } - -fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { - for (name, definition) in module.iter() { - let value = match definition { - codex::Def::Symbol(s) => Value::Symbol(s.into()), - codex::Def::Module(m) => Value::Module(Module::new(name, m.into())), - }; - scope.define(name, value); - } -} - -/// Hook up all `symbol` definitions. -pub(super) fn define(global: &mut Scope) { - global.start_category(SYMBOLS); - extend_scope_from_codex_module(global, codex::ROOT); -} - -/// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. -pub(super) fn define_math(math: &mut Scope) { - extend_scope_from_codex_module(math, codex::SYM); -} diff --git a/tests/ref/symbol-sect-deprecated.png b/tests/ref/symbol-sect-deprecated.png new file mode 100644 index 0000000000000000000000000000000000000000..da647d5f7254282a95d1ef707ba7bb211c4e60dc GIT binary patch literal 391 zcmV;20eJq2P)8$rI;HctnB4#jYUoPe(=Zbclf0 zt^eOtFqHjttpZVRD{Aw%K}6L5{XZH)zQy|w=zypRkN^L_yb#3ttMUK;$w&-!3s)q9 zsG9Hp|1UoaVqFRN|9^5H*%tqr`%er+cfg5Lc3Udqe*qaUy20QDBm%ab_Jk?<)emA# zP2L>QMSdJlo;X+FCy?&^4 Date: Tue, 4 Feb 2025 16:22:24 +0100 Subject: [PATCH 059/101] Bump dependencies (#5808) --- Cargo.lock | 843 +++++++++++++++++++++++++++-------------------------- Cargo.toml | 12 +- 2 files changed, 428 insertions(+), 427 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 215731282..8b7754aef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,18 +8,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -46,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -61,36 +49,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -104,9 +93,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -192,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -213,9 +202,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "by_address" @@ -225,9 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -243,9 +232,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" -version = "1.1.24" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -278,14 +267,14 @@ checksum = "7588475145507237ded760e52bf2f1085495245502033756d28ea72ade0e498b" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -333,9 +322,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -343,9 +332,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -356,18 +345,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.32" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" +checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -377,15 +366,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" -version = "0.2.23" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ "clap", "roff", @@ -415,18 +404,18 @@ checksum = "2e0ee2092c3513f63588d51c3f81b98e6b1aa8ddcca3b5892b288f093516497d" [[package]] name = "color-print" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee543c60ff3888934877a5671f45494dd27ed4ba25c6670b9a7576b7ed7a8c0" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" dependencies = [ "color-print-proc-macro", ] [[package]] name = "color-print-proc-macro" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ff1a80c5f3cb1ca7c06ffdd71b6a6dd6d8f896c42141fbd43f50ed28dcdb93" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" dependencies = [ "nom", "proc-macro2", @@ -442,9 +431,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "comemo" @@ -455,7 +444,7 @@ dependencies = [ "comemo-macros", "once_cell", "parking_lot", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -487,9 +476,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core_maths" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" dependencies = [ "libm", ] @@ -505,18 +494,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -533,21 +522,21 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -581,9 +570,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", @@ -592,23 +581,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -630,9 +619,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "ecow" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54bfbb1708988623190a6c4dbedaeaf0f53c20c6395abd6a01feb327b3146f4b" +checksum = "e42fc0a93992b20c58b99e59d61eaf1635a25bfbe49e4275c34ba0aee98119ba" dependencies = [ "serde", ] @@ -693,12 +682,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -719,15 +708,15 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -746,9 +735,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -766,6 +755,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -851,7 +846,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -880,20 +887,14 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "hayagriva" @@ -904,12 +905,12 @@ dependencies = [ "biblatex", "ciborium", "citationberg", - "indexmap 2.6.0", + "indexmap 2.7.1", "numerals", "paste", "serde", "serde_yaml 0.9.34+deprecated", - "thiserror", + "thiserror 1.0.69", "unic-langid", "unicode-segmentation", "unscanny", @@ -1003,6 +1004,30 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + [[package]] name = "icu_properties" version = "1.5.1" @@ -1107,12 +1132,23 @@ checksum = "f739ee737260d955e330bc83fdeaaf1631f7fb7ed218761d3c04bb13bb7d79df" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1165,9 +1201,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -1175,19 +1211,13 @@ dependencies = [ "serde", ] -[[package]] -name = "indexmap-nostd" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" - [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "inotify-sys", "libc", ] @@ -1228,9 +1258,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -1243,18 +1273,19 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "kamadak-exif" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" dependencies = [ "mutate_once", ] @@ -1291,33 +1322,33 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libdeflate-sys" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b14a6afa4e2e1d343fd793a1c0a7e5857a73a2697c2ff2c98ac00d6c4ecc820" +checksum = "413b667c8a795fcbe6287a75a8ce92b1dae928172c716fe95044cb2ec7877941" dependencies = [ "cc", ] [[package]] name = "libdeflater" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17fe2badabdaf756f620748311e99ef99a5fdd681562dfd343fdb16ed7d4797" +checksum = "d78376c917eec0550b9c56c858de50e1b7ebf303116487562e624e63ce51453a" dependencies = [ "libdeflate-sys", ] [[package]] name = "libfuzzer-sys" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" dependencies = [ "arbitrary", "cc", @@ -1325,9 +1356,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1335,7 +1366,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -1348,9 +1379,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lipsum" @@ -1364,9 +1395,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" dependencies = [ "serde", ] @@ -1389,9 +1420,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lzma-sys" @@ -1427,9 +1458,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", "simd-adler32", @@ -1437,14 +1468,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.48.0", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", ] [[package]] @@ -1461,9 +1492,9 @@ checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -1488,12 +1519,11 @@ dependencies = [ [[package]] name = "notify" -version = "6.1.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.6.0", - "crossbeam-channel", + "bitflags 2.8.0", "filetime", "fsevent-sys", "inotify", @@ -1501,10 +1531,17 @@ dependencies = [ "libc", "log", "mio", + "notify-types", "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "num-bigint" version = "0.4.6" @@ -1547,18 +1584,15 @@ checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31" [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "open" -version = "5.3.0" +version = "5.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" dependencies = [ "is-wsl", "libc", @@ -1571,7 +1605,7 @@ version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -1593,15 +1627,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] @@ -1627,22 +1661,19 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "oxipng" -version = "9.1.2" +version = "9.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec25597808aff9f632f018f0fe8985c6f670598ac5241d220a9f2d32ff46812e" +checksum = "aa3202b10a7ffac89508bb091fe420048c47926b37c5ff84d78dc8af7044fa86" dependencies = [ "bitvec", - "clap", - "clap_mangen", "crossbeam-channel", "filetime", - "indexmap 2.6.0", + "indexmap 2.7.1", "libdeflater", "log", "rayon", "rgb", "rustc-hash", - "rustc_version", "zopfli", ] @@ -1690,7 +1721,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1701,17 +1732,17 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be17f48d7fbbd22c6efedb58af5d409aa578e407f40b29a0bcb4e66ed84c5c98" +checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "itoa", "memchr", "ryu", @@ -1725,9 +1756,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -1735,9 +1766,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1745,9 +1776,9 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", @@ -1758,11 +1789,11 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -1793,7 +1824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64", - "indexmap 2.6.0", + "indexmap 2.7.1", "quick-xml 0.32.0", "serde", "time", @@ -1801,9 +1832,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -1814,15 +1845,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "postcard" -version = "1.0.10" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -1847,18 +1878,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] @@ -1869,7 +1900,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "getopts", "memchr", "unicase", @@ -1908,9 +1939,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -1968,29 +1999,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.11", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2000,9 +2031,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2065,37 +2096,28 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustybuzz" @@ -2103,7 +2125,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "core_maths", "log", @@ -2117,9 +2139,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -2132,9 +2154,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -2151,7 +2173,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -2160,9 +2182,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2181,24 +2203,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2207,9 +2229,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -2244,7 +2266,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "itoa", "ryu", "serde", @@ -2280,19 +2302,13 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -2350,12 +2366,11 @@ dependencies = [ [[package]] name = "string-interner" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6a0d765f5807e98a091107bae0a56ea3799f66a5de47b2c84c94a39c09974e" +checksum = "1a3275464d7a9f2d4cac57c89c2ef96a8524dba2864c8d6f82e3980baf136f9b" dependencies = [ - "cfg-if", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] @@ -2406,7 +2421,7 @@ dependencies = [ "once_cell", "pdf-writer", "resvg", - "siphasher 1.0.1", + "siphasher", "subsetter", "tiny-skia", "ttf-parser", @@ -2415,19 +2430,19 @@ dependencies = [ [[package]] name = "svgtypes" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", - "siphasher 1.0.1", + "siphasher", ] [[package]] name = "syn" -version = "2.0.79" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -2462,7 +2477,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] @@ -2475,9 +2490,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" dependencies = [ "filetime", "libc", @@ -2486,12 +2501,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2508,9 +2524,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", @@ -2524,18 +2540,38 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -2544,9 +2580,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -2565,9 +2601,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2624,9 +2660,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -2660,11 +2696,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", @@ -2797,7 +2833,7 @@ dependencies = [ "comemo", "ecow", "if_chain", - "indexmap 2.6.0", + "indexmap 2.7.1", "stacker", "toml", "typst-library", @@ -2907,7 +2943,7 @@ name = "typst-library" version = "0.12.0" dependencies = [ "az", - "bitflags 2.6.0", + "bitflags 2.8.0", "bumpalo", "chinese-number", "ciborium", @@ -2922,7 +2958,7 @@ dependencies = [ "icu_provider", "icu_provider_blob", "image", - "indexmap 2.6.0", + "indexmap 2.7.1", "kamadak-exif", "kurbo", "lipsum", @@ -2939,7 +2975,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml 0.9.34+deprecated", - "siphasher 1.0.1", + "siphasher", "smallvec", "syntect", "time", @@ -2981,7 +3017,7 @@ dependencies = [ "comemo", "ecow", "image", - "indexmap 2.6.0", + "indexmap 2.7.1", "miniz_oxide", "pdf-writer", "serde", @@ -3105,7 +3141,7 @@ dependencies = [ "once_cell", "portable-atomic", "rayon", - "siphasher 1.0.1", + "siphasher", "thin-vec", "unicode-math-class", ] @@ -3131,12 +3167,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" @@ -3158,9 +3191,9 @@ checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-math-class" @@ -3221,9 +3254,9 @@ checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" [[package]] name = "ureq" -version = "2.10.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ "base64", "flate2", @@ -3237,9 +3270,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3264,7 +3297,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -3274,6 +3307,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3315,25 +3354,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.93" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -3342,9 +3390,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3352,9 +3400,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -3365,15 +3413,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasmi" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7a1acc721dd73e4fff2dc3796cc3efda6e008369e859a20fdbe058bddeebc3" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" dependencies = [ "arrayvec", "multi-stash", @@ -3382,23 +3433,23 @@ dependencies = [ "wasmi_collections", "wasmi_core", "wasmi_ir", - "wasmparser-nostd", + "wasmparser", ] [[package]] name = "wasmi_collections" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142fda775f9cda587681ff0ec63c7a7e5679dc95da75f3f9b7e3979ce3506a5b" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" dependencies = [ "string-interner", ] [[package]] name = "wasmi_core" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "281a49ca3c12c8efa052cb67758454fc861d80ab5a03def352e04eb08c20beb2" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" dependencies = [ "downcast-rs", "libm", @@ -3406,27 +3457,28 @@ dependencies = [ [[package]] name = "wasmi_ir" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbadcf529808086a74bacd3ce8aedece444a847292198a56dcde920d1fb213c" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" dependencies = [ "wasmi_core", ] [[package]] -name = "wasmparser-nostd" -version = "0.100.2" +name = "wasmparser" +version = "0.221.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" dependencies = [ - "indexmap-nostd", + "bitflags 2.8.0", + "indexmap 2.7.1", ] [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -3453,16 +3505,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -3471,7 +3514,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3480,22 +3523,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -3504,46 +3532,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3556,48 +3566,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3606,13 +3592,28 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.5.5" @@ -3630,9 +3631,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", "linux-raw-sys", @@ -3653,9 +3654,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8254499146a4fd0c86e3e99cf4a9f468f595808fb49ff8f3e495f2b117bf4ebc" +checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487" [[package]] name = "xz2" @@ -3687,9 +3688,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -3699,9 +3700,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -3732,18 +3733,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", @@ -3788,18 +3789,18 @@ dependencies = [ [[package]] name = "zip" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.6.0", + "indexmap 2.7.1", "memchr", - "thiserror", + "thiserror 2.0.11", "zopfli", ] @@ -3825,9 +3826,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 3550963e8..d91827ae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ color-print = "0.3.6" comemo = "0.4" csv = "1" ctrlc = "3.4.1" -dirs = "5" +dirs = "6" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" flate2 = "1" @@ -69,13 +69,13 @@ icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } -kamadak-exif = "0.5" +kamadak-exif = "0.6" kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" miniz_oxide = "0.8" native-tls = "0.2" -notify = "6" +notify = "8" once_cell = "1" open = "5.0.1" openssl = "0.10" @@ -83,7 +83,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" pathdiff = "0.2" -pdf-writer = "0.12" +pdf-writer = "0.12.1" phf = { version = "0.11", features = ["macros"] } pixglyph = "0.5.1" png = "0.17" @@ -133,11 +133,11 @@ unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } usvg = { version = "0.43", default-features = false, features = ["text"] } walkdir = "2" -wasmi = "0.39.0" +wasmi = "0.40.0" web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" -xmp-writer = "0.3" +xmp-writer = "0.3.1" xz2 = { version = "0.1", features = ["static"] } yaml-front-matter = "0.1" zip = { version = "2", default-features = false, features = ["deflate"] } From 85b0318158cc1f71825f45c5fb7915b764f75776 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Wed, 5 Feb 2025 13:40:54 +0100 Subject: [PATCH 060/101] Fix small copy-paste oversight (#5811) --- crates/typst-layout/src/math/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index e5a3d94c9..708a4443d 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -644,7 +644,7 @@ fn layout_h( } /// Lays out a [`ClassElem`]. -#[typst_macros::time(name = "math.op", span = elem.span())] +#[typst_macros::time(name = "math.class", span = elem.span())] fn layout_class( elem: &Packed, ctx: &mut MathContext, From 25f6a7ab161b2106c22a9997a68afee60ddb7412 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 5 Feb 2025 13:58:43 +0100 Subject: [PATCH 061/101] Bump more dependencies (#5813) --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 6 +++--- tests/ref/bibliography-before-content.png | Bin 17109 -> 17122 bytes tests/ref/bibliography-indent-par.png | Bin 9087 -> 9096 bytes tests/ref/bibliography-math.png | Bin 4605 -> 4610 bytes tests/ref/cite-footnote.png | Bin 13525 -> 13532 bytes 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b7754aef..e5daf731f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "citationberg" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fea693c83bd967604be367dc1e1b4895625eabafec2eec66c51092e18e700e" +checksum = "e4595e03beafb40235070080b5286d3662525efc622cca599585ff1d63f844fa" dependencies = [ "quick-xml 0.36.2", "serde", @@ -398,9 +398,9 @@ dependencies = [ [[package]] name = "codex" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e0ee2092c3513f63588d51c3f81b98e6b1aa8ddcca3b5892b288f093516497d" +checksum = "724d27a0ee38b700e5e164350e79aba601a0db673ac47fce1cb74c3e38864036" [[package]] name = "color-print" @@ -898,9 +898,9 @@ dependencies = [ [[package]] name = "hayagriva" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3635c2577f77499c9dc3dceeef2e64e6c146e711b1861507a0f15b20641348" +checksum = "954907554bb7fcba29a4f917c2d43e289ec21b69d872ccf97db160eca6caeed8" dependencies = [ "biblatex", "ciborium", @@ -2718,9 +2718,9 @@ dependencies = [ [[package]] name = "two-face" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccd4843ea031c609fe9c16cae00e9657bad8a9f735a3cc2e420955d802b4268" +checksum = "384eda438ddf62e2c6f39a174452d952d9d9df5a8ad5ade22198609f8dcaf852" dependencies = [ "once_cell", "serde", diff --git a/Cargo.toml b/Cargo.toml index d91827ae4..469439d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = "0.1.0" +codex = "0.1.1" color-print = "0.3.6" comemo = "0.4" csv = "1" @@ -58,7 +58,7 @@ env_proxy = "0.4" flate2 = "1" fontdb = { version = "0.21", default-features = false } fs_extra = "1.3" -hayagriva = "0.8" +hayagriva = "0.8.1" heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } @@ -122,7 +122,7 @@ tiny_http = "0.12" tiny-skia = "0.11" toml = { version = "0.8", default-features = false, features = ["parse", "display"] } ttf-parser = "0.24.1" -two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] } +two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] } typed-arena = "2" unicode-bidi = "0.3.18" unicode-ident = "1.0" diff --git a/tests/ref/bibliography-before-content.png b/tests/ref/bibliography-before-content.png index ea5ece267fce5eafe8071f4f311d1492416457bc..eb9f26d8321256dfb56d2842cc26c7ba8624a312 100644 GIT binary patch delta 17051 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 delta 17021 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 nlmo0&PC39juO5)4#mr?=6-Q6ScxHLQ*&$v7B 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 From 029ae4a5ea7ad1e52112ce26b6d38ce1750dae3f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 5 Feb 2025 14:24:10 +0100 Subject: [PATCH 062/101] Export target docs (#5812) Co-authored-by: Martin Haug <3874949+reknih@users.noreply.github.com> --- Cargo.lock | 2 + crates/typst-library/src/foundations/mod.rs | 10 +- crates/typst-library/src/foundations/scope.rs | 46 +---- .../typst-library/src/foundations/target.rs | 46 ++++- crates/typst-library/src/html/mod.rs | 50 +++-- crates/typst-library/src/introspection/mod.rs | 18 +- crates/typst-library/src/layout/mod.rs | 11 +- crates/typst-library/src/lib.rs | 52 ++++- crates/typst-library/src/loading/cbor.rs | 3 - crates/typst-library/src/loading/csv.rs | 3 - crates/typst-library/src/loading/json.rs | 3 - crates/typst-library/src/loading/mod.rs | 12 +- crates/typst-library/src/loading/toml.rs | 3 - crates/typst-library/src/loading/xml.rs | 3 - crates/typst-library/src/loading/yaml.rs | 3 - crates/typst-library/src/math/mod.rs | 113 +---------- crates/typst-library/src/model/mod.rs | 13 +- crates/typst-library/src/pdf/mod.rs | 19 +- crates/typst-library/src/symbols.rs | 13 +- crates/typst-library/src/text/mod.rs | 15 +- .../typst-library/src/visualize/image/mod.rs | 25 ++- crates/typst-library/src/visualize/mod.rs | 13 +- crates/typst-library/src/visualize/path.rs | 3 - crates/typst-macros/src/category.rs | 59 ------ crates/typst-macros/src/lib.rs | 10 - docs/Cargo.toml | 4 +- docs/guides/guide-for-latex-users.md | 7 +- docs/reference/export/html.md | 61 ++++++ docs/reference/export/pdf.md | 71 +++++++ docs/reference/export/png.md | 61 ++++++ docs/reference/export/svg.md | 48 +++++ docs/reference/{ => language}/context.md | 0 docs/reference/{ => language}/scripting.md | 0 docs/reference/{ => language}/styling.md | 0 docs/reference/{ => language}/syntax.md | 0 docs/reference/library/data-loading.md | 4 + docs/reference/library/foundations.md | 4 + docs/reference/library/introspection.md | 10 + docs/reference/library/layout.md | 3 + docs/reference/library/math.md | 101 ++++++++++ docs/reference/library/model.md | 5 + docs/reference/library/symbols.md | 5 + docs/reference/library/text.md | 3 + docs/reference/library/visualize.md | 5 + docs/reference/packages.md | 6 - docs/src/html.rs | 13 +- docs/src/lib.rs | 184 ++++++++++++------ docs/src/link.rs | 9 +- docs/src/model.rs | 5 +- 49 files changed, 709 insertions(+), 448 deletions(-) delete mode 100644 crates/typst-macros/src/category.rs create mode 100644 docs/reference/export/html.md create mode 100644 docs/reference/export/pdf.md create mode 100644 docs/reference/export/png.md create mode 100644 docs/reference/export/svg.md rename docs/reference/{ => language}/context.md (100%) rename docs/reference/{ => language}/scripting.md (100%) rename docs/reference/{ => language}/styling.md (100%) rename docs/reference/{ => language}/syntax.md (100%) create mode 100644 docs/reference/library/data-loading.md create mode 100644 docs/reference/library/foundations.md create mode 100644 docs/reference/library/introspection.md create mode 100644 docs/reference/library/layout.md create mode 100644 docs/reference/library/math.md create mode 100644 docs/reference/library/model.md create mode 100644 docs/reference/library/symbols.md create mode 100644 docs/reference/library/text.md create mode 100644 docs/reference/library/visualize.md delete mode 100644 docs/reference/packages.md diff --git a/Cargo.lock b/Cargo.lock index e5daf731f..140dccf74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2822,6 +2822,8 @@ dependencies = [ "typst-assets", "typst-dev-assets", "typst-render", + "typst-utils", + "unicode-math-class", "unscanny", "yaml-front-matter", ] diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index c335484fa..8e3aa060d 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -85,16 +85,9 @@ use crate::engine::Engine; use crate::routines::EvalMode; use crate::{Feature, Features}; -/// Foundational types and functions. -/// -/// Here, you'll find documentation for basic data types like [integers]($int) -/// and [strings]($str) as well as details about core computational functions. -#[category] -pub static FOUNDATIONS: Category; - /// Hook up all `foundations` definitions. pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { - global.start_category(FOUNDATIONS); + global.start_category(crate::Category::Foundations); global.define_type::(); global.define_type::(); global.define_type::(); @@ -125,6 +118,7 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { } global.define("calc", calc::module()); global.define("sys", sys::module(inputs)); + global.reset_category(); } /// Fails with an error. diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index d6c5a8d05..e1ce61b8a 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -1,6 +1,3 @@ -#[doc(inline)] -pub use typst_macros::category; - use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; @@ -8,14 +5,13 @@ use ecow::{eco_format, EcoString}; use indexmap::map::Entry; use indexmap::IndexMap; use typst_syntax::Span; -use typst_utils::Static; use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, Type, Value, }; -use crate::Library; +use crate::{Category, Library}; /// A stack of scopes. #[derive(Debug, Default, Clone)] @@ -361,46 +357,6 @@ pub enum Capturer { Context, } -/// A group of related definitions. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct Category(Static); - -impl Category { - /// Create a new category from raw data. - pub const fn from_data(data: &'static CategoryData) -> Self { - Self(Static(data)) - } - - /// The category's name. - pub fn name(&self) -> &'static str { - self.0.name - } - - /// The type's title case name, for use in documentation (e.g. `String`). - pub fn title(&self) -> &'static str { - self.0.title - } - - /// Documentation for the category. - pub fn docs(&self) -> &'static str { - self.0.docs - } -} - -impl Debug for Category { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Category({})", self.name()) - } -} - -/// Defines a category. -#[derive(Debug)] -pub struct CategoryData { - pub name: &'static str, - pub title: &'static str, - pub docs: &'static str, -} - /// The error message when trying to mutate a variable from the standard /// library. #[cold] diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index 5841552e4..2a21fd42b 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -3,7 +3,7 @@ use comemo::Tracked; use crate::diag::HintedStrResult; use crate::foundations::{elem, func, Cast, Context}; -/// The compilation target. +/// The export target. #[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)] pub enum Target { /// The target that is used for paged, fully laid-out content. @@ -28,7 +28,49 @@ pub struct TargetElem { pub target: Target, } -/// Returns the current compilation target. +/// Returns the current export target. +/// +/// This function returns either +/// - `{"paged"}` (for PDF, PNG, and SVG export), or +/// - `{"html"}` (for HTML export). +/// +/// The design of this function is not yet finalized and for this reason it is +/// guarded behind the `html` feature. Visit the [HTML documentation +/// page]($html) for more details. +/// +/// # When to use it +/// This function allows you to format your document properly across both HTML +/// and paged export targets. It should primarily be used in templates and show +/// rules, rather than directly in content. This way, the document's contents +/// can be fully agnostic to the export target and content can be shared between +/// PDF and HTML export. +/// +/// # Varying targets +/// This function is [contextual]($context) as the target can vary within a +/// single compilation: When exporting to HTML, the target will be `{"paged"}` +/// while within an [`html.frame`]. +/// +/// # Example +/// ```example +/// #let kbd(it) = context { +/// if target() == "html" { +/// html.elem("kbd", it) +/// } else { +/// set text(fill: rgb("#1f2328")) +/// let r = 3pt +/// box( +/// fill: rgb("#f6f8fa"), +/// stroke: rgb("#d1d9e0b3"), +/// outset: (y: r), +/// inset: (x: r), +/// radius: r, +/// raw(it) +/// ) +/// } +/// } +/// +/// Press #kbd("F1") for help. +/// ``` #[func(contextual)] pub fn target(context: Tracked) -> HintedStrResult { Ok(TargetElem::target_in(context.styles()?)) diff --git a/crates/typst-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs index c412b4607..1d88781c1 100644 --- a/crates/typst-library/src/html/mod.rs +++ b/crates/typst-library/src/html/mod.rs @@ -6,53 +6,77 @@ pub use self::dom::*; use ecow::EcoString; -use crate::foundations::{category, elem, Category, Content, Module, Scope}; - -/// HTML output. -#[category] -pub static HTML: Category; +use crate::foundations::{elem, Content, Module, Scope}; /// Create a module with all HTML definitions. pub fn module() -> Module { let mut html = Scope::deduplicating(); - html.start_category(HTML); + html.start_category(crate::Category::Html); html.define_elem::(); html.define_elem::(); Module::new("html", html) } -/// A HTML element that can contain Typst content. +/// An HTML element that can contain Typst content. +/// +/// Typst's HTML export automatically generates the appropriate tags for most +/// elements. However, sometimes, it is desirable to retain more control. For +/// example, when using Typst to generate your blog, you could use this function +/// to wrap each article in an `
    ` tag. +/// +/// Typst is aware of what is valid HTML. A tag and its attributes must form +/// syntactically valid HTML. Some tags, like `meta` do not accept content. +/// Hence, you must not provide a body for them. We may add more checks in the +/// future, so be sure that you are generating valid HTML when using this +/// function. +/// +/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If +/// you instead create them with this function, Typst will omit its own tags. +/// +/// ```typ +/// #html.elem("div", attrs: (style: "background: aqua"))[ +/// A div with _Typst content_ inside! +/// ] +/// ``` #[elem(name = "elem")] pub struct HtmlElem { /// The element's tag. #[required] pub tag: HtmlTag, - /// The element's attributes. + /// The element's HTML attributes. #[borrowed] pub attrs: HtmlAttrs, /// The contents of the HTML element. + /// + /// The body can be arbitrary Typst content. #[positional] #[borrowed] pub body: Option, } impl HtmlElem { - /// Add an atribute to the element. + /// Add an attribute to the element. pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into) -> Self { self.attrs.get_or_insert_with(Default::default).push(attr, value); self } } -/// An element that forces its contents to be laid out. +/// An element that lays out its content as an inline SVG. /// -/// Integrates content that requires layout (e.g. a plot) into HTML output -/// by turning it into an inline SVG. +/// Sometimes, converting Typst content to HTML is not desirable. This can be +/// the case for plots and other content that relies on positioning and styling +/// to convey its message. +/// +/// This function allows you to use the Typst layout engine that would also be +/// used for PDF, SVG, and PNG export to render a part of your document exactly +/// how it would appear when exported in one of these formats. It embeds the +/// content as an inline SVG. #[elem] pub struct FrameElem { - /// The contents that shall be laid out. + /// The content that shall be laid out. #[positional] #[required] pub body: Content, diff --git a/crates/typst-library/src/introspection/mod.rs b/crates/typst-library/src/introspection/mod.rs index d8184330d..995fbd7b5 100644 --- a/crates/typst-library/src/introspection/mod.rs +++ b/crates/typst-library/src/introspection/mod.rs @@ -25,24 +25,11 @@ pub use self::query_::*; pub use self::state::*; pub use self::tag::*; -use crate::foundations::{category, Category, Scope}; - -/// Interactions between document parts. -/// -/// This category is home to Typst's introspection capabilities: With the -/// `counter` function, you can access and manipulate page, section, figure, and -/// equation counters or create custom ones. Meanwhile, the `query` function -/// lets you search for elements in the document to construct things like a list -/// of figures or headers which show the current chapter title. -/// -/// Most of the functions are _contextual._ It is recommended to read the chapter -/// on [context] before continuing here. -#[category] -pub static INTROSPECTION: Category; +use crate::foundations::Scope; /// Hook up all `introspection` definitions. pub fn define(global: &mut Scope) { - global.start_category(INTROSPECTION); + global.start_category(crate::Category::Introspection); global.define_type::(); global.define_type::(); global.define_type::(); @@ -50,4 +37,5 @@ pub fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index 57518fe72..ef1ecdb36 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -64,17 +64,11 @@ pub use self::spacing::*; pub use self::stack::*; pub use self::transform::*; -use crate::foundations::{category, Category, Scope}; - -/// Arranging elements on the page in different ways. -/// -/// By combining layout functions, you can create complex and automatic layouts. -#[category] -pub static LAYOUT: Category; +use crate::foundations::Scope; /// Hook up all `layout` definitions. pub fn define(global: &mut Scope) { - global.start_category(LAYOUT); + global.start_category(crate::Category::Layout); global.define_type::(); global.define_type::(); global.define_type::(); @@ -103,4 +97,5 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_func::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 460321aa3..c39024f71 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -29,6 +29,7 @@ pub mod visualize; use std::ops::{Deref, Range}; +use serde::{Deserialize, Serialize}; use typst_syntax::{FileId, Source, Span}; use typst_utils::{LazyHash, SmallBitSet}; @@ -236,31 +237,72 @@ pub enum Feature { Html, } +/// A group of related standard library definitions. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Category { + Foundations, + Introspection, + Layout, + DataLoading, + Math, + Model, + Symbols, + Text, + Visualize, + Pdf, + Html, + Svg, + Png, +} + +impl Category { + /// The kebab-case name of the category. + pub fn name(&self) -> &'static str { + match self { + Self::Foundations => "foundations", + Self::Introspection => "introspection", + Self::Layout => "layout", + Self::DataLoading => "data-loading", + Self::Math => "math", + Self::Model => "model", + Self::Symbols => "symbols", + Self::Text => "text", + Self::Visualize => "visualize", + Self::Pdf => "pdf", + Self::Html => "html", + Self::Svg => "svg", + Self::Png => "png", + } + } +} + /// Construct the module with global definitions. fn global(math: Module, inputs: Dict, features: &Features) -> Module { let mut global = Scope::deduplicating(); + self::foundations::define(&mut global, inputs, features); self::model::define(&mut global); self::text::define(&mut global); - global.reset_category(); - global.define("math", math); self::layout::define(&mut global); self::visualize::define(&mut global); self::introspection::define(&mut global); self::loading::define(&mut global); self::symbols::define(&mut global); - self::pdf::define(&mut global); - global.reset_category(); + + global.define("math", math); + global.define("pdf", self::pdf::module()); if features.is_enabled(Feature::Html) { global.define("html", self::html::module()); } + prelude(&mut global); + Module::new("global", global) } /// Defines scoped values that are globally available, too. fn prelude(global: &mut Scope) { - global.reset_category(); global.define("black", Color::BLACK); global.define("gray", Color::GRAY); global.define("silver", Color::SILVER); diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index bd65e8442..801ca617a 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -34,9 +34,6 @@ pub fn cbor( #[scope] impl cbor { /// Reads structured data from CBOR bytes. - /// - /// This function is deprecated. The [`cbor`] function now accepts bytes - /// directly. #[func(title = "Decode CBOR")] #[deprecated = "`cbor.decode` is deprecated, directly pass bytes to `cbor` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index d01d687ba..6fdec4459 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -96,9 +96,6 @@ pub fn csv( #[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")] #[deprecated = "`csv.decode` is deprecated, directly pass bytes to `csv` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 52c87371f..185bac143 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -65,9 +65,6 @@ pub fn json( #[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")] #[deprecated = "`json.decode` is deprecated, directly pass bytes to `json` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index c645b691d..c57e02888 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -29,19 +29,12 @@ pub use self::yaml_::*; use crate::diag::{At, SourceResult}; use crate::foundations::OneOrMultiple; -use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; +use crate::foundations::{cast, Bytes, Scope, Str}; use crate::World; -/// Data loading from external files. -/// -/// These functions help you with loading and embedding data, for example from -/// the results of an experiment. -#[category] -pub static DATA_LOADING: Category; - /// Hook up all `data-loading` definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(DATA_LOADING); + global.start_category(crate::Category::DataLoading); global.define_func::(); global.define_func::(); global.define_func::(); @@ -49,6 +42,7 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } /// Something we can retrieve byte data from. diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 456112463..2660e7e7f 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -44,9 +44,6 @@ pub fn toml( #[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")] #[deprecated = "`toml.decode` is deprecated, directly pass bytes to `toml` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 0172071be..32ed6f24b 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -77,9 +77,6 @@ pub fn xml( #[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")] #[deprecated = "`xml.decode` is deprecated, directly pass bytes to `xml` instead"] pub fn decode( diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 511c676cb..4eeec28f1 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -55,9 +55,6 @@ pub fn yaml( #[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")] #[deprecated = "`yaml.decode` is deprecated, directly pass bytes to `yaml` instead"] pub fn decode( diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index a97a19b09..2e6d42b13 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -27,119 +27,10 @@ pub use self::underover::*; use typst_utils::singleton; use unicode_math_class::MathClass; -use crate::foundations::{ - category, elem, Category, Content, Module, NativeElement, Scope, -}; +use crate::foundations::{elem, Content, Module, NativeElement, Scope}; use crate::layout::{Em, HElem}; use crate::text::TextElem; -/// Typst has special [syntax]($syntax/#math) and library functions to typeset -/// mathematical formulas. Math formulas can be displayed inline with text or as -/// separate blocks. They will be typeset into their own block if they start and -/// end with at least one space (e.g. `[$ x^2 $]`). -/// -/// # Variables -/// In math, single letters are always displayed as is. Multiple letters, -/// however, are interpreted as variables and functions. To display multiple -/// letters verbatim, you can place them into quotes and to access single letter -/// variables, you can use the [hash syntax]($scripting/#expressions). -/// -/// ```example -/// $ A = pi r^2 $ -/// $ "area" = pi dot "radius"^2 $ -/// $ cal(A) := -/// { x in RR | x "is natural" } $ -/// #let x = 5 -/// $ #x < 17 $ -/// ``` -/// -/// # Symbols -/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like -/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in -/// different variants. You can select between different variants by applying -/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of -/// shorthand sequences like `=>` that approximate a symbol. When such a -/// shorthand exists, the symbol's documentation lists it. -/// -/// ```example -/// $ x < y => x gt.eq.not y $ -/// ``` -/// -/// # Line Breaks -/// Formulas can also contain line breaks. Each line can contain one or multiple -/// _alignment points_ (`&`) which are then aligned. -/// -/// ```example -/// $ sum_(k=0)^n k -/// &= 1 + ... + n \ -/// &= (n(n+1)) / 2 $ -/// ``` -/// -/// # Function calls -/// Math mode supports special function calls without the hash prefix. In these -/// "math calls", the argument list works a little differently than in code: -/// -/// - 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, 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. -/// -/// ```example -/// $ 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 $ -/// ``` -/// -/// To write a verbatim comma or semicolon in a math call, escape it with a -/// backslash. The colon on the other hand is only recognized in a special way -/// if directly preceded by an identifier, so to display it verbatim in those -/// cases, you can just insert a space before it. -/// -/// Functions calls preceded by a hash are normal code function calls and not -/// affected by these rules. -/// -/// # Alignment -/// When equations include multiple _alignment points_ (`&`), this creates -/// blocks of alternatingly right- and left-aligned columns. In the example -/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is -/// left-aligned. The word "given" is also left-aligned because `&&` creates two -/// alignment points in a row, alternating the alignment twice. `& &` and `&&` -/// behave exactly the same way. Meanwhile, "multiply by 7" is right-aligned -/// because just one `&` precedes it. Each alignment point simply alternates -/// between right-aligned/left-aligned. -/// -/// ```example -/// $ (3x + y) / 7 &= 9 && "given" \ -/// 3x + y &= 63 & "multiply by 7" \ -/// 3x &= 63 - y && "subtract y" \ -/// x &= 21 - y/3 & "divide by 3" $ -/// ``` -/// -/// # Math fonts -/// You can set the math font by with a [show-set rule]($styling/#show-rules) as -/// demonstrated below. Note that only special OpenType math fonts are suitable -/// for typesetting maths. -/// -/// ```example -/// #show math.equation: set text(font: "Fira Math") -/// $ sum_(i in NN) 1 + i $ -/// ``` -/// -/// # Math module -/// All math functions are part of the `math` [module]($scripting/#modules), -/// which is available by default in equations. Outside of equations, they can -/// be accessed with the `math.` prefix. -#[category] -pub static MATH: Category; - // Spacings. pub const THIN: Em = Em::new(1.0 / 6.0); pub const MEDIUM: Em = Em::new(2.0 / 9.0); @@ -150,7 +41,7 @@ pub const WIDE: Em = Em::new(2.0); /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); - math.start_category(MATH); + math.start_category(crate::Category::Math); math.define_elem::(); math.define_elem::(); math.define_elem::(); diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 586e10ec1..9bdbf0013 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -40,19 +40,11 @@ pub use self::strong::*; pub use self::table::*; pub use self::terms::*; -use crate::foundations::{category, Category, Scope}; - -/// Document structuring. -/// -/// Here, you can find functions to structure your document and interact with -/// that structure. This includes section headings, figures, bibliography -/// management, cross-referencing and more. -#[category] -pub static MODEL: Category; +use crate::foundations::Scope; /// Hook up all `model` definitions. pub fn define(global: &mut Scope) { - global.start_category(MODEL); + global.start_category(crate::Category::Model); global.define_elem::(); global.define_elem::(); global.define_elem::(); @@ -72,4 +64,5 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs index 3bd3b0c52..786a36372 100644 --- a/crates/typst-library/src/pdf/mod.rs +++ b/crates/typst-library/src/pdf/mod.rs @@ -4,21 +4,12 @@ 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.start_category(PDF); - global.define("pdf", module()); -} +use crate::foundations::{Module, Scope}; /// Hook up all `pdf` definitions. pub fn module() -> Module { - let mut scope = Scope::deduplicating(); - scope.define_elem::(); - Module::new("pdf", scope) + let mut pdf = Scope::deduplicating(); + pdf.start_category(crate::Category::Pdf); + pdf.define_elem::(); + Module::new("pdf", pdf) } diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index 777f8172f..0588ace95 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -1,19 +1,12 @@ //! Modifiable symbols. -use crate::foundations::{category, Category, Module, Scope, Symbol, Value}; - -/// These two modules give names to symbols and emoji to make them easy to -/// insert with a normal keyboard. Alternatively, you can also always directly -/// enter Unicode symbols into your text and formulas. In addition to the -/// symbols listed below, math mode defines `dif` and `Dif`. These are not -/// normal symbol values because they also affect spacing and font style. -#[category] -pub static SYMBOLS: Category; +use crate::foundations::{Module, Scope, Symbol, Value}; /// Hook up all `symbol` definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(SYMBOLS); + global.start_category(crate::Category::Symbols); extend_scope_from_codex_module(global, codex::ROOT); + global.reset_category(); } /// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index f506397e1..12f4e4c59 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -45,9 +45,9 @@ use typst_utils::singleton; use crate::diag::{bail, warning, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, category, dict, elem, Args, Array, Cast, Category, Construct, Content, Dict, - Fold, IntoValue, NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr, - Resolve, Scope, Set, Smart, StyleChain, + cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue, + NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr, Resolve, Scope, Set, + Smart, StyleChain, }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::math::{EquationElem, MathSize}; @@ -55,15 +55,9 @@ use crate::model::ParElem; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; -/// Text styling. -/// -/// The [text function]($text) is of particular interest. -#[category] -pub static TEXT: Category; - /// Hook up all `text` definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(TEXT); + global.start_category(crate::Category::Text); global.define_elem::(); global.define_elem::(); global.define_elem::(); @@ -78,6 +72,7 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } /// Customizes the look and layout of text in a variety of ways. diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 9306eb6f2..18d40caa8 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -50,6 +50,17 @@ pub struct ImageElem { /// supported [formats]($image.format). /// /// For more details about paths, see the [Paths section]($syntax/#paths). + /// + /// ```example + /// #let original = read("diagram.svg") + /// #let changed = original.replace( + /// "#2B80FF", // blue + /// green.to-hex(), + /// ) + /// + /// #image(bytes(original)) + /// #image(bytes(changed)) + /// ``` #[required] #[parse( let source = args.expect::>("source")?; @@ -156,20 +167,6 @@ pub struct ImageElem { #[allow(clippy::too_many_arguments)] 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( - /// "#2B80FF", // blue - /// green.to-hex(), - /// ) - /// - /// #image.decode(original) - /// #image.decode(changed) - /// ``` #[func(title = "Decode Image")] #[deprecated = "`image.decode` is deprecated, directly pass bytes to `image` instead"] pub fn decode( diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index 76849ac86..72a420657 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -24,19 +24,11 @@ pub use self::shape::*; pub use self::stroke::*; pub use self::tiling::*; -use crate::foundations::{category, Category, Element, Scope, Type}; - -/// Drawing and data visualization. -/// -/// If you want to create more advanced drawings or plots, also have a look at -/// the [CetZ](https://github.com/johannes-wolf/cetz) package as well as more -/// specialized [packages]($universe) for your use case. -#[category] -pub static VISUALIZE: Category; +use crate::foundations::{Element, Scope, Type}; /// Hook up all visualize definitions. pub(super) fn define(global: &mut Scope) { - global.start_category(VISUALIZE); + global.start_category(crate::Category::Visualize); global.define_type::(); global.define_type::(); global.define_type::(); @@ -55,4 +47,5 @@ pub(super) fn define(global: &mut Scope) { global .define("pattern", Type::of::()) .deprecated("the name `pattern` is deprecated, use `tiling` instead"); + global.reset_category(); } diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index 5d3439c08..c1cfde94a 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -21,9 +21,6 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// ((50%, 0pt), (40pt, 0pt)), /// ) /// ``` -/// -/// # Deprecation -/// This function is deprecated. The [`curve`] function should be used instead. #[elem(Show)] pub struct PathElem { /// How to fill the path. diff --git a/crates/typst-macros/src/category.rs b/crates/typst-macros/src/category.rs deleted file mode 100644 index 26ec879cc..000000000 --- a/crates/typst-macros/src/category.rs +++ /dev/null @@ -1,59 +0,0 @@ -use heck::{ToKebabCase, ToTitleCase}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, Result, Token, Type, Visibility}; - -use crate::util::{documentation, foundations}; - -/// Expand the `#[category]` macro. -pub fn category(_: TokenStream, item: syn::Item) -> Result { - let syn::Item::Verbatim(stream) = item else { - bail!(item, "expected bare static"); - }; - - let BareStatic { attrs, vis, ident, ty, .. } = syn::parse2(stream)?; - - let name = ident.to_string().to_kebab_case(); - let title = name.to_title_case(); - let docs = documentation(&attrs); - - Ok(quote! { - #(#attrs)* - #[allow(rustdoc::broken_intra_doc_links)] - #vis static #ident: #ty = { - static DATA: #foundations::CategoryData = #foundations::CategoryData { - name: #name, - title: #title, - docs: #docs, - }; - #foundations::Category::from_data(&DATA) - }; - }) -} - -/// Parse a bare `pub static CATEGORY: Category;` item. -#[allow(dead_code)] -pub struct BareStatic { - pub attrs: Vec, - pub vis: Visibility, - pub static_token: Token![static], - pub ident: Ident, - pub colon_token: Token![:], - pub ty: Type, - pub semi_token: Token![;], -} - -impl Parse for BareStatic { - fn parse(input: ParseStream) -> Result { - Ok(Self { - attrs: input.call(Attribute::parse_outer)?, - vis: input.parse()?, - static_token: input.parse()?, - ident: input.parse()?, - colon_token: input.parse()?, - ty: input.parse()?, - semi_token: input.parse()?, - }) - } -} diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs index 578389c7f..82e63ddc8 100644 --- a/crates/typst-macros/src/lib.rs +++ b/crates/typst-macros/src/lib.rs @@ -5,7 +5,6 @@ extern crate proc_macro; #[macro_use] mod util; mod cast; -mod category; mod elem; mod func; mod scope; @@ -266,15 +265,6 @@ pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { .into() } -/// Defines a category of definitions. -#[proc_macro_attribute] -pub fn category(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { - let item = syn::parse_macro_input!(item as syn::Item); - category::category(stream.into(), item) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - /// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. /// /// - `Reflect` makes Typst's runtime aware of the type's characteristics. diff --git a/docs/Cargo.toml b/docs/Cargo.toml index 41a5645e8..acc551754 100644 --- a/docs/Cargo.toml +++ b/docs/Cargo.toml @@ -17,6 +17,8 @@ cli = ["clap", "typst-render", "serde_json"] [dependencies] typst = { workspace = true } +typst-render = { workspace = true, optional = true } +typst-utils = { workspace = true } typst-assets = { workspace = true, features = ["fonts"] } typst-dev-assets = { workspace = true } clap = { workspace = true, optional = true } @@ -28,7 +30,7 @@ serde_json = { workspace = true, optional = true } serde_yaml = { workspace = true } syntect = { workspace = true, features = ["html"] } typed-arena = { workspace = true } -typst-render = { workspace = true, optional = true } +unicode-math-class = { workspace = true } unscanny = { workspace = true } yaml-front-matter = { workspace = true } diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md index 743afa5a6..5137ae1a9 100644 --- a/docs/guides/guide-for-latex-users.md +++ b/docs/guides/guide-for-latex-users.md @@ -657,7 +657,8 @@ applicable, contains possible workarounds. - **Well-established plotting ecosystem.** LaTeX users often create elaborate charts along with their documents in PGF/TikZ. The Typst ecosystem does not yet offer the same breadth of available options, but the ecosystem around the - [`cetz`](https://github.com/cetz-package/cetz) package is catching up quickly. + [`cetz` package](https://typst.app/universe/package/cetz) is catching up + quickly. - **Change page margins without a pagebreak.** In LaTeX, margins can always be adjusted, even without a pagebreak. To change margins in Typst, you use the @@ -670,4 +671,6 @@ applicable, contains possible workarounds. format, but you can easily convert both into SVG files with [online tools](https://cloudconvert.com/pdf-to-svg) or [Inkscape](https://inkscape.org/). The web app will automatically convert PDF - files to SVG files upon uploading them. + files to SVG files upon uploading them. You can also use the + community-provided [`muchpdf` package](https://typst.app/universe/package/muchpdf) + to embed PDFs. It internally converts PDFs to SVGs on-the-fly. diff --git a/docs/reference/export/html.md b/docs/reference/export/html.md new file mode 100644 index 000000000..330c2e136 --- /dev/null +++ b/docs/reference/export/html.md @@ -0,0 +1,61 @@ +
    + +Typst's HTML export is currently under active development. The feature is still +very incomplete and only available for experimentation behind a feature flag. Do +not use this feature for production use cases. In the CLI, you can experiment +with HTML export by passing `--features html` or setting the `TYPST_FEATURES` +environment variables to `html`. In the web app, HTML export is not available at +this time. Visit the [tracking issue](https://github.com/typst/typst/issues/5512) +to follow progress on HTML export and learn more about planned features. +
    + +HTML files describe a document structurally. The aim of Typst's HTML export is +to capture the structure of an input document and produce semantically rich HTML +that retains this structure. The resulting HTML should be accessible, +human-readable, and editable by hand and downstream tools. + +PDF, PNG, and SVG export, in contrast, all produce _visual_ representations of a +fully-laid out document. This divergence in the formats' intents means that +Typst cannot simply produce perfect HTML for your existing Typst documents. It +cannot always know what the best semantic HTML representation of your content +is. + +Instead, it gives _you_ full control: You can check the current export format +through the [`target`] function and when it is set to HTML, generate [raw HTML +elements]($html.elem). The primary intended use of these elements is in +templates and show rules. This way, the document's contents can be fully +agnostic to the export target and content can be shared between PDF and HTML +export. + +Currently, Typst will always output a single HTML file. Support for outputting +directories with multiple HTML documents and assets, as well as support for +outputting fragments that can be integrated into other HTML documents is +planned. + +Typst currently does not output CSS style sheets, instead focussing on emitting +semantic markup. You can of course write your own CSS styles and still benefit +from sharing your _content_ between PDF and HTML. For the future, we plan to +give you the option of automatically emitting CSS, taking more of your existing +set rules into account. + +# Exporting as HTML +## Command Line +Pass `--format html` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.html`. Note that you must also pass `--features html` +or set `TYPST_FEATURES=html` to enable this experimental export target. + +When using `typst watch`, Typst will spin up a live-reloading HTTP server. You +can configure it as follows: + +- Pass `--port` to change the port. (Defaults to the first free port in the + range 3000-3005.) +- Pass `--no-reload` to disable injection of a live reload script. (The HTML + that is written to disk isn't affected either way.) +- Pass `--no-serve` to disable the server altogether. + +## Web App +Not currently available. + +# HTML-specific functionality +Typst exposes HTML-specific functionality in the global `html` module. See below +for the definitions it contains. diff --git a/docs/reference/export/pdf.md b/docs/reference/export/pdf.md new file mode 100644 index 000000000..b220ae946 --- /dev/null +++ b/docs/reference/export/pdf.md @@ -0,0 +1,71 @@ +PDF files focus on accurately describing documents visually, but also have +facilities for annotating their structure. This hybrid approach makes +them a good fit for document exchange: They render exactly the same on every +device, but also support extraction of a document's content and structure (at +least to an extent). Unlike PNG files, PDFs are not bound to a specific +resolution. Hence, you can view them at any size without incurring a loss of +quality. + +# PDF standards +The International Standards Organization (ISO) has published the base PDF +standard and various standards that extend it to make PDFs more suitable for +specific use-cases. By default, Typst exports PDF 1.7 files. Adobe Acrobat 8 and +later as well as all other commonly used PDF viewers are compatible with this +PDF version. + +## PDF/A +Typst optionally supports emitting PDF/A-conformant files. PDF/A files are +geared towards maximum compatibility with current and future PDF tooling. They +do not rely on difficult-to-implement or proprietary features and contain +exhaustive metadata. This makes them suitable for long-term archival. + +The PDF/A Standard has multiple versions (_parts_ in ISO terminology) and most +parts have multiple profiles that indicate the file's conformance level. +Currently, Typst supports these PDF/A output profiles: + +- PDF/A-2b: The basic conformance level of ISO 19005-2. This version of PDF/A is + based on PDF 1.7 and results in self-contained, archivable PDF files. + +- PDF/A-3b: The basic conformance level of ISO 19005-3. This version of PDF/A is + based on PDF 1.7 and results in archivable PDF files that can contain + arbitrary other related files as [attachments]($pdf.embed). The only + difference between it and PDF/A-2b is the capability to embed + non-PDF/A-conformant files within. + +When choosing between exporting PDF/A and regular PDF, keep in mind that PDF/A +files contain additional metadata, and that some readers will prevent the user +from modifying a PDF/A file. Some features of Typst may be disabled depending on +the PDF standard you choose. + +# Exporting as PDF +## Command Line +PDF is Typst's default export format. Running the `compile` or `watch` +subcommand without specifying a format will create a PDF. When exporting to PDF, +you have the following configuration options: + +- Which PDF standards Typst should enforce conformance with by specifying + `--pdf-standard` followed by one or multiple comma-separated standards. Valid + standards are `1.7`, `a-2b`, and `a-3b`. By default, Typst outputs + PDF-1.7-compliant files. + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click the quick download button at the top right to export a PDF with default +settings. For further configuration, click "File" > "Export as" > "PDF" or click +the downwards-facing arrow next to the quick download button and select "Export +as PDF". When exporting to PDF, you have the following configuration options: + +- Which PDF standards Typst should enforce conformance with. By default, Typst + outputs PDF-1.7-compliant files. Valid additional standards are `A-2b` and + `A-3b`. + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. + +# PDF-specific functionality +Typst exposes PDF-specific functionality in the global `pdf` module. See below +for the definitions it contains. diff --git a/docs/reference/export/png.md b/docs/reference/export/png.md new file mode 100644 index 000000000..fe122f4d3 --- /dev/null +++ b/docs/reference/export/png.md @@ -0,0 +1,61 @@ +Instead of creating a PDF, Typst can also directly render pages to PNG raster +graphics. PNGs are losslessly compressed images that can contain one page at a +time. When exporting a multi-page document, Typst will emit multiple PNGs. PNGs +are a good choice when you want to use Typst's output in an image editing +software or when you can use none of Typst's other export formats. + +In contrast to Typst's other export formats, PNGs are bound to a specific +resolution. When exporting to PNG, you can configure the resolution as pixels +per inch (PPI). If the medium you view the PNG on has a finer resolution than +the PNG you exported, you will notice a loss of quality. Typst calculates the +resolution of your PNGs based on each page's physical dimensions and the PPI. If +you need guidance for choosing a PPI value, consider the following: + +- A DPI value of 300 or 600 is typical for desktop printing. +- Professional prints of detailed graphics can go up to 1200 PPI. +- If your document is only viewed at a distance, e.g. a poster, you may choose a + smaller value than 300. +- If your document is viewed on screens, a typical PPI value for a smartphone is + 400-500. + +Because PNGs only contain a pixel raster, the text within cannot be extracted +automatically (without OCR), for example by copy/paste or a screen reader. If +you need the text to be accessible, export a PDF or HTML file instead. + +PNGs can have transparent backgrounds. By default, Typst will output a PNG with +an opaque white background. You can make the background transparent using +`[#set page(fill: none)]`. Learn more on the +[`page` function's reference page]($page.fill). + +# Exporting as PNG +## Command Line +Pass `--format png` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.png`. + +If your document has more than one page, Typst will create multiple image files. +The output file name must then be a template string containing at least one of +- `[{p}]`, which will be replaced by the page number +- `[{0p}]`, which will be replaced by the zero-padded page number (so that all + numbers have the same length) +- `[{t}]`, which will be replaced by the total number of pages + +When exporting to PNG, you have the following configuration options: + +- Which resolution to render at by specifying `--ppi` followed by a number of + pixels per inch. The default is `144`. + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click "File" > "Export as" > "PNG" or click the downwards-facing arrow next to +the quick download button and select "Export as PNG". When exporting to PNG, you +have the following configuration options: + +- The resolution at which the pages should be rendered, as a number of pixels + per inch. The default is `144`. + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. diff --git a/docs/reference/export/svg.md b/docs/reference/export/svg.md new file mode 100644 index 000000000..630ab8452 --- /dev/null +++ b/docs/reference/export/svg.md @@ -0,0 +1,48 @@ +Instead of creating a PDF, Typst can also directly render pages to scalable +vector graphics (SVGs), which are the preferred format for embedding vector +graphics in web pages. Like PDF files, SVGs display your document exactly how +you have laid it out in Typst. Likewise, they share the benefit of not being +bound to a specific resolution. Hence, you can print or view SVG files on any +device without incurring a loss of quality. (Note that font printing quality may +be better with a PDF.) In contrast to a PDF, an SVG cannot contain multiple +pages. When exporting a multi-page document, Typst will emit multiple SVGs. + +SVGs can represent text in two ways: By embedding the text itself and rendering +it with the fonts available on the viewer's computer or by embedding the shapes +of each glyph in the font used to create the document. To ensure that the SVG +file looks the same across all devices it is viewed on, Typst chooses the latter +method. This means that the text in the SVG cannot be extracted automatically, +for example by copy/paste or a screen reader. If you need the text to be +accessible, export a PDF or HTML file instead. + +SVGs can have transparent backgrounds. By default, Typst will output an SVG with +an opaque white background. You can make the background transparent using +`[#set page(fill: none)]`. Learn more on the +[`page` function's reference page]($page.fill). + +# Exporting as SVG +## Command Line +Pass `--format svg` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.svg`. + +If your document has more than one page, Typst will create multiple image files. +The output file name must then be a template string containing at least one of +- `[{p}]`, which will be replaced by the page number +- `[{0p}]`, which will be replaced by the zero-padded page number (so that all + numbers have the same length) +- `[{t}]`, which will be replaced by the total number of pages + +When exporting to SVG, you have the following configuration options: + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click "File" > "Export as" > "SVG" or click the downwards-facing arrow next to +the quick download button and select "Export as SVG". When exporting to SVG, you +have the following configuration options: + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. diff --git a/docs/reference/context.md b/docs/reference/language/context.md similarity index 100% rename from docs/reference/context.md rename to docs/reference/language/context.md diff --git a/docs/reference/scripting.md b/docs/reference/language/scripting.md similarity index 100% rename from docs/reference/scripting.md rename to docs/reference/language/scripting.md diff --git a/docs/reference/styling.md b/docs/reference/language/styling.md similarity index 100% rename from docs/reference/styling.md rename to docs/reference/language/styling.md diff --git a/docs/reference/syntax.md b/docs/reference/language/syntax.md similarity index 100% rename from docs/reference/syntax.md rename to docs/reference/language/syntax.md diff --git a/docs/reference/library/data-loading.md b/docs/reference/library/data-loading.md new file mode 100644 index 000000000..659a8cccc --- /dev/null +++ b/docs/reference/library/data-loading.md @@ -0,0 +1,4 @@ +Data loading from external files. + +These functions help you with loading and embedding data, for example from the +results of an experiment. diff --git a/docs/reference/library/foundations.md b/docs/reference/library/foundations.md new file mode 100644 index 000000000..738c3789d --- /dev/null +++ b/docs/reference/library/foundations.md @@ -0,0 +1,4 @@ +Foundational types and functions. + +Here, you'll find documentation for basic data types like [integers]($int) and +[strings]($str) as well as details about core computational functions. diff --git a/docs/reference/library/introspection.md b/docs/reference/library/introspection.md new file mode 100644 index 000000000..f48a9937c --- /dev/null +++ b/docs/reference/library/introspection.md @@ -0,0 +1,10 @@ +Interactions between document parts. + +This category is home to Typst's introspection capabilities: With the `counter` +function, you can access and manipulate page, section, figure, and equation +counters or create custom ones. Meanwhile, the `query` function lets you search +for elements in the document to construct things like a list of figures or +headers which show the current chapter title. + +Most of the functions are _contextual._ It is recommended to read the chapter on +[context] before continuing here. diff --git a/docs/reference/library/layout.md b/docs/reference/library/layout.md new file mode 100644 index 000000000..450058d4c --- /dev/null +++ b/docs/reference/library/layout.md @@ -0,0 +1,3 @@ +Arranging elements on the page in different ways. + +By combining layout functions, you can create complex and automatic layouts. diff --git a/docs/reference/library/math.md b/docs/reference/library/math.md new file mode 100644 index 000000000..61f2bb58f --- /dev/null +++ b/docs/reference/library/math.md @@ -0,0 +1,101 @@ +Typst has special [syntax]($syntax/#math) and library functions to typeset +mathematical formulas. Math formulas can be displayed inline with text or as +separate blocks. They will be typeset into their own block if they start and end +with at least one space (e.g. `[$ x^2 $]`). + +# Variables +In math, single letters are always displayed as is. Multiple letters, however, +are interpreted as variables and functions. To display multiple letters +verbatim, you can place them into quotes and to access single letter variables, +you can use the [hash syntax]($scripting/#expressions). + +```example +$ A = pi r^2 $ +$ "area" = pi dot "radius"^2 $ +$ cal(A) := + { x in RR | x "is natural" } $ +#let x = 5 +$ #x < 17 $ +``` + +# Symbols +Math mode makes a wide selection of [symbols]($category/symbols/sym) like `pi`, +`dot`, or `RR` available. Many mathematical symbols are available in different +variants. You can select between different variants by applying +[modifiers]($symbol) to the symbol. Typst further recognizes a number of +shorthand sequences like `=>` that approximate a symbol. When such a shorthand +exists, the symbol's documentation lists it. + +```example +$ x < y => x gt.eq.not y $ +``` + +# Line Breaks +Formulas can also contain line breaks. Each line can contain one or multiple +_alignment points_ (`&`) which are then aligned. + +```example +$ sum_(k=0)^n k + &= 1 + ... + n \ + &= (n(n+1)) / 2 $ +``` + +# Function calls +Math mode supports special function calls without the hash prefix. In these +"math calls", the argument list works a little differently than in code: + +- 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, 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. + +```example +$ 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 $ +``` + +To write a verbatim comma or semicolon in a math call, escape it with a +backslash. The colon on the other hand is only recognized in a special way if +directly preceded by an identifier, so to display it verbatim in those cases, +you can just insert a space before it. + +Functions calls preceded by a hash are normal code function calls and not +affected by these rules. + +# Alignment +When equations include multiple _alignment points_ (`&`), this creates blocks of +alternatingly right- and left-aligned columns. In the example below, the +expression `(3x + y) / 7` is right-aligned and `= 9` is left-aligned. The word +"given" is also left-aligned because `&&` creates two alignment points in a row, +alternating the alignment twice. `& &` and `&&` behave exactly the same way. +Meanwhile, "multiply by 7" is right-aligned because just one `&` precedes it. +Each alignment point simply alternates between right-aligned/left-aligned. + +```example +$ (3x + y) / 7 &= 9 && "given" \ + 3x + y &= 63 & "multiply by 7" \ + 3x &= 63 - y && "subtract y" \ + x &= 21 - y/3 & "divide by 3" $ +``` + +# Math fonts +You can set the math font by with a [show-set rule]($styling/#show-rules) as +demonstrated below. Note that only special OpenType math fonts are suitable for +typesetting maths. + +```example +#show math.equation: set text(font: "Fira Math") +$ sum_(i in NN) 1 + i $ +``` + +# Math module +All math functions are part of the `math` [module]($scripting/#modules), which +is available by default in equations. Outside of equations, they can be accessed +with the `math.` prefix. diff --git a/docs/reference/library/model.md b/docs/reference/library/model.md new file mode 100644 index 000000000..e433ed53b --- /dev/null +++ b/docs/reference/library/model.md @@ -0,0 +1,5 @@ +Document structuring. + +Here, you can find functions to structure your document and interact with that +structure. This includes section headings, figures, bibliography management, +cross-referencing and more. diff --git a/docs/reference/library/symbols.md b/docs/reference/library/symbols.md new file mode 100644 index 000000000..2e6f48cdb --- /dev/null +++ b/docs/reference/library/symbols.md @@ -0,0 +1,5 @@ +These two modules give names to symbols and emoji to make them easy to insert +with a normal keyboard. Alternatively, you can also always directly enter +Unicode symbols into your text and formulas. In addition to the symbols listed +below, math mode defines `dif` and `Dif`. These are not normal symbol values +because they also affect spacing and font style. diff --git a/docs/reference/library/text.md b/docs/reference/library/text.md new file mode 100644 index 000000000..239c0b265 --- /dev/null +++ b/docs/reference/library/text.md @@ -0,0 +1,3 @@ +Text styling. + +The [text function]($text) is of particular interest. diff --git a/docs/reference/library/visualize.md b/docs/reference/library/visualize.md new file mode 100644 index 000000000..9259401f8 --- /dev/null +++ b/docs/reference/library/visualize.md @@ -0,0 +1,5 @@ +Drawing and data visualization. + +If you want to create more advanced drawings or plots, also have a look at the +[CetZ](https://github.com/johannes-wolf/cetz) package as well as more +specialized [packages]($universe) for your use case. diff --git a/docs/reference/packages.md b/docs/reference/packages.md deleted file mode 100644 index bfd1ef580..000000000 --- a/docs/reference/packages.md +++ /dev/null @@ -1,6 +0,0 @@ -Typst [packages]($scripting/#packages) encapsulate reusable building blocks -and make them reusable across projects. Below is a list of Typst packages -created by the community. Due to the early and experimental nature of Typst's -package management, they all live in a `preview` namespace. Click on a package's -name to view its documentation and use the copy button on the right to get a -full import statement for it. diff --git a/docs/src/html.rs b/docs/src/html.rs index 4eb3954c3..9077d5c47 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -301,7 +301,10 @@ impl<'a> Handler<'a> { return; } - let default = self.peeked.as_ref().map(|text| text.to_kebab_case()); + let body = self.peeked.as_ref(); + let default = body.map(|text| text.to_kebab_case()); + let has_id = id_slot.is_some(); + let id: &'a str = match (&id_slot, default) { (Some(id), default) => { if Some(*id) == default.as_deref() { @@ -316,10 +319,10 @@ impl<'a> Handler<'a> { *id_slot = (!id.is_empty()).then_some(id); // Special case for things like "v0.3.0". - let name = if id.starts_with('v') && id.contains('.') { - id.into() - } else { - id.to_title_case().into() + let name = match &body { + _ if id.starts_with('v') && id.contains('.') => id.into(), + Some(body) if !has_id => body.as_ref().into(), + _ => id.to_title_case().into(), }; let mut children = &mut self.outline; diff --git a/docs/src/lib.rs b/docs/src/lib.rs index ff745c9c2..f9ee05bbd 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -12,27 +12,20 @@ pub use self::model::*; use std::collections::HashSet; use ecow::{eco_format, EcoString}; +use heck::ToTitleCase; use serde::Deserialize; use serde_yaml as yaml; use std::sync::LazyLock; use typst::diag::{bail, StrResult}; -use typst::foundations::Binding; use typst::foundations::{ - AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr, - Scope, Smart, Type, Value, FOUNDATIONS, + AutoValue, Binding, Bytes, CastInfo, Func, Module, NoneValue, ParamInfo, Repr, Scope, + Smart, Type, Value, }; -use typst::html::HTML; -use typst::introspection::INTROSPECTION; -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::layout::{Abs, Margin, PageElem, PagedDocument}; +use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::visualize::VISUALIZE; -use typst::{Feature, Library, LibraryBuilder}; +use typst::{Category, Feature, Library, LibraryBuilder}; +use unicode_math_class::MathClass; macro_rules! load { ($path:literal) => { @@ -64,9 +57,10 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { let scope = lib.global.scope_mut(); // Add those types, so that they show up in the docs. - scope.start_category(FOUNDATIONS); + scope.start_category(Category::Foundations); scope.define_type::(); scope.define_type::(); + scope.reset_category(); // Adjust the default look. lib.styles @@ -155,21 +149,24 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { let mut page = md_page(resolver, resolver.base(), load!("reference/welcome.md")); let base = format!("{}reference/", resolver.base()); page.children = vec![ - md_page(resolver, &base, load!("reference/syntax.md")).with_part("Language"), - md_page(resolver, &base, load!("reference/styling.md")), - md_page(resolver, &base, load!("reference/scripting.md")), - md_page(resolver, &base, load!("reference/context.md")), - category_page(resolver, FOUNDATIONS).with_part("Library"), - category_page(resolver, MODEL), - category_page(resolver, TEXT), - category_page(resolver, MATH), - category_page(resolver, SYMBOLS), - category_page(resolver, LAYOUT), - category_page(resolver, VISUALIZE), - category_page(resolver, INTROSPECTION), - category_page(resolver, DATA_LOADING), - category_page(resolver, PDF), - category_page(resolver, HTML), + md_page(resolver, &base, load!("reference/language/syntax.md")) + .with_part("Language"), + md_page(resolver, &base, load!("reference/language/styling.md")), + md_page(resolver, &base, load!("reference/language/scripting.md")), + md_page(resolver, &base, load!("reference/language/context.md")), + category_page(resolver, Category::Foundations).with_part("Library"), + category_page(resolver, Category::Model), + category_page(resolver, Category::Text), + category_page(resolver, Category::Math), + category_page(resolver, Category::Symbols), + category_page(resolver, Category::Layout), + category_page(resolver, Category::Visualize), + category_page(resolver, Category::Introspection), + category_page(resolver, Category::DataLoading), + category_page(resolver, Category::Pdf).with_part("Export"), + category_page(resolver, Category::Html), + category_page(resolver, Category::Png), + category_page(resolver, Category::Svg), ]; page } @@ -219,14 +216,16 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { let mut markup = vec![]; let mut math = vec![]; - let (module, path): (&Module, &[&str]) = if category == MATH { - (&LIBRARY.math, &["math"]) - } else { - (&LIBRARY.global, &[]) + let docs = category_docs(category); + let (module, path): (&Module, &[&str]) = match category { + Category::Math => (&LIBRARY.math, &["math"]), + Category::Pdf => (get_module(&LIBRARY.global, "pdf").unwrap(), &["pdf"]), + Category::Html => (get_module(&LIBRARY.global, "html").unwrap(), &["html"]), + _ => (&LIBRARY.global, &[]), }; // Add groups. - for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() { + for group in GROUPS.iter().filter(|g| g.category == category).cloned() { if matches!(group.name.as_str(), "sym" | "emoji") { let subpage = symbols_page(resolver, &route, &group); let BodyModel::Symbols(model) = &subpage.body else { continue }; @@ -243,7 +242,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { items.push(CategoryItem { name: group.name.clone(), route: subpage.route.clone(), - oneliner: oneliner(category.docs()).into(), + oneliner: oneliner(docs).into(), code: true, }); children.push(subpage); @@ -256,15 +255,15 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } // Add symbol pages. These are ordered manually. - if category == SYMBOLS { + if category == Category::Symbols { shorthands = Some(ShorthandsModel { markup, math }); } let mut skip = HashSet::new(); - if category == MATH { + if category == Category::Math { skip = GROUPS .iter() - .filter(|g| g.category == category.name()) + .filter(|g| g.category == category) .flat_map(|g| &g.filter) .map(|s| s.as_str()) .collect(); @@ -273,6 +272,11 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { skip.insert("text"); } + // Tiling would be duplicate otherwise. + if category == Category::Visualize { + skip.insert("pattern"); + } + // Add values and types. let scope = module.scope(); for (name, binding) in scope.iter() { @@ -287,8 +291,8 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { match binding.read() { Value::Func(func) => { let name = func.name().unwrap(); - - let subpage = func_page(resolver, &route, func, path); + let subpage = + func_page(resolver, &route, func, path, binding.deprecation()); items.push(CategoryItem { name: name.into(), route: subpage.route.clone(), @@ -311,31 +315,39 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } } - if category != SYMBOLS { + if category != Category::Symbols { children.sort_by_cached_key(|child| child.title.clone()); items.sort_by_cached_key(|item| item.name.clone()); } - let name = category.title(); - let details = Html::markdown(resolver, category.docs(), Some(1)); + let title = EcoString::from(match category { + Category::Pdf | Category::Html | Category::Png | Category::Svg => { + category.name().to_uppercase() + } + _ => category.name().to_title_case(), + }); + + let details = Html::markdown(resolver, docs, Some(1)); let mut outline = vec![OutlineItem::from_name("Summary")]; outline.extend(details.outline()); - outline.push(OutlineItem::from_name("Definitions")); + if !items.is_empty() { + outline.push(OutlineItem::from_name("Definitions")); + } if shorthands.is_some() { outline.push(OutlineItem::from_name("Shorthands")); } PageModel { route, - title: name.into(), + title: title.clone(), description: eco_format!( - "Documentation for functions related to {name} in Typst." + "Documentation for functions related to {title} in Typst." ), part: None, outline, body: BodyModel::Category(CategoryModel { name: category.name(), - title: category.title(), + title, details, items, shorthands, @@ -344,14 +356,34 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } } +/// Retrieve the docs for a category. +fn category_docs(category: Category) -> &'static str { + match category { + Category::Foundations => load!("reference/library/foundations.md"), + Category::Introspection => load!("reference/library/introspection.md"), + Category::Layout => load!("reference/library/layout.md"), + Category::DataLoading => load!("reference/library/data-loading.md"), + Category::Math => load!("reference/library/math.md"), + Category::Model => load!("reference/library/model.md"), + Category::Symbols => load!("reference/library/symbols.md"), + Category::Text => load!("reference/library/text.md"), + Category::Visualize => load!("reference/library/visualize.md"), + Category::Pdf => load!("reference/export/pdf.md"), + Category::Html => load!("reference/export/html.md"), + Category::Svg => load!("reference/export/svg.md"), + Category::Png => load!("reference/export/png.md"), + } +} + /// Create a page for a function. fn func_page( resolver: &dyn Resolver, parent: &str, func: &Func, path: &[&str], + deprecation: Option<&'static str>, ) -> PageModel { - let model = func_model(resolver, func, path, false); + let model = func_model(resolver, func, path, false, deprecation); let name = func.name().unwrap(); PageModel { route: eco_format!("{parent}{}/", urlify(name)), @@ -370,6 +402,7 @@ fn func_model( func: &Func, path: &[&str], nested: bool, + deprecation: Option<&'static str>, ) -> FuncModel { let name = func.name().unwrap(); let scope = func.scope().unwrap(); @@ -383,7 +416,11 @@ fn func_model( } let mut returns = vec![]; - casts(resolver, &mut returns, &mut vec![], func.returns().unwrap()); + let mut strings = vec![]; + casts(resolver, &mut returns, &mut strings, func.returns().unwrap()); + if !strings.is_empty() && !returns.contains(&"str") { + returns.push("str"); + } returns.sort_by_key(|ty| type_index(ty)); if returns == ["none"] { returns.clear(); @@ -401,6 +438,7 @@ fn func_model( oneliner: oneliner(details), element: func.element().is_some(), contextual: func.contextual().unwrap_or(false), + deprecation, details: Html::markdown(resolver, details, nesting), example: example.map(|md| Html::markdown(resolver, md, None)), self_, @@ -483,7 +521,7 @@ fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec() else { panic!("not a function") }; - let func = func_model(resolver, func, &path, true); + let binding = group.module().scope().get(name).unwrap(); + let Ok(ref func) = binding.read().clone().cast::() else { + panic!("not a function") + }; + let func = func_model(resolver, func, &path, true, binding.deprecation()); let id_base = urlify(&eco_format!("functions-{}", func.name)); let children = func_outline(&func, &id_base); outline_items.push(OutlineItem { @@ -628,7 +668,7 @@ fn type_model(resolver: &dyn Resolver, ty: &Type) -> TypeModel { constructor: ty .constructor() .ok() - .map(|func| func_model(resolver, &func, &[], true)), + .map(|func| func_model(resolver, &func, &[], true, None)), scope: scope_models(resolver, ty.short_name(), ty.scope()), } } @@ -682,10 +722,19 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) }; + let name = complete(variant); + let deprecation = match name.as_str() { + "integral.sect" => { + Some("`integral.sect` is deprecated, use `integral.inter` instead") + } + _ => binding.deprecation(), + }; + list.push(SymbolModel { - name: complete(variant), + name, markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST), math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST), + math_class: typst_utils::default_math_class(c).map(math_class_name), codepoint: c as _, accent: typst::math::Accent::combine(c).is_some(), alternates: symbol @@ -693,6 +742,7 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { .filter(|(other, _)| other != &variant) .map(|(other, _)| complete(other)) .collect(), + deprecation, }); } } @@ -769,12 +819,32 @@ const TYPE_ORDER: &[&str] = &[ "stroke", ]; +fn math_class_name(class: MathClass) -> &'static str { + match class { + MathClass::Normal => "Normal", + MathClass::Alphabetic => "Alphabetic", + MathClass::Binary => "Binary", + MathClass::Closing => "Closing", + MathClass::Diacritic => "Diacritic", + MathClass::Fence => "Fence", + MathClass::GlyphPart => "Glyph Part", + MathClass::Large => "Large", + MathClass::Opening => "Opening", + MathClass::Punctuation => "Punctuation", + MathClass::Relation => "Relation", + MathClass::Space => "Space", + MathClass::Unary => "Unary", + MathClass::Vary => "Vary", + MathClass::Special => "Special", + } +} + /// Data about a collection of functions. #[derive(Debug, Clone, Deserialize)] struct GroupData { name: EcoString, title: EcoString, - category: EcoString, + category: Category, #[serde(default)] path: Vec, #[serde(default)] diff --git a/docs/src/link.rs b/docs/src/link.rs index c55261b84..2e836b6ce 100644 --- a/docs/src/link.rs +++ b/docs/src/link.rs @@ -44,6 +44,8 @@ fn resolve_known(head: &str, base: &str) -> Option { "$styling" => format!("{base}reference/styling"), "$scripting" => format!("{base}reference/scripting"), "$context" => format!("{base}reference/context"), + "$html" => format!("{base}reference/html"), + "$pdf" => format!("{base}reference/pdf"), "$guides" => format!("{base}guides"), "$changelog" => format!("{base}changelog"), "$universe" => "https://typst.app/universe".into(), @@ -73,11 +75,14 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { // Handle grouped functions. if let Some(group) = GROUPS.iter().find(|group| { - group.category == category.name() && group.filter.iter().any(|func| func == name) + group.category == category && group.filter.iter().any(|func| func == name) }) { let mut route = format!( "{}reference/{}/{}/#functions-{}", - base, group.category, group.name, name + base, + group.category.name(), + group.name, + name ); if let Some(param) = parts.next() { route.push('-'); diff --git a/docs/src/model.rs b/docs/src/model.rs index b222322a7..801c60c7f 100644 --- a/docs/src/model.rs +++ b/docs/src/model.rs @@ -64,7 +64,7 @@ pub enum BodyModel { #[derive(Debug, Serialize)] pub struct CategoryModel { pub name: &'static str, - pub title: &'static str, + pub title: EcoString, pub details: Html, pub items: Vec, pub shorthands: Option, @@ -89,6 +89,7 @@ pub struct FuncModel { pub oneliner: &'static str, pub element: bool, pub contextual: bool, + pub deprecation: Option<&'static str>, pub details: Html, /// This example is only for nested function models. Others can have /// their example directly in their details. @@ -163,6 +164,8 @@ pub struct SymbolModel { pub alternates: Vec, pub markup_shorthand: Option<&'static str>, pub math_shorthand: Option<&'static str>, + pub math_class: Option<&'static str>, + pub deprecation: Option<&'static str>, } /// Shorthands listed on a category page. From 4a9a5d2716fc91f60734769eb001aef32fe15403 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 5 Feb 2025 14:47:32 +0100 Subject: [PATCH 063/101] 0.13 changelog (#5801) --- docs/changelog/0.13.0.md | 324 ++++++++++++++++++++++++++++++++++++++ docs/changelog/welcome.md | 1 + docs/src/lib.rs | 1 + 3 files changed, 326 insertions(+) create mode 100644 docs/changelog/0.13.0.md diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md new file mode 100644 index 000000000..50819f659 --- /dev/null +++ b/docs/changelog/0.13.0.md @@ -0,0 +1,324 @@ +--- +title: Unreleased changes planned for 0.13.0 +description: Changes slated to appear in Typst 0.13.0 +--- + +# Unreleased + +## Highlights +- There is now a distinction between [proper paragraphs]($par) and just + inline-level content. This is important for future work on accessibility and + means that [first line indent]($par.first-line-indent) can now be enabled for + all paragraphs instead of just consecutive ones. +- The [`outline`] has a better out-of-the-box look and is more customizable +- The new [`curve`] function (that supersedes the `path` function) provides a + simpler and more flexible interface for creating Bézier curves +- The `image` function now supports raw [pixel raster formats]($image.format) + for generating images from within Typst +- Functions that accept [file paths]($syntax/#paths) now also accept raw + [bytes] instead, for full flexibility +- WebAssembly [plugins]($plugin) are more flexible and automatically run + multi-threaded +- Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`) + would be displayed in italics +- You can now specify which charset should be [covered]($text.font) by which + font family +- The [`pdf.embed`] function lets you embed arbitrary files in the exported + PDF +- HTML export is currently under active development. The feature is still _very_ + incomplete, but already available for experimentation behind a feature flag. + +## Model +- There is now a distinction between [proper paragraphs]($par) and just + inline-level content **(Breaking change)** + - All text at the root of a document is wrapped in paragraphs. Meanwhile, 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 content is + 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 accessibility, HTML + export, and for properties like `first-line-indent`. + - Show rules on `par` now only affect proper paragraphs + - The `first-line-indent` and `hanging-indent` properties also only affect + proper paragraphs + - Creating a `{par[..]}` with body content that is not fully inline-level will + result in a warning + - The default show rules of various built-in elements like lists, quotes, etc. + were adjusted to ensure they produce/don't produce paragraphs as appropriate +- The [`outline`] function was fully reworked to improve its out-of-the-box + behavior **(Breaking change)** + - [Outline entries]($outline.entry) are now [blocks]($block) and are thus + affected by block spacing + - The `{auto}` indentation mode now aligns numberings and titles outline-wide + for a grid-like look + - Automatic indentation now also indents entries without a numbering + - Titles wrapping over multiple lines now have hanging indent + - The page number won't appear alone on its own line anymore + - The link now spans the full entry instead of just the title and page number + - The default spacing between outline leader dots was increased + - The [`fill`]($outline.entry.fill) parameter was moved from `outline` to + `outline.entry` and can thus be configured through show-set rules + - Removed `body` and `page` fields from outline entry + - Added `indented`, `prefix`, `inner`, `body`, and `page` methods on outline + entries to simplify writing of show rules +- Added configuration to [`par.first-line-indent`] for indenting all paragraphs + instead of just consecutive ones +- Added [`form`]($ref.form) parameter to `ref` function. Setting the form to + `{"page"}` will produce a page reference instead of a textual one. +- Added [`document.description`] field, which results in corresponding PDF and + HTML metadata +- Added [`enum.reversed`] parameter +- Added support for Greek [numbering] +- When the [`link`] function wraps around a container like a [block], it will + now generate only one link for the whole block instead of individual links for + all the visible leaf elements. This significantly reduces PDF file sizes when + combining `link` and [`repeat`]. +- The [`link`] function will now only strip one prefix (like `mailto:` or + `tel:`) instead of multiple +- The link function now suppresses hyphenation via a built-in show-set rule + rather than through its default show rule +- Displaying the page counter without a specified numbering will now take the + page numbering into account + +## Visualization +- Added new [`curve`] function that supersedes the [`path`] function and + provides a simpler and more flexible interface. The `path` function is now + deprecated. +- The `image` function now supports raw [pixel raster formats]($image.format). + This can be used to generate images from within Typst without the need for + encoding in an image exchange format. +- Added [`image.scaling`] parameter for configuring how an image is scaled by + PNG export and PDF viewers (smooth or pixelated) +- Added [`image.icc`] parameter for providing or overriding the ICC profile of + an image +- Renamed `pattern` to [`tiling`]. The name `pattern` remains as a deprecated + alias. +- Added [`gradient.center`], [`gradient.radius`], [`gradient.focal-center`], and + [`gradient.focal-radius`] methods +- Fixed interaction of clipping and outset on [`box`] and [`block`] +- Fixed panic with [`path`] of infinite length +- Fixed non-solid (e.g. tiling) text fills in clipped blocks +- Auto-detection of image formats from a raw buffer now has basic support for + SVGs + +## Scripting +- Functions that accept [file paths]($syntax/#paths) now also accept raw + [bytes] + - [`image`], [`cbor`], [`csv`], [`json`], [`toml`], [`xml`], and [`yaml`] now + support a path string or bytes and their `.decode` variants are deprecated + - [`plugin`], [`bibliography`], [`bibliography.style`], [`cite.style`], + [`raw.theme`], and [`raw.syntaxes`] now accept bytes in addition to path + strings. These did not have `.decode` variants, so this adds new + flexibility. + - The `path` argument/field of [`image`] and [`bibliography`] was renamed to + `source` and `sources`, respectively **(Minor breaking change)** +- Improved WebAssembly [plugins]($plugin) + - The `plugin` type is replaced by a [`plugin` function]($plugin) that returns + a [module] containing normal Typst functions. This module can be used with + import syntax. **(Breaking change)** + - Plugins now automatically run in multiple threads without any changes by + plugin authors + - A new [`plugin.transition`] API is introduced which allows plugins to run + impure initialization in a way that doesn't break Typst's purity guarantees +- The variable name bound by a bare import (no renaming, no import list) is now + determined statically and dynamic imports without `{as}` renaming (e.g. + `{import "ot" + "her.typ"}`) are a hard error **(Breaking change)** +- Values of the [`arguments`] type can now be added with `+` and + [joined]($scripting/#blocks) in curly-braced code blocks +- Functions in an element function's scope can now be called with method syntax, + bringing elements and types closer (in anticipation of a future full + unification of the two). Currently, this is only useful for [`outline.entry`] + as no other element function defines methods. +- Added [`calc.norm`] function +- Added support for 32-bit floats in [`float.from-bytes`] and [`float.to-bytes`] +- The [`decimal`] constructor now also accepts decimal values +- Improved `repr` of [symbols]($symbol), [arguments], and [types]($type) +- Duplicate [symbol] variants and modifiers are now a hard error + **(Breaking change)** + +## Math +- Fixed a bug where single letter strings in math (`[$"a"$]`) would be displayed + in italics +- Math function calls can now have hyphenated named arguments and support + [argument spreading]($arguments/#spreading) +- Better looking accents thanks to support for the `flac` (Flattened Accent + Forms) and `dtls` (Dotless Forms) OpenType features +- Added `lcm` [text operator]($math.op) +- The [`bold`]($math.bold) function now works with ϝ and Ϝ +- The [`italic`]($math.italic) function now works with ħ +- Fixed a bug where the extent of a math equation was wrongly affected by + internal metadata +- Fixed interaction of [`lr`]($math.lr) and [context] expressions +- Fixed weak spacing being unconditionally ignored in [`lr`]($math.lr) +- Fixed sub/superscripts sometimes being in the wrong position with + [`lr`]($math.lr) +- Fixed multi-line annotations (e.g. overbrace) changing the math baseline +- Fixed merging of attachments when the base is a nested equation +- Fixed resolving of contextual (em-based) text sizes within math +- Fixed spacing around ⊥ + +## Bibliography +- Prose and author-only citations now use editor names if the author names are + unavailable +- Some non-standard but widely used BibLaTeX `editortype`s like `producer`, + `writer`, `scriptwriter`, and `none` (defined by widespread style + `biblatex-chicago` to mean performers within `music` and `video` entries) are + now recognized +- CSL styles can now render affixes around the bibliography +- For BibTeX entries with `eprinttype = {pubmed}`, the PubMed ID will now be + correctly processed +- Whitespace handling for strings delimiting initialized names has been improved +- Uppercase spelling after apostrophes used as quotation marks is now possible +- Fixed bugs around the handling of CSL delimiting characters +- Fixed a problem with parsing multibyte characters in page ranges that could + prevent Hayagriva from parsing some BibTeX page ranges +- Updated CSL APA style +- Updated CSL locales for Finnish, Swiss German, Austrian German, German, and + Arabic + +## Text +- Added support for specifying which charset should be [covered]($text.font) by + which font family +- Added [`all`]($smallcaps.all) parameter to `smallcaps` function that also + enables small capitals on uppercase letters +- Added basic i18n for Basque and Bulgarian +- [Justification]($par.justify) does not affect [raw] blocks anymore +- [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text + anymore +- Fixed wrong language codes being used for Greek and Ukrainian +- Fixed default quotes for Croatian +- Fixed crash in RTL text handling +- Added support for [`raw`] syntax highlighting for a few new languages: CFML, + NSIS, and WGSL +- New font metadata exception for New Computer Modern Sans Math +- Updated bundled New Computer Modern fonts to version 7.0 + +## Layout +- Fixed various bugs with footnotes + - Fixed footnotes getting lost when multiple footnotes were nested within + another footnote + - Fixed endless loops with empty and overlarge footnotes + - Fixed crash with overlarge footnotes within a floating placement +- Fixed sizing of quadratic shapes ([`square`] and [`circle`]) +- Fixed [`block.sticky`] not working properly at the top of a container +- Fixed crash due to consecutive weak spacing +- Fixed crash when a [block] or text have negative sizes +- Fixed unnecessary hyphenations occurring in rare scenarios due to a bad + interaction between padding and paragraph optimization +- Fixed lone [citations]($cite) in [`align`] not becoming their own paragraph + +## Syntax +- Top-level closing square brackets that do not have a matching opening square + bracket are now a hard error **(Minor breaking change)** +- Adding a space between the identifier and the parentheses in a set rule is not + allowed anymore **(Minor breaking change)** +- Numbers with a unit cannot have a base prefix anymore, e.g. `0b100000pt` is + not allowed anymore. Previously, it was syntactically allowed but always + resolved to a value of zero. **(Minor breaking change)** +- Using `is` as an identifier will now warn as it might become a keyword in the + future +- Fixed minor whitespace handling bugs + - in math mode argument lists + - at the end of headings + - between a term list's term and description +- Fixed parsing of empty single line raw blocks with 3+ backticks and a language + tag +- Fixed minor bug with parentheses parsing in math +- Markup that can only appear at the start of the line (headings, lists) can now + also appear at the start of a list item +- A shebang `#!` at the very start of a file is now ignored + +## PDF export +- Added `pdf.embed` function for embedding arbitrary files in the exported PDF +- Added support for PDF/A-3b export +- The PDF timestamp will now contain the timezone by default + +## HTML export +**Note:** HTML export is currently under active development. The feature is +still _very_ incomplete, but already available for experimentation behind a +feature flag. + +- Added HTML output support for some (but not all) of the built-in elements +- Added [`html.elem`] function for outputting an arbitrary HTML element +- Added [`html.frame`] function for integrating content that requires layout + into HTML (by embedding an SVG) +- Added [`target`] function which returns either `{"paged"}` or `{"html"}` + depending on the export target + +## Tooling and Diagnostics +- Autocompletion improvements + - Added autocompletion for file paths + - Smarter autocompletion of variables: Completing `{rect(fill: |)}` will now + only show variables which contain a valid fill (either directly or nested, + e.g. a dictionary containing a valid fill) + - Different functions will now autocomplete with different brackets (round vs + square) depending on which kind is more useful + - Positional parameters which are already provided aren't autocompleted again + anymore + - Fixed variable autocompletion not considering parameters + - Added autocompletion snippets for common figure usages + - Fixed autocompletion after half-completed import item + - Fixed autocompletion for `cite` function +- Added warning when an unconditional return in a code block discards joined + content +- Fixed error message when accessing non-existent label +- Fixed handling of nested imports in IDE functionality + +## Command Line Interface +- Added `--features` argument and `TYPST_FEATURES` environment variable for + opting into experimental features. The only feature so far is `html`. +- Added a live reloading HTTP server to `typst watch` when targeting HTML +- Fixed self-update not being aware about certain target architectures +- Fixed crash when piping `typst fonts` output to another command + +## Symbols +- New + - `inter`, `inter.and`, `inter.big`, `inter.dot`, `inter.double`, `inter.sq`, + `inter.sq.big`, `inter.sq.double`, `integral.inter` + - `asymp`, `asymp.not` + - `mapsto`, `mapsto.long` + - `divides.not.rev`, `divides.struck` + - `interleave`, `interleave.big`, `interleave.struck` + - `eq.triple.not`, `eq.dots`, `eq.dots.down`, `eq.dots.up` + - `smt`, `smt.eq`, `lat`, `lat.eq` + - `colon.tri`, `colon.tri.op` + - `dagger.triple`, `dagger.l`, `dagger.r`, `dagger.inv` + - `hourglass.stroked`, `hourglass.filled` + - `die.six`, `die.five`, `die.four`, `die.three`, `die.two`, `die.one` + - `errorbar.square.stroked`, `errorbar.square.filled`, + `errorbar.diamond.stroked`, `errorbar.diamond.filled`, + `errorbar.circle.stroked`, `errorbar.circle.filled` + - `numero` + - `Omega.inv` +- Renamed + - `ohm.inv` to `Omega.inv` +- Changed codepoint + - `angle.l.double` from `《` to `⟪` + - `angle.r.double` from `》` to `⟫` + - `angstrom` from U+212B (`Å`) to U+00C5 (`Å`) +- Deprecated + - `sect` and all its variants in favor of `inter` + - `integral.sect` in favor of `integral.inter` +- Removed + - `degree.c` in favor of `°C` (`[$upright(°C)$]` or `[$upright(degree C)$]` in math) + - `degree.f` in favor of `°F` (`[$upright(°F)$]` or `[$upright(degree F)$]` in math) + - `kelvin` in favor of just K (`[$upright(K)$]` in math) + +## Deprecations +- The [`path`] function in favor of the [`curve`] function +- The name `pattern` for tiling patterns in favor of the new name [`tiling`] +- [`image.decode`], [`cbor.decode`], [`csv.decode`], [`json.decode`], + [`toml.decode`], [`xml.decode`], [`yaml.decode`] in favor of the top-level + functions directly accepting both paths and bytes +- The `sect` and its variants in favor of `inter`, and `integral.sect` in favor + of `integral.inter` +- Fully removed type/str compatibility behavior (e.g. `{int == "integer"}`) + which was temporarily introduced in Typst 0.8 **(Breaking change)** + +## Development +- The `typst::compile` function is now generic and can return either a + `PagedDocument` or an `HtmlDocument` +- `typst-timing` now supports WebAssembly targets via `web-sys` when the `wasm` + feature is enabled +- Increased minimum supported Rust version to 1.80 +- Fixed linux/arm64 Docker image diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index 12b6b896b..bb245eb01 100644 --- a/docs/changelog/welcome.md +++ b/docs/changelog/welcome.md @@ -10,6 +10,7 @@ forward. This section documents all changes to Typst since its initial public release. ## Versions +- [Unreleased changes planned for Typst 0.13.0]($changelog/0.13.0) - [Typst 0.12.0]($changelog/0.12.0) - [Typst 0.11.1]($changelog/0.11.1) - [Typst 0.11.0]($changelog/0.11.0) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index f9ee05bbd..fae74e0fc 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -188,6 +188,7 @@ fn changelog_pages(resolver: &dyn Resolver) -> PageModel { let mut page = md_page(resolver, resolver.base(), load!("changelog/welcome.md")); let base = format!("{}changelog/", resolver.base()); page.children = vec![ + md_page(resolver, &base, load!("changelog/0.13.0.md")), md_page(resolver, &base, load!("changelog/0.12.0.md")), md_page(resolver, &base, load!("changelog/0.11.1.md")), md_page(resolver, &base, load!("changelog/0.11.0.md")), From d897ab5e7d2e941494df8ba137a1f92f8aada03a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 10:34:28 +0100 Subject: [PATCH 064/101] Autocomplete content methods (#5822) --- crates/typst-ide/src/complete.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index c1f08cf09..7df788dc3 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -398,7 +398,17 @@ fn field_access_completions( value: &Value, styles: &Option, ) { - for (name, binding) in value.ty().scope().iter() { + let scopes = { + let ty = value.ty().scope(); + let elem = match value { + Value::Content(content) => Some(content.elem().scope()), + _ => None, + }; + elem.into_iter().chain(Some(ty)) + }; + + // Autocomplete methods from the element's or type's scope. + for (name, binding) in scopes.flat_map(|scope| scope.iter()) { ctx.call_completion(name.clone(), binding.read()); } @@ -1747,4 +1757,15 @@ mod tests { .must_include(["this", "that"]) .must_exclude(["*", "figure"]); } + + #[test] + fn test_autocomplete_type_methods() { + test("#\"hello\".", -1).must_include(["len", "contains"]); + } + + #[test] + fn test_autocomplete_content_methods() { + test("#show outline.entry: it => it.\n#outline()\n= Hi", 30) + .must_include(["indented", "body", "page"]); + } } From ca702c7f82ef8e027e559228dc9c469e1a65ac6f Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:18:10 +0100 Subject: [PATCH 065/101] Documentation fixes and improvements (#5816) --- crates/typst-layout/src/shapes.rs | 4 ++-- crates/typst-library/src/foundations/plugin.rs | 4 +--- crates/typst-library/src/loading/cbor.rs | 4 +--- crates/typst-library/src/loading/csv.rs | 4 +--- crates/typst-library/src/loading/json.rs | 4 +--- crates/typst-library/src/loading/toml.rs | 4 +--- crates/typst-library/src/loading/xml.rs | 4 +--- crates/typst-library/src/loading/yaml.rs | 4 +--- crates/typst-library/src/model/outline.rs | 2 +- crates/typst-library/src/pdf/embed.rs | 4 +--- crates/typst-library/src/visualize/curve.rs | 18 +++++++++--------- .../typst-library/src/visualize/image/mod.rs | 7 ++++--- crates/typst-library/src/visualize/path.rs | 6 +++--- crates/typst-library/src/visualize/shape.rs | 2 +- docs/changelog/0.13.0.md | 6 +++--- docs/reference/export/png.md | 2 +- 16 files changed, 32 insertions(+), 47 deletions(-) diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index eb665f06a..21d0a518f 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -1281,7 +1281,7 @@ impl ControlPoints { } } -/// Helper to draw arcs with bezier curves. +/// Helper to draw arcs with Bézier curves. trait CurveExt { fn arc(&mut self, start: Point, center: Point, end: Point); fn arc_move(&mut self, start: Point, center: Point, end: Point); @@ -1305,7 +1305,7 @@ impl CurveExt for Curve { } } -/// Get the control points for a bezier curve that approximates a circular arc for +/// Get the control points for a Bézier curve that approximates a circular arc for /// a start point, an end point and a center of the circle whose arc connects /// the two. fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index a33f1cb91..31f8cd732 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -148,9 +148,7 @@ use crate::loading::{DataSource, Load}; #[func(scope)] pub fn plugin( engine: &mut Engine, - /// A path to a WebAssembly file or raw WebAssembly bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 801ca617a..aa14c5c77 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -20,9 +20,7 @@ use crate::loading::{DataSource, Load}; #[func(scope, title = "CBOR")] pub fn cbor( engine: &mut Engine, - /// A path to a CBOR file or raw CBOR bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6fdec4459..6afb5baeb 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -26,9 +26,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "CSV")] pub fn csv( engine: &mut Engine, - /// Path to a CSV file or raw CSV bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a CSV file or raw CSV bytes. source: Spanned, /// The delimiter that separates columns in the CSV file. /// Must be a single ASCII character. diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 185bac143..aa908cca4 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -51,9 +51,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "JSON")] pub fn json( engine: &mut Engine, - /// Path to a JSON file or raw JSON bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a JSON file or raw JSON bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 2660e7e7f..f04b2e746 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -29,9 +29,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "TOML")] pub fn toml( engine: &mut Engine, - /// A path to a TOML file or raw TOML bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a TOML file or raw TOML bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 32ed6f24b..daccd02fc 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -58,9 +58,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "XML")] pub fn xml( engine: &mut Engine, - /// A path to an XML file or raw XML bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to an XML file or raw XML bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 4eeec28f1..3f48113e8 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -41,9 +41,7 @@ use crate::loading::{DataSource, Load, Readable}; #[func(scope, title = "YAML")] pub fn yaml( engine: &mut Engine, - /// A path to a YAML file or raw YAML bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes. source: Spanned, ) -> SourceResult { let data = source.load(engine.world)?; diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index f413189ba..7ceb530f8 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -623,7 +623,7 @@ impl OutlineEntry { /// 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 + /// [`body`]($heading.body); for a figure a caption and for equations, it is /// empty. #[func] pub fn body(&self) -> StrResult { diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index f9ca3ca09..001078e5e 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -32,12 +32,10 @@ use crate::World; /// embedded file conforms to PDF/A-1 or PDF/A-2. #[elem(Show, Locatable)] pub struct EmbedElem { - /// Path of the file to be embedded. + /// The [path]($syntax/#paths) of the file to be embedded. /// /// 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 } = diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index 607d92ab1..fb5151e8f 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -10,12 +10,12 @@ use crate::foundations::{ use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; -/// A curve consisting of movements, lines, and Beziér segments. +/// A curve consisting of movements, lines, and Bézier segments. /// /// At any point in time, there is a conceptual pen or cursor. /// - Move elements move the cursor without drawing. /// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new -/// position, potentially with control point for a Beziér curve. +/// position, potentially with control point for a Bézier curve. /// - Close elements draw a straight or smooth line back to the start of the /// curve or the latest preceding move segment. /// @@ -26,7 +26,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// or relative to the current pen/cursor position, that is, the position where /// the previous segment ended. /// -/// Beziér curve control points can be skipped by passing `{none}` or +/// Bézier curve control points can be skipped by passing `{none}` or /// automatically mirrored from the preceding segment by passing `{auto}`. /// /// # Example @@ -88,7 +88,7 @@ pub struct CurveElem { #[fold] pub stroke: Smart>, - /// The components of the curve, in the form of moves, line and Beziér + /// The components of the curve, in the form of moves, line and Bézier /// segment, and closes. #[variadic] pub components: Vec, @@ -225,7 +225,7 @@ pub struct CurveLine { pub relative: bool, } -/// Adds a quadratic Beziér curve segment from the last point to `end`, using +/// Adds a quadratic Bézier curve segment from the last point to `end`, using /// `control` as the control point. /// /// ```example @@ -245,9 +245,9 @@ pub struct CurveLine { /// ``` #[elem(name = "quad", title = "Curve Quadratic Segment")] pub struct CurveQuad { - /// The control point of the quadratic Beziér curve. + /// The control point of the quadratic Bézier curve. /// - /// - If `{auto}` and this segment follows another quadratic Beziér curve, + /// - If `{auto}` and this segment follows another quadratic Bézier curve, /// the previous control point will be mirrored. /// - If `{none}`, the control point defaults to `end`, and the curve will /// be a straight line. @@ -272,7 +272,7 @@ pub struct CurveQuad { pub relative: bool, } -/// Adds a cubic Beziér curve segment from the last point to `end`, using +/// Adds a cubic Bézier curve segment from the last point to `end`, using /// `control-start` and `control-end` as the control points. /// /// ```example @@ -388,7 +388,7 @@ pub enum CloseMode { Straight, } -/// A curve consisting of movements, lines, and Beziér segments. +/// A curve consisting of movements, lines, and Bézier segments. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct Curve(pub Vec); diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 18d40caa8..97189e22d 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -46,10 +46,11 @@ use crate::text::LocalName; /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { - /// A path to an image file or raw bytes making up an image in one of the - /// supported [formats]($image.format). + /// A [path]($syntax/#paths) to an image file or raw bytes making up an + /// image in one of the supported [formats]($image.format). /// - /// For more details about paths, see the [Paths section]($syntax/#paths). + /// Bytes can be used to specify raw pixel data in a row-major, + /// left-to-right, top-to-bottom format. /// /// ```example /// #let original = read("diagram.svg") diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index c1cfde94a..968146cda 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -8,7 +8,7 @@ use crate::foundations::{ use crate::layout::{Axes, BlockElem, Length, Rel}; use crate::visualize::{FillRule, Paint, Stroke}; -/// A path through a list of points, connected by Bezier curves. +/// A path through a list of points, connected by Bézier curves. /// /// # Example /// ```example @@ -59,8 +59,8 @@ pub struct PathElem { #[fold] pub stroke: Smart>, - /// Whether to close this path with one last bezier curve. This curve will - /// takes into account the adjacent control points. If you want to close + /// Whether to close this path with one last Bézier curve. This curve will + /// take into account the adjacent control points. If you want to close /// with a straight line, simply add one last point that's the same as the /// start point. #[default(false)] diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index 3c62b210f..439b4cd98 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -412,7 +412,7 @@ pub enum Geometry { Line(Point), /// A rectangle with its origin in the topleft corner. Rect(Size), - /// A curve consisting of movements, lines, and Bezier segments. + /// A curve consisting of movements, lines, and Bézier segments. Curve(Curve), } diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 50819f659..4e4dd0c2d 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -16,7 +16,7 @@ description: Changes slated to appear in Typst 0.13.0 - The `image` function now supports raw [pixel raster formats]($image.format) for generating images from within Typst - Functions that accept [file paths]($syntax/#paths) now also accept raw - [bytes] instead, for full flexibility + [bytes], for full flexibility - WebAssembly [plugins]($plugin) are more flexible and automatically run multi-threaded - Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`) @@ -155,7 +155,7 @@ description: Changes slated to appear in Typst 0.13.0 - Fixed multi-line annotations (e.g. overbrace) changing the math baseline - Fixed merging of attachments when the base is a nested equation - Fixed resolving of contextual (em-based) text sizes within math -- Fixed spacing around ⊥ +- Fixed spacing around up tacks (⊥) ## Bibliography - Prose and author-only citations now use editor names if the author names are @@ -229,7 +229,7 @@ description: Changes slated to appear in Typst 0.13.0 - A shebang `#!` at the very start of a file is now ignored ## PDF export -- Added `pdf.embed` function for embedding arbitrary files in the exported PDF +- Added [`pdf.embed`] function for embedding arbitrary files in the exported PDF - Added support for PDF/A-3b export - The PDF timestamp will now contain the timezone by default diff --git a/docs/reference/export/png.md b/docs/reference/export/png.md index fe122f4d3..0e817e0f1 100644 --- a/docs/reference/export/png.md +++ b/docs/reference/export/png.md @@ -11,7 +11,7 @@ the PNG you exported, you will notice a loss of quality. Typst calculates the resolution of your PNGs based on each page's physical dimensions and the PPI. If you need guidance for choosing a PPI value, consider the following: -- A DPI value of 300 or 600 is typical for desktop printing. +- A value of 300 or 600 is typical for desktop printing. - Professional prints of detailed graphics can go up to 1200 PPI. - If your document is only viewed at a distance, e.g. a poster, you may choose a smaller value than 300. From d61f57365b931e7cd57ed0a88b21c79f3042e3f5 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 11:18:35 +0100 Subject: [PATCH 066/101] Fix docs outline for nested definitions (#5823) --- docs/src/lib.rs | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index fae74e0fc..e9771738d 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -550,8 +550,6 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec { .collect(), }); } - - outline.extend(scope_outline(&model.scope)); } else { outline.extend(model.params.iter().map(|param| OutlineItem { id: eco_format!("{id_base}-{}", urlify(param.name)), @@ -560,27 +558,30 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec { })); } + outline.extend(scope_outline(&model.scope, id_base)); + outline } /// Produce an outline for a function scope. -fn scope_outline(scope: &[FuncModel]) -> Option { +fn scope_outline(scope: &[FuncModel], id_base: &str) -> Option { if scope.is_empty() { return None; } - Some(OutlineItem { - id: "definitions".into(), - name: "Definitions".into(), - children: scope - .iter() - .map(|func| { - let id = urlify(&eco_format!("definitions-{}", func.name)); - let children = func_outline(func, &id); - OutlineItem { id, name: func.title.into(), children } - }) - .collect(), - }) + let dash = if id_base.is_empty() { "" } else { "-" }; + let id = eco_format!("{id_base}{dash}definitions"); + + let children = scope + .iter() + .map(|func| { + let id = urlify(&eco_format!("{id}-{}", func.name)); + let children = func_outline(func, &id); + OutlineItem { id, name: func.title.into(), children } + }) + .collect(); + + Some(OutlineItem { id, name: "Definitions".into(), children }) } /// Create a page for a group of functions. @@ -687,7 +688,7 @@ fn type_outline(model: &TypeModel) -> Vec { }); } - outline.extend(scope_outline(&model.scope)); + outline.extend(scope_outline(&model.scope, "")); outline } From a1c73b41b862eb95f609f18ee99bdb6da039f478 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 21:57:46 +0100 Subject: [PATCH 067/101] Document removals in changelog (#5827) --- docs/changelog/0.13.0.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 4e4dd0c2d..2caace723 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -45,6 +45,7 @@ description: Changes slated to appear in Typst 0.13.0 result in a warning - The default show rules of various built-in elements like lists, quotes, etc. were adjusted to ensure they produce/don't produce paragraphs as appropriate + - Removed support for booleans and content in [`outline.indent`] - The [`outline`] function was fully reworked to improve its out-of-the-box behavior **(Breaking change)** - [Outline entries]($outline.entry) are now [blocks]($block) and are thus @@ -312,8 +313,20 @@ feature flag. functions directly accepting both paths and bytes - The `sect` and its variants in favor of `inter`, and `integral.sect` in favor of `integral.inter` -- Fully removed type/str compatibility behavior (e.g. `{int == "integer"}`) - which was temporarily introduced in Typst 0.8 **(Breaking change)** + +## Removals +- Removed `style` function and `styles` argument of [`measure`], use a [context] + expression instead **(Breaking change)** +- Removed `state.display` function, use [`state.get`] instead + **(Breaking change)** +- Removed `location` argument of [`state.at`], [`counter.at`], and [`query`] + **(Breaking change)** +- Removed compatibility behavior where [`counter.display`] worked without + [context] **(Breaking change)** +- Removed compatibility behavior of [`locate`] **(Breaking change)** +- Removed compatibility behavior of type/str comparisons + (e.g. `{int == "integer"}`) which was temporarily introduced in Typst 0.8 + **(Breaking change)** ## Development - The `typst::compile` function is now generic and can return either a From e4f8e57c534db8a31d51e0342c46b913a7e22422 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 22:10:43 +0100 Subject: [PATCH 068/101] Fix unnecessary import rename warning (#5828) --- crates/typst-eval/src/import.rs | 6 +++--- tests/suite/scripting/import.typ | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 27b06af41..1b1641487 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -44,11 +44,10 @@ impl Eval for ast::ModuleImport<'_> { } // If there is a rename, import the source itself under that name. - let bare_name = self.bare_name(); let new_name = self.new_name(); if let Some(new_name) = new_name { - if let Ok(source_name) = &bare_name { - if source_name == new_name.as_str() { + if let ast::Expr::Ident(ident) = self.source() { + if ident.as_str() == new_name.as_str() { // Warn on `import x as x` vm.engine.sink.warn(warning!( new_name.span(), @@ -57,6 +56,7 @@ impl Eval for ast::ModuleImport<'_> { } } + // Define renamed module on the scope. vm.define(new_name, source.clone()); } diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ index 03e2efc6b..49b66ee56 100644 --- a/tests/suite/scripting/import.typ +++ b/tests/suite/scripting/import.typ @@ -255,6 +255,10 @@ // Warning: 17-21 unnecessary import rename to same name #import enum as enum +--- import-rename-necessary --- +#import "module.typ" as module: a +#test(module.a, a) + --- import-rename-unnecessary-mixed --- // Warning: 17-21 unnecessary import rename to same name #import enum as enum: item @@ -263,10 +267,6 @@ // Warning: 31-35 unnecessary import rename to same name #import enum as enum: item as item ---- import-item-rename-unnecessary-string --- -// Warning: 25-31 unnecessary import rename to same name -#import "module.typ" as module - --- import-item-rename-unnecessary-but-ok --- #import "modul" + "e.typ" as module #test(module.b, 1) From 3fba256405c4aae9f121a07ddaa29cc10b825fc9 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:39:04 -0300 Subject: [PATCH 069/101] Don't crash on image with zero DPI (#5835) --- crates/typst-layout/src/image.rs | 2 ++ crates/typst-library/src/visualize/image/raster.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index d963ea50d..3e5b7d8bd 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -95,6 +95,8 @@ pub fn layout_image( } else { // If neither is forced, take the natural image size at the image's // DPI bounded by the available space. + // + // Division by DPI is fine since it's guaranteed to be positive. let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); Size::new( diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index d43b15486..0883fe71d 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -160,6 +160,8 @@ impl RasterImage { } /// The image's pixel density in pixels per inch, if known. + /// + /// This is guaranteed to be positive. pub fn dpi(&self) -> Option { self.0.dpi } @@ -334,6 +336,9 @@ fn apply_rotation(image: &mut DynamicImage, rotation: u32) { } /// Try to determine the DPI (dots per inch) of the image. +/// +/// This is guaranteed to be a positive value, or `None` if invalid or +/// unspecified. fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { // Try to extract the DPI from the EXIF metadata. If that doesn't yield // anything, fall back to specialized procedures for extracting JPEG or PNG @@ -341,6 +346,7 @@ fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { exif.and_then(exif_dpi) .or_else(|| jpeg_dpi(data)) .or_else(|| png_dpi(data)) + .filter(|&dpi| dpi > 0.0) } /// Try to get the DPI from the EXIF metadata. From 25e27169e1413c9e14184267be57fdbbb09e7c34 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:39:32 +0100 Subject: [PATCH 070/101] Add warning for `pdf.embed` elem used with HTML (#5829) --- crates/typst-library/src/pdf/embed.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index 001078e5e..f902e7f14 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -1,9 +1,12 @@ use ecow::EcoString; +use typst_library::foundations::Target; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{warning, At, SourceResult}; use crate::engine::Engine; -use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; +use crate::foundations::{ + elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem, +}; use crate::introspection::Locatable; use crate::World; @@ -78,7 +81,12 @@ pub struct EmbedElem { } impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles) == Target::Html { + engine + .sink + .warn(warning!(self.span(), "embed was ignored during HTML export")); + } Ok(Content::empty()) } } From ee47cb846924235be6eae968a7853ea7860ccc51 Mon Sep 17 00:00:00 2001 From: TwoF1nger <140991913+TwoF1nger@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:42:16 +0000 Subject: [PATCH 071/101] Add smart quotes for Bulgarian (#5807) --- crates/typst-library/src/text/smartquote.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 2f89fe298..f457a6371 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> { "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), + "bg" => ("’", "’", "„", "“"), _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), _ => default, }; From 89e71acecd4a3a06943d0bd4443fc80a9b8f41e4 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 15:37:19 +0100 Subject: [PATCH 072/101] Respect `par` constructor arguments (#5842) --- crates/typst-layout/src/flow/collect.rs | 13 +- crates/typst-layout/src/flow/mod.rs | 77 ++++--- crates/typst-layout/src/inline/collect.rs | 54 +---- crates/typst-layout/src/inline/finalize.rs | 10 +- crates/typst-layout/src/inline/line.rs | 51 +++-- crates/typst-layout/src/inline/linebreak.rs | 44 ++-- crates/typst-layout/src/inline/mod.rs | 191 +++++++++++++++++- crates/typst-layout/src/inline/prepare.rs | 73 +------ crates/typst-layout/src/math/text.rs | 1 - crates/typst-library/src/model/link.rs | 4 +- crates/typst-library/src/text/mod.rs | 25 +-- crates/typst-library/src/text/raw.rs | 6 +- tests/ref/issue-5831-par-constructor-args.png | Bin 0 -> 1356 bytes tests/suite/model/par.typ | 14 ++ 14 files changed, 314 insertions(+), 249 deletions(-) create mode 100644 tests/ref/issue-5831-par-constructor-args.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 34362a6c5..2c14f7a37 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - None, )? .into_frames(); @@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> { self.output.push(Child::Tag(&elem.tag)); } - self.lines(lines, styles); + let leading = ParElem::leading_in(styles); + self.lines(lines, leading, styles); for (c, _) in &self.children[end..] { let elem = c.to_packed::().unwrap(); @@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> { )? .into_frames(); - let spacing = ParElem::spacing_in(styles); + let spacing = elem.spacing(styles); + let leading = elem.leading(styles); + self.output.push(Child::Rel(spacing.into(), 4)); - self.lines(lines, styles); + self.lines(lines, leading, styles); self.output.push(Child::Rel(spacing.into(), 4)); self.par_situation = ParSituation::Consecutive; @@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> { } /// Collect laid-out lines. - fn lines(&mut self, lines: Vec, styles: StyleChain<'a>) { + fn lines(&mut self, lines: Vec, leading: Abs, 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. diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2acbbcef3..cba228bcd 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -197,7 +197,50 @@ pub fn layout_flow<'a>( mode: FlowMode, ) -> SourceResult { // Prepare configuration that is shared across the whole flow. - let config = Config { + let config = configuration(shared, regions, columns, column_gutter, mode); + + // Collect the elements into pre-processed children. These are much easier + // to handle than the raw elements. + let bump = Bump::new(); + let children = collect( + engine, + &bump, + children, + locator.next(&()), + Size::new(config.columns.width, regions.full), + regions.expand.x, + mode, + )?; + + let mut work = Work::new(&children); + let mut finished = vec![]; + + // This loop runs once per region produced by the flow layout. + loop { + let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?; + finished.push(frame); + + // Terminate the loop when everything is processed, though draining the + // backlog if necessary. + if work.done() && (!regions.expand.y || regions.backlog.is_empty()) { + break; + } + + regions.next(); + } + + Ok(Fragment::frames(finished)) +} + +/// Determine the flow's configuration. +fn configuration<'x>( + shared: StyleChain<'x>, + regions: Regions, + columns: NonZeroUsize, + column_gutter: Rel, + mode: FlowMode, +) -> Config<'x> { + Config { mode, shared, columns: { @@ -235,39 +278,7 @@ pub fn layout_flow<'a>( ) }, }), - }; - - // Collect the elements into pre-processed children. These are much easier - // to handle than the raw elements. - let bump = Bump::new(); - let children = collect( - engine, - &bump, - children, - locator.next(&()), - Size::new(config.columns.width, regions.full), - regions.expand.x, - mode, - )?; - - let mut work = Work::new(&children); - let mut finished = vec![]; - - // This loop runs once per region produced by the flow layout. - loop { - let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?; - finished.push(frame); - - // Terminate the loop when everything is processed, though draining the - // backlog if necessary. - if work.done() && (!regions.expand.y || regions.backlog.is_empty()) { - break; - } - - regions.next(); } - - Ok(Fragment::frames(finished)) } /// The work that is left to do by flow layout. diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 14cf2e3b8..5a1b7b4fc 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -2,10 +2,8 @@ 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, + Abs, 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, @@ -123,40 +121,20 @@ pub fn collect<'a>( children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: StyleChain<'a>, + config: &Config, region: Size, - 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 !config.first_line_indent.is_zero() { + collector.push_item(Item::Absolute(config.first_line_indent, false)); + collector.spans.push(1, Span::detached()); + } - if let Some(situation) = situation { - let first_line_indent = ParElem::first_line_indent_in(styles); - 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.amount.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 !config.hanging_indent.is_zero() { + collector.push_item(Item::Absolute(-config.hanging_indent, false)); + collector.spans.push(1, Span::detached()); } for &(child, styles) in children { @@ -167,7 +145,7 @@ pub fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.build_text(styles, |full| { let dir = TextElem::dir_in(styles); - if dir != outer_dir { + if dir != config.dir { // Insert "Explicit Directional Embedding". match dir { Dir::LTR => full.push_str(LTR_EMBEDDING), @@ -182,7 +160,7 @@ pub fn collect<'a>( full.push_str(&elem.text); } - if dir != outer_dir { + if dir != config.dir { // Insert "Pop Directional Formatting". full.push_str(POP_EMBEDDING); } @@ -265,16 +243,6 @@ 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/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 7ad287c45..c9de0085e 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -9,7 +9,6 @@ pub fn finalize( engine: &mut Engine, p: &Preparation, lines: &[Line], - styles: StyleChain, region: Size, expand: bool, locator: &mut SplitLocator<'_>, @@ -19,9 +18,10 @@ pub fn finalize( let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) { - region - .x - .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + region.x.min( + p.config.hanging_indent + + lines.iter().map(|line| line.width).max().unwrap_or_default(), + ) } else { region.x }; @@ -29,7 +29,7 @@ pub fn finalize( // Stack the lines into one frame per region. lines .iter() - .map(|line| commit(engine, p, line, width, region.y, locator, styles)) + .map(|line| commit(engine, p, line, width, region.y, locator)) .collect::>() .map(Fragment::frames) } diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 9f6973807..bd08f30ef 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; use typst_library::engine::Engine; -use typst_library::foundations::NativeElement; use typst_library::introspection::{SplitLocator, Tag}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; -use typst_library::model::{ParLine, ParLineMarker}; +use typst_library::model::ParLineMarker; use typst_library::text::{Lang, TextElem}; use typst_utils::Numeric; @@ -135,7 +134,7 @@ pub fn line<'a>( // Whether the line is justified. let justify = full.ends_with(LINE_SEPARATOR) - || (p.justify && breakpoint != Breakpoint::Mandatory); + || (p.config.justify && breakpoint != Breakpoint::Mandatory); // Process dashes. let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { @@ -157,14 +156,14 @@ pub fn line<'a>( // Add a hyphen at the line start, if a previous dash should be repeated. if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { if let Some(shaped) = items.first_text_mut() { - shaped.prepend_hyphen(engine, p.fallback); + shaped.prepend_hyphen(engine, p.config.fallback); } } // Add a hyphen at the line end, if we ended on a soft hyphen. if dash == Some(Dash::Soft) { if let Some(shaped) = items.last_text_mut() { - shaped.push_hyphen(engine, p.fallback); + shaped.push_hyphen(engine, p.config.fallback); } } @@ -234,13 +233,13 @@ where { // If there is nothing bidirectional going on, skip reordering. let Some(bidi) = &p.bidi else { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; }; // The bidi crate panics for empty lines. if range.is_empty() { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; } @@ -308,13 +307,13 @@ fn collect_range<'a>( /// punctuation marks at line start or line end. fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { if text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script)) { adjust_cj_at_line_start(p, items); } if text.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script)) { adjust_cj_at_line_end(p, items); } @@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { let shrink = glyph.shrinkability().0; glyph.shrink_left(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { + } else if p.config.cjk_latin_spacing + && glyph.is_cj_script() + && glyph.x_offset > Em::zero() + { // If the first glyph is a CJK character adjusted by // [`add_cjk_latin_spacing`], restore the original width. let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); @@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { let punct = shaped.glyphs.to_mut().last_mut().unwrap(); punct.shrink_right(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing + } else if p.config.cjk_latin_spacing && glyph.is_cj_script() && (glyph.x_advance - glyph.x_offset) > Em::one() { @@ -424,16 +426,15 @@ pub fn commit( width: Abs, full: Abs, locator: &mut SplitLocator<'_>, - styles: StyleChain, ) -> SourceResult { - let mut remaining = width - line.width - p.hang; + let mut remaining = width - line.width - p.config.hanging_indent; 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. In an RTL paragraph, the // hanging indent arises naturally due to the line width. - if p.dir == Dir::LTR { - offset += p.hang; + if p.config.dir == Dir::LTR { + offset += p.config.hanging_indent; } // Handle hanging punctuation to the left. @@ -554,11 +555,13 @@ pub fn commit( let mut output = Frame::soft(size); output.set_baseline(top); - add_par_line_marker(&mut output, styles, engine, locator, top); + if let Some(marker) = &p.config.numbering_marker { + add_par_line_marker(&mut output, marker, engine, locator, top); + } // Construct the line's frame. for (offset, frame) in frames { - let x = offset + p.align.position(remaining); + let x = offset + p.config.align.position(remaining); let y = top - frame.baseline(); output.push_frame(Point::new(x, y), frame); } @@ -575,26 +578,18 @@ pub fn commit( /// number in the margin, is aligned to the line's baseline. fn add_par_line_marker( output: &mut Frame, - styles: StyleChain, + marker: &Packed, engine: &mut Engine, locator: &mut SplitLocator, top: Abs, ) { - let Some(numbering) = ParLine::numbering_in(styles) else { return }; - let margin = ParLine::number_margin_in(styles); - let align = ParLine::number_align_in(styles); - - // Delay resolving the number clearance until line numbers are laid out to - // avoid inconsistent spacing depending on varying font size. - let clearance = ParLine::number_clearance_in(styles); - // Elements in tags must have a location for introspection to work. We do // the work here instead of going through all of the realization process // just for this, given we don't need to actually place the marker as we // manually search for it in the frame later (when building a root flow, // where line numbers can be displayed), so we just need it to be in a tag // and to be valid (to have a location). - let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); + let mut marker = marker.clone(); let key = typst_utils::hash128(&marker); let loc = locator.next_location(engine.introspector, key); marker.set_location(loc); @@ -606,7 +601,7 @@ fn add_par_line_marker( // line's general baseline. However, the line number will still need to // manually adjust its own 'y' position based on its own baseline. let pos = Point::with_y(top); - output.push(pos, FrameItem::Tag(Tag::Start(marker))); + output.push(pos, FrameItem::Tag(Tag::Start(marker.pack()))); output.push(pos, FrameItem::Tag(Tag::End(loc, key))); } diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 87113c689..a9f21188b 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -110,15 +110,7 @@ pub fn linebreak<'a>( p: &'a Preparation<'a>, width: Abs, ) -> Vec> { - let linebreaks = p.linebreaks.unwrap_or_else(|| { - if p.justify { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }); - - match linebreaks { + match p.config.linebreaks { Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width), } @@ -384,7 +376,7 @@ fn linebreak_optimized_approximate( // Whether the line is justified. This is not 100% accurate w.r.t // to line()'s behaviour, but good enough. - let justify = p.justify && breakpoint != Breakpoint::Mandatory; + let justify = p.config.justify && breakpoint != Breakpoint::Mandatory; // We don't really know whether the line naturally ends with a dash // here, so we can miss that case, but it's ok, since all of this @@ -573,7 +565,7 @@ fn raw_ratio( // calculate the extra amount. Also, don't divide by zero. let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; // Normalize the amount by half the em size. - ratio = 1.0 + extra_stretch / (p.size / 2.0); + ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0); } // The min value must be < MIN_RATIO, but how much smaller doesn't matter @@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { return; } - let hyphenate = p.hyphenate != Some(false); + let hyphenate = p.config.hyphenate != Some(false); let lb = LINEBREAK_DATA.as_borrowed(); - let segmenter = match p.lang { + let segmenter = match p.config.lang { Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, _ => &SEGMENTER, }; @@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { /// Whether hyphenation is enabled at the given offset. fn hyphenate_at(p: &Preparation, offset: usize) -> bool { - p.hyphenate - .or_else(|| { - let (_, item) = p.get(offset); - let styles = item.text()?.styles; - Some(TextElem::hyphenate_in(styles)) - }) - .unwrap_or(false) + p.config.hyphenate.unwrap_or_else(|| { + let (_, item) = p.get(offset); + match item.text() { + Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), + None => false, + } + }) } /// The text language at the given offset. fn lang_at(p: &Preparation, offset: usize) -> Option { - let lang = p.lang.or_else(|| { + let lang = p.config.lang.or_else(|| { let (_, item) = p.get(offset); let styles = item.text()?.styles; Some(TextElem::lang_in(styles)) @@ -865,13 +857,13 @@ impl CostMetrics { fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. - min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, - min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, + min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 }, + min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 }, // Approximate hyphen width for estimates. - approx_hyphen_width: Em::new(0.33).at(p.size), + approx_hyphen_width: Em::new(0.33).at(p.config.font_size), // Costs. - hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), - runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), + hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(), + runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(), } } diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index f8a36368d..5ef820d07 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,12 +13,17 @@ 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::{Packed, StyleChain}; +use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; -use typst_library::layout::{Fragment, Size}; -use typst_library::model::ParElem; +use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size}; +use typst_library::model::{ + EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker, + TermsElem, +}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::text::{Costs, Lang, TextElem}; use typst_library::World; +use typst_utils::{Numeric, SliceExt}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; @@ -98,7 +103,7 @@ fn layout_par_impl( styles, )?; - layout_inline( + layout_inline_impl( &mut engine, &children, &mut locator, @@ -106,33 +111,134 @@ fn layout_par_impl( region, expand, Some(situation), + &ConfigBase { + justify: elem.justify(styles), + linebreaks: elem.linebreaks(styles), + first_line_indent: elem.first_line_indent(styles), + hanging_indent: elem.hanging_indent(styles), + }, ) } /// 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>, + shared: StyleChain<'a>, + region: Size, + expand: bool, +) -> SourceResult { + layout_inline_impl( + engine, + children, + locator, + shared, + region, + expand, + None, + &ConfigBase { + justify: ParElem::justify_in(shared), + linebreaks: ParElem::linebreaks_in(shared), + first_line_indent: ParElem::first_line_indent_in(shared), + hanging_indent: ParElem::hanging_indent_in(shared), + }, + ) +} + +/// The internal implementation of [`layout_inline`]. +#[allow(clippy::too_many_arguments)] +fn layout_inline_impl<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, region: Size, expand: bool, par: Option, + base: &ConfigBase, ) -> SourceResult { + // Prepare configuration that is shared across the whole inline layout. + let config = configuration(base, children, shared, par); + // Collect all text into one string for BiDi analysis. - let (text, segments, spans) = - collect(children, engine, locator, styles, region, par)?; + let (text, segments, spans) = collect(children, engine, locator, &config, region)?; // Perform BiDi analysis and performs some preparation steps before we // proceed to line breaking. - let p = prepare(engine, children, &text, segments, spans, styles, par)?; + let p = prepare(engine, &config, &text, segments, spans)?; // Break the text into lines. - let lines = linebreak(engine, &p, region.x - p.hang); + let lines = linebreak(engine, &p, region.x - config.hanging_indent); // Turn the selected lines into frames. - finalize(engine, &p, &lines, styles, region, expand, locator) + finalize(engine, &p, &lines, region, expand, locator) +} + +/// Determine the inline layout's configuration. +fn configuration( + base: &ConfigBase, + children: &[Pair], + shared: StyleChain, + situation: Option, +) -> Config { + let justify = base.justify; + let font_size = TextElem::size_in(shared); + let dir = TextElem::dir_in(shared); + + Config { + justify, + linebreaks: base.linebreaks.unwrap_or_else(|| { + if justify { + Linebreaks::Optimized + } else { + Linebreaks::Simple + } + }), + first_line_indent: { + let FirstLineIndent { amount, all } = base.first_line_indent; + if !amount.is_zero() + && match situation { + // First-line indent for the first paragraph after a list + // bullet just looks bad. + Some(ParSituation::First) => all && !in_list(shared), + Some(ParSituation::Consecutive) => true, + Some(ParSituation::Other) => all, + None => false, + } + && AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into() + { + amount.at(font_size) + } else { + Abs::zero() + } + }, + hanging_indent: if situation.is_some() { + base.hanging_indent + } else { + Abs::zero() + }, + numbering_marker: ParLine::numbering_in(shared).map(|numbering| { + Packed::new(ParLineMarker::new( + numbering, + ParLine::number_align_in(shared), + ParLine::number_margin_in(shared), + // Delay resolving the number clearance until line numbers are + // laid out to avoid inconsistent spacing depending on varying + // font size. + ParLine::number_clearance_in(shared), + )) + }), + align: AlignElem::alignment_in(shared).fix(dir).x, + font_size, + dir, + hyphenate: shared_get(children, shared, TextElem::hyphenate_in) + .map(|uniform| uniform.unwrap_or(justify)), + lang: shared_get(children, shared, TextElem::lang_in), + fallback: TextElem::fallback_in(shared), + cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(), + costs: TextElem::costs_in(shared), + } } /// Distinguishes between a few different kinds of paragraphs. @@ -148,3 +254,66 @@ pub enum ParSituation { /// Any other kind of paragraph. Other, } + +/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`]. +struct ConfigBase { + justify: bool, + linebreaks: Smart, + first_line_indent: FirstLineIndent, + hanging_indent: Abs, +} + +/// Shared configuration for the whole inline layout. +struct Config { + /// Whether to justify text. + justify: bool, + /// How to determine line breaks. + linebreaks: Linebreaks, + /// The indent the first line of a paragraph should have. + first_line_indent: Abs, + /// The indent that all but the first line of a paragraph should have. + hanging_indent: Abs, + /// Configuration for line numbering. + numbering_marker: Option>, + /// The resolved horizontal alignment. + align: FixedAlignment, + /// The text size. + font_size: Abs, + /// The dominant direction. + dir: Dir, + /// A uniform hyphenation setting (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + hyphenate: Option, + /// The text language (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + lang: Option, + /// Whether font fallback is enabled. + fallback: bool, + /// Whether to add spacing between CJK and Latin characters. + cjk_latin_spacing: bool, + /// Costs for various layout decisions. + costs: Costs, +} + +/// 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) +} + +/// 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) +} diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 0344d4331..5d7fcd7cb 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,9 +1,4 @@ -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 typst_library::layout::{Dir, Em}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; @@ -17,6 +12,8 @@ use super::*; pub struct Preparation<'a> { /// The full text. pub text: &'a str, + /// Configuration for inline layout. + pub config: &'a Config, /// Bidirectional text embedding levels. /// /// This is `None` if all text directions are uniform (all the base @@ -28,28 +25,6 @@ pub struct Preparation<'a> { pub indices: Vec, /// The span mapper. pub spans: SpanMapper, - /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option, - /// Costs for various layout decisions. - pub costs: Costs, - /// The dominant direction. - pub dir: Dir, - /// The text language if it's the same for all children. - pub lang: Option, - /// The resolved horizontal alignment. - pub align: FixedAlignment, - /// Whether to justify text. - pub justify: bool, - /// 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. - pub fallback: bool, - /// How to determine line breaks. - pub linebreaks: Smart, - /// The text size. - pub size: Abs, } impl<'a> Preparation<'a> { @@ -80,15 +55,12 @@ impl<'a> Preparation<'a> { #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &[Pair<'a>], + config: &'a Config, text: &'a str, segments: Vec>, spans: SpanMapper, - styles: StyleChain<'a>, - situation: Option, ) -> SourceResult> { - let dir = TextElem::dir_in(styles); - let default_level = match dir { + let default_level = match config.dir { Dir::RTL => BidiLevel::rtl(), _ => BidiLevel::ltr(), }; @@ -124,51 +96,20 @@ pub fn prepare<'a>( indices.extend(range.clone().map(|_| i)); } - let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); - if cjk_latin_spacing { + if config.cjk_latin_spacing { add_cjk_latin_spacing(&mut items); } - // Only apply hanging indent to real paragraphs. - let hang = if situation.is_some() { - ParElem::hanging_indent_in(styles) - } else { - Abs::zero() - }; - Ok(Preparation { + config, text, bidi: is_bidi.then_some(bidi), items, indices, spans, - hyphenate: shared_get(children, styles, TextElem::hyphenate_in), - costs: TextElem::costs_in(styles), - dir, - lang: shared_get(children, styles, TextElem::lang_in), - align: AlignElem::alignment_in(styles).resolve(styles).x, - justify: ParElem::justify_in(styles), - hang, - cjk_latin_spacing, - fallback: TextElem::fallback_in(styles), - linebreaks: ParElem::linebreaks_in(styles), - size: TextElem::size_in(styles), }) } -/// 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/math/text.rs b/crates/typst-layout/src/math/text.rs index 9a64992aa..59ac5b089 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -107,7 +107,6 @@ fn layout_inline_text( styles, Size::splat(Abs::inf()), false, - None, )? .into_frame(); diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 24b746b7e..ea85aa945 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -11,7 +11,7 @@ use crate::foundations::{ use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; use crate::layout::Position; -use crate::text::{Hyphenate, TextElem}; +use crate::text::TextElem; /// Links to a URL or a location in the document. /// @@ -138,7 +138,7 @@ impl Show for Packed { 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.set(TextElem::set_hyphenate(Smart::Custom(false))); out } } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 12f4e4c59..30c2ea1d1 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -51,7 +51,6 @@ use crate::foundations::{ }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::math::{EquationElem, MathSize}; -use crate::model::ParElem; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; @@ -504,9 +503,8 @@ pub struct TextElem { /// enabling hyphenation can /// improve justification. /// ``` - #[resolve] #[ghost] - pub hyphenate: Hyphenate, + pub hyphenate: Smart, /// The "cost" of various choices when laying out text. A higher cost means /// the layout engine will make the choice less often. Costs are specified @@ -1110,27 +1108,6 @@ impl Resolve for TextDir { } } -/// Whether to hyphenate text. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub Smart); - -cast! { - Hyphenate, - self => self.0.into_value(), - v: Smart => Self(v), -} - -impl Resolve for Hyphenate { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => ParElem::justify_in(styles), - Smart::Custom(v) => v, - } - } -} - /// A set of stylistic sets to enable. #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] pub struct StylisticSets(u32); diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 5bb21e43a..b330c01ef 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -21,9 +21,7 @@ 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, -}; +use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize}; use crate::visualize::Color; use crate::World; @@ -472,7 +470,7 @@ impl ShowSet for Packed { let mut out = Styles::new(); out.set(TextElem::set_overhang(false)); out.set(TextElem::set_lang(Lang::ENGLISH)); - out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out.set(TextElem::set_hyphenate(Smart::Custom(false))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); diff --git a/tests/ref/issue-5831-par-constructor-args.png b/tests/ref/issue-5831-par-constructor-args.png new file mode 100644 index 0000000000000000000000000000000000000000..440b612ba938a714148f598c52501450b6fe85aa GIT binary patch literal 1356 zcmV-S1+)5zP)e!Amty9JdH6Cr0@k9ib)FL`~)DA^aL==tIa*S3aps_WI7!m|Y zxFm8E5iG3+fk2BwOcqTzB4-jpf+07_{z?KbiX9NGm^b@dzf>25RyYT zs0D5-@$jV-NA4G?-k~?$>~K6F+X3hRMP)ZIVm& zAxx^J4#S0*9O?TS@CfpfU{UNgDFHmUkI$(!BY>lI6h>{23jh+X4tI>0JDm0&ivoa3 zkEDw_n+oVx16pIB4xW~dwgFITff2dw6)@Z|6w6sVML1p;_7nh424pd6)y&FRL(HV3$R&Z^hfP+{YE&z$wlW5_yB$yb}sfP;BGNA zTVarFY*PDGlQV5F#T_7Te6Sca2M%JrfV`QKPIm^sxo2>L-i+err>efBd!l=@ZrSZWp0Kgs^ zXTQ(LTg0kfpue)t9B@Y1pSPzz*Ka5#M!eq<&NP3`2Y{CMHPxRE_|*iT$tvNNy%Hvf zK`04}g0MQ8hv0!Dnhb!<+HR8*Mh{HL?daC_po@4hb}3{!I9o|2F%0b!IEXe2(GCDT zgua_FXr3E;OpiL>WC6JoiPy{4{&rTbtLHmG;-yGTooEF-wTl1$lIf4hT`R=Y+36EU z^Ne6ud0H<#4RJ3Ikvv`_a&BcY4XEUo|2?0tJo+%X>xEymLpIqS)pRxas-eSpaDTy zo$7VSKSypN^LkmuIA4JP+2hX?R=8thdmlMhB8kSg7VQKqIFHZKwHXXU8FX6PRIn|Z zziE812*COdBwv?CK==#J(4}rTRE6R7C{1n&1uzD$RpDpevv7>p%HL888u~)4i{Y4n zZ>NBUfvx!6_vUC~ylg5sxl$VdPjd#Ki5B2MUGrY#q$#_eguxdGQNX$s**|GNa*XOz z3lNou$OhSVcyg>k>;w4Vc@RD{YyBo*7M5Du)}zY2+%?Us{xhzC@eJ<7JJ`eK;~Xwh zcj%5q5^>j?aF8WqqyXp6rhRY7+nere+$Ax^aWZk?8yP$u=EWyguWP*-()rsDnT$tp zm!FOzCVxv?j3E2nZUnB~o$=>13dqy1*{Yy|C=JDwzq-+=TGe$j*0TEEPy~07F3sTC=IM0v?AQC=K8$jO zR8(J8+*47kHi;V0AySvvr1$j2w_qGQm5UxVi`wc;)GrJLQ!oX45&j3xlV%AnmsgDd O0000).len(), 1) +--- issue-5831-par-constructor-args --- +// Make sure that all arguments are also respected in the constructor. +A +#par( + leading: 2pt, + spacing: 20pt, + justify: true, + linebreaks: "simple", + first-line-indent: (amount: 1em, all: true), + hanging-indent: 5pt, +)[ + The par function has a constructor and justification. +] + --- show-par-set-block-hint --- // Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore // Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore From 81021fa1a277b04e8726d82fdf8f5fe11aeacab6 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 16:39:14 +0100 Subject: [PATCH 073/101] Bump `typst-assets` (#5845) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 140dccf74..bf69c12af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2753,7 +2753,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577" +source = "git+https://github.com/typst/typst-assets?rev=8536748#8536748e4350198f34e519adff8593f258259cca" [[package]] name = "typst-cli" diff --git a/Cargo.toml b/Cargo.toml index 469439d38..ea9f8510e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.12.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } typst-timing = { path = "crates/typst-timing", version = "0.12.0" } typst-utils = { path = "crates/typst-utils", version = "0.12.0" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8536748" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } arrayvec = "0.7.4" az = "1.2" From a0cd89b478437e53ece754a901ccfc035b4f2acf Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 11 Feb 2025 11:30:30 +0100 Subject: [PATCH 074/101] Fix autocomplete and jumps in math (#5849) --- crates/typst-ide/src/complete.rs | 17 +++++++++++++++-- crates/typst-ide/src/jump.rs | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 7df788dc3..564b97bd7 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { } // Behind existing atom or identifier: "$a|$" or "$abc|$". - if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { + if matches!( + ctx.leaf.kind(), + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent + ) { ctx.from = ctx.leaf.offset(); math_completions(ctx); return true; @@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { // Behind an expression plus dot: "emoji.|". if_chain! { if ctx.leaf.kind() == SyntaxKind::Dot - || (ctx.leaf.kind() == SyntaxKind::Text + || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText) && ctx.leaf.text() == "."); if ctx.leaf.range().end == ctx.cursor; if let Some(prev) = ctx.leaf.prev_sibling(); @@ -1768,4 +1771,14 @@ mod tests { test("#show outline.entry: it => it.\n#outline()\n= Hi", 30) .must_include(["indented", "body", "page"]); } + + #[test] + fn test_autocomplete_symbol_variants() { + test("#sym.arrow.", -1) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); + test("$ arrow. $", -3) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); + } } diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index ed74df226..428335426 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -73,7 +73,10 @@ pub fn jump_from_click( let Some(id) = span.id() else { continue }; let source = world.source(id).ok()?; let node = source.find(span)?; - let pos = if node.kind() == SyntaxKind::Text { + let pos = if matches!( + node.kind(), + SyntaxKind::Text | SyntaxKind::MathText + ) { let range = node.range(); let mut offset = range.start + usize::from(span_offset); if (click.x - pos.x) > width / 2.0 { @@ -115,7 +118,7 @@ pub fn jump_from_cursor( cursor: usize, ) -> Vec { fn is_text(node: &LinkedNode) -> bool { - node.get().kind() == SyntaxKind::Text + matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText) } let root = LinkedNode::new(source.root()); @@ -261,6 +264,11 @@ mod tests { test_click(s, point(21.0, 12.0), cursor(56)); } + #[test] + fn test_jump_from_click_math() { + test_click("$a + b$", point(28.0, 14.0), cursor(5)); + } + #[test] fn test_jump_from_cursor() { let s = "*Hello* #box[ABC] World"; @@ -268,6 +276,11 @@ mod tests { test_cursor(s, 14, pos(1, 37.55, 16.58)); } + #[test] + fn test_jump_from_cursor_math() { + test_cursor("$a + b$", -3, pos(1, 27.51, 16.83)); + } + #[test] fn test_backlink() { let s = "#footnote[Hi]"; From 83ad407d3ccff4a8de1e7ffe198bfed874f5c0c7 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:35:03 -0500 Subject: [PATCH 075/101] Update documentation for `float.{to-bits, from-bits}` (#5836) --- crates/typst-library/src/foundations/float.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index fcc46b034..21d0a8d81 100644 --- a/crates/typst-library/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -110,7 +110,7 @@ impl f64 { f64::signum(self) } - /// Converts bytes to a float. + /// Interprets bytes as a float. /// /// ```example /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ @@ -120,8 +120,10 @@ impl f64 { pub fn from_bytes( /// The bytes that should be converted to a float. /// - /// Must be of length exactly 8 so that the result fits into a 64-bit - /// float. + /// Must have a length of either 4 or 8. The bytes are then + /// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s + /// binary32 (single-precision) or binary64 (double-precision) format + /// depending on the length of the bytes. bytes: Bytes, /// The endianness of the conversion. #[named] @@ -158,6 +160,13 @@ impl f64 { #[named] #[default(Endianness::Little)] endian: Endianness, + /// The size of the resulting bytes. + /// + /// This must be either 4 or 8. The call will return the + /// representation of this float in either + /// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32 + /// (single-precision) or binary64 (double-precision) format + /// depending on the provided size. #[named] #[default(8)] size: u32, From 02cd43e27f2aafd7c332d7672a837e1b11cce120 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:38:40 -0500 Subject: [PATCH 076/101] `Gradient::repeat`: Fix floating-point error in stop calculation (#5837) --- crates/typst-library/src/visualize/gradient.rs | 7 +++---- tests/suite/visualize/gradient.typ | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 431f07dd4..d6530dd09 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -582,12 +582,11 @@ impl Gradient { let mut stops = stops .iter() .map(move |&(color, offset)| { - let t = i as f64 / n as f64; let r = offset.get(); if i % 2 == 1 && mirror { - (color, Ratio::new(t + (1.0 - r) / n as f64)) + (color, Ratio::new((i as f64 + 1.0 - r) / n as f64)) } else { - (color, Ratio::new(t + r / n as f64)) + (color, Ratio::new((i as f64 + r) / n as f64)) } }) .collect::>(); @@ -1230,7 +1229,7 @@ fn process_stops(stops: &[Spanned]) -> SourceResult Date: Wed, 12 Feb 2025 16:50:48 +0100 Subject: [PATCH 077/101] Lazy parsing of the package index (#5851) --- Cargo.lock | 2 + crates/typst-kit/Cargo.toml | 2 + crates/typst-kit/src/package.rs | 75 ++++++++++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf69c12af..66a1e3a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2901,6 +2901,8 @@ dependencies = [ "native-tls", "once_cell", "openssl", + "serde", + "serde_json", "tar", "typst-assets", "typst-library", diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 266eba0b4..52aa407c3 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } once_cell = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } tar = { workspace = true, optional = true } ureq = { workspace = true, optional = true } diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index e7eb71ee4..172d8740a 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -5,10 +5,9 @@ use std::path::{Path, PathBuf}; use ecow::eco_format; use once_cell::sync::OnceCell; +use serde::Deserialize; use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; -use typst_syntax::package::{ - PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec, -}; +use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec}; use crate::download::{Downloader, Progress}; @@ -32,7 +31,7 @@ pub struct PackageStorage { /// The downloader used for fetching the index and packages. downloader: Downloader, /// The cached index of the default namespace. - index: OnceCell>, + index: OnceCell>, } impl PackageStorage { @@ -42,6 +41,18 @@ impl PackageStorage { package_cache_path: Option, package_path: Option, downloader: Downloader, + ) -> Self { + Self::with_index(package_cache_path, package_path, downloader, OnceCell::new()) + } + + /// Creates a new package storage with a pre-defined index. + /// + /// Useful for testing. + fn with_index( + package_cache_path: Option, + package_path: Option, + downloader: Downloader, + index: OnceCell>, ) -> Self { Self { package_cache_path: package_cache_path.or_else(|| { @@ -51,7 +62,7 @@ impl PackageStorage { dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR)) }), downloader, - index: OnceCell::new(), + index, } } @@ -109,6 +120,7 @@ impl PackageStorage { // version. self.download_index()? .iter() + .filter_map(|value| MinimalPackageInfo::deserialize(value).ok()) .filter(|package| package.name == spec.name) .map(|package| package.version) .max() @@ -131,7 +143,7 @@ impl PackageStorage { } /// Download the package index. The result of this is cached for efficiency. - pub fn download_index(&self) -> StrResult<&[PackageInfo]> { + pub fn download_index(&self) -> StrResult<&[serde_json::Value]> { self.index .get_or_try_init(|| { let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); @@ -186,3 +198,54 @@ impl PackageStorage { }) } } + +/// Minimal information required about a package to determine its latest +/// version. +#[derive(Deserialize)] +struct MinimalPackageInfo { + name: String, + version: PackageVersion, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lazy_deser_index() { + let storage = PackageStorage::with_index( + None, + None, + Downloader::new("typst/test"), + OnceCell::with_value(vec![ + serde_json::json!({ + "name": "charged-ieee", + "version": "0.1.0", + "entrypoint": "lib.typ", + }), + serde_json::json!({ + "name": "unequivocal-ams", + // This version number is currently not valid, so this package + // can't be parsed. + "version": "0.2.0-dev", + "entrypoint": "lib.typ", + }), + ]), + ); + + let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "charged-ieee".into(), + }); + assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 })); + + let ams_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "unequivocal-ams".into(), + }); + assert_eq!( + ams_version, + Err("failed to find package @preview/unequivocal-ams".into()) + ) + } +} From 5fc679f3e7501ee5831f1b4b7789350f43b4c221 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 16 Feb 2025 14:18:39 +0100 Subject: [PATCH 078/101] Remove Linux Libertine warning (#5876) --- crates/typst-library/src/text/mod.rs | 19 +------------------ tests/suite/text/font.typ | 5 ----- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 30c2ea1d1..3aac15ba5 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -1380,24 +1380,7 @@ pub fn is_default_ignorable(c: char) -> bool { fn check_font_list(engine: &mut Engine, list: &Spanned) { let book = engine.world.book(); for family in &list.v { - let found = book.contains_family(family.as_str()); - if family.as_str() == "linux libertine" { - let mut warning = warning!( - list.span, - "Typst's default font has changed from Linux Libertine to its successor Libertinus Serif"; - hint: "please set the font to `\"Libertinus Serif\"` instead" - ); - - if found { - warning.hint( - "Linux Libertine is available on your system - \ - you can ignore this warning if you are sure you want to use it", - ); - warning.hint("this warning will be removed in Typst 0.13"); - } - - engine.sink.warn(warning); - } else if !found { + if !book.contains_family(family.as_str()) { engine.sink.warn(warning!( list.span, "unknown font family: {}", diff --git a/tests/suite/text/font.typ b/tests/suite/text/font.typ index 9e5c0150d..60a1cd94d 100644 --- a/tests/suite/text/font.typ +++ b/tests/suite/text/font.typ @@ -77,11 +77,6 @@ I #let var = text(font: ("list-of", "nonexistent-fonts"))[don't] #var ---- text-font-linux-libertine --- -// Warning: 17-34 Typst's default font has changed from Linux Libertine to its successor Libertinus Serif -// Hint: 17-34 please set the font to `"Libertinus Serif"` instead -#set text(font: "Linux Libertine") - --- issue-5499-text-fill-in-clip-block --- #let t = tiling( From 25c86accbb4adc0e7542d2c5957dff6e939cbf48 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 17 Feb 2025 11:56:00 +0100 Subject: [PATCH 079/101] More robust SVG auto-detection (#5878) --- Cargo.lock | 1 + Cargo.toml | 1 + crates/typst-library/Cargo.toml | 1 + .../typst-library/src/visualize/image/mod.rs | 18 ++++++++++++++++-- docs/changelog/0.13.0.md | 3 +-- tests/ref/image-svg-auto-detection.png | Bin 0 -> 129 bytes tests/suite/visualize/image.typ | 15 +++++++++++++-- 7 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 tests/ref/image-svg-auto-detection.png diff --git a/Cargo.lock b/Cargo.lock index 66a1e3a12..249ee3bc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2966,6 +2966,7 @@ dependencies = [ "kamadak-exif", "kurbo", "lipsum", + "memchr", "palette", "phf", "png", diff --git a/Cargo.toml b/Cargo.toml index ea9f8510e..6fb64d3ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ kamadak-exif = "0.6" kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" +memchr = "2" miniz_oxide = "0.8" native-tls = "0.2" notify = "8" diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index cc5e26712..fb45ec862 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -38,6 +38,7 @@ indexmap = { workspace = true } kamadak-exif = { workspace = true } kurbo = { workspace = true } lipsum = { workspace = true } +memchr = { workspace = true } palette = { workspace = true } phf = { workspace = true } png = { workspace = true } diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 97189e22d..258eb96f3 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -398,8 +398,7 @@ impl ImageFormat { return Some(Self::Raster(RasterFormat::Exchange(format))); } - // SVG or compressed SVG. - if data.starts_with(b" bool { + // Check for the gzip magic bytes. This check is perhaps a bit too + // permissive as other formats than SVGZ could use gzip. + if data.starts_with(&[0x1f, 0x8b]) { + return true; + } + + // If the first 2048 bytes contain the SVG namespace declaration, we assume + // that it's an SVG. Note that, if the SVG does not contain a namespace + // declaration, usvg will reject it. + let head = &data[..data.len().min(2048)]; + memchr::memmem::find(head, b"http://www.w3.org/2000/svg").is_some() +} + /// A vector graphics format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum VectorFormat { diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 2caace723..e5315e5b6 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -99,8 +99,7 @@ description: Changes slated to appear in Typst 0.13.0 - Fixed interaction of clipping and outset on [`box`] and [`block`] - Fixed panic with [`path`] of infinite length - Fixed non-solid (e.g. tiling) text fills in clipped blocks -- Auto-detection of image formats from a raw buffer now has basic support for - SVGs +- Auto-detection of image formats from a raw buffer now has support for SVGs ## Scripting - Functions that accept [file paths]($syntax/#paths) now also accept raw diff --git a/tests/ref/image-svg-auto-detection.png b/tests/ref/image-svg-auto-detection.png new file mode 100644 index 0000000000000000000000000000000000000000..0240f8f5cf74eaa704282288c12784b981ebcf37 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^6+j%%#0(_k7Qa3Zq<8{+LR|m<|KFdT-QeKxpMgPb zzGnhZ+`!YtF{I*FvIOhm1P;b+p%Mv9lT5zX^L$7Mse-`@58)NOZm&-S8niGl$Zgtk U`tV(DZlGQUPgg&ebxsLQ04L`tRR910 literal 0 HcmV?d00001 diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index e37932f28..7ce0c8c0a 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -65,6 +65,17 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B caption: [Bilingual text] ) +--- image-svg-auto-detection --- +#image(bytes( + ``` + + + + + + ```.text +)) + --- image-pixmap-rgb8 --- #image( bytes(( @@ -152,8 +163,8 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image("path/does/not/exist") --- image-bad-format --- -// Error: 2-22 unknown image format -#image("./image.typ") +// Error: 2-37 unknown image format +#image("/assets/plugins/hello.wasm") --- image-bad-svg --- // Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4) From 74e4f78687d7acb5db3d531959c956717cce837a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=A1=A5=E1=A0=A0=E1=A1=B3=E1=A1=A4=E1=A1=B3=E1=A0=B6?= =?UTF-8?q?=E1=A0=A0=20=E1=A1=A5=E1=A0=A0=E1=A0=AF=E1=A0=A0=C2=B7=E1=A0=A8?= =?UTF-8?q?=E1=A1=9D=E1=A1=B4=E1=A0=A3=20=E7=8C=AB?= Date: Tue, 18 Feb 2025 18:16:19 +0800 Subject: [PATCH 080/101] HTML export: Use `` for inline `RawElem` (#5884) --- crates/typst-library/src/text/raw.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index b330c01ef..1ce8bfc61 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -446,10 +446,14 @@ impl Show for Packed { let mut realized = Content::sequence(seq); if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::pre) - .with_body(Some(realized)) - .pack() - .spanned(self.span())); + return Ok(HtmlElem::new(if self.block(styles) { + tag::pre + } else { + tag::code + }) + .with_body(Some(realized)) + .pack() + .spanned(self.span())); } if self.block(styles) { From 3de3813ca06c332cd1eae14c64913725a9333aff Mon Sep 17 00:00:00 2001 From: Matthew Toohey Date: Tue, 18 Feb 2025 13:04:40 -0500 Subject: [PATCH 081/101] --make-deps fixes (#5873) --- crates/typst-cli/src/compile.rs | 83 +++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 515a777a2..2b6a7d820 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -6,8 +6,9 @@ use std::path::{Path, PathBuf}; use chrono::{DateTime, Datelike, Timelike, Utc}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term; -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use parking_lot::RwLock; +use pathdiff::diff_paths; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use typst::diag::{ bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, @@ -188,7 +189,7 @@ pub fn compile_once( match output { // Export the PDF / PNG. - Ok(()) => { + Ok(outputs) => { let duration = start.elapsed(); if config.watching { @@ -202,7 +203,7 @@ pub fn compile_once( print_diagnostics(world, &[], &warnings, config.diagnostic_format) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; - write_make_deps(world, config)?; + write_make_deps(world, config, outputs)?; open_output(config)?; } @@ -226,12 +227,15 @@ pub fn compile_once( fn compile_and_export( world: &mut SystemWorld, config: &mut CompileConfig, -) -> Warned> { +) -> Warned>> { match config.output_format { OutputFormat::Html => { let Warned { output, warnings } = typst::compile::(world); let result = output.and_then(|document| export_html(&document, config)); - Warned { output: result, warnings } + Warned { + output: result.map(|()| vec![config.output.clone()]), + warnings, + } } _ => { let Warned { output, warnings } = typst::compile::(world); @@ -257,9 +261,14 @@ fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult< } /// Export to a paged target format. -fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> { +fn export_paged( + document: &PagedDocument, + config: &CompileConfig, +) -> SourceResult> { match config.output_format { - OutputFormat::Pdf => export_pdf(document, config), + OutputFormat::Pdf => { + export_pdf(document, config).map(|()| vec![config.output.clone()]) + } OutputFormat::Png => { export_image(document, config, ImageExportFormat::Png).at(Span::detached()) } @@ -327,7 +336,7 @@ fn export_image( document: &PagedDocument, config: &CompileConfig, fmt: ImageExportFormat, -) -> StrResult<()> { +) -> StrResult> { // Determine whether we have indexable templates in output let can_handle_multiple = match config.output { Output::Stdout => false, @@ -383,7 +392,7 @@ fn export_image( && config.export_cache.is_cached(*i, &page.frame) && path.exists() { - return Ok(()); + return Ok(Output::Path(path.to_path_buf())); } Output::Path(path.to_owned()) @@ -392,11 +401,9 @@ fn export_image( }; export_image_page(config, page, &output, fmt)?; - Ok(()) + Ok(output) }) - .collect::, EcoString>>()?; - - Ok(()) + .collect::>>() } mod output_template { @@ -501,14 +508,25 @@ impl ExportCache { /// Writes a Makefile rule describing the relationship between the output and /// its dependencies to the path specified by the --make-deps argument, if it /// was provided. -fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> { +fn write_make_deps( + world: &mut SystemWorld, + config: &CompileConfig, + outputs: Vec, +) -> StrResult<()> { let Some(ref make_deps_path) = config.make_deps else { return Ok(()) }; - let Output::Path(output_path) = &config.output else { - bail!("failed to create make dependencies file because output was stdout") - }; - let Some(output_path) = output_path.as_os_str().to_str() else { + let Ok(output_paths) = outputs + .into_iter() + .filter_map(|o| match o { + Output::Path(path) => Some(path.into_os_string().into_string()), + Output::Stdout => None, + }) + .collect::, _>>() + else { bail!("failed to create make dependencies file because output path was not valid unicode") }; + if output_paths.is_empty() { + bail!("failed to create make dependencies file because output was stdout") + } // Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't // perfect as some special characters can't be escaped. @@ -522,6 +540,10 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult res.push('$'); slashes = 0; } + ':' => { + res.push('\\'); + slashes = 0; + } ' ' | '\t' => { // `munge`'s source contains a comment here that says: "A // space or tab preceded by 2N+1 backslashes represents N @@ -544,18 +566,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult fn write( make_deps_path: &Path, - output_path: &str, + output_paths: Vec, root: PathBuf, dependencies: impl Iterator, ) -> io::Result<()> { let mut file = File::create(make_deps_path)?; + let current_dir = std::env::current_dir()?; + let relative_root = diff_paths(&root, ¤t_dir).unwrap_or(root.clone()); - file.write_all(munge(output_path).as_bytes())?; + for (i, output_path) in output_paths.into_iter().enumerate() { + if i != 0 { + file.write_all(b" ")?; + } + file.write_all(munge(&output_path).as_bytes())?; + } file.write_all(b":")?; for dependency in dependencies { - let Some(dependency) = - dependency.strip_prefix(&root).unwrap_or(&dependency).to_str() - else { + let relative_dependency = match dependency.strip_prefix(&root) { + Ok(root_relative_dependency) => { + relative_root.join(root_relative_dependency) + } + Err(_) => dependency, + }; + let Some(relative_dependency) = relative_dependency.to_str() else { // Silently skip paths that aren't valid unicode so we still // produce a rule that will work for the other paths that can be // processed. @@ -563,14 +596,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult }; file.write_all(b" ")?; - file.write_all(munge(dependency).as_bytes())?; + file.write_all(munge(relative_dependency).as_bytes())?; } file.write_all(b"\n")?; Ok(()) } - write(make_deps_path, output_path, world.root().to_owned(), world.dependencies()) + write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies()) .map_err(|err| { eco_format!("failed to create make dependencies file due to IO error ({err})") }) From a543ee9445c0541b34a2bb5ea3b48ca596b71152 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 19 Feb 2025 10:59:27 +0100 Subject: [PATCH 082/101] Update changelog (#5894) --- docs/changelog/0.13.0.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index e5315e5b6..4212c8251 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -99,6 +99,8 @@ description: Changes slated to appear in Typst 0.13.0 - Fixed interaction of clipping and outset on [`box`] and [`block`] - Fixed panic with [`path`] of infinite length - Fixed non-solid (e.g. tiling) text fills in clipped blocks +- Fixed a crash for images with a DPI value of zero +- Fixed floating-point error in [`gradient.repeat`] - Auto-detection of image formats from a raw buffer now has support for SVGs ## Scripting @@ -186,12 +188,12 @@ description: Changes slated to appear in Typst 0.13.0 - [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text anymore - Fixed wrong language codes being used for Greek and Ukrainian -- Fixed default quotes for Croatian +- Fixed default quotes for Croatian and Bulgarian - Fixed crash in RTL text handling - Added support for [`raw`] syntax highlighting for a few new languages: CFML, NSIS, and WGSL - New font metadata exception for New Computer Modern Sans Math -- Updated bundled New Computer Modern fonts to version 7.0 +- Updated bundled New Computer Modern fonts to version 7.0.1 ## Layout - Fixed various bugs with footnotes @@ -270,6 +272,9 @@ feature flag. - Added a live reloading HTTP server to `typst watch` when targeting HTML - Fixed self-update not being aware about certain target architectures - Fixed crash when piping `typst fonts` output to another command +- Fixed handling of relative paths in `--make-deps` output +- Fixed handling of multipage SVG and PNG export in `--make-deps` output +- Colons in filenames are now correctly escaped in `--make-deps` output ## Symbols - New @@ -312,6 +317,9 @@ feature flag. functions directly accepting both paths and bytes - The `sect` and its variants in favor of `inter`, and `integral.sect` in favor of `integral.inter` +- The compatibility behavior of type/str comparisons (e.g. `{int == "integer"}`) + which was temporarily introduced in Typst 0.8 now emits warnings. It will be + removed in Typst 0.14. ## Removals - Removed `style` function and `styles` argument of [`measure`], use a [context] @@ -323,9 +331,6 @@ feature flag. - Removed compatibility behavior where [`counter.display`] worked without [context] **(Breaking change)** - Removed compatibility behavior of [`locate`] **(Breaking change)** -- Removed compatibility behavior of type/str comparisons - (e.g. `{int == "integer"}`) which was temporarily introduced in Typst 0.8 - **(Breaking change)** ## Development - The `typst::compile` function is now generic and can return either a From d199546f9fe92b2d380dc337298fdca3e6fca8c8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 19 Feb 2025 11:25:09 +0100 Subject: [PATCH 083/101] Bump version on main The tagged commit itself is on the 0.13 branch. --- Cargo.lock | 46 +++++++++++++++++++-------------------- Cargo.toml | 38 ++++++++++++++++---------------- docs/changelog/0.13.0.md | 9 +++++--- docs/changelog/welcome.md | 2 +- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 249ee3bc5..1851134a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2752,12 +2752,12 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8536748#8536748e4350198f34e519adff8593f258259cca" +version = "0.13.0" +source = "git+https://github.com/typst/typst-assets?rev=fa0f8a4#fa0f8a438cc4bc2113cc0aa3304cd68cdc2bc020" [[package]] name = "typst-cli" -version = "0.12.0" +version = "0.13.0" dependencies = [ "chrono", "clap", @@ -2802,12 +2802,12 @@ dependencies = [ [[package]] name = "typst-dev-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c" +version = "0.13.0" +source = "git+https://github.com/typst/typst-dev-assets?rev=61aebe9#61aebe9575a5abff889f76d73c7b01dc8e17e340" [[package]] name = "typst-docs" -version = "0.12.0" +version = "0.13.0" dependencies = [ "clap", "ecow", @@ -2830,7 +2830,7 @@ dependencies = [ [[package]] name = "typst-eval" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2848,7 +2848,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "libfuzzer-sys", @@ -2860,7 +2860,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2874,7 +2874,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2891,7 +2891,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.12.0" +version = "0.13.0" dependencies = [ "dirs", "ecow", @@ -2914,7 +2914,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.12.0" +version = "0.13.0" dependencies = [ "az", "bumpalo", @@ -2944,7 +2944,7 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.12.0" +version = "0.13.0" dependencies = [ "az", "bitflags 2.8.0", @@ -3004,7 +3004,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.12.0" +version = "0.13.0" dependencies = [ "heck", "proc-macro2", @@ -3014,7 +3014,7 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.12.0" +version = "0.13.0" dependencies = [ "arrayvec", "base64", @@ -3040,7 +3040,7 @@ dependencies = [ [[package]] name = "typst-realize" -version = "0.12.0" +version = "0.13.0" dependencies = [ "arrayvec", "bumpalo", @@ -3056,7 +3056,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.12.0" +version = "0.13.0" dependencies = [ "bytemuck", "comemo", @@ -3072,7 +3072,7 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.12.0" +version = "0.13.0" dependencies = [ "base64", "comemo", @@ -3090,7 +3090,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.12.0" +version = "0.13.0" dependencies = [ "ecow", "serde", @@ -3106,7 +3106,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.12.0" +version = "0.13.0" dependencies = [ "clap", "comemo", @@ -3131,7 +3131,7 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.12.0" +version = "0.13.0" dependencies = [ "parking_lot", "serde", @@ -3141,7 +3141,7 @@ dependencies = [ [[package]] name = "typst-utils" -version = "0.12.0" +version = "0.13.0" dependencies = [ "once_cell", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index 6fb64d3ab..198aff3c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"] resolver = "2" [workspace.package] -version = "0.12.0" +version = "0.13.0" rust-version = "1.80" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" @@ -16,24 +16,24 @@ keywords = ["typst"] readme = "README.md" [workspace.dependencies] -typst = { path = "crates/typst", version = "0.12.0" } -typst-cli = { path = "crates/typst-cli", version = "0.12.0" } -typst-eval = { path = "crates/typst-eval", version = "0.12.0" } -typst-html = { path = "crates/typst-html", version = "0.12.0" } -typst-ide = { path = "crates/typst-ide", version = "0.12.0" } -typst-kit = { path = "crates/typst-kit", version = "0.12.0" } -typst-layout = { path = "crates/typst-layout", version = "0.12.0" } -typst-library = { path = "crates/typst-library", version = "0.12.0" } -typst-macros = { path = "crates/typst-macros", version = "0.12.0" } -typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" } -typst-realize = { path = "crates/typst-realize", version = "0.12.0" } -typst-render = { path = "crates/typst-render", version = "0.12.0" } -typst-svg = { path = "crates/typst-svg", version = "0.12.0" } -typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } -typst-timing = { path = "crates/typst-timing", version = "0.12.0" } -typst-utils = { path = "crates/typst-utils", version = "0.12.0" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8536748" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } +typst = { path = "crates/typst", version = "0.13.0" } +typst-cli = { path = "crates/typst-cli", version = "0.13.0" } +typst-eval = { path = "crates/typst-eval", version = "0.13.0" } +typst-html = { path = "crates/typst-html", version = "0.13.0" } +typst-ide = { path = "crates/typst-ide", version = "0.13.0" } +typst-kit = { path = "crates/typst-kit", version = "0.13.0" } +typst-layout = { path = "crates/typst-layout", version = "0.13.0" } +typst-library = { path = "crates/typst-library", version = "0.13.0" } +typst-macros = { path = "crates/typst-macros", version = "0.13.0" } +typst-pdf = { path = "crates/typst-pdf", version = "0.13.0" } +typst-realize = { path = "crates/typst-realize", version = "0.13.0" } +typst-render = { path = "crates/typst-render", version = "0.13.0" } +typst-svg = { path = "crates/typst-svg", version = "0.13.0" } +typst-syntax = { path = "crates/typst-syntax", version = "0.13.0" } +typst-timing = { path = "crates/typst-timing", version = "0.13.0" } +typst-utils = { path = "crates/typst-utils", version = "0.13.0" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fa0f8a4" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "61aebe9" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 4212c8251..6c2fe4275 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -1,9 +1,9 @@ --- -title: Unreleased changes planned for 0.13.0 -description: Changes slated to appear in Typst 0.13.0 +title: 0.13.0 +description: Changes in Typst 0.13.0 --- -# Unreleased +# Version 0.13.0 (February 19, 2025) ## Highlights - There is now a distinction between [proper paragraphs]($par) and just @@ -339,3 +339,6 @@ feature flag. feature is enabled - Increased minimum supported Rust version to 1.80 - Fixed linux/arm64 Docker image + +## Contributors + diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index bb245eb01..8fb85f870 100644 --- a/docs/changelog/welcome.md +++ b/docs/changelog/welcome.md @@ -10,7 +10,7 @@ forward. This section documents all changes to Typst since its initial public release. ## Versions -- [Unreleased changes planned for Typst 0.13.0]($changelog/0.13.0) +- [Typst 0.13.0]($changelog/0.13.0) - [Typst 0.12.0]($changelog/0.12.0) - [Typst 0.11.1]($changelog/0.11.1) - [Typst 0.11.0]($changelog/0.11.0) From 240f238eee4d6dfce7e3c4cabb9315ad052ca230 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Sun, 23 Feb 2025 08:26:14 -0300 Subject: [PATCH 084/101] Fix HTML export of table with gutter (#5920) --- .../typst-library/src/layout/grid/resolve.rs | 21 +++++++++++---- crates/typst-library/src/model/table.rs | 2 +- tests/ref/html/col-gutter-table.html | 26 ++++++++++++++++++ tests/ref/html/col-row-gutter-table.html | 26 ++++++++++++++++++ tests/ref/html/row-gutter-table.html | 26 ++++++++++++++++++ tests/suite/layout/grid/html.typ | 27 +++++++++++++++++++ 6 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 tests/ref/html/col-gutter-table.html create mode 100644 tests/ref/html/col-row-gutter-table.html create mode 100644 tests/ref/html/row-gutter-table.html diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index f6df57a37..762f94ed0 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -1526,11 +1526,7 @@ impl<'a> CellGrid<'a> { self.entry(x, y).map(|entry| match entry { Entry::Cell(_) => Axes::new(x, y), Entry::Merged { parent } => { - let c = if self.has_gutter { - 1 + self.cols.len() / 2 - } else { - self.cols.len() - }; + let c = self.non_gutter_column_count(); let factor = if self.has_gutter { 2 } else { 1 }; Axes::new(factor * (*parent % c), factor * (*parent / c)) } @@ -1602,6 +1598,21 @@ impl<'a> CellGrid<'a> { cell.rowspan.get() } } + + #[inline] + pub fn non_gutter_column_count(&self) -> usize { + if self.has_gutter { + // Calculation: With gutters, we have + // 'cols = 2 * (non-gutter cols) - 1', since there is a gutter + // column between each regular column. Therefore, + // 'floor(cols / 2)' will be equal to + // 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1', + // so 'non-gutter cols = 1 + floor(cols / 2)'. + 1 + self.cols.len() / 2 + } else { + self.cols.len() + } + } } /// Given a cell's requested x and y, the vector with the resolved cell diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 82c1cc08b..6f4461bd4 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -282,7 +282,7 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { 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 mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect(); let tr = |tag, row: &[Entry]| { let row = row diff --git a/tests/ref/html/col-gutter-table.html b/tests/ref/html/col-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/col-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
    abc
    def
    ghi
    + + diff --git a/tests/ref/html/col-row-gutter-table.html b/tests/ref/html/col-row-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/col-row-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
    abc
    def
    ghi
    + + diff --git a/tests/ref/html/row-gutter-table.html b/tests/ref/html/row-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/row-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
    abc
    def
    ghi
    + + diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ index 2a7dfc2ce..10345cb06 100644 --- a/tests/suite/layout/grid/html.typ +++ b/tests/suite/layout/grid/html.typ @@ -30,3 +30,30 @@ [row], ), ) + +--- col-gutter-table html --- +#table( + columns: 3, + column-gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) + +--- row-gutter-table html --- +#table( + columns: 3, + row-gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) + +--- col-row-gutter-table html --- +#table( + columns: 3, + gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) From 55bc5f4c940c86377f1ffe25b42fdb01a6827358 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 23 Feb 2025 11:28:24 +0000 Subject: [PATCH 085/101] Make math shorthands noncontinuable (#5925) --- crates/typst-syntax/src/parser.rs | 9 +++++---- tests/ref/math-shorthands-noncontinuable.png | Bin 0 -> 475 bytes tests/suite/math/syntax.typ | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 tests/ref/math-shorthands-noncontinuable.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index e187212da..c5d13c8b3 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -271,10 +271,11 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { - continuable = matches!( - math_class(p.current_text()), - None | Some(MathClass::Alphabetic) - ); + continuable = !p.at(SyntaxKind::MathShorthand) + && matches!( + math_class(p.current_text()), + None | Some(MathClass::Alphabetic) + ); if !maybe_delimited(p) { p.eat(); } diff --git a/tests/ref/math-shorthands-noncontinuable.png b/tests/ref/math-shorthands-noncontinuable.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1ad1d14e0ebda63769157fe6a64631dfe58a31 GIT binary patch literal 475 zcmV<10VMv3P)LBEL@~mBuQWyMo>h%CY9761u02MG_@Qx zNm5Z8=fpv+UoiJT-FGuDvw0Smd+zA~qI7VW!yM)?CG6I?KurPr7jBGuE$I%VUVB2D(T@AvTS9$n7S&jjwZk@Yt{W{Hq;&9BN(G8cHT|SwLGrPxe zP=(}Hk1U(3c-niIlHzCF1FwJy%pZIKrjfx&3d5s9fAkE?u2W_@GSiPaKVXVkjKqFc zUs^FK+Sf_)K$--$8)y6^F!)?R0^j&+&lGTQePXOa0oS0ympu;uF}NYrp+Es2nVj_j z3RoOTxR!_(`ju-EcqGtSWd>hWerhD<9tURd%c|j{Rrhx`^UA{)jyrp^<@7qS*~$#= zv5?eSw%SmpD+~Z~9@9`5mS9k9)#A1`h2N?PQ&JcXOI_f_BI-BtEJvo=GJ6=ra{fs) ztDQznf<0WXV2Gc^=wwtNfqmKO0tpPZERew3w}%h~?DLK5`Lf4h4s-Y)z~4s$EOM5) RtCj!&002ovPDHLkV1k2W)8YUC literal 0 HcmV?d00001 diff --git a/tests/suite/math/syntax.typ b/tests/suite/math/syntax.typ index cd1124c37..7091d908c 100644 --- a/tests/suite/math/syntax.typ +++ b/tests/suite/math/syntax.typ @@ -13,6 +13,11 @@ $ underline(f' : NN -> RR) \ 1 - 0 thick &..., ) $ +--- math-shorthands-noncontinuable --- +// Test that shorthands are not continuable. +$ x >=(y) / z \ + x >= (y) / z $ + --- math-common-symbols --- // Test common symbols. $ dot \ dots \ ast \ tilde \ star $ From 56f4fa2b4d4d772c5b19c9842419dcc4e078744b Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:31:28 +0100 Subject: [PATCH 086/101] Documentation improvements (#5888) --- crates/typst-library/src/foundations/symbol.rs | 1 + crates/typst-library/src/visualize/color.rs | 2 +- crates/typst-library/src/visualize/gradient.rs | 11 +++++------ docs/reference/groups.yml | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 8a80506fe..2c391ee4c 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -21,6 +21,7 @@ use crate::foundations::{ /// be accessed using [field access notation]($scripting/#fields): /// /// - General symbols are defined in the [`sym` module]($category/symbols/sym) +/// and are accessible without the `sym.` prefix in math mode. /// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) /// /// Moreover, you can define custom symbols with this type's constructor diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index b14312513..20b0f5719 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -130,7 +130,7 @@ static TO_SRGB: LazyLock = LazyLock::new(|| { /// /// # Predefined color maps /// Typst also includes a number of preset color maps that can be used for -/// [gradients]($gradient.linear). These are simply arrays of colors defined in +/// [gradients]($gradient/#stops). These are simply arrays of colors defined in /// the module `color.map`. /// /// ```example diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index d6530dd09..1a723a9f5 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -70,6 +70,9 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// the offsets when defining a gradient. In this case, Typst will space all /// stops evenly. /// +/// Typst predefines color maps that you can use as stops. See the +/// [`color`]($color/#predefined-color-maps) documentation for more details. +/// /// # Relativeness /// The location of the `{0%}` and `{100%}` stops depends on the dimensions /// of a container. This container can either be the shape that it is being @@ -157,10 +160,6 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// ) /// ``` /// -/// # Presets -/// Typst predefines color maps that you can use with your gradients. See the -/// [`color`]($color/#predefined-color-maps) documentation for more details. -/// /// # Note on file sizes /// /// Gradients can be quite large, especially if they have many stops. This is @@ -288,7 +287,7 @@ impl Gradient { /// )), /// ) /// ``` - #[func] + #[func(title = "Radial Gradient")] fn radial( span: Span, /// The color [stops](#stops) of the gradient. @@ -402,7 +401,7 @@ impl Gradient { /// )), /// ) /// ``` - #[func] + #[func(title = "Conic Gradient")] pub fn conic( span: Span, /// The color [stops](#stops) of the gradient. diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 961d675dc..8fea3a1f2 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -170,8 +170,8 @@ category: symbols path: ["emoji"] details: | - Named emoji. + Named emojis. For example, `#emoji.face` produces the 😀 emoji. If you frequently use certain emojis, you can also import them from the `emoji` module (`[#import - emoji: face]`) to use them without the `#emoji.` prefix. + emoji: face]`) to use them without the `emoji.` prefix. From ebe25432641a729780578a2440eaf9fb07c80e38 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 24 Feb 2025 12:17:31 +0100 Subject: [PATCH 087/101] Fix comparison of `Func` and `NativeFuncData` (#5943) --- crates/typst-library/src/foundations/func.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 3ed1562f6..66c6b70a5 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -437,10 +437,10 @@ impl PartialEq for Func { } } -impl PartialEq<&NativeFuncData> for Func { - fn eq(&self, other: &&NativeFuncData) -> bool { +impl PartialEq<&'static NativeFuncData> for Func { + fn eq(&self, other: &&'static NativeFuncData) -> bool { match &self.repr { - Repr::Native(native) => native.function == other.function, + Repr::Native(native) => *native == Static(*other), _ => false, } } From 69c3f957051358eff961addbcae4ff02448513dc Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 24 Feb 2025 13:28:01 +0100 Subject: [PATCH 088/101] Bump MSRV to 1.83 and Rust in CI to 1.85 (#5946) --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 2 +- Cargo.toml | 2 +- crates/typst-cli/src/compile.rs | 2 +- crates/typst-ide/src/complete.rs | 2 +- crates/typst-layout/src/grid/layouter.rs | 8 ++++---- crates/typst-layout/src/grid/lines.rs | 2 +- crates/typst-layout/src/grid/rowspans.rs | 2 +- crates/typst-layout/src/inline/line.rs | 4 ++-- crates/typst-layout/src/inline/linebreak.rs | 4 ++-- crates/typst-layout/src/inline/shaping.rs | 6 +++--- crates/typst-library/src/foundations/symbol.rs | 2 +- crates/typst-library/src/layout/grid/resolve.rs | 2 +- crates/typst-library/src/text/font/book.rs | 2 +- crates/typst-library/src/text/shift.rs | 2 +- crates/typst-pdf/src/outline.rs | 4 ++-- crates/typst-syntax/src/node.rs | 2 +- crates/typst-syntax/src/package.rs | 4 ++-- crates/typst-utils/src/scalar.rs | 13 +------------ flake.lock | 6 +++--- flake.nix | 2 +- tests/src/collect.rs | 4 ++-- tests/src/run.rs | 2 +- 23 files changed, 37 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01b3e8c3a..9f0ada9f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --no-run - run: cargo test --workspace --no-fail-fast @@ -59,7 +59,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 @@ -73,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.80.0 + - uses: dtolnay/rust-toolchain@1.83.0 - uses: Swatinem/rust-cache@v2 - run: cargo check --workspace diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5be6bfa2c..0d235aec5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 with: target: ${{ matrix.target }} diff --git a/Cargo.toml b/Cargo.toml index 198aff3c6..36195230e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] version = "0.13.0" -rust-version = "1.80" # also change in ci.yml +rust-version = "1.83" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" homepage = "https://typst.app" diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 2b6a7d820..ae71e298c 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -350,7 +350,7 @@ fn export_image( .iter() .enumerate() .filter(|(i, _)| { - config.pages.as_ref().map_or(true, |exported_page_ranges| { + config.pages.as_ref().is_none_or(|exported_page_ranges| { exported_page_ranges.includes_page_index(*i) }) }) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 564b97bd7..e3dcc442e 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1455,7 +1455,7 @@ impl<'a> CompletionContext<'a> { let mut defined = BTreeMap::>::new(); named_items(self.world, self.leaf.clone(), |item| { let name = item.name(); - if !name.is_empty() && item.value().as_ref().map_or(true, filter) { + if !name.is_empty() && item.value().as_ref().is_none_or(filter) { defined.insert(name.clone(), item.value()); } diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 1f9cf6796..af47ff72f 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -1377,7 +1377,7 @@ impl<'a> GridLayouter<'a> { .footer .as_ref() .and_then(Repeatable::as_repeated) - .map_or(true, |footer| footer.start != header.end) + .is_none_or(|footer| footer.start != header.end) && self.lrows.last().is_some_and(|row| row.index() < header.end) && !in_last_with_offset( self.regions, @@ -1446,7 +1446,7 @@ impl<'a> GridLayouter<'a> { .iter_mut() .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) .filter(|rowspan| { - rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) + rowspan.max_resolved_row.is_none_or(|max_row| y > max_row) }) { // If the first region wasn't defined yet, it will have the @@ -1494,7 +1494,7 @@ impl<'a> GridLayouter<'a> { // laid out at the first frame of the row). // Any rowspans ending before this row are laid out even // on this row's first frame. - if laid_out_footer_start.map_or(true, |footer_start| { + if laid_out_footer_start.is_none_or(|footer_start| { // If this is a footer row, then only lay out this rowspan // if the rowspan is contained within the footer. y < footer_start || rowspan.y >= footer_start @@ -1580,5 +1580,5 @@ pub(super) fn points( /// our case, headers). pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { regions.backlog.is_empty() - && regions.last.map_or(true, |height| regions.size.y + offset == height) + && regions.last.is_none_or(|height| regions.size.y + offset == height) } diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 1227953d1..7549673f1 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -463,7 +463,7 @@ pub fn hline_stroke_at_column( // region, we have the last index, and (as a failsafe) we don't have the // last row of cells above us. let use_bottom_border_stroke = !in_last_region - && local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) + && local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len()) && y == grid.rows.len(); let bottom_y = if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 5039695d8..21992ed02 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -588,7 +588,7 @@ impl GridLayouter<'_> { measurement_data: &CellMeasurementData<'_>, ) -> bool { if sizes.len() <= 1 - && sizes.first().map_or(true, |&first_frame_size| { + && sizes.first().is_none_or(|&first_frame_size| { first_frame_size <= measurement_data.height_in_this_region }) { diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index bd08f30ef..659d33f4a 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -154,7 +154,7 @@ pub fn line<'a>( let mut items = collect_items(engine, p, range, trim); // Add a hyphen at the line start, if a previous dash should be repeated. - if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { + if pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) { if let Some(shaped) = items.first_text_mut() { shaped.prepend_hyphen(engine, p.config.fallback); } @@ -406,7 +406,7 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { // // See § 4.1.1.1.2.e on the "Ortografía de la lengua española" // https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea - Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()), + Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()), _ => false, } diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index a9f21188b..31512604f 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -290,7 +290,7 @@ fn linebreak_optimized_bounded<'a>( } // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { + if best.as_ref().is_none_or(|best| best.total >= total) { best = Some(Entry { pred: pred_index, total, line: attempt, end }); } } @@ -423,7 +423,7 @@ fn linebreak_optimized_approximate( let total = pred.total + line_cost; // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { + if best.as_ref().is_none_or(|best| best.total >= total) { best = Some(Entry { pred: pred_index, total, diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index b688981ae..159619eb3 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -465,7 +465,7 @@ impl<'a> ShapedText<'a> { None }; let mut chain = families(self.styles) - .filter(|family| family.covers().map_or(true, |c| c.is_match("-"))) + .filter(|family| family.covers().is_none_or(|c| c.is_match("-"))) .map(|family| book.select(family.as_str(), self.variant)) .chain(fallback_func.iter().map(|f| f())) .flatten(); @@ -570,7 +570,7 @@ impl<'a> ShapedText<'a> { // for the next line. let dec = if ltr { usize::checked_sub } else { usize::checked_add }; while let Some(next) = dec(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { + if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) { break; } idx = next; @@ -812,7 +812,7 @@ fn shape_segment<'a>( .nth(1) .map(|(i, _)| offset + i) .unwrap_or(text.len()); - covers.map_or(true, |cov| cov.is_match(&text[offset..end])) + covers.is_none_or(|cov| cov.is_match(&text[offset..end])) }; // Collect the shaped glyphs, doing fallback and shaping parts again with diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 2c391ee4c..50fcfb403 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -411,7 +411,7 @@ fn find<'a>( } let score = (matching, Reverse(total)); - if best_score.map_or(true, |b| score > b) { + if best_score.is_none_or(|b| score > b) { best = Some(candidate.1); best_score = Some(score); } diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index 762f94ed0..08d0130da 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -1387,7 +1387,7 @@ impl<'a> CellGrid<'a> { // Include the gutter right before the footer, unless there is // none, or the gutter is already included in the header (no // rows between the header and the footer). - if header_end.map_or(true, |header_end| header_end != footer.start) { + if header_end != Some(footer.start) { footer.start = footer.start.saturating_sub(1); } } diff --git a/crates/typst-library/src/text/font/book.rs b/crates/typst-library/src/text/font/book.rs index 23e27f64c..9f8acce87 100644 --- a/crates/typst-library/src/text/font/book.rs +++ b/crates/typst-library/src/text/font/book.rs @@ -160,7 +160,7 @@ impl FontBook { current.variant.weight.distance(variant.weight), ); - if best_key.map_or(true, |b| key < b) { + if best_key.is_none_or(|b| key < b) { best = Some(id); best_key = Some(key); } diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 3eec0758b..dbf1be8a1 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -159,7 +159,7 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool { { let covers = family.covers(); return text.chars().all(|c| { - covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) + covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) && font.ttf().glyph_index(c).is_some() }); } diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index ff72eb86a..eff1182c1 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -70,7 +70,7 @@ pub(crate) fn write_outline( // (not exceeding whichever is the most restrictive depth limit // of those two). while children.last().is_some_and(|last| { - last_skipped_level.map_or(true, |l| last.level < l) + last_skipped_level.is_none_or(|l| last.level < l) && last.level < leaf.level }) { children = &mut children.last_mut().unwrap().children; @@ -83,7 +83,7 @@ pub(crate) fn write_outline( // needed, following the usual rules listed above. last_skipped_level = None; children.push(leaf); - } else if last_skipped_level.map_or(true, |l| leaf.level < l) { + } else if last_skipped_level.is_none_or(|l| leaf.level < l) { // Only the topmost / lowest-level skipped heading matters when you // have consecutive skipped headings (since none of them are being // added to the bookmark tree), hence the condition above. diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs index b7e1809d7..fde2eaca0 100644 --- a/crates/typst-syntax/src/node.rs +++ b/crates/typst-syntax/src/node.rs @@ -753,7 +753,7 @@ impl<'a> LinkedNode<'a> { // sibling's span number is larger than the target span's number. if children .peek() - .map_or(true, |next| next.span().number() > span.number()) + .is_none_or(|next| next.span().number() > span.number()) { if let Some(found) = child.find(span) { return Some(found); diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs index 387057f37..aa537863d 100644 --- a/crates/typst-syntax/src/package.rs +++ b/crates/typst-syntax/src/package.rs @@ -327,8 +327,8 @@ impl PackageVersion { /// missing in the bound are ignored. pub fn matches_eq(&self, bound: &VersionBound) -> bool { self.major == bound.major - && bound.minor.map_or(true, |minor| self.minor == minor) - && bound.patch.map_or(true, |patch| self.patch == patch) + && bound.minor.is_none_or(|minor| self.minor == minor) + && bound.patch.is_none_or(|patch| self.patch == patch) } /// Performs a `>` match with the given version bound. The match only diff --git a/crates/typst-utils/src/scalar.rs b/crates/typst-utils/src/scalar.rs index 4036c2310..6d84fbfdf 100644 --- a/crates/typst-utils/src/scalar.rs +++ b/crates/typst-utils/src/scalar.rs @@ -28,7 +28,7 @@ impl Scalar { /// /// If the value is NaN, then it is set to `0.0` in the result. pub const fn new(x: f64) -> Self { - Self(if is_nan(x) { 0.0 } else { x }) + Self(if x.is_nan() { 0.0 } else { x }) } /// Gets the value of this [`Scalar`]. @@ -37,17 +37,6 @@ impl Scalar { } } -// We have to detect NaNs this way since `f64::is_nan` isn’t const -// on stable yet: -// ([tracking issue](https://github.com/rust-lang/rust/issues/57241)) -#[allow(clippy::unusual_byte_groupings)] -const fn is_nan(x: f64) -> bool { - // Safety: all bit patterns are valid for u64, and f64 has no padding bits. - // We cannot use `f64::to_bits` because it is not const. - let x_bits = unsafe { std::mem::transmute::(x) }; - (x_bits << 1 >> (64 - 12 + 1)) == 0b0_111_1111_1111 && (x_bits << 12) != 0 -} - impl Numeric for Scalar { fn zero() -> Self { Self(0.0) diff --git a/flake.lock b/flake.lock index c02466422..ad47d29cd 100644 --- a/flake.lock +++ b/flake.lock @@ -112,13 +112,13 @@ "rust-manifest": { "flake": false, "locked": { - "narHash": "sha256-Yqu2/i9170R7pQhvOCR1f5SyFr7PcFbO6xcMr9KWruQ=", + "narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=", "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" }, "original": { "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" } }, "systems": { diff --git a/flake.nix b/flake.nix index abdad27aa..6938f6e57 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; rust-manifest = { - url = "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml"; + url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"; flake = false; }; }; diff --git a/tests/src/collect.rs b/tests/src/collect.rs index c6deba77b..33f4f7366 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -149,7 +149,7 @@ impl Collector { for entry in walkdir::WalkDir::new(crate::SUITE_PATH).sort_by_file_name() { let entry = entry.unwrap(); let path = entry.path(); - if !path.extension().is_some_and(|ext| ext == "typ") { + if path.extension().is_none_or(|ext| ext != "typ") { continue; } @@ -168,7 +168,7 @@ impl Collector { for entry in walkdir::WalkDir::new(crate::REF_PATH).sort_by_file_name() { let entry = entry.unwrap(); let path = entry.path(); - if !path.extension().is_some_and(|ext| ext == "png") { + if path.extension().is_none_or(|ext| ext != "png") { continue; } diff --git a/tests/src/run.rs b/tests/src/run.rs index f9a3c0434..4d08362cf 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -161,7 +161,7 @@ impl<'a> Runner<'a> { // Compare against reference output if available. // Test that is ok doesn't need to be updated. - if ref_data.as_ref().map_or(false, |r| D::matches(&live, r)) { + if ref_data.as_ref().is_ok_and(|r| D::matches(&live, r)) { return; } From 81efc82d3c0f7ccbcb40959ac8bddeca49e4c9f8 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 24 Feb 2025 16:05:36 +0000 Subject: [PATCH 089/101] Fix math accent base height calculation (#5941) --- crates/typst-layout/src/math/accent.rs | 4 ++-- tests/ref/gradient-math-misc.png | Bin 2993 -> 2993 bytes tests/ref/issue-math-realize-scripting.png | Bin 2610 -> 2605 bytes tests/ref/math-accent-align.png | Bin 614 -> 625 bytes tests/ref/math-accent-bounds.png | Bin 327 -> 327 bytes tests/ref/math-accent-dotless.png | Bin 1026 -> 1024 bytes tests/ref/math-accent-func.png | Bin 284 -> 284 bytes tests/ref/math-accent-sym-call.png | Bin 926 -> 930 bytes tests/ref/math-spacing-decorated.png | Bin 2385 -> 2375 bytes 9 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 951870d68..f2dfa2c45 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -34,7 +34,7 @@ pub fn layout_accent( // Try to replace accent glyph with flattened variant. let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.height() > flattened_base_height { + if base.ascent() > flattened_base_height { glyph.make_flattened_accent_form(ctx); } @@ -50,7 +50,7 @@ pub fn layout_accent( // minus the accent base height. Only if the base is very small, we need // a larger gap so that the accent doesn't move too low. let accent_base_height = scaled!(ctx, styles, accent_base_height); - let gap = -accent.descent() - base.height().min(accent_base_height); + let gap = -accent.descent() - base.ascent().min(accent_base_height); let size = Size::new(base.width(), accent.height() + gap + base.height()); let accent_pos = Point::with_x(base_attach - accent_attach); let base_pos = Point::with_y(accent.height() + gap); diff --git a/tests/ref/gradient-math-misc.png b/tests/ref/gradient-math-misc.png index acf14c6fe20e84146001d4117700a949a4fda6d1..13f5c27b38c0a80e502068b2784146ae58b838ed 100644 GIT binary patch delta 2915 zcmV-p3!L<^7qJ(RFn{Nn$MODoMQ;X}i$O02MQ1K%fWgclzyt-_GzntIY171U>`WZT zjgpZiHIAg%k|Il%ENZ80trRJWq)3VTz8{|byvuJK*D~l>3n`LapAQIK=<G9$oA%V+M-u^_6NdskB@Z3gc$u4R4OkNtx@|}GJE2A& zTIS=y8^$1oZrgt4!?-q%mc>N!l75pyx6M<0BWo_BWq(CkeqH_D-&$xga3;|XeL9NPZ9GEz*0TS4sNQXB>G*H;3NBBVOTXlaz>Tm(LGvn>=-(a3=rw zx1-QpAb*mE!w^lww0acUK%*v$03;*_>q=WSQU@c+!{KCnI8k)v&I!G+hkO;kz6EaxQ}btqt{h44 z5cdx{!2vUTM?buy#s?w!xuGSbm(35`*8yzs1%JQTX(jZMn)t4I^j$eVQA~~JvtyaO zx2EEl!Q`W;K6hmP?`mUcOFt@c99!8nCl}NySL5MqefD8(ZmPWRX*4eyn-A0bYlYE| z{Om@HRP(eJ=U$IV>$g#L8T8(92lys4%;6C_{RB6 zg@1x3VG;cg^&PaSY6=OA1$1}JYOjFea3;4OWuE?8TB9HKh0_O$R1d+&(=@pE7@tpK%?H;LaNeUeIw=C1Y;Kz`jXV8a zxfu5Q>+2cw_328Y#^=KpTDCMC3#=7?|BI8|11C5-11WjbmCzQK=>~HI;#H1zFk2K; zn&98plcIwI%b$IF=g$1gtNZA2>-v%Kt@F7P_tF2QzWcY6JOmwo@1=|xv@9-CZ)rCu zblW1e-yf;NXqkumuNnOmx^2nE{eW^CEt_}T{pJ{jZd=9N-mYH8D|9Fdssh}A{)17U|EW6oGcW+`;1@@)6WS;Q&! z#h#0~=#BsGcp5s%ckqgw%uKL*^bJclnX-;oXlT$^S@ijT8==q?nowv8O`!>erqF~! zQ)mkPS1rVkKGP;{))umkiYA65e>>6m=oF%LX{Z~_Bb^iCx+kS%|m(Pk+0MTExE`PC9mB_TNE^PdEl5EQyS<&2@%oQ1dj zcGXt>H3*y_;e~gEh3@LHudCU+`N~L6cTwo}_*V;myOSCJH_6OgzOhs3%0BwgXgti7 zKJynnd5c2#@W664zHe~RaKz8X^Ax&=q1Wm~M@mP?e;&&TNuhguKYAio|1P4@ME5py zSk}BYzZOr7$b(>Eg>a=w9F!Yz1$Xou%8|#@5pHyk0>6sygrqE}H9yS)2 zjBt&ATWDf)%=pZS6^%l7ha%MgOYGYjQ!+FW5}vr@JG2WWMxp6VB|@PoG@;NGnnDu_ zO`!>erqC1`y*GP1^59oK@28R2t%Jhn`|52E|Kc(F=5X^aZrs4_D;RnMF*k+oRBX3; z`zK5Oz9>$Vh+z6AM*ovScfrP2vHL$>te^3JUPod>l>HRCQ&M2*wVjQhF7vvL(=Y?i z*sPe*9tO-|w8OIG%)LBa458<(C;zyRdUFHkkVe$`g?>(d z+M@FBQ{wJUyP-28|G?b%PW4^QZr;r%Mia%+NSz71@bSl4=12kdZGDX%AFswDI)#RCWawQVE1Bp_ z)T%QnX&-$m&&LN^SixzsK9OLzQ!*waax;mN zf}J2d9ssE25PwXed%)q4^c0+bP-qHGC^Utp(1b!$XhNYWG=(OA7on@lVorBOtjU_p zFgO&tCj@I8CEo@Rvt3W@Z)l=>MM@`~|H9(sBYVc@Jd}_Wx_ctq*uCG}nh6zvE^Bxu zCv$r|JZE}ebT_%gGeW|>XRbWfN(F~Px8TtSINb){1Td_Muk*uromfSM}4 z8mbiYHid3uqLtkJWbP9@vtsve^F;sxV%gcMOTEJN1EaQGulI!u8#(i!#Ey!*qKL-R z);H4>`aB04n7Sluho^;qo?)|Jn4_OG9+bKMgQX)@;_Crx(X9seGz#4YB{%j;=o1u8 zM!SwKho>T!AV7alXA3B{I04ddG=)uA*{ zEg=(u6m0bpnwYXumpMGsZw~-)nZ852Org(FjcTD=5i6;kuylmCa4;|HF&tCukS6k8 zXWD@cASd%47I0f;L0UT6r`tGPPM?q43tnf6?%wr~5GKLqee`G=(M@<$! ziIU2aCbq2Dk|Il%ENZ7@trRJWq)3VTz8{|byvwhSYZ-K`g%rtRmgfUP7rOl7!F%5K zEI$GHfAJGaK%pr#q0khXLK6y2p$Ubi(A`xP(SS3J*F;3nD}Ou`x;u1Ha2uaVNStJi zuLMMZLU&GV=$|ndINhKc$EY8u77@&LSKwnEdC21@Tz4t9u__erVR^CJ~D0ji zI||JOB7bQ(4AC@9t4E;?G-|R4Ktgh`u7p-?8L2RvBUE5$ho!T0_(eS_P7f_5Hm1@? z-s<3`-L08S@M#gp;u{t3{nY;b)WQAa;czlOoG7|-=Y*c$L%xdN-hwxT$+@#aSB|82 zi2Dbf;D8yvqaWT;i{+pFJ}Fd!;e7r5}|zjxFz+6Z7h%tMPcIKJ&OXJ6YcMG;oYLV)Jo&f3-03 ziQoM&k=U=D8rssCZSv51Y2~{((D52w)sU;=_Gq`3j-~WJ1eHECabT#jI&6Om;GXk^ z3V#Jn!Xo-_>U(HY)f5sK4e0Ke)m{O`;Y@Bn$vpeDv_?Pf3#Shht!8s3JDO+!E)SM> z;V?AR&L?^^_eM&e!K)&CbBoOa=(B%3yg6BTL;YRr6W#3n`I=(Mf+_%LEA+F;-<`|;&)YkRKz(aE)6qolr!?_k4PQ=R)d$y8aNeUeIw=C1Y<8P3jye5Z zxfu5Q>+2cw&8bSF#^=HoTDCYG3#=CZ_^XrM11C5*11WjbmCzQK=>~HI;#H1zFk2K; zn&3awlcHw_mfmmg;LiNh>-*?2>-v%Kt@GIv_tF2MzW>*gJOmwoAEb%dEt_-P{pKizZd=9N-mYH8Yjh|Issh}A{-bAx zllBsYZkv$SeBqaP6$hJhh8Nh_6ZRbkLz6Gf!Kg@6=!;md{95Cgla}LU0~(qLhO&QNfMX6U00vYvQJ{Qn5767B9bwYg(m%(UMxD`&%u?u1<=O6$vw&0V zi#-=}(HsBW@ho(b@8C5#nHgsf=o^-9GG!gF(a@l;vgq@FH$tH)G@;NGnnDu_O`!>e zrqC4nFItEneW8uttj%W~6-^9B{(iji$+xwoRh2?_!8+Rwumny~QN+ktjY4-hbj~&& zWlNI@g+li>v?;h>QNIqBDRj@5j(rMEp(*s=VCbC$(xnbV_fhC`iQi4Zv@aYwX=+1> z>@{y;J!4XT=u?S!O)iFMAKiT^U;St%U)`&hXEMBCJPS<+mhRJPc6zT~3@Cda1sl5+ zC(*#`Vjg(@W3u$Q5;wlSSA*KIy^sMFHX!_Z%6BJcPkZ>?uOep;o8re}r}nnIr)de~T4 zG{QB1ZlQ_IG3_(Qmo*CA9g0)~EU|BAOv%thNOC_4WqNBc*Nf!c+YkPBO0~4Z(2|!D-j=TzRPw z`ssu*dVup-ymVUV*?L0b9IvV=Vt!t!R~-udLjJn1_;>eZwA|g$>ps)vst0x(6#4~! zX^YCgPKvub?S{^X{3CPYd)0R}yLmU87)caIB6TM4(x;zhnIkFm3wBCteEbv=ErkB0 z^@%Q3d4GTLFsM=JA27v9U)qu;!Y^)lK=oA{lk)ii}tYo4y zQLE0Rq?h;O98CW01pMD9+DRf8t3;8=gUtPbv zAG++#yuYnnakJk)!AGOGavSfxhu{1Hmw$$p&*@%Ar}9T?+EXj;)apAGeW&Dj@^Uu~ zUmX4jfKZodf48TRH7nWxZQ5SqzXLk$4>PRW>v$ju~5 z3U-3E%8SEGf<2eI@A8Eral&%3NI#+|-`%mEOPed-eqVF;!$_9#_VY+*POKus0j z3{{GGn?koS(Ms-NGWQvtTekbR`2qj|vE*#krC#Csfl=G8*ZabSjhuNzio`eV&62OkR?;!_z{4PqUe?%#qI;kIG#C(b5qs@%4bU;8ufs8ij6yk{f+3^a+Y4 zqg_XrL(}%IJ`WTlj^q^j90_-6|9VsCxd2|; zUqLMefUB{5=^We(5v+XDbxrti=$Q|NP4qgv=z#7b%>EFIx39L&jj4965Zq=~%O znQ~wQ$jQ8i1>9CerqF~!Q)miJDD+>X{{?rUzwPilq4)p* N002ovPDHLkV1m<1w@d&4 diff --git a/tests/ref/issue-math-realize-scripting.png b/tests/ref/issue-math-realize-scripting.png index ee2d4cdf7a64d95e4bd8fffe538a89de1bde1ae4..7d721ed776199bd6611d1178bb7582c1dd6fe7c8 100644 GIT binary patch delta 2596 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 LNkvXXu0mjf*}K?_ diff --git a/tests/ref/math-accent-align.png b/tests/ref/math-accent-align.png index 84e8dc8ccda955a6b747d1462610b51219f43f43..efc66ec3faf784dcc17fcb8d3fbcb7a58c151773 100644 GIT binary patch delta 600 zcmV-e0;m1v1n~rrB!4(bL_t(|+U?ihOH*MS$8rBN$w3kfBPv$eMcov2VL?y{MM8Gd z57tG+P#LwUIa5}x)1Z##bk5AS;bf#!r!0#&JL+oI**ZJt6W_~c-3eb0>hr#O_};vD z9u8kP4RELi2}@YQ65cUb>$&>GV*>U>jQR%P-C18S!VZ7Ng@1IO4#^ox1Mso6P;xWF z)B#ihSiJ%23r(7!o@a+U7MiOeIutdk0wCwcnPql(Tuls_5bElDzUahPZ6D16PQZo1 zDY-x-yvt$Ra0piK?60S;1Rf6Gt^qLqP!@pf{ngoic6h|RqP_-T`5sdl0MU;4#$9HZ zI%tB*%~L<=*ne#QNU*~LS`#7wthaBdJk47066|o-i$JdiF@N-{+*}ft@HXI$?hn(B zz$jG>N8s4#oC-00c-~-##}}^$+D*Xn-lNj=_$R8K1Up>Dkxhu*>c=6I9WGjp$UwO9 zsg*5vmK{FXb1pOkA*IjXs<6YbQ2T#m%grU>T?`lW@_&+*L0vXKocq2$_csAC^V%MM z>R)hCJgvkbG!Rw7{BUK< mETOK_Z@IZ7EMW=nVE+J61?9wOxq%M=0000@y^($#u_1zm+% zU4*?s7G`a@&AB-$v&h;&a;CDE7F1JbTFoVF%9^cBx3hD;@m!wW6+9kf&-Wkra9;R5 z@U&o2Eeck!f)%`FaK7KsdrSi(@=o*R>~LN6 zq%jrs`p-wyfGAkO{|tXvT$!r8q@e}*aG_Gmv5YybPk%m4ZA1S}i{HEd0uQ^@uw>x;^K>s?!o;_IoS)CbKf?i6j{pu)Hwcu$%!1JYD@< J);T3K0RVa;6*B+; delta 53 zcmX@kbew6z2hrCT?4_2y>JfK%bbHQ$Ri`Dy>}_k?CbKf?iKI9dFkf_)7H0qgPgg&e IbxsLQ0CcJp7XSbN diff --git a/tests/ref/math-accent-dotless.png b/tests/ref/math-accent-dotless.png index 81eb4fa2bd7b48cba24fa3e1c8d6beda09bb6506..389ceb634aad124cc19196123c8ea188a72a0633 100644 GIT binary patch delta 1002 zcmVCXn9AOw3HbC%@)O|M?u=yLe9RO2H|3+iKUIDXjeaBY#OPp@e5UJcz12iXlDm zA-RGw>lT-6aDPQ+rU#X9caat;a~o`z$u4bE=^-+Nsx>^#}PaW7~pC^5tMD}(lY;D7d!kM2=(2TCgkMhb!Qj!O}a z2H@(cc;N`l?XGb?JO_dO$D+kU0)M>-9WPx|3DdhuuN~(9W=;_$ox znC}j#fGO#e89pWxerIQRGc%kB6MNad9ennP8hGEDw-LBnO{*JLGqLn z5d-eF3A~mIP@NM8OpCICa$<)^=1*hN?`~t!M Y0Al&n0a`kXD*ylh07*qoM6N<$g5P%2D*ylh delta 1004 zcmVpnSbg;BivD_N6K0T_;x!|MRqw-ah_GybnFYHL&Y-nmojC2*{5Wi&jGA_ z`Lb4c>d1g>JVVCz2}q4)NJZVfvcY%VNNoZ$d^8v7z<3a&m=q@cuH_(-~R?Le-F|~6~J^2$x~2djYBil z0Mr`*R;oh3O+k2Q=(;XBjQ&@dr6Y^LeCaI{K)g7=I0V30Q&UrfwN5(a=`RLAHIzIA zp!AB&!a*CbIy!2o)c_M?U2)&qF~)!HUWDYZy?^RDQg#^NyKP9f-`@|?Pq|(|a(*0_ z?Ro>rd+ohW0QPQ0%2WZyUO%7#HuoQwjik#2G5~2CNI%rgfTZ7X4x}8Uj&M#n0E1~r z=>qfh4h=A8+hhjz$*jJ$IULPeO@xWP=-3E8xkn4UeaTk`T&$qc1B+SMde-1N9|GSo z1AkCf0o<4oI9LVH_ZrR?1RggE>}dq@^EJTncL*EOd4!2`44 z_rQD2(|yP9dwM`@0K7wD~l&E{xDzw0#8>zmvv4FO#o$i9smFU delta 69 zcmbQkG>2(|yBc@TM`@0K7wz|+;wWt~$(698sT9m@a! diff --git a/tests/ref/math-accent-sym-call.png b/tests/ref/math-accent-sym-call.png index 0837a86c9e861a960cbca4d157ea7e719b54cd0f..609197f3c3da71d9f4b4efa72b92bcc64311b935 100644 GIT binary patch delta 908 zcmV;719SYI2cid%B!6j1L_t(|+U?cdPg`XG$MMC##Ka44jLDMua>Iqg7~_IVhFRvK zbB3sM5rUh8xj`l{7fet$L4t{Mq@fFRFaaAFAVpxPY|t|3RyMKix{j`ID?0^B*VFSg zd7hI>7Bwc%7B1G$&G{wg;`=;T&n7m=6Pq?jKD@!<3F@gD2)<+#AwBM2 zK=9&K3u`u{m%$W!>&GlWrz1WFz-qO#4M87W^dQtmE7iicQwY&rw~-9%r)dD@8!=rA zU`j@i!ZU_axPNk}yaRx)J#$qE_x7BO0iYdhmZxg8pXf&DZLpNeLmjyEp-T9|$CP>i zD7%{7g0wS^(B|;e05CiF%?I+n&olG|0I|NV=TyQK{S-z3*v`dwkqVLg$9v8LQ2xEA zBn)tIKSzGO3t->7QV>=MpX(cV&B{kN;j{32x7eKol7BmZnkgVs8K=w#1%Wd~JvTmq z#KAvaY`<&mfVv-?&z?~TGkeFQarH7Vp}{~1vcEI~0SBaF!5Tc1ppS=d+|O8nJG1cx z;&#C8j;h>P`We#@@W$a3FsVVX(>lzsfs}P%3S3804rkVnP!w|!TNFbw3``jm!byNf&iZ%x&=Tn zf?=@?0uZ=dxJJ?Z2*L{W2LN+dSECjl+Z^C*1fdF{VpQ7-fYbK^6i3b@S$31tQi598 zb!w`3aS*8=fVF2Btpi~7rz6hg&DX{e241~SJ5u*{UAHNPs~b4FpMaM10iynIiXDzy5op9!~y=;KG!+2{|svnaMwk i`EWj*4?kAnzW^mukOPEx)hPe~002ovP6b4+LSTZ{g0Nix delta 904 zcmV;319$wQ2c8FzB!6W|L_t(|+U?cdPg7?A$8od2WXUdevn(-jFE?CFmSveCCbAf3 zHs?aNIWq{;S)4)~;0#OHrh^fd%t<C+g0$zm zz7@PCBL+)wE^rpA2%fUaXT zBLD>5Zuc7M?i*f>V2b-01IQq2Bm#ke@wmfuMLV}k%JSGb!IR_rq_gsMoHTKIMvA+qZ>lA(RE3cz$7NK-mU z$uLrA+)xUa^?#PO0?@H%vK(P?&&enNiGvODRCSIMHiQec)?#_610$cPgfD(Zu@``n zYuPPGJ7WmVPH!au6FuL5Ebsd=yTLPjAAPp$~7dI|(Fr0DqM(AY2xs#0Po$nF9Nb&mn&B zpI2M%XsuBFv-`y}3SlPZy$P;e1%?vP9fa(q20(X0IvS|NI}Dl2p&NJCG+=%rHbcw- zcsvo68%w`~)C;_QI1LOZAkeP$(PtoKognGgk(9%0{~y7Kxrok*AsNQ{R|;Vzxo|H0 zNZ~ldd4CWCj3*UtuC8(-m8ZBzU(Lx@Sl4bKO-?I^Nv?tPm%+V4*TK}UY1{lrmJ^y5 zQww|k#?+lfn74tH868Sv3I~I;NuDd56)1@f{e@ifKGY{6kt<~{- z8-im;+lA*GN@06rGo~$RgwzO<)l=IsdTt4!A%8>3H%RpcMU5zh{dqcH9l8ZTA%bDG z_5h$?&0nElx*uVQngd|&iVD=igPV2Eh7rmUECY$H0JwdxKw@5GXEJR{;fh+0?nh4~eZYXhBrNR2((~t{ z4u9C^IxqUpt{5J`m6zc3AQ>+(^x86~MwP<8eW#m_f7is=M}Q@@v=z{7Kh!0LivgSt zM@M^l;SFH#vHGLC3_=CKJ5}|E-$Nz5&SgF%b3J^c9!~y?Fa}cmCX)HqG@kn{=EAvf eF8ox5{{vrCkOUezRbl`D002ovPDHLkU;%=IT%-#C diff --git a/tests/ref/math-spacing-decorated.png b/tests/ref/math-spacing-decorated.png index b8846ff0595667a56f7004ac4b0fcc5925670200..2f3c704e5d981ce119ac1e903e154c4a186e1553 100644 GIT binary patch delta 2364 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 From 3744c99b07f97a954a8468bef5fdb08c5c7914d7 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:15:17 +0100 Subject: [PATCH 090/101] Override the default math class of some characters (#5949) --- crates/typst-utils/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 34d6a9432..b346a8096 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -360,6 +360,21 @@ pub fn default_math_class(c: char) -> Option { // https://github.com/typst/typst/pull/5714 '\u{22A5}' => Some(MathClass::Normal), + // Used as a binary connector in linear logic, where it is referred to + // as "par". + // https://github.com/typst/typst/issues/5764 + '⅋' => Some(MathClass::Binary), + + // Those overrides should become the default in the next revision of + // MathClass.txt. + // https://github.com/typst/typst/issues/5764#issuecomment-2632435247 + '⎰' | '⟅' => Some(MathClass::Opening), + '⎱' | '⟆' => Some(MathClass::Closing), + + // Both ∨ and ⟑ are classified as Binary. + // https://github.com/typst/typst/issues/5764 + '⟇' => Some(MathClass::Binary), + c => unicode_math_class::class(c), } } From 36d83c8c092e7984eaa03dbecc1083f49da13129 Mon Sep 17 00:00:00 2001 From: Sharzy Date: Tue, 25 Feb 2025 00:35:13 +0800 Subject: [PATCH 091/101] HTML export: fix elem counting on classify_output (#5910) Co-authored-by: Laurenz --- crates/typst-html/src/lib.rs | 6 +++--- tests/ref/html/html-elem-alone-context.html | 2 ++ tests/suite/html/elem.typ | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tests/ref/html/html-elem-alone-context.html create mode 100644 tests/suite/html/elem.typ diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 25d0cd5d8..236a32544 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -307,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement { /// Determine which kind of output the user generated. fn classify_output(mut output: Vec) -> SourceResult { - let len = output.len(); + let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); for node in &mut output { let HtmlNode::Element(elem) = node else { continue }; let tag = elem.tag; let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); - match (tag, len) { + match (tag, count) { (tag::html, 1) => return Ok(OutputKind::Html(take())), (tag::body, 1) => return Ok(OutputKind::Body(take())), (tag::html | tag::body, _) => bail!( elem.span, "`{}` element must be the only element in the document", - elem.tag + elem.tag, ), _ => {} } diff --git a/tests/ref/html/html-elem-alone-context.html b/tests/ref/html/html-elem-alone-context.html new file mode 100644 index 000000000..69e9da411 --- /dev/null +++ b/tests/ref/html/html-elem-alone-context.html @@ -0,0 +1,2 @@ + + diff --git a/tests/suite/html/elem.typ b/tests/suite/html/elem.typ new file mode 100644 index 000000000..81ab94577 --- /dev/null +++ b/tests/suite/html/elem.typ @@ -0,0 +1,7 @@ +--- html-elem-alone-context html --- +#context html.elem("html") + +--- html-elem-not-alone html --- +// Error: 2-19 `` element must be the only element in the document +#html.elem("html") +Text From 225e845021b9cfb37e6dc719c8bc85ccdc1ff69f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 12:31:15 +0100 Subject: [PATCH 092/101] Fix introspection of HTML root sibling metadata (#5953) --- crates/typst-html/src/lib.rs | 2 +- .../src/introspection/introspector.rs | 18 +++++++++--------- tests/ref/html/html-elem-metadata.html | 2 ++ tests/suite/html/elem.typ | 8 ++++++++ 4 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 tests/ref/html/html-elem-metadata.html diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 236a32544..aa769976e 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -83,8 +83,8 @@ fn html_document_impl( )?; let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; + let introspector = Introspector::html(&output); let root = root_element(output, &info)?; - let introspector = Introspector::html(&root); Ok(HtmlDocument { info, root, introspector }) } diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index 8cbaea891..9751dfcb8 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -10,7 +10,7 @@ use typst_utils::NonZeroExt; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::html::{HtmlElement, HtmlNode}; +use crate::html::HtmlNode; use crate::introspection::{Location, Tag}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; @@ -55,8 +55,8 @@ impl Introspector { /// Creates an introspector for HTML. #[typst_macros::time(name = "introspect html")] - pub fn html(root: &HtmlElement) -> Self { - IntrospectorBuilder::new().build_html(root) + pub fn html(output: &[HtmlNode]) -> Self { + IntrospectorBuilder::new().build_html(output) } /// Iterates over all locatable elements. @@ -392,9 +392,9 @@ impl IntrospectorBuilder { } /// Build an introspector for an HTML document. - fn build_html(mut self, root: &HtmlElement) -> Introspector { + fn build_html(mut self, output: &[HtmlNode]) -> Introspector { let mut elems = Vec::new(); - self.discover_in_html(&mut elems, root); + self.discover_in_html(&mut elems, output); self.finalize(elems) } @@ -434,16 +434,16 @@ impl IntrospectorBuilder { } /// Processes the tags in the HTML element. - fn discover_in_html(&mut self, sink: &mut Vec, elem: &HtmlElement) { - for child in &elem.children { - match child { + fn discover_in_html(&mut self, sink: &mut Vec, nodes: &[HtmlNode]) { + for node in nodes { + match node { HtmlNode::Tag(tag) => self.discover_in_tag( sink, tag, Position { page: NonZeroUsize::ONE, point: Point::zero() }, ), HtmlNode::Text(_, _) => {} - HtmlNode::Element(elem) => self.discover_in_html(sink, elem), + HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), HtmlNode::Frame(frame) => self.discover_in_frame( sink, frame, diff --git a/tests/ref/html/html-elem-metadata.html b/tests/ref/html/html-elem-metadata.html new file mode 100644 index 000000000..c37a7d2ef --- /dev/null +++ b/tests/ref/html/html-elem-metadata.html @@ -0,0 +1,2 @@ + +Hi diff --git a/tests/suite/html/elem.typ b/tests/suite/html/elem.typ index 81ab94577..b416fdf94 100644 --- a/tests/suite/html/elem.typ +++ b/tests/suite/html/elem.typ @@ -5,3 +5,11 @@ // Error: 2-19 `` element must be the only element in the document #html.elem("html") Text + +--- html-elem-metadata html --- +#html.elem("html", context { + let val = query().first().value + test(val, "Hi") + val +}) +#metadata("Hi") From acd3a5b7a5999d22fbf2da488744d564b2f3638e Mon Sep 17 00:00:00 2001 From: aodenis <45949528+aodenis@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:41:54 +0100 Subject: [PATCH 093/101] Fix high CPU usage due to inotify watch triggering itself (#5905) Co-authored-by: Laurenz --- crates/typst-cli/src/watch.rs | 57 +++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index 91132fc30..cc727f0fc 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -204,6 +204,10 @@ impl Watcher { let event = event .map_err(|err| eco_format!("failed to watch dependencies ({err})"))?; + if !is_relevant_event_kind(&event.kind) { + continue; + } + // Workaround for notify-rs' implicit unwatch on remove/rename // (triggered by some editors when saving files) with the // inotify backend. By keeping track of the potentially @@ -224,7 +228,17 @@ impl Watcher { } } - relevant |= self.is_event_relevant(&event); + // Don't recompile because the output file changed. + // FIXME: This doesn't work properly for multifile image export. + if event + .paths + .iter() + .all(|path| is_same_file(path, &self.output).unwrap_or(false)) + { + continue; + } + + relevant = true; } // If we found a relevant event or if any of the missing files now @@ -234,32 +248,23 @@ impl Watcher { } } } +} - /// Whether a watch event is relevant for compilation. - fn is_event_relevant(&self, event: ¬ify::Event) -> bool { - // Never recompile because the output file changed. - if event - .paths - .iter() - .all(|path| is_same_file(path, &self.output).unwrap_or(false)) - { - return false; - } - - match &event.kind { - notify::EventKind::Any => true, - notify::EventKind::Access(_) => false, - notify::EventKind::Create(_) => true, - notify::EventKind::Modify(kind) => match kind { - notify::event::ModifyKind::Any => true, - notify::event::ModifyKind::Data(_) => true, - notify::event::ModifyKind::Metadata(_) => false, - notify::event::ModifyKind::Name(_) => true, - notify::event::ModifyKind::Other => false, - }, - notify::EventKind::Remove(_) => true, - notify::EventKind::Other => false, - } +/// Whether a kind of watch event is relevant for compilation. +fn is_relevant_event_kind(kind: ¬ify::EventKind) -> bool { + match kind { + notify::EventKind::Any => true, + notify::EventKind::Access(_) => false, + notify::EventKind::Create(_) => true, + notify::EventKind::Modify(kind) => match kind { + notify::event::ModifyKind::Any => true, + notify::event::ModifyKind::Data(_) => true, + notify::event::ModifyKind::Metadata(_) => false, + notify::event::ModifyKind::Name(_) => true, + notify::event::ModifyKind::Other => false, + }, + notify::EventKind::Remove(_) => true, + notify::EventKind::Other => false, } } From f31c9716240eb5c81ae225455c069089088015bc Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 13:47:41 +0100 Subject: [PATCH 094/101] Deduplicate watcher update call (#5955) --- crates/typst-cli/src/watch.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index cc727f0fc..0813d8ffd 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> { // Perform initial compilation. timer.record(&mut world, |world| compile_once(world, &mut config))??; - // Watch all dependencies of the initial compilation. - watcher.update(world.dependencies())?; - // Recompile whenever something relevant happens. loop { + // Watch all dependencies of the most recent compilation. + watcher.update(world.dependencies())?; + // Wait until anything relevant happens. watcher.wait()?; @@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> { // Evict the cache. comemo::evict(10); - - // Adjust the file watching. - watcher.update(world.dependencies())?; } } From bad343748b834cdc155c5fe76cd944e74f4665cf Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 14:00:22 +0100 Subject: [PATCH 095/101] Fix paper name in page setup guide (#5956) --- docs/guides/page-setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md index c93a778e2..36ed0fa23 100644 --- a/docs/guides/page-setup.md +++ b/docs/guides/page-setup.md @@ -56,7 +56,7 @@ requirements with examples. Typst's default page size is A4 paper. Depending on your region and your use case, you will want to change this. You can do this by using the [`{page}`]($page) set rule and passing it a string argument to use a common page -size. Options include the complete ISO 216 series (e.g. `"iso-a4"`, `"iso-c2"`), +size. Options include the complete ISO 216 series (e.g. `"a4"` and `"iso-c2"`), customary US formats like `"us-legal"` or `"us-letter"`, and more. Check out the reference for the [page's paper argument]($page.paper) to learn about all available options. From d11ad80dee669c5e2285ca8df8ebc99abc031ccd Mon Sep 17 00:00:00 2001 From: evie <50974538+mi2ebi@users.noreply.github.com> Date: Tue, 25 Feb 2025 06:01:01 -0800 Subject: [PATCH 096/101] Add `#str.normalize(form)` (#5631) Co-authored-by: +merlan #flirora Co-authored-by: Laurenz --- Cargo.lock | 1 + Cargo.toml | 1 + crates/typst-library/Cargo.toml | 1 + crates/typst-library/src/foundations/str.rs | 46 ++++++++++++++++++++- tests/suite/foundations/str.typ | 7 ++++ 5 files changed, 55 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 1851134a5..86f04ee52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2995,6 +2995,7 @@ dependencies = [ "typst-timing", "typst-utils", "unicode-math-class", + "unicode-normalization", "unicode-segmentation", "unscanny", "usvg", diff --git a/Cargo.toml b/Cargo.toml index 36195230e..f643856e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ unicode-bidi = "0.3.18" unicode-ident = "1.0" unicode-math-class = "0.1" unicode-script = "0.5" +unicode-normalization = "0.1.24" unicode-segmentation = "1" unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index fb45ec862..71729b63a 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -61,6 +61,7 @@ ttf-parser = { workspace = true } two-face = { workspace = true } typed-arena = { workspace = true } unicode-math-class = { workspace = true } +unicode-normalization = { workspace = true } unicode-segmentation = { workspace = true } unscanny = { workspace = true } usvg = { workspace = true } diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 551ac04f5..23a1bd4cf 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -7,12 +7,13 @@ use comemo::Tracked; use ecow::EcoString; use serde::{Deserialize, Serialize}; use typst_syntax::{Span, Spanned}; +use unicode_normalization::UnicodeNormalization; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func, + cast, dict, func, repr, scope, ty, Array, Bytes, Cast, Context, Decimal, Dict, Func, IntoValue, Label, Repr, Type, Value, Version, }; use crate::layout::Alignment; @@ -286,6 +287,30 @@ impl Str { Ok(c.into()) } + /// Normalizes the string to the given Unicode normal form. + /// + /// This is useful when manipulating strings containing Unicode combining + /// characters. + /// + /// ```typ + /// #assert.eq("é".normalize(form: "nfd"), "e\u{0301}") + /// #assert.eq("ſ́".normalize(form: "nfkc"), "ś") + /// ``` + #[func] + pub fn normalize( + &self, + #[named] + #[default(UnicodeNormalForm::Nfc)] + form: UnicodeNormalForm, + ) -> Str { + match form { + UnicodeNormalForm::Nfc => self.nfc().collect(), + UnicodeNormalForm::Nfd => self.nfd().collect(), + UnicodeNormalForm::Nfkc => self.nfkc().collect(), + UnicodeNormalForm::Nfkd => self.nfkd().collect(), + } + } + /// Whether the string contains the specified pattern. /// /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` @@ -788,6 +813,25 @@ cast! { v: Str => Self::Str(v), } +/// A Unicode normalization form. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum UnicodeNormalForm { + /// Canonical composition where e.g. accented letters are turned into a + /// single Unicode codepoint. + #[string("nfc")] + Nfc, + /// Canonical decomposition where e.g. accented letters are split into a + /// separate base and diacritic. + #[string("nfd")] + Nfd, + /// Like NFC, but using the Unicode compatibility decompositions. + #[string("nfkc")] + Nfkc, + /// Like NFD, but using the Unicode compatibility decompositions. + #[string("nfkd")] + Nfkd, +} + /// Convert an item of std's `match_indices` to a dictionary. fn match_to_dict((start, text): (usize, &str)) -> Dict { dict! { diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ index 56756416d..66fb912c0 100644 --- a/tests/suite/foundations/str.typ +++ b/tests/suite/foundations/str.typ @@ -86,6 +86,13 @@ // Error: 2-28 0x110000 is not a valid codepoint #str.from-unicode(0x110000) // 0x10ffff is the highest valid code point +--- str-normalize --- +// Test the `normalize` method. +#test("e\u{0301}".normalize(form: "nfc"), "é") +#test("é".normalize(form: "nfd"), "e\u{0301}") +#test("ſ\u{0301}".normalize(form: "nfkc"), "ś") +#test("ſ\u{0301}".normalize(form: "nfkd"), "s\u{0301}") + --- string-len --- // Test the `len` method. #test("Hello World!".len(), 12) From 2eef9e84e117670ea0db964a5a8addc89e0ee785 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:09:52 +0100 Subject: [PATCH 097/101] Improve hints for show rule recursion depth (#5856) --- crates/typst-library/src/engine.rs | 3 ++- tests/suite/scripting/recursion.typ | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/engine.rs b/crates/typst-library/src/engine.rs index 80aaef224..43a7b4671 100644 --- a/crates/typst-library/src/engine.rs +++ b/crates/typst-library/src/engine.rs @@ -312,7 +312,8 @@ impl Route<'_> { if !self.within(Route::MAX_SHOW_RULE_DEPTH) { bail!( "maximum show rule depth exceeded"; - hint: "check whether the show rule matches its own output" + hint: "maybe a show rule matches its own output"; + hint: "maybe there are too deeply nested elements" ); } Ok(()) diff --git a/tests/suite/scripting/recursion.typ b/tests/suite/scripting/recursion.typ index 6be96c1ec..e92b67fb7 100644 --- a/tests/suite/scripting/recursion.typ +++ b/tests/suite/scripting/recursion.typ @@ -44,18 +44,21 @@ --- recursion-via-include-in-layout --- // Test cyclic imports during layout. // Error: 2-38 maximum show rule depth exceeded -// Hint: 2-38 check whether the show rule matches its own output +// Hint: 2-38 maybe a show rule matches its own output +// Hint: 2-38 maybe there are too deeply nested elements #layout(_ => include "recursion.typ") --- recursion-show-math --- // Test recursive show rules. // Error: 22-25 maximum show rule depth exceeded -// Hint: 22-25 check whether the show rule matches its own output +// Hint: 22-25 maybe a show rule matches its own output +// Hint: 22-25 maybe there are too deeply nested elements #show math.equation: $x$ $ x $ --- recursion-show-math-realize --- // Error: 22-33 maximum show rule depth exceeded -// Hint: 22-33 check whether the show rule matches its own output +// Hint: 22-33 maybe a show rule matches its own output +// Hint: 22-33 maybe there are too deeply nested elements #show heading: it => heading[it] $ #heading[hi] $ From 8f039dd614ba518976b8b486e0a138bd6a9c660c Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 15:10:01 +0100 Subject: [PATCH 098/101] Only autocomplete methods which take self (#5824) --- crates/typst-ide/src/complete.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index e3dcc442e..e3d777115 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -410,9 +410,17 @@ fn field_access_completions( elem.into_iter().chain(Some(ty)) }; - // Autocomplete methods from the element's or type's scope. + // Autocomplete methods from the element's or type's scope. We only complete + // those which have a `self` parameter. for (name, binding) in scopes.flat_map(|scope| scope.iter()) { - ctx.call_completion(name.clone(), binding.read()); + let Ok(func) = binding.read().clone().cast::() else { continue }; + if func + .params() + .and_then(|params| params.first()) + .is_some_and(|param| param.name == "self") + { + ctx.call_completion(name.clone(), binding.read()); + } } if let Some(scope) = value.scope() { @@ -1764,6 +1772,7 @@ mod tests { #[test] fn test_autocomplete_type_methods() { test("#\"hello\".", -1).must_include(["len", "contains"]); + test("#table().", -1).must_exclude(["cell"]); } #[test] From d6b0d68ffa4963459f52f7d774080f1f128841d4 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:19:17 +0100 Subject: [PATCH 099/101] Add more methods to `direction` (#5893) --- crates/typst-library/src/layout/dir.rs | 52 ++++++++++++++++++++++++++ tests/suite/layout/dir.typ | 27 ++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/crates/typst-library/src/layout/dir.rs b/crates/typst-library/src/layout/dir.rs index 9a2e77105..699c8c481 100644 --- a/crates/typst-library/src/layout/dir.rs +++ b/crates/typst-library/src/layout/dir.rs @@ -50,6 +50,42 @@ impl Dir { pub const TTB: Self = Self::TTB; pub const BTT: Self = Self::BTT; + /// Returns a direction from a starting point. + /// + /// ```example + /// direction.from(left) \ + /// direction.from(right) \ + /// direction.from(top) \ + /// direction.from(bottom) + /// ``` + #[func] + pub const fn from(side: Side) -> Dir { + match side { + Side::Left => Self::LTR, + Side::Right => Self::RTL, + Side::Top => Self::TTB, + Side::Bottom => Self::BTT, + } + } + + /// Returns a direction from an end point. + /// + /// ```example + /// direction.to(left) \ + /// direction.to(right) \ + /// direction.to(top) \ + /// direction.to(bottom) + /// ``` + #[func] + pub const fn to(side: Side) -> Dir { + match side { + Side::Right => Self::LTR, + Side::Left => Self::RTL, + Side::Bottom => Self::TTB, + Side::Top => Self::BTT, + } + } + /// The axis this direction belongs to, either `{"horizontal"}` or /// `{"vertical"}`. /// @@ -65,6 +101,22 @@ impl Dir { } } + /// The corresponding sign, for use in calculations. + /// + /// ```example + /// #ltr.sign() \ + /// #rtl.sign() \ + /// #ttb.sign() \ + /// #btt.sign() + /// ``` + #[func] + pub const fn sign(self) -> i64 { + match self { + Self::LTR | Self::TTB => 1, + Self::RTL | Self::BTT => -1, + } + } + /// The start point of this direction, as an alignment. /// /// ```example diff --git a/tests/suite/layout/dir.typ b/tests/suite/layout/dir.typ index 139a2285d..e6db54da5 100644 --- a/tests/suite/layout/dir.typ +++ b/tests/suite/layout/dir.typ @@ -1,10 +1,35 @@ +--- dir-from --- +#test(direction.from(left), ltr) +#test(direction.from(right), rtl) +#test(direction.from(top), ttb) +#test(direction.from(bottom), btt) + +--- dir-from-invalid --- +// Error: 17-23 cannot convert this alignment to a side +#direction.from(center) + +--- dir-to --- +#test(direction.to(left), rtl) +#test(direction.to(right), ltr) +#test(direction.to(top), btt) +#test(direction.to(bottom), ttb) + +-- dir-to-invalid --- +// Error: 15-21 cannot convert this alignment to a side +#direction.to(center) + --- dir-axis --- -// Test direction methods. #test(ltr.axis(), "horizontal") #test(rtl.axis(), "horizontal") #test(ttb.axis(), "vertical") #test(btt.axis(), "vertical") +--- dir-sign --- +#test(ltr.sign(), 1) +#test(rtl.sign(), -1) +#test(ttb.sign(), 1) +#test(btt.sign(), -1) + --- dir-start --- #test(ltr.start(), left) #test(rtl.start(), right) From 52f1f53973414be72bf22c3253ab365f8db067df Mon Sep 17 00:00:00 2001 From: Emmanuel Lesueur <48604057+Emm54321@users.noreply.github.com> Date: Wed, 26 Feb 2025 19:07:29 +0100 Subject: [PATCH 100/101] Fix curve with multiple non-closed components. (#5963) --- crates/typst-layout/src/shapes.rs | 1 + tests/ref/curve-multiple-non-closed.png | Bin 0 -> 85 bytes tests/suite/visualize/curve.typ | 10 ++++++++++ 3 files changed, 11 insertions(+) create mode 100644 tests/ref/curve-multiple-non-closed.png diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 21d0a518f..7ab41e9d4 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> { self.last_point = point; self.last_control_from = point; self.is_started = true; + self.is_empty = true; } /// Add a line segment. diff --git a/tests/ref/curve-multiple-non-closed.png b/tests/ref/curve-multiple-non-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..f4332e363f7500fbfdf1745ddb07156cd699804e GIT binary patch literal 85 zcmeAS@N?(olHy`uVBq!ia0vp^6+o=P2qYL}Co*>cDH%@}$B>F!$v^tJB-rNEH#ADl iwSQ&zYG?k0qvj00FFdmKHq?9ssrPjCb6Mw<&;$TP`50CJ literal 0 HcmV?d00001 diff --git a/tests/suite/visualize/curve.typ b/tests/suite/visualize/curve.typ index f98f634a7..14a1c0cc8 100644 --- a/tests/suite/visualize/curve.typ +++ b/tests/suite/visualize/curve.typ @@ -38,6 +38,16 @@ curve.close(mode: "smooth"), ) +--- curve-multiple-non-closed --- +#curve( + stroke: 2pt, + curve.line((20pt, 0pt)), + curve.move((0pt, 10pt)), + curve.line((20pt, 10pt)), + curve.move((0pt, 20pt)), + curve.line((20pt, 20pt)), +) + --- curve-line --- #curve( fill: purple, From cfb3b1a2709107f0f06f89ea25cabc939cec15e5 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:10:36 -0500 Subject: [PATCH 101/101] Improve clarity of `ast.rs` for newcomers to the codebase (#5784) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: T0mstone <39707032+T0mstone@users.noreply.github.com> --- crates/typst-eval/src/call.rs | 8 +- crates/typst-eval/src/code.rs | 36 +- crates/typst-eval/src/markup.rs | 4 +- crates/typst-eval/src/rules.rs | 2 +- crates/typst-ide/src/complete.rs | 8 +- crates/typst-ide/src/matchers.rs | 4 +- crates/typst-ide/src/tooltip.rs | 2 +- crates/typst-syntax/src/ast.rs | 641 ++++++++++++++++++------------- crates/typst-syntax/src/node.rs | 21 - 9 files changed, 415 insertions(+), 311 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index c68bef963..1ca7b4b8f 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -466,7 +466,7 @@ impl<'a> CapturesVisitor<'a> { } // Code and content blocks create a scope. - Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { + Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => { self.internal.enter(); for child in node.children() { self.visit(child); @@ -516,7 +516,7 @@ impl<'a> CapturesVisitor<'a> { // A let expression contains a binding, but that binding is only // active after the body is evaluated. - Some(ast::Expr::Let(expr)) => { + Some(ast::Expr::LetBinding(expr)) => { if let Some(init) = expr.init() { self.visit(init.to_untyped()); } @@ -529,7 +529,7 @@ impl<'a> CapturesVisitor<'a> { // A for loop contains one or two bindings in its pattern. These are // active after the iterable is evaluated but before the body is // evaluated. - Some(ast::Expr::For(expr)) => { + Some(ast::Expr::ForLoop(expr)) => { self.visit(expr.iterable().to_untyped()); self.internal.enter(); @@ -544,7 +544,7 @@ impl<'a> CapturesVisitor<'a> { // An import contains items, but these are active only after the // path is evaluated. - Some(ast::Expr::Import(expr)) => { + Some(ast::Expr::ModuleImport(expr)) => { self.visit(expr.source().to_untyped()); if let Some(ast::Imports::Items(items)) = expr.imports() { for item in items.iter() { diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index a7b6b6f90..9078418e4 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -30,7 +30,7 @@ fn eval_code<'a>( while let Some(expr) = exprs.next() { let span = expr.span(); let value = match expr { - ast::Expr::Set(set) => { + ast::Expr::SetRule(set) => { let styles = set.eval(vm)?; if vm.flow.is_some() { break; @@ -39,7 +39,7 @@ fn eval_code<'a>( let tail = eval_code(vm, exprs)?.display(); Value::Content(tail.styled_with_map(styles)) } - ast::Expr::Show(show) => { + ast::Expr::ShowRule(show) => { let recipe = show.eval(vm)?; if vm.flow.is_some() { break; @@ -94,9 +94,9 @@ impl Eval for ast::Expr<'_> { Self::Label(v) => v.eval(vm), Self::Ref(v) => v.eval(vm).map(Value::Content), Self::Heading(v) => v.eval(vm).map(Value::Content), - Self::List(v) => v.eval(vm).map(Value::Content), - Self::Enum(v) => v.eval(vm).map(Value::Content), - Self::Term(v) => v.eval(vm).map(Value::Content), + Self::ListItem(v) => v.eval(vm).map(Value::Content), + Self::EnumItem(v) => v.eval(vm).map(Value::Content), + Self::TermItem(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), @@ -116,8 +116,8 @@ impl Eval for ast::Expr<'_> { Self::Float(v) => v.eval(vm), Self::Numeric(v) => v.eval(vm), Self::Str(v) => v.eval(vm), - Self::Code(v) => v.eval(vm), - Self::Content(v) => v.eval(vm).map(Value::Content), + Self::CodeBlock(v) => v.eval(vm), + Self::ContentBlock(v) => v.eval(vm).map(Value::Content), Self::Array(v) => v.eval(vm).map(Value::Array), Self::Dict(v) => v.eval(vm).map(Value::Dict), Self::Parenthesized(v) => v.eval(vm), @@ -126,19 +126,19 @@ impl Eval for ast::Expr<'_> { Self::Closure(v) => v.eval(vm), Self::Unary(v) => v.eval(vm), Self::Binary(v) => v.eval(vm), - Self::Let(v) => v.eval(vm), - Self::DestructAssign(v) => v.eval(vm), - Self::Set(_) => bail!(forbidden("set")), - Self::Show(_) => bail!(forbidden("show")), + Self::LetBinding(v) => v.eval(vm), + Self::DestructAssignment(v) => v.eval(vm), + Self::SetRule(_) => bail!(forbidden("set")), + Self::ShowRule(_) => bail!(forbidden("show")), Self::Contextual(v) => v.eval(vm).map(Value::Content), Self::Conditional(v) => v.eval(vm), - Self::While(v) => v.eval(vm), - Self::For(v) => v.eval(vm), - Self::Import(v) => v.eval(vm), - Self::Include(v) => v.eval(vm).map(Value::Content), - Self::Break(v) => v.eval(vm), - Self::Continue(v) => v.eval(vm), - Self::Return(v) => v.eval(vm), + Self::WhileLoop(v) => v.eval(vm), + Self::ForLoop(v) => v.eval(vm), + Self::ModuleImport(v) => v.eval(vm), + Self::ModuleInclude(v) => v.eval(vm).map(Value::Content), + Self::LoopBreak(v) => v.eval(vm), + Self::LoopContinue(v) => v.eval(vm), + Self::FuncReturn(v) => v.eval(vm), }? .spanned(span); diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs index 3a5ebe1fc..5beefa912 100644 --- a/crates/typst-eval/src/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -33,7 +33,7 @@ fn eval_markup<'a>( while let Some(expr) = exprs.next() { match expr { - ast::Expr::Set(set) => { + ast::Expr::SetRule(set) => { let styles = set.eval(vm)?; if vm.flow.is_some() { break; @@ -41,7 +41,7 @@ fn eval_markup<'a>( seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) } - ast::Expr::Show(show) => { + ast::Expr::ShowRule(show) => { let recipe = show.eval(vm)?; if vm.flow.is_some() { break; diff --git a/crates/typst-eval/src/rules.rs b/crates/typst-eval/src/rules.rs index 646354d4b..f4c1563f3 100644 --- a/crates/typst-eval/src/rules.rs +++ b/crates/typst-eval/src/rules.rs @@ -45,7 +45,7 @@ impl Eval for ast::ShowRule<'_> { let transform = self.transform(); let transform = match transform { - ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?), + ast::Expr::SetRule(set) => Transformation::Style(set.eval(vm)?), expr => expr.eval(vm)?.cast::().at(transform.span())?, }; diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index e3d777115..91fa53f9a 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -517,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { // "#import "path.typ": a, b, |". if_chain! { if let Some(prev) = ctx.leaf.prev_sibling(); - if let Some(ast::Expr::Import(import)) = prev.get().cast(); + if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast(); if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(source) = prev.children().find(|child| child.is::()); then { @@ -536,7 +536,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { if let Some(grand) = parent.parent(); if grand.kind() == SyntaxKind::ImportItems; if let Some(great) = grand.parent(); - if let Some(ast::Expr::Import(import)) = great.get().cast(); + if let Some(ast::Expr::ModuleImport(import)) = great.get().cast(); if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(source) = great.children().find(|child| child.is::()); then { @@ -677,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool { if let Some(args) = parent.get().cast::(); if let Some(grand) = parent.parent(); if let Some(expr) = grand.get().cast::(); - let set = matches!(expr, ast::Expr::Set(_)); + let set = matches!(expr, ast::Expr::SetRule(_)); if let Some(callee) = match expr { ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), + ast::Expr::SetRule(set) => Some(set.target()), _ => None, }; then { diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index 270d2f43c..93fdc5dd5 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -232,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option> { ast::Expr::FuncCall(call) => { DerefTarget::Callee(expr_node.find(call.callee().span())?) } - ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?), + ast::Expr::SetRule(set) => { + DerefTarget::Callee(expr_node.find(set.target().span())?) + } ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => { DerefTarget::VarAccess(expr_node) } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index cfb977733..cbfffe530 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -201,7 +201,7 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option(); if let Some(ast::Expr::Ident(callee)) = match expr { ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), + ast::Expr::SetRule(set) => Some(set.target()), _ => None, }; diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 640138e77..f79e65982 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -1,6 +1,81 @@ -//! A typed layer over the untyped syntax tree. -//! -//! The AST is rooted in the [`Markup`] node. +/*! +# Abstract Syntax Tree Interface + +Typst's Abstract Syntax Tree (AST) is a lazy, typed view over the untyped +Concrete Syntax Tree (CST) and is rooted in the [`Markup`] node. + +## The AST is a View + +Most AST nodes are wrapper structs around [`SyntaxNode`] pointers. This summary +will use a running example of the [`Raw`] node type, which is declared (after +macro expansion) as: `struct Raw<'a>(&'a SyntaxNode);`. + +[`SyntaxNode`]s are generated by the parser and constitute the Concrete Syntax +Tree (CST). The CST is _concrete_ because it has the property that an in-order +tree traversal will recreate the text of the source file exactly. + +[`SyntaxNode`]s in the CST contain their [`SyntaxKind`], but don't themselves +provide access to the semantic meaning of their contents. That semantic meaning +is available through the Abstract Syntax Tree by iterating over CST nodes and +inspecting their contents. The format is prepared ahead-of-time by the parser so +that this module can unpack the abstract meaning from the CST's structure. + +Raw nodes are parsed by recognizing paired backtick delimiters, which you will +find as CST nodes with the [`RawDelim`] kind. However, the AST doesn't include +these delimiters because it _abstracts_ over the backticks. Instead, the parent +raw node will only use its child [`RawDelim`] CST nodes to determine whether the +element is a block or inline. + +## The AST is Typed + +AST nodes all implement the [`AstNode`] trait, but nodes can also implement +their own unique methods. These unique methods are the "real" interface of the +AST, and provide access to the abstract, semantic, representation of each kind +of node. For example, the [`Raw`] node provides 3 methods that specify its +abstract representation: [`Raw::lines()`] returns the raw text as an iterator of +lines, [`Raw::lang()`] provides the optionally present [`RawLang`] language tag, +and [`Raw::block()`] gives a bool for whether the raw element is a block or +inline. + +This semantic information is unavailable in the CST. Only by converting a CST +node to an AST struct will Rust let you call a method of that struct. This is a +safe interface because the only way to create an AST node outside this file is +to call [`AstNode::from_untyped`]. The `node!` macro implements `from_untyped` +by checking the node's kind before constructing it, returning `Some()` only if +the kind matches. So we know that it will have the expected children underneath, +otherwise the parser wouldn't have produced this node. + +## The AST is rooted in the [`Markup`] node + +The AST is rooted in the [`Markup`] node, which provides only one method: +[`Markup::exprs`]. This returns an iterator of the main [`Expr`] enum. [`Expr`] +is important because it contains the majority of expressions that Typst will +evaluate. Not just markup, but also math and code expressions. Not all +expression types are available from the parser at every step, but this does +decrease the amount of wrapper enums needed in the AST (and this file is long +enough already). + +Expressions also branch off into the remaining tree. You can view enums in this +file as edges on a graph: areas where the tree has paths from one type to +another (accessed through methods), then structs are the nodes of the graph, +providing methods that return enums, etc. etc. + +## The AST is Lazy + +Being lazy means that the untyped CST nodes are converted to typed AST nodes +only as the tree is traversed. If we parse a file and a raw block is contained +in a branch of an if-statement that we don't take, then we won't pay the cost of +creating an iterator over the lines or checking whether it was a block or +inline (although it will still be parsed into nodes). + +This is also a factor of the current "tree-interpreter" evaluation model. A +bytecode interpreter might instead eagerly convert the AST into bytecode, but it +would still traverse using this lazy interface. While the tree-interpreter +evaluation is straightforward and easy to add new features onto, it has to +re-traverse the AST every time a function is evaluated. A bytecode interpreter +using the lazy interface would only need to traverse each node once, improving +throughput at the cost of initial latency and development flexibility. +*/ use std::num::NonZeroUsize; use std::ops::Deref; @@ -27,8 +102,55 @@ pub trait AstNode<'a>: Sized { } } +// A generic interface for converting untyped nodes into typed AST nodes. +impl SyntaxNode { + /// Whether the node can be cast to the given AST node. + pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool { + self.cast::().is_some() + } + + /// Try to convert the node to a typed AST node. + pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option { + T::from_untyped(self) + } + + /// Find the first child that can cast to the AST type `T`. + fn try_cast_first<'a, T: AstNode<'a>>(&'a self) -> Option { + self.children().find_map(Self::cast) + } + + /// Find the last child that can cast to the AST type `T`. + fn try_cast_last<'a, T: AstNode<'a>>(&'a self) -> Option { + self.children().rev().find_map(Self::cast) + } + + /// Get the first child of AST type `T` or a placeholder if none. + fn cast_first<'a, T: AstNode<'a> + Default>(&'a self) -> T { + self.try_cast_first().unwrap_or_default() + } + + /// Get the last child of AST type `T` or a placeholder if none. + fn cast_last<'a, T: AstNode<'a> + Default>(&'a self) -> T { + self.try_cast_last().unwrap_or_default() + } +} + +/// Implements [`AstNode`] for a struct whose name matches a [`SyntaxKind`] +/// variant. +/// +/// The struct becomes a wrapper around a [`SyntaxNode`] pointer, and the +/// implementation of [`AstNode::from_untyped`] checks that the pointer's kind +/// matches when converting, returning `Some` or `None` respectively. +/// +/// The generated struct is the basis for typed accessor methods for properties +/// of this AST node. For example, the [`Raw`] struct has methods for accessing +/// its content by lines, its optional language tag, and whether the raw element +/// is inline or a block. These methods are accessible only _after_ a +/// `SyntaxNode` is coerced to the `Raw` struct type (via `from_untyped`), +/// guaranteeing their implementations will work with the expected structure. macro_rules! node { - ($(#[$attr:meta])* $name:ident) => { + ($(#[$attr:meta])* struct $name:ident) => { + // Create the struct as a wrapper around a `SyntaxNode` reference. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[repr(transparent)] $(#[$attr])* @@ -63,7 +185,7 @@ macro_rules! node { node! { /// The syntactical root capable of representing a full parsed document. - Markup + struct Markup } impl<'a> Markup<'a> { @@ -117,11 +239,11 @@ pub enum Expr<'a> { /// A section heading: `= Introduction`. Heading(Heading<'a>), /// An item in a bullet list: `- ...`. - List(ListItem<'a>), + ListItem(ListItem<'a>), /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - Enum(EnumItem<'a>), + EnumItem(EnumItem<'a>), /// An item in a term list: `/ Term: Details`. - Term(TermItem<'a>), + TermItem(TermItem<'a>), /// A mathematical equation: `$x$`, `$ x^2 $`. Equation(Equation<'a>), /// The contents of a mathematical equation: `x^2 + 1`. @@ -161,9 +283,9 @@ pub enum Expr<'a> { /// A quoted string: `"..."`. Str(Str<'a>), /// A code block: `{ let x = 1; x + 2 }`. - Code(CodeBlock<'a>), + CodeBlock(CodeBlock<'a>), /// A content block: `[*Hi* there!]`. - Content(ContentBlock<'a>), + ContentBlock(ContentBlock<'a>), /// A grouped expression: `(1 + 2)`. Parenthesized(Parenthesized<'a>), /// An array: `(1, "hi", 12cm)`. @@ -181,37 +303,37 @@ pub enum Expr<'a> { /// A closure: `(x, y) => z`. Closure(Closure<'a>), /// A let binding: `let x = 1`. - Let(LetBinding<'a>), + LetBinding(LetBinding<'a>), /// A destructuring assignment: `(x, y) = (1, 2)`. - DestructAssign(DestructAssignment<'a>), + DestructAssignment(DestructAssignment<'a>), /// A set rule: `set text(...)`. - Set(SetRule<'a>), + SetRule(SetRule<'a>), /// A show rule: `show heading: it => emph(it.body)`. - Show(ShowRule<'a>), + ShowRule(ShowRule<'a>), /// A contextual expression: `context text.lang`. Contextual(Contextual<'a>), /// An if-else conditional: `if x { y } else { z }`. Conditional(Conditional<'a>), /// A while loop: `while x { y }`. - While(WhileLoop<'a>), + WhileLoop(WhileLoop<'a>), /// A for loop: `for x in y { z }`. - For(ForLoop<'a>), + ForLoop(ForLoop<'a>), /// A module import: `import "utils.typ": a, b, c`. - Import(ModuleImport<'a>), + ModuleImport(ModuleImport<'a>), /// A module include: `include "chapter1.typ"`. - Include(ModuleInclude<'a>), + ModuleInclude(ModuleInclude<'a>), /// A break from a loop: `break`. - Break(LoopBreak<'a>), + LoopBreak(LoopBreak<'a>), /// A continue in a loop: `continue`. - Continue(LoopContinue<'a>), + LoopContinue(LoopContinue<'a>), /// A return from a function: `return`, `return x + 1`. - Return(FuncReturn<'a>), + FuncReturn(FuncReturn<'a>), } impl<'a> Expr<'a> { fn cast_with_space(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Space => node.cast().map(Self::Space), + SyntaxKind::Space => Some(Self::Space(Space(node))), _ => Self::from_untyped(node), } } @@ -220,64 +342,69 @@ impl<'a> Expr<'a> { impl<'a> AstNode<'a> for Expr<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Linebreak => node.cast().map(Self::Linebreak), - SyntaxKind::Parbreak => node.cast().map(Self::Parbreak), - SyntaxKind::Text => node.cast().map(Self::Text), - SyntaxKind::Escape => node.cast().map(Self::Escape), - SyntaxKind::Shorthand => node.cast().map(Self::Shorthand), - SyntaxKind::SmartQuote => node.cast().map(Self::SmartQuote), - SyntaxKind::Strong => node.cast().map(Self::Strong), - SyntaxKind::Emph => node.cast().map(Self::Emph), - SyntaxKind::Raw => node.cast().map(Self::Raw), - SyntaxKind::Link => node.cast().map(Self::Link), - SyntaxKind::Label => node.cast().map(Self::Label), - SyntaxKind::Ref => node.cast().map(Self::Ref), - SyntaxKind::Heading => node.cast().map(Self::Heading), - SyntaxKind::ListItem => node.cast().map(Self::List), - SyntaxKind::EnumItem => node.cast().map(Self::Enum), - 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), - SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), - SyntaxKind::MathAttach => node.cast().map(Self::MathAttach), - SyntaxKind::MathPrimes => node.cast().map(Self::MathPrimes), - SyntaxKind::MathFrac => node.cast().map(Self::MathFrac), - SyntaxKind::MathRoot => node.cast().map(Self::MathRoot), - SyntaxKind::Ident => node.cast().map(Self::Ident), - SyntaxKind::None => node.cast().map(Self::None), - SyntaxKind::Auto => node.cast().map(Self::Auto), - SyntaxKind::Bool => node.cast().map(Self::Bool), - SyntaxKind::Int => node.cast().map(Self::Int), - SyntaxKind::Float => node.cast().map(Self::Float), - SyntaxKind::Numeric => node.cast().map(Self::Numeric), - SyntaxKind::Str => node.cast().map(Self::Str), - SyntaxKind::CodeBlock => node.cast().map(Self::Code), - SyntaxKind::ContentBlock => node.cast().map(Self::Content), - SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), - SyntaxKind::Array => node.cast().map(Self::Array), - SyntaxKind::Dict => node.cast().map(Self::Dict), - SyntaxKind::Unary => node.cast().map(Self::Unary), - SyntaxKind::Binary => node.cast().map(Self::Binary), - SyntaxKind::FieldAccess => node.cast().map(Self::FieldAccess), - SyntaxKind::FuncCall => node.cast().map(Self::FuncCall), - SyntaxKind::Closure => node.cast().map(Self::Closure), - SyntaxKind::LetBinding => node.cast().map(Self::Let), - SyntaxKind::DestructAssignment => node.cast().map(Self::DestructAssign), - SyntaxKind::SetRule => node.cast().map(Self::Set), - SyntaxKind::ShowRule => node.cast().map(Self::Show), - SyntaxKind::Contextual => node.cast().map(Self::Contextual), - SyntaxKind::Conditional => node.cast().map(Self::Conditional), - SyntaxKind::WhileLoop => node.cast().map(Self::While), - SyntaxKind::ForLoop => node.cast().map(Self::For), - SyntaxKind::ModuleImport => node.cast().map(Self::Import), - SyntaxKind::ModuleInclude => node.cast().map(Self::Include), - SyntaxKind::LoopBreak => node.cast().map(Self::Break), - SyntaxKind::LoopContinue => node.cast().map(Self::Continue), - SyntaxKind::FuncReturn => node.cast().map(Self::Return), + SyntaxKind::Space => Option::None, // Skipped unless using `cast_with_space`. + SyntaxKind::Linebreak => Some(Self::Linebreak(Linebreak(node))), + SyntaxKind::Parbreak => Some(Self::Parbreak(Parbreak(node))), + SyntaxKind::Text => Some(Self::Text(Text(node))), + SyntaxKind::Escape => Some(Self::Escape(Escape(node))), + SyntaxKind::Shorthand => Some(Self::Shorthand(Shorthand(node))), + SyntaxKind::SmartQuote => Some(Self::SmartQuote(SmartQuote(node))), + SyntaxKind::Strong => Some(Self::Strong(Strong(node))), + SyntaxKind::Emph => Some(Self::Emph(Emph(node))), + SyntaxKind::Raw => Some(Self::Raw(Raw(node))), + SyntaxKind::Link => Some(Self::Link(Link(node))), + SyntaxKind::Label => Some(Self::Label(Label(node))), + SyntaxKind::Ref => Some(Self::Ref(Ref(node))), + SyntaxKind::Heading => Some(Self::Heading(Heading(node))), + SyntaxKind::ListItem => Some(Self::ListItem(ListItem(node))), + SyntaxKind::EnumItem => Some(Self::EnumItem(EnumItem(node))), + SyntaxKind::TermItem => Some(Self::TermItem(TermItem(node))), + SyntaxKind::Equation => Some(Self::Equation(Equation(node))), + SyntaxKind::Math => Some(Self::Math(Math(node))), + SyntaxKind::MathText => Some(Self::MathText(MathText(node))), + SyntaxKind::MathIdent => Some(Self::MathIdent(MathIdent(node))), + SyntaxKind::MathShorthand => Some(Self::MathShorthand(MathShorthand(node))), + SyntaxKind::MathAlignPoint => { + Some(Self::MathAlignPoint(MathAlignPoint(node))) + } + SyntaxKind::MathDelimited => Some(Self::MathDelimited(MathDelimited(node))), + SyntaxKind::MathAttach => Some(Self::MathAttach(MathAttach(node))), + SyntaxKind::MathPrimes => Some(Self::MathPrimes(MathPrimes(node))), + SyntaxKind::MathFrac => Some(Self::MathFrac(MathFrac(node))), + SyntaxKind::MathRoot => Some(Self::MathRoot(MathRoot(node))), + SyntaxKind::Ident => Some(Self::Ident(Ident(node))), + SyntaxKind::None => Some(Self::None(None(node))), + SyntaxKind::Auto => Some(Self::Auto(Auto(node))), + SyntaxKind::Bool => Some(Self::Bool(Bool(node))), + SyntaxKind::Int => Some(Self::Int(Int(node))), + SyntaxKind::Float => Some(Self::Float(Float(node))), + SyntaxKind::Numeric => Some(Self::Numeric(Numeric(node))), + SyntaxKind::Str => Some(Self::Str(Str(node))), + SyntaxKind::CodeBlock => Some(Self::CodeBlock(CodeBlock(node))), + SyntaxKind::ContentBlock => Some(Self::ContentBlock(ContentBlock(node))), + SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))), + SyntaxKind::Array => Some(Self::Array(Array(node))), + SyntaxKind::Dict => Some(Self::Dict(Dict(node))), + SyntaxKind::Unary => Some(Self::Unary(Unary(node))), + SyntaxKind::Binary => Some(Self::Binary(Binary(node))), + SyntaxKind::FieldAccess => Some(Self::FieldAccess(FieldAccess(node))), + SyntaxKind::FuncCall => Some(Self::FuncCall(FuncCall(node))), + SyntaxKind::Closure => Some(Self::Closure(Closure(node))), + SyntaxKind::LetBinding => Some(Self::LetBinding(LetBinding(node))), + SyntaxKind::DestructAssignment => { + Some(Self::DestructAssignment(DestructAssignment(node))) + } + SyntaxKind::SetRule => Some(Self::SetRule(SetRule(node))), + SyntaxKind::ShowRule => Some(Self::ShowRule(ShowRule(node))), + SyntaxKind::Contextual => Some(Self::Contextual(Contextual(node))), + SyntaxKind::Conditional => Some(Self::Conditional(Conditional(node))), + SyntaxKind::WhileLoop => Some(Self::WhileLoop(WhileLoop(node))), + SyntaxKind::ForLoop => Some(Self::ForLoop(ForLoop(node))), + SyntaxKind::ModuleImport => Some(Self::ModuleImport(ModuleImport(node))), + SyntaxKind::ModuleInclude => Some(Self::ModuleInclude(ModuleInclude(node))), + SyntaxKind::LoopBreak => Some(Self::LoopBreak(LoopBreak(node))), + SyntaxKind::LoopContinue => Some(Self::LoopContinue(LoopContinue(node))), + SyntaxKind::FuncReturn => Some(Self::FuncReturn(FuncReturn(node))), _ => Option::None, } } @@ -298,9 +425,9 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Label(v) => v.to_untyped(), Self::Ref(v) => v.to_untyped(), Self::Heading(v) => v.to_untyped(), - Self::List(v) => v.to_untyped(), - Self::Enum(v) => v.to_untyped(), - Self::Term(v) => v.to_untyped(), + Self::ListItem(v) => v.to_untyped(), + Self::EnumItem(v) => v.to_untyped(), + Self::TermItem(v) => v.to_untyped(), Self::Equation(v) => v.to_untyped(), Self::Math(v) => v.to_untyped(), Self::MathText(v) => v.to_untyped(), @@ -320,8 +447,8 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Float(v) => v.to_untyped(), Self::Numeric(v) => v.to_untyped(), Self::Str(v) => v.to_untyped(), - Self::Code(v) => v.to_untyped(), - Self::Content(v) => v.to_untyped(), + Self::CodeBlock(v) => v.to_untyped(), + Self::ContentBlock(v) => v.to_untyped(), Self::Array(v) => v.to_untyped(), Self::Dict(v) => v.to_untyped(), Self::Parenthesized(v) => v.to_untyped(), @@ -330,19 +457,19 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::FieldAccess(v) => v.to_untyped(), Self::FuncCall(v) => v.to_untyped(), Self::Closure(v) => v.to_untyped(), - Self::Let(v) => v.to_untyped(), - Self::DestructAssign(v) => v.to_untyped(), - Self::Set(v) => v.to_untyped(), - Self::Show(v) => v.to_untyped(), + Self::LetBinding(v) => v.to_untyped(), + Self::DestructAssignment(v) => v.to_untyped(), + Self::SetRule(v) => v.to_untyped(), + Self::ShowRule(v) => v.to_untyped(), Self::Contextual(v) => v.to_untyped(), Self::Conditional(v) => v.to_untyped(), - Self::While(v) => v.to_untyped(), - Self::For(v) => v.to_untyped(), - Self::Import(v) => v.to_untyped(), - Self::Include(v) => v.to_untyped(), - Self::Break(v) => v.to_untyped(), - Self::Continue(v) => v.to_untyped(), - Self::Return(v) => v.to_untyped(), + Self::WhileLoop(v) => v.to_untyped(), + Self::ForLoop(v) => v.to_untyped(), + Self::ModuleImport(v) => v.to_untyped(), + Self::ModuleInclude(v) => v.to_untyped(), + Self::LoopBreak(v) => v.to_untyped(), + Self::LoopContinue(v) => v.to_untyped(), + Self::FuncReturn(v) => v.to_untyped(), } } } @@ -360,25 +487,25 @@ impl Expr<'_> { | Self::Float(_) | Self::Numeric(_) | Self::Str(_) - | Self::Code(_) - | Self::Content(_) + | Self::CodeBlock(_) + | Self::ContentBlock(_) | Self::Array(_) | Self::Dict(_) | Self::Parenthesized(_) | Self::FieldAccess(_) | Self::FuncCall(_) - | Self::Let(_) - | Self::Set(_) - | Self::Show(_) + | Self::LetBinding(_) + | Self::SetRule(_) + | Self::ShowRule(_) | Self::Contextual(_) | Self::Conditional(_) - | Self::While(_) - | Self::For(_) - | Self::Import(_) - | Self::Include(_) - | Self::Break(_) - | Self::Continue(_) - | Self::Return(_) + | Self::WhileLoop(_) + | Self::ForLoop(_) + | Self::ModuleImport(_) + | Self::ModuleInclude(_) + | Self::LoopBreak(_) + | Self::LoopContinue(_) + | Self::FuncReturn(_) ) } @@ -405,7 +532,7 @@ impl Default for Expr<'_> { node! { /// Plain text without markup. - Text + struct Text } impl<'a> Text<'a> { @@ -418,22 +545,22 @@ impl<'a> Text<'a> { node! { /// Whitespace in markup or math. Has at most one newline in markup, as more /// indicate a paragraph break. - Space + struct Space } node! { /// A forced line break: `\`. - Linebreak + struct Linebreak } node! { /// A paragraph break, indicated by one or multiple blank lines. - Parbreak + struct Parbreak } node! { /// An escape sequence: `\#`, `\u{1F5FA}`. - Escape + struct Escape } impl Escape<'_> { @@ -456,7 +583,7 @@ impl Escape<'_> { node! { /// A shorthand for a unicode codepoint. For example, `~` for a non-breaking /// space or `-?` for a soft hyphen. - Shorthand + struct Shorthand } impl Shorthand<'_> { @@ -482,7 +609,7 @@ impl Shorthand<'_> { node! { /// A smart quote: `'` or `"`. - SmartQuote + struct SmartQuote } impl SmartQuote<'_> { @@ -494,31 +621,31 @@ impl SmartQuote<'_> { node! { /// Strong content: `*Strong*`. - Strong + struct Strong } impl<'a> Strong<'a> { /// The contents of the strong node. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// Emphasized content: `_Emphasized_`. - Emph + struct Emph } impl<'a> Emph<'a> { /// The contents of the emphasis node. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// Raw text with optional syntax highlighting: `` `...` ``. - Raw + struct Raw } impl<'a> Raw<'a> { @@ -530,18 +657,18 @@ impl<'a> Raw<'a> { /// An optional identifier specifying the language to syntax-highlight in. pub fn lang(self) -> Option> { // Only blocky literals are supposed to contain a language. - let delim: RawDelim = self.0.cast_first_match()?; + let delim: RawDelim = self.0.try_cast_first()?; if delim.0.len() < 3 { return Option::None; } - self.0.cast_first_match() + self.0.try_cast_first() } /// Whether the raw text should be displayed in a separate block. pub fn block(self) -> bool { self.0 - .cast_first_match() + .try_cast_first() .is_some_and(|delim: RawDelim| delim.0.len() >= 3) && self.0.children().any(|e| { e.kind() == SyntaxKind::RawTrimmed && e.text().chars().any(is_newline) @@ -551,7 +678,7 @@ impl<'a> Raw<'a> { node! { /// A language tag at the start of raw element: ``typ ``. - RawLang + struct RawLang } impl<'a> RawLang<'a> { @@ -563,12 +690,12 @@ impl<'a> RawLang<'a> { node! { /// A raw delimiter in single or 3+ backticks: `` ` ``. - RawDelim + struct RawDelim } node! { /// A hyperlink: `https://typst.org`. - Link + struct Link } impl<'a> Link<'a> { @@ -580,7 +707,7 @@ impl<'a> Link<'a> { node! { /// A label: ``. - Label + struct Label } impl<'a> Label<'a> { @@ -592,7 +719,7 @@ impl<'a> Label<'a> { node! { /// A reference: `@target`, `@target[..]`. - Ref + struct Ref } impl<'a> Ref<'a> { @@ -607,19 +734,19 @@ impl<'a> Ref<'a> { /// Get the supplement. pub fn supplement(self) -> Option> { - self.0.cast_last_match() + self.0.try_cast_last() } } node! { /// A section heading: `= Introduction`. - Heading + struct Heading } impl<'a> Heading<'a> { /// The contents of the heading. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The section depth (number of equals signs). @@ -634,19 +761,19 @@ impl<'a> Heading<'a> { node! { /// An item in a bullet list: `- ...`. - ListItem + struct ListItem } impl<'a> ListItem<'a> { /// The contents of the list item. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - EnumItem + struct EnumItem } impl<'a> EnumItem<'a> { @@ -660,36 +787,36 @@ impl<'a> EnumItem<'a> { /// The contents of the list item. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An item in a term list: `/ Term: Details`. - TermItem + struct TermItem } impl<'a> TermItem<'a> { /// The term described by the item. pub fn term(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The description of the term. pub fn description(self) -> Markup<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A mathematical equation: `$x$`, `$ x^2 $`. - Equation + struct Equation } impl<'a> Equation<'a> { /// The contained math. pub fn body(self) -> Math<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// Whether the equation should be displayed as a separate block. @@ -703,7 +830,7 @@ impl<'a> Equation<'a> { node! { /// The contents of a mathematical equation: `x^2 + 1`. - Math + struct Math } impl<'a> Math<'a> { @@ -715,7 +842,7 @@ impl<'a> Math<'a> { node! { /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. - MathText + struct MathText } /// The underlying text kind. @@ -743,7 +870,7 @@ impl<'a> MathText<'a> { node! { /// An identifier in math: `pi`. - MathIdent + struct MathIdent } impl<'a> MathIdent<'a> { @@ -770,7 +897,7 @@ impl Deref for MathIdent<'_> { node! { /// A shorthand for a unicode codepoint in math: `a <= b`. - MathShorthand + struct MathShorthand } impl MathShorthand<'_> { @@ -828,40 +955,40 @@ impl MathShorthand<'_> { node! { /// An alignment point in math: `&`. - MathAlignPoint + struct MathAlignPoint } node! { /// Matched delimiters in math: `[x + y]`. - MathDelimited + struct MathDelimited } impl<'a> MathDelimited<'a> { /// The opening delimiter. pub fn open(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The contents, including the delimiters. pub fn body(self) -> Math<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The closing delimiter. pub fn close(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A base with optional attachments in math: `a_1^2`. - MathAttach + struct MathAttach } impl<'a> MathAttach<'a> { /// The base, to which things are attached. pub fn base(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The bottom attachment. @@ -892,7 +1019,7 @@ impl<'a> MathAttach<'a> { node! { /// Grouped primes in math: `a'''`. - MathPrimes + struct MathPrimes } impl MathPrimes<'_> { @@ -907,24 +1034,24 @@ impl MathPrimes<'_> { node! { /// A fraction in math: `x/2` - MathFrac + struct MathFrac } impl<'a> MathFrac<'a> { /// The numerator. pub fn num(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The denominator. pub fn denom(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A root in math: `√x`, `∛x` or `∜x`. - MathRoot + struct MathRoot } impl<'a> MathRoot<'a> { @@ -940,13 +1067,13 @@ impl<'a> MathRoot<'a> { /// The radicand. pub fn radicand(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An identifier: `it`. - Ident + struct Ident } impl<'a> Ident<'a> { @@ -973,17 +1100,17 @@ impl Deref for Ident<'_> { node! { /// The `none` literal. - None + struct None } node! { /// The `auto` literal. - Auto + struct Auto } node! { /// A boolean: `true`, `false`. - Bool + struct Bool } impl Bool<'_> { @@ -995,7 +1122,7 @@ impl Bool<'_> { node! { /// An integer: `120`. - Int + struct Int } impl Int<'_> { @@ -1017,7 +1144,7 @@ impl Int<'_> { node! { /// A floating-point number: `1.2`, `10e-4`. - Float + struct Float } impl Float<'_> { @@ -1029,7 +1156,7 @@ impl Float<'_> { node! { /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. - Numeric + struct Numeric } impl Numeric<'_> { @@ -1086,7 +1213,7 @@ pub enum Unit { node! { /// A quoted string: `"..."`. - Str + struct Str } impl Str<'_> { @@ -1136,19 +1263,19 @@ impl Str<'_> { node! { /// A code block: `{ let x = 1; x + 2 }`. - CodeBlock + struct CodeBlock } impl<'a> CodeBlock<'a> { /// The contained code. pub fn body(self) -> Code<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// The body of a code block. - Code + struct Code } impl<'a> Code<'a> { @@ -1160,19 +1287,19 @@ impl<'a> Code<'a> { node! { /// A content block: `[*Hi* there!]`. - ContentBlock + struct ContentBlock } impl<'a> ContentBlock<'a> { /// The contained markup. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// A grouped expression: `(1 + 2)`. - Parenthesized + struct Parenthesized } impl<'a> Parenthesized<'a> { @@ -1180,20 +1307,20 @@ impl<'a> Parenthesized<'a> { /// /// Should only be accessed if this is contained in an `Expr`. pub fn expr(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The wrapped pattern. /// /// Should only be accessed if this is contained in a `Pattern`. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An array: `(1, "hi", 12cm)`. - Array + struct Array } impl<'a> Array<'a> { @@ -1215,7 +1342,7 @@ pub enum ArrayItem<'a> { impl<'a> AstNode<'a> for ArrayItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1230,7 +1357,7 @@ impl<'a> AstNode<'a> for ArrayItem<'a> { node! { /// A dictionary: `(thickness: 3pt, dash: "solid")`. - Dict + struct Dict } impl<'a> Dict<'a> { @@ -1254,9 +1381,9 @@ pub enum DictItem<'a> { impl<'a> AstNode<'a> for DictItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Keyed => node.cast().map(Self::Keyed), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Keyed => Some(Self::Keyed(Keyed(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => Option::None, } } @@ -1272,13 +1399,13 @@ impl<'a> AstNode<'a> for DictItem<'a> { node! { /// A named pair: `thickness: 3pt`. - Named + struct Named } impl<'a> Named<'a> { /// The name: `thickness`. pub fn name(self) -> Ident<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the pair: `3pt`. @@ -1286,7 +1413,7 @@ impl<'a> Named<'a> { /// This should only be accessed if this `Named` is contained in a /// `DictItem`, `Arg`, or `Param`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } /// The right-hand side of the pair as a pattern. @@ -1294,19 +1421,19 @@ impl<'a> Named<'a> { /// This should only be accessed if this `Named` is contained in a /// `Destructuring`. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A keyed pair: `"spacy key": true`. - Keyed + struct Keyed } impl<'a> Keyed<'a> { /// The key: `"spacy key"`. pub fn key(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the pair: `true`. @@ -1314,13 +1441,13 @@ impl<'a> Keyed<'a> { /// This should only be accessed if this `Keyed` is contained in a /// `DictItem`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A spread: `..x` or `..x.at(0)`. - Spread + struct Spread } impl<'a> Spread<'a> { @@ -1329,7 +1456,7 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in an /// `ArrayItem`, `DictItem`, or `Arg`. pub fn expr(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The sink identifier, if present. @@ -1337,7 +1464,7 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in a /// `Param` or binding `DestructuringItem`. pub fn sink_ident(self) -> Option> { - self.0.cast_first_match() + self.0.try_cast_first() } /// The sink expressions, if present. @@ -1345,13 +1472,13 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in a /// `DestructuringItem`. pub fn sink_expr(self) -> Option> { - self.0.cast_first_match() + self.0.try_cast_first() } } node! { /// A unary operation: `-x`. - Unary + struct Unary } impl<'a> Unary<'a> { @@ -1365,7 +1492,7 @@ impl<'a> Unary<'a> { /// The expression to operate on: `x`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } @@ -1411,7 +1538,7 @@ impl UnOp { node! { /// A binary operation: `a + b`. - Binary + struct Binary } impl<'a> Binary<'a> { @@ -1433,12 +1560,12 @@ impl<'a> Binary<'a> { /// The left-hand side of the operation: `a`. pub fn lhs(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the operation: `b`. pub fn rhs(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } @@ -1598,41 +1725,41 @@ pub enum Assoc { node! { /// A field access: `properties.age`. - FieldAccess + struct FieldAccess } impl<'a> FieldAccess<'a> { /// The expression to access the field on. pub fn target(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The name of the field. pub fn field(self) -> Ident<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// An invocation of a function or method: `f(x, y)`. - FuncCall + struct FuncCall } impl<'a> FuncCall<'a> { /// The function to call. pub fn callee(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The arguments to the function. pub fn args(self) -> Args<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A function call's argument list: `(12pt, y)`. - Args + struct Args } impl<'a> Args<'a> { @@ -1666,8 +1793,8 @@ pub enum Arg<'a> { impl<'a> AstNode<'a> for Arg<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1683,7 +1810,7 @@ impl<'a> AstNode<'a> for Arg<'a> { node! { /// A closure: `(x, y) => z`. - Closure + struct Closure } impl<'a> Closure<'a> { @@ -1696,18 +1823,18 @@ impl<'a> Closure<'a> { /// The parameter bindings. pub fn params(self) -> Params<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The body of the closure. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A closure's parameters: `(x, y)`. - Params + struct Params } impl<'a> Params<'a> { @@ -1731,8 +1858,8 @@ pub enum Param<'a> { impl<'a> AstNode<'a> for Param<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1762,9 +1889,9 @@ pub enum Pattern<'a> { impl<'a> AstNode<'a> for Pattern<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Underscore => node.cast().map(Self::Placeholder), - SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), - SyntaxKind::Destructuring => node.cast().map(Self::Destructuring), + SyntaxKind::Underscore => Some(Self::Placeholder(Underscore(node))), + SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))), + SyntaxKind::Destructuring => Some(Self::Destructuring(Destructuring(node))), _ => node.cast().map(Self::Normal), } } @@ -1799,12 +1926,12 @@ impl Default for Pattern<'_> { node! { /// An underscore: `_` - Underscore + struct Underscore } node! { /// A destructuring pattern: `x` or `(x, _, ..y)`. - Destructuring + struct Destructuring } impl<'a> Destructuring<'a> { @@ -1841,8 +1968,8 @@ pub enum DestructuringItem<'a> { impl<'a> AstNode<'a> for DestructuringItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pattern), } } @@ -1858,7 +1985,7 @@ impl<'a> AstNode<'a> for DestructuringItem<'a> { node! { /// A let binding: `let x = 1`. - LetBinding + struct LetBinding } /// The kind of a let binding, either a normal one or a closure. @@ -1883,11 +2010,11 @@ impl<'a> LetBindingKind<'a> { impl<'a> LetBinding<'a> { /// The kind of the let binding. pub fn kind(self) -> LetBindingKind<'a> { - match self.0.cast_first_match::() { - Some(Pattern::Normal(Expr::Closure(closure))) => { + match self.0.cast_first() { + Pattern::Normal(Expr::Closure(closure)) => { LetBindingKind::Closure(closure.name().unwrap_or_default()) } - pattern => LetBindingKind::Normal(pattern.unwrap_or_default()), + pattern => LetBindingKind::Normal(pattern), } } @@ -1897,43 +2024,43 @@ impl<'a> LetBinding<'a> { LetBindingKind::Normal(Pattern::Normal(_) | Pattern::Parenthesized(_)) => { self.0.children().filter_map(SyntaxNode::cast).nth(1) } - LetBindingKind::Normal(_) => self.0.cast_first_match(), - LetBindingKind::Closure(_) => self.0.cast_first_match(), + LetBindingKind::Normal(_) => self.0.try_cast_first(), + LetBindingKind::Closure(_) => self.0.try_cast_first(), } } } node! { /// An assignment expression `(x, y) = (1, 2)`. - DestructAssignment + struct DestructAssignment } impl<'a> DestructAssignment<'a> { /// The pattern of the assignment. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match::().unwrap_or_default() + self.0.cast_first() } /// The expression that is assigned. pub fn value(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A set rule: `set text(...)`. - SetRule + struct SetRule } impl<'a> SetRule<'a> { /// The function to set style properties for. pub fn target(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The style properties to set. pub fn args(self) -> Args<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } /// A condition under which the set rule applies. @@ -1947,7 +2074,7 @@ impl<'a> SetRule<'a> { node! { /// A show rule: `show heading: it => emph(it.body)`. - ShowRule + struct ShowRule } impl<'a> ShowRule<'a> { @@ -1962,31 +2089,31 @@ impl<'a> ShowRule<'a> { /// The transformation recipe. pub fn transform(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A contextual expression: `context text.lang`. - Contextual + struct Contextual } impl<'a> Contextual<'a> { /// The expression which depends on the context. pub fn body(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An if-else conditional: `if x { y } else { z }`. - Conditional + struct Conditional } impl<'a> Conditional<'a> { /// The condition which selects the body to evaluate. pub fn condition(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to evaluate if the condition is true. @@ -2006,30 +2133,30 @@ impl<'a> Conditional<'a> { node! { /// A while loop: `while x { y }`. - WhileLoop + struct WhileLoop } impl<'a> WhileLoop<'a> { /// The condition which selects whether to evaluate the body. pub fn condition(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to evaluate while the condition is true. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A for loop: `for x in y { z }`. - ForLoop + struct ForLoop } impl<'a> ForLoop<'a> { /// The pattern to assign to. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to iterate over. @@ -2043,19 +2170,19 @@ impl<'a> ForLoop<'a> { /// The expression to evaluate for each iteration. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A module import: `import "utils.typ": a, b, c`. - ModuleImport + struct ModuleImport } impl<'a> ModuleImport<'a> { /// The module or path from which the items should be imported. pub fn source(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The items to be imported. @@ -2135,7 +2262,7 @@ pub enum Imports<'a> { node! { /// Items to import from a module: `a, b, c`. - ImportItems + struct ImportItems } impl<'a> ImportItems<'a> { @@ -2151,7 +2278,7 @@ impl<'a> ImportItems<'a> { node! { /// A path to a submodule's imported name: `a.b.c`. - ImportItemPath + struct ImportItemPath } impl<'a> ImportItemPath<'a> { @@ -2162,7 +2289,7 @@ impl<'a> ImportItemPath<'a> { /// The name of the imported item. This is the last segment in the path. pub fn name(self) -> Ident<'a> { - self.iter().last().unwrap_or_default() + self.0.cast_last() } } @@ -2207,13 +2334,13 @@ impl<'a> ImportItem<'a> { node! { /// A renamed import item: `a as d` - RenamedImportItem + struct RenamedImportItem } impl<'a> RenamedImportItem<'a> { /// The path to the imported item. pub fn path(self) -> ImportItemPath<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The original name of the imported item (`a` in `a as d` or `c.b.a as d`). @@ -2223,45 +2350,41 @@ impl<'a> RenamedImportItem<'a> { /// The new name of the imported item (`d` in `a as d`). pub fn new_name(self) -> Ident<'a> { - self.0 - .children() - .filter_map(SyntaxNode::cast) - .last() - .unwrap_or_default() + self.0.cast_last() } } node! { /// A module include: `include "chapter1.typ"`. - ModuleInclude + struct ModuleInclude } impl<'a> ModuleInclude<'a> { /// The module or path from which the content should be included. pub fn source(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A break from a loop: `break`. - LoopBreak + struct LoopBreak } node! { /// A continue in a loop: `continue`. - LoopContinue + struct LoopContinue } node! { /// A return from a function: `return`, `return x + 1`. - FuncReturn + struct FuncReturn } impl<'a> FuncReturn<'a> { /// The expression to return. pub fn body(self) -> Option> { - self.0.cast_last_match() + self.0.try_cast_last() } } diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs index fde2eaca0..948657ca4 100644 --- a/crates/typst-syntax/src/node.rs +++ b/crates/typst-syntax/src/node.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; -use crate::ast::AstNode; use crate::{FileId, Span, SyntaxKind}; /// A node in the untyped syntax tree. @@ -119,26 +118,6 @@ impl SyntaxNode { } } - /// Whether the node can be cast to the given AST node. - pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool { - self.cast::().is_some() - } - - /// Try to convert the node to a typed AST node. - pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option { - T::from_untyped(self) - } - - /// Cast the first child that can cast to the AST type `T`. - pub fn cast_first_match<'a, T: AstNode<'a>>(&'a self) -> Option { - self.children().find_map(Self::cast) - } - - /// Cast the last child that can cast to the AST type `T`. - pub fn cast_last_match<'a, T: AstNode<'a>>(&'a self) -> Option { - self.children().rev().find_map(Self::cast) - } - /// Whether the node or its children contain an error. pub fn erroneous(&self) -> bool { match &self.0 {