From 1bd8ff0e0fa7966f4bd2a4426241781bed168df7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 23 Jan 2025 11:16:04 +0100 Subject: [PATCH 01/79] 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 0a9e1c48..69b274bb 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 5deea2cf..566e9d9a 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 02/79] 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 e423410a..9579f185 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 a9173c43..1e346280 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 52dca966..ce7460c9 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 db131afe..00931c81 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 84661c1c..0db056e4 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 1261ea4f..c91eeb17 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 d392e409..f3fe79d2 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -276,6 +276,15 @@ pub trait Get { fn set(&mut self, index: Index, component: Self::Component) { *self.get_mut(index) = component; } + + /// Builder-style method for setting a component. + fn with(mut self, index: Index, component: Self::Component) -> Self + where + Self: Sized, + { + self.set(index, component); + self + } } /// A numeric type. diff --git a/tests/ref/heading-hanging-indent-auto.png b/tests/ref/heading-hanging-indent-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..823feb145eac4422dec40e50ce855ff14cb7346f GIT binary patch literal 849 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 delta 3465 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 literal 0 HcmV?d00001 diff --git a/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png b/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c359a1b990069b38118603790d635046de9fee GIT binary patch literal 3341 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ⅈL;bFt8&b1d-{@o7R?=mZ~aNZtg@E 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 JR|hc-`5%sQ_ox5> diff --git a/tests/ref/issue-4859-outline-entry-show-set.png b/tests/ref/issue-4859-outline-entry-show-set.png new file mode 100644 index 0000000000000000000000000000000000000000..33ff442d95767d3c5b26e0c038b8b41f728cb06c GIT binary patch literal 749 zcmVh9?2?dRz1X=`&TEHr6qaEy(Uhlq}kkd~>dv)bF? zl$D#8n4Yt=x^#AaF*7^6y~TcigMEI2!o$aljg>1cHNC&b&(PLvZFAJs*@}ygsH(C_ zOH)@^UOGHP%+A(+e}!&ubANz@qou7~UuW9f|r^P4i^Bv0Hm|Xrhx(vC>1D8Xmx;2TmCAPqClR-67B8lpP%cYl8?y6@X#O;~=5=)5tynch(>;3)x=H}+Pxw-iG_~_{9z{1MY)6=rDvMTo0^)M zIyyQnEiGAFV?jYd;NalS&dzprcAlP|-QC}Ugp8q~p@oN!tE;PheSQ1;`>n05uCA`G zuCP#0Qha`fe}RdCfq^M0DZRbDb8~aFw6r!hHsa#qx3{-BIXUU+=~-D>jg5`!>g+#1 zLBhhqSXfw-lat%q+rq@m;o;%U&CL!D4&2<_$;r#h%gwH^xNU81d3kxv&em;jcdxOz zQdC@ohK|(K)UdL;WM*#8(Acf7x3#yws;snTXmGy3$Y*JBXlim^U}%nzn3~cpp;}yIqNJ>Se}_m&O2ELvu&}a`lbgA_!+3gvUS437mY&GU(#*`v z8X6iQBP+49yVlm$K0ZEygNtctY4P##xVX5%!NFHoSG>N)DJwIeprE3nqH1btnw+Fg zP*_S#RGy%yOG`_%w!X^B%+S!%I66X5Qd*CZnYg;b!^O?m+Tz;W!2ng-%?J_bl>gwwJ{QTwRHAC3-YwThxuP(g8md+)vXmWq4ty>Kh;y%&fI zZ2`fe;#N?ER~{|*rrSu3n- zvzj7&DBHSj_0sT%?FWUkz$tpG7XBC_EDngXIp^jHB^=!bPi+ma-_R!sSFNAB)%5Ja zhxZasE7-D`_^xpM8bhbDeJ93IqZyI$mz11@p-!4oFX1b_cR$WTFyvFG&&VZn=S-Ri z7}~;ji4KuOwa54a?$pq&JKlFtUCGvMc%tEU1o6|@u?t4(m^q`NG+7^zsCsTPnR*c% zNxMY4=-F~76tY~dvQ^i!_(WDMY*_f;-n+YVWEWck0_tBbQg@XW;1>Qb&}^FsdI8b!sjf!a^WIDF{H*kiiHOc0k;c&fnJABuT10I z_`g9E#7ZPoLZotTd5~h2*NHI=C0D%!9=%@g)h=lb4UI@~07*qo IM6N<$f|jtSP)+9{Zw7jvi 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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 zcmY*s8e~FG?9IgN{m!3IhX!E+;Fg`hHA%-)E8G-tFm~zYPowoq(LAxVqQ! zX_lpxh8*rtKD&uyBuI*#6<-~fgt1y&Duh(9h`uNQt1j?qKz4+-L=z;7jDk`k2ATm@ zlq5(aqf}&v-5AJT2%pE3wuU|^cyH7@PDG5hS?-VhbbNK481sKCequ1sjQIo&3RRB& z4)~Zl^z}*llh{eZiflk+1Z2fABc#K zPEKYQf0mX?^)+)LCnqI+uGV{e{A)Rol$nWAlqJk2AmGyqa3WRwM^Q3j;=YILy;1j*dHpt*fx*FWydLED8yAz4ljBJR0inCQ3pIwOM1+K3 zF0SXxt^Vm^nT;-gAlTa4`rrM<(7^2M-@CIF`lqKSeSLkmfWQ0bT|PcO)PElyyaYB{ zokN_xu-M2c)Y;C@&lR>qkTB<(@d*f$O%)XsreO@2AULtGuxPA|4DxVtO5heHZ@9U;FY(pcEjOY7x8S1b-+wsu zM^plcj5FQG!GVFZ^`HM%S{$h*YL_~_IX$)}@`i4Eg5dRjr$w`|jcd3qcKQE%|Jus$ z-Dql$C*N{W=CTP%pW_&*e$FLF}akVL;ipNc9+f;mZh`?F%1`2EJ~{M&a*Jxmhw zguG>aac5&@7V>)le=)Aar}*-wW@&b_kPiq)-cZlUjqYs@BU8_dRcbcA0uiF#-W3j55#MSFJX-( zCJaoCUV?ykC3#d_T~rN!y?{=TNXoLq}2f`iYC=@0|gs0Si69YHChtGiNAKz^wfzHO;o3L8h$DsfzP zdUp0sYHcDkWHsbyA|fI-gU-N+qWJf%t&Lxj_M(Rce*K-j%*?SmSeTgcdx%?4w+d4~0XX%wtN!qsc>%2IYx9 znW@vI-ma1zd? zd8M;c2>IpLFG&Cr9#~@W?5Jn>*J>JzE+K+vN-~`i<3$ME?lyj5&+Y5Wzg=Th#NR|4 z9iA7U8uh+J-$(uuhwobaBn_mO{cU?lGm=QnFan&kL9c8b7WK(s+_0^d%{?#lDMaag zxhZ<6GX-iHEsu?vO4Ss-QK)*d30#cF7JQ@vhWzY9n6#;zb>DFg*SPQjhSSB{{rc>U zChmaqhj5b?WI%|O%tOm5%`-wasteXE*GqsXt4r9^9Q`K9j8!4jA}<^hM70J)fa^|s zv9@``Gl(5@w8XP#|8O86Qn5H;N|{M>sK4x;>cy92)pWtRWIjUBURT#9VccCZMV@Xr z&RXT>>p`7Gy=c|k`L$!f#fn08Rgmzjfpa#mJ)kg+oC>SZmpQL`zB+1wq-8Qh&e-4A zLBwobd(P#X9_O*lF++dZOk3||>EEfCLDehC!DBb;l;$C0~^m=>1XzH-7Scv+Vj}#{GPY`4c4e`j^J#r(lMHn>Y=s8RR zDdcb5W{|!p`98E;`;~BnRxtcvj~#Vu>xX9{vfaB9I77x`Oh?4dCiARo8@8G|6S%?* zF?w1o)X@L;Yzxy>Uvu8y!|-e<+4KA*VB_HLOVuCuwR(}2j(g?NN=A%L-tXeQMYQ8H zU^Uci+7gKE95oW@AlWNb6jsL37@u4rJ_DC9HN{j=(qNWfplZQSN%_j4CkYl12O86r zij4(-SACyqUy;aV(|FAW|NR92>X6rje)j0z{o=v>qHRsn{cFI2PWQ?c3X^6vF0(C`)$`xUeC^#}BM2`X*@pkKGiS0o0_WSmif++a2xnMZ_p`b0xXK%% zE7IQj^{io_`4vpLPTUL*b()wLEyQ7GFpdu?6Bzn?^2Lv_m^2i`>LFnS5z4j} zk?DlGrhSJEp0E#4dc!6t?{D#1S|9hb*#YM&^6_-hU_9rO{TN$b_9}3HgApb5u;ay@EX^D{-GuQ^YoLPsV*+- z{s8MIMCXtT;rmrrHPKb(*Vyg$`q!W`I_OtbJ^us|7kzS{T4zl||Jwlq!!eW)nE^aa zJ?CMH2tA*p(q9cew>D71x~=yFP3Oy%@b5av)%o)>mG|{;Jc=s5%|Ljroj&C*dM>nY zwDM2Uy?x1JOUY1ys9AVOsth7(I%L)RAa~;0O#B!$ld5U3^t=ugdE8Zl@|$1$y#M+% zqX|hZ7(VYs`e+vtABM_-=DfVr-9T$m%U@S`fxXAhj2-pVSHB{=ngg!iiWD&`COVt5){h8O#7ffLYF=?@upJ(& zerv`XYN#wj4Z!oQ&S#feWjrfdJ1cr<@I9$&;hz|D9UjAU`nY zqw#XXQ3(6_Je!cVs zKh)&_PzZQgeZ1dPc6qpn@Lzv4Oqe*1BgW2-x%;LoX7jWqn)IKCqmyLVCLfPy^RDcb z;cdT;aZWa#puD6-P~9VUD8Rv=t9WaftM2>9by?>7H|ra?8D!oKHAQ?(x8&rqVprcLp3Y=;K&s2`HZ|L$H zc5VJ$bc@an`b4fY^|koqP0O(bOat$iZ6`CW8LU^D1&POm1C}0#lQj+hDpr8DXoXhb z6D*8v*DMkb)Gz1)XC+$}_t)(7t**=}FtG6s=BrSEh=09fc;~Mn`tPKL^`sO;S9e` zvhwM?6sCyv=SawJ+?GG(lTo#iYgD611agkKJ`jiz1OWccPJ6XoQ8Tfnrdl)U@BQZjU0Zl!b65Nu03} zn(E==^WwmV>ze_<<1!AV-yl*>4z>e0@uS- zx!h>2^&tnjo_U*g4;(>TshfN#AT_#j-jn5AkSWg%JV{a$2`#%J0NR3O8m<5)^7tg1 ze^mDy&TFz5QjL>*DyjoOy;$nZI^a?bo68++Z87TxlVubl87V0@HH9rwP-TPMY>S4^ocoxOOj z>PvU48yFv!x)iVXe#oo}_@pB}Zf&r9QpoqBv#eB$%mQX~RED*q>0^9H$H#p#Pf`=r zJT!^Nnc3$U{&t#(#};lmUT+d5eAWt=_*fV07)l`CD|~~7*KY&gC-rgMzSV^<_kf4A zwEAZotAwbcKfIx?Qzv=pDXUD#r~&PtO@)FVA^{9%_$Q;xwbBe}o_?ER{Lb>bL}lr+ zNc}6Z_lbBb^aQXw#`3L>=uZj8>R$J!_uo@}PwPw=jL&_)_~v(-I?;R0Q}(Uqpz~kA znI$7CMtv-5sznFhi3g@!kaa7MYT+Sg|J4@Zgx?7Alcumr-NCnvj=2F{X|up0$kH=l z-{P6<2(CC_&k2g`W4X%ceXB~hJ~NsSFf?*Cj88e~sAwTqXB+-D-haxPe=dcz=%)1M zjlBKr63Tkarz5`Oa(wnD!XJj`N?Ch&2vbXUbV#CFNFHUe;=_GD8T_Aez9Sy;U_%a; z^whW#c#PKICw(mp0~|tp36v?=TW1a-??VvR>DfY&b=qap6+Hg5 zJlPh~M!Rp32vZF#V-`gihbEP)EI-kFUy**8fRl+EbjLy-zDF?ke`*E`tsOp+=M99l zako5@?Ph6m5WFXV<;hK0s=-|q<`*3SWe?_CYs@SWk?A6~5A1M{D9u3(&>0T&5fF&X zoW{;0wU^6`M^6`3>Ub2)XvYzVTb-+0Qb1V9qfm4(9r2uy&;FpL3o|mVvo$)->}$vQ z13y8^pGX(8SVU3~pgk!cJd0~bV?TkQHF?R<O#_4Eg!Y@om%ezgS^IY;K+2m{$9 z13Ky9(xP?x*mFy+0AJiWQ~8HlgQlF@Zht|&{L$V!QI<`qIcuaBkLH-0JQl;J%2NMA zPK{5Y|HXeJ@jn)1oC?}{BVexM;`;L>e{&^x#7ExEM_#gQ_b0I1NKRpW%xVuw&;c&- z<4g6et7eGD(`&TTHwUtkN327f_sdC8u9Mts4Som*i0cUO zVGo0VV)YNW}iWilH?pdM;22C;2Q5)J%gZ?<+eK{|UBcxMK97<_u zi!3bK(j{C|G7U$s>&f`*g0_FvhK_tMAxdvMj*Q(d z`RUz%%RTwJHY)FzEHBc9AO3CRy57l*#jfA4-*Ucr-(3xn1#DmCW>50HH0Qs?8m(X5 zwUB!M+WHo73GeKse>=GF(aHUX_SIaP#m8*Jv zxnDDtetK3cmMLTi+6`&y%vN^`SHzjOJy%Pj53G{ZD*X@4fA0Vhm4&YlcOVL5g}xcz zj-uoq3zRzc(h+ma6WTi5%`Sf68NiM0(_5^(T9o~?TCogV!xXiPGNEbE>b0I<>rvRE zvWjNyvA+WHG|Zj-Q6eU02jNdkE(#(hVqd8%%kwV6cfufO{mMW+QQJg(7|d#vr;Ut) zh0{XD9A^Sl0s0S4pvsuFjEit1rtY$WSD&0vKL&extTfw$`C*vV2gH?S;qcSt>gM5y zZ3m@rjrp+Eaota)oF_a?y_03Jxf8|k#VLx0kwG8|2sSM%A`Tyyp&i!mDon^Ah2MnE zE6|$hOwR~A2}xN+S1n!TxZ$)%x`%cddG*tb6YcQ9yKK|p{sGe^EzU?KWnC79lR1+m z19?1*d>W;=NUX>9(=07*f&&R-V=G6aVFv9r7(Q4|0FI<25&$(SfK8R+Mj27stSjVB zvW}@vc93s7^yoQ3h--fP%EzjQEVzH=k`M)rC5Ad5cPdBvQ@#THKl=x=fep$q}_`1AM z)3!B8%5!~R7oYDrJ!=X4hjsb!Cr=TLA4s^Vn(@&I_nBXp6_K#yIU z_i2t3X z91&(_`eB39bZyXu|KvvIW4?$inbFh|MIj7<)>u&aAVB* zhJGh7@!G0&ig(XH%%X#P(2|Fk3T z9f>L>Gxo%ju)#x9pOg>mi@bSPidMX4;F>eA#>UAb>aq>);Xv+O@U~#35KpZC5b+n; zu$NU>C>dPLkPnBU-odF*JF@p^3#N-a51I)pwut8CDZTI<>1Xq10VGnlgy)pA9s1we z3nDMUt`ulNY`>9ZW{%xN0m-M{RV5f4{FLf#{yMaw>Xm6`K5gkE*Npr46yIEIz!IcT zDpDqrWCM?Jf+GEQF zV{KHS-KAtkaoVjqVMsppNkwFnUZ zjS|yKVVV+Zf)ccO%+7gaj>~WB3NRo-z z@9`-y{$SfybhR~+AzZhR4g1z;>oQgslyBN@e~1*ktz5$@@8u2n8JZjsr&0O&YM?&o zSl&g2{kzTew2LyG1QE0YXZs zscSKqp}JEnEmo78_!!sl@@2-j{C}v2SC>gwaG>23dqX|Kzei(YcBRQ zoL|Ba){-+R-s!P$C|d;i{J2TDk~F`-fNCFBT(6sv!m+8R9_tPDwphy3NXA&!Hf8yH zQo0&hBh7MFnS%Td`TtW8Y0xT#u5MOQGB2k+bfqN~IWbG8CnB?~^$g+r%`ZzZ$pYOr zgOGF(qbZZQWvX*k9L9vn_CcOcrOatgr@%orlb>=o2|Y`>HjPzKiytAUNcD4+f+&lBQa!ejF>Lj9u;TpkoRz7~_u zWwZm(5Z?A+W=W7)vZsY;1Iy;_zlcg4*s(j|6$8dm?HCDb^0kY<#{_aL6($9bpdw1Z?ql=W{mg{ zT>KQ!T6vb3?*n&r4|a;$EQI^01%^cixu2&ewn_q?A415=D)K%VdMRu&?e{84-uJJ7 zEU5<9vbSJfp}_P7H$XMZBMD8mL@1gyA>|5k z!Bp+9W{dHxTF3{6KH+HqIx%)IJ&jzfB;LpYG7ANs)i)}HqhO1VnXuE2n8};$)o>}Q z<>1*|)*@O!|6LSaUU~6i;FN$YH*5)yWEP8{#P?d?P3u`A`sR5@@Y~e3?pGbuh=QGl zpL`LaY5b{HpRT2^xULT``!N^hPg(xR(aO)1W*rLmcy`#CJgT6xPJCXW75%*6V3nY+ zljYZtz~CU07DK^(u^>IeZZfk+K><^x;OOHjq{v`M->j4bfTs&{0_R~7!Y_yG_SCnh ze3-VQTH`QT;(qzPkbp7&5dQlXp2wPARXCmvK~`aR2MbB!#;nO zt>z!F;uEn?2`7uVIz!EBQ#51`pkgZ_29*6ki0+Nzw;BmAi(crY9(ngV+U*UeFaP^oU}Y^_aydk zg&8Bd!R60Dnf$fu0*kH<;k1LPjp#nKoXs^Y7{nCUg3|NHQ1y-C zc6fy3rJ7Vj+ge2qop?Btkc5$(^ z>%{eY3i%gX4NJp#8{eX^=jfE3nC?Sk(PzY>?qSJ#JVRga|U-2LO$%Gs2uab&AwtLo+9)6}BiQ!sun z(SXs7svW5Z8Bet$vdyP|ehG;p=@Br}g5uHiQ6Of~kcQzw*f@HV#B41FH<(~Z<>m6A zGsAQ{15=F>pRr8`c$~nyw`O&dYmKGZb$7MKO*Y(eJ;i!-BBjn2ADGEf)5mWjl`&{u z^L3&ZG;!cHySzflT*ugj5`9F22CWsz8RNOIDyjT36YwNx?DP1a8lDIP9hq9zypnY! zrPCZf5y-S*c+Dq=L|e%B^nG#gkt`P-^v7>aMGuSD`Jjs&7>x0X3I_byEw;!`bF!%~ z`Zg{GCA+4!37Kw~LBe=Q5IzN5z+z}r@u%(W%pTY1ccdxdhE9IU$A`lq4)2-FNXH#& zic~hos%(;>6t+V~f?2q=Xb`c1T}ghU1}t@W=^*tsf9^%AO>nnt+)RS$&m^& zm})AlWqk9VqW=cz5E2y!I}42w43q*<=?3Dt*0*P-%717lg4&Qnhzjt2d{@NI+(TrRy)d+BWT~v3SA56LrUdku!|IQ@nwJxIuJw!Uo7QWkM>Ic38fsC zn86_K*8;O}XI#PaSX|1-NKDuv>&H;hb3u7`qRS+D#3wyC=lt#7lTZ#Sk(= zE=h(PZDom^NbLB4g%Rjo7>35CPO)I$gb)!U)cK?eCW?=aIVGCQ|AofVvdzZC>5k*2gU8Z?Es#xnP8peXJxOV-bv@w z3bs2cD)b7c0jH)h_#-Pe8p<_Q3g)_vqA)oi-fEn~PxCP-`HwkTEZ^V{^1 z#S5PEk$44qU@?Ol5x;{(C27E@@lyuv_CRlPu`YGxoEM#dCgRDZP#L{I;3uZu0ICxTHbjC&vJTMIo zB>@9F@d8v~p0*s(95+9VRbvF@$y)R7u8^MLkyiju>ys6l<(Q~(4Ag6R$V&>V^-*(LVmFK2TWf79k{N93{9Ww4F(dB(<)WX%XMNnl>!&o$%)&ws3w(_LK#u8Hk%lVp1-BJ`MQ*R9HAn zZYjjM7AfL>5Q=+}<2qq1%58OA+!^ObPnOeDH$-NO{A;H+6Jv&Ab>hzvS2vZ9txoo*;cfzYV5g?$3%8xAO>l2(`Yl|(08j&Z7V&}7Kr zQP}GBJ|McL|L(W#ReqgPlj6j7oqa%a;v3C1#mye8se&6&M!@k@Ei0j$aMnd{g`+ zxIt<(q3JPX#Adi~{0%A3I`!c2NUUa*ZV!C9xqn;@ki|80Im^fdJ==_Ttux&PChKuB z8G(7_=9gyX^&VdJZ33=owl_Modq7=ToUb(`$+coEfaALt4J>=Lgb?+yfb^#i)H0~K z@P@`zqLy}XUBU6?UQw2?Wt<39%Hmuo1D|XJ)g}6AqEGY{Jl)e+aG6hH#7i(p@SY;2F{yYz!7+^_Pr_~7Ui^)(95THqgTl^or j@?Ew3Z&@O*`3jV-e$JAZQF>oX2LmG~1(mFZ7zO_?=V|&$ 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 03/79] 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 62146f86..71422a0f 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 504159e8..f6df57a3 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 fa44cb58..ba792442 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 00000000..6ba1864e --- /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 00000000..2a7dfc2c --- /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 04/79] 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 eb3c2ea4..2d774cbb 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 00000000..8a4ff37f --- /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 258c6f6b..e957ae9e 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 05/79] 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 2eaa32d4..774384ac 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 00000000..4da8b47f --- /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 00000000..c652bd97 --- /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 00000000..fc052d10 --- /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 2c93f92c..d0dcc55d 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 06/79] 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 a65e5ff6..f9fb8b61 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 22674050..ca82427d 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 07/79] 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 ef24705a..5b3f58be 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 08/79] 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 f9fb8b61..5b9e66e2 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 e957ae9e..288392d4 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 45e5b50a..4e529fdf 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 b3d9a830..96ddf3c1 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 61fe20b0..23ac6e51 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 09/79] 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 de8ed65c..c59a0338 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 0a7c160b..b4a97a3e 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 6b5d2816..17401044 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 5b9e66e2..5de71caf 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 00000000..c2eb2e43 --- /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 10/79] 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 95db8a22..762a97fd 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 6cca2458..edbd2413 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 1e88974f..924a45e8 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 11/79] 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 34373fd4..2baf4ea9 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 51dc0a3d..f93f147e 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 19e12372..014e8392 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 c59a0338..cd815694 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 b4a97a3e..c24b47fe 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 17401044..b8f2bf25 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 5de71caf..55d5550b 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 9eb457b8..a7b9a594 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 12/79] 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 06dc6653..905e159a 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 85a041b6..7dbdde8f 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 3045970d..8a80506f 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 efc480d3..8d9f5933 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 b87e527f..b162c52b 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 6ab6d81c..99db2ef1 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 31ef1c54..9ddee597 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 65a48316..6dd9db62 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 13/79] 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 69b274bb..f59235c7 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 f93f147e..bfb54aa8 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 8a67d53b..e1d7d7c9 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 63463d76..6d3caac4 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 e5dabfaa..1cf656ae 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 965f5351..7558717a 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 5b3f58be..55696e53 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 415488fc..93545fc4 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 14/79] 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 99db2ef1..ff42c3e9 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 15/79] 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 905e159a..702816ee 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 7e849c46..6b9703aa 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 9ddee597..c3c119e3 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 63033ef5..941c2055 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 ca82427d..794ffd8a 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 1377f4d2..d145f72a 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 16/79] 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 ffd8e250..1fa6aa21 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 5b6eab4d..2acd839d 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 2d774cbb..4dc834ab 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 774384ac..79e9b4e3 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 ba792442..82c1cc08 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 00000000..98d971b8 --- /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 00000000..5c970a6b --- /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 bb53a041..f15ddfe4 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 17/79] 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 71422a0f..612f923f 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 2acd839d..1b725d54 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 6ba1864e..189a5b31 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 98d971b8..d1716c6d 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 5c970a6b..b2a26533 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 8a4ff37f..fc9b3c06 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 56b1e32b..54a22faf 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 5d998667..89cb54db 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 4da8b47f..753807db 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 c652bd97..6b05a94a 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 fc052d10..f516adc2 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 18/79] 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 1fa6aa21..25d0cd5d 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 76d7b743..f2c7ebd1 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 3cf66f9e..76af8f65 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 2f0ec39a..2acbbcef 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 6dfbc969..e21928d3 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 6023f5c6..cbc490ba 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 57044f0e..7ad287c4 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 fba4bef8..9f697380 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 7b66fcdb..87113c68 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 bedc54d6..83ca82bf 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 2dd79aec..e26c9b14 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 2ed95f14..b688981a 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 56d7afe1..443e90d6 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 63127474..f8d910ab 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 19176ee8..bf823541 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 702816ee..e5a3d94c 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 6b9703aa..5897c3c0 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 0bbae9f4..8eab18a6 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 27002a6c..14dc0f3f 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 79ff5ab0..6d2d29da 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 37094dcd..98380330 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 c8c74269..725f177b 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 1e346280..32be216a 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 762a97fd..a391e580 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 4dc834ab..a4126e72 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 ce7460c9..78a79a8e 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 f3b2a19e..dfa3933b 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 1e369d54..d93ec917 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 0db056e4..1214f2b0 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 8b82abdf..0bdbe4ea 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 79e9b4e3..919ab12c 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 c91eeb17..9a2ed6aa 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 a1126860..b283052a 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 ff42c3e9..754e89aa 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 f3fe79d2..b59fe2f7 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 7d02aa42..580ba9e8 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 00000000..60d4592b --- /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 00000000..7c747ff4 --- /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 00000000..09c7d2fd --- /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 753807db..c12d2ae2 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 f516adc2..03983508 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 00000000..78bc5df1 --- /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 zFXp!w6ai)Wx@)Bc?f=Q|q%_xpIj08Wc^g-9t@NDhz6#u`$g_fd)BPDBJ=Ub0&eS&FTG)&$t8EEx-;~0ULI}MioxGYh$O@ z23T?Jtvi+A1Z+4k1-JzVLFFH60VU9>R3e_5(~s z6~lXZ$G5RVSrmLgp->{QMaG@yyVh(^{r3U?a^D;FQ`{% zeQ~TZU)`{I+xrRwe0Ho%rS4K|(5r9isXB1Os#0~SvpB1>=EyZbioziVS8au#l`!GfO*> zheQZtathx$HJK*UOlMBhZ07S`{T9D-7Oy&|bH2X^LjR-(5C9s`2+)8AGy*iB(cgyt zfOsbe`lhVY>E0tAeS;DX&0UPAo==<=paBhNKyzKM*G{K17z{R>O{G#{Sr*Vj3I>BB zk*LvV5K+6`mdRv3pYLvu-EI$u!>iRwB9UyjTR;oxcsxp_Qo5y5spj+f-JVP)6NyBK zNUzuLb~``|iDg-tO!oPfTCH9#mqb*pR`dCMG#dRG`f|Ah0)b>Q8H>dL{Rhy`&(E1# zVzHQmo=&F*gQ3-GX*8N%uXhKXN~O~2G!ePoZf*f+zR+^HoNj5gS`Ip&&)aOa*=#nM zOwQ-?9kj(_VHk#pN~My+;Q%zR*6a1l%M0C7DwV_Ga6BF>6pGX7wBPSJ*?2te_xp)R zqtPrDi)=P)Hk*ma>-APD6+rVUkw{pr*6&{(Hk&OJ3K3B*mvgyX#bS|VS&zrV3H1AY zyWO74<%pbMFpNNGt`*`P&J1xl3upvr zKqEi{8qf&PfJQ>0d%fP%(-YL>0#O)- zufdhzEo947s70XLN=2OV?-H_wi*CSZRY^)ji;y;Lbd`4?VnmK4DoQ^b14Xn+2pN)t z_0owU910oSgx||qoHIuRA3o3UIgb_P^-UYB)jl{pUS3(ne6A}R8iv>F73QZb%Vx7# zFDFWp)ND3KqmkWi|M(^C&db_{^zhUXBXx4Vy|b$=#nxPdMn%VBF};fftfI`3a5xOm zm;kgU(=f&+KyP@q1lC)*etLGX?%vcuV~apvxm>Q)R}zVY01d?p1`I42zu&J14d*Bx zkK>RD27{b)J!p*Un+lc$`mc%>9|X=3M7l^Mq6h78I8;>?M@pekAkfA^N25{Li{hdl zG-NwrM&sU?&*usBRDvj4S*O#9R!y7p|Z?w)ggr78aKZ#WI2Z23@UIYqc6^VhP0cdcD*9eA&z1fp81cr8u@&FRei@84%JerTrS@+_AIW5EoxY16OYFOvmd9* z#2+#;nT*wHrOt3>L7Ai8#eGr>=e*zVD~dv(|7Q!1ZuOEfMF7e;XnaT9Zns34A~?`{ zy&gTvqGyH#nnIunG=)GDXaY?k&;*)d6g2&aF@21KP9{G!#mLBLbR2^iU~FtWdGh4Z zVbal+N}~adUGu+83ejjlQ!SuByia=iBo4^9e=qv`x75*qCMTemEp_qol3TOd4H#sn zPX&jEtJK$<{P~@Z9F*DFIyE)MPaenW=m=fE7BL#oWTt3oX>NF^`S~dTgBvXrYxXSr4eLEd z1Db?@mXqU|I>i=(jEqEp0sZS|8Zc?Ucpe`asj_dcA6h^kI~rI~VF=_l)SJwi?Jye9 zggLp!}byk)(uw?l1IR#iyBqnN{IUNj4??7>&%KkpvG@x;U-@kvSrD*|2 zYk}dlcaJYOH_M}ku|PI(j5jV$?enJ;T#47rOzhj&R5&*(O7+sku+f0V2kzXCvbL6h zgqOFs{P}aCK*o~A&JQ2NKm`B&&433eaA5N99}w5kQ5qi5z=@93RL#qmMgy8q8EtMZ z2An$vhM<*|M0K?hPy{$o4kUq-ZEdaQz~S7PGwgw(2^?+D$_SFZ(M7T%DvK-)QU`G+HH$Lx7L|)OlITJ~$+C?DUC3@4wJY)(X((j1 zQc1H~cq2s!S2>hzYS%+w3}Fj@<^qkp9|t+dnb|yZ`2I8Bv`#BLgv1(a1#^Tr}eiEu)%Q}Eb$T~Jk2RnqcT2r^2y_roSbz7Z;$TX zyPWSypyvlYI$K;)q2e+<5P?E(GIA7?p%G(Ama;Fm{+Pw>xIJs#e}8#tm2z01MN z3$%W)2(*Gq3MvV-OrQl?paoi>RT4JO5NLt^??Dq?q)vC{O92sa0xliT(*-&#pg|lT zzsyoTfYy!KXF6>YmbiS-m`r6qa{`a;Z7wF%t5c>f(82oXq@-20o#9K{ z1a9Q~`8Zmy$R!g{H_-GS3Usib$>^|)&3lm6WYXQ0LDbxGYb;4aR9Ed1=wLy^*FclR ztgPJ0q@_8H3GwoTgyk&TT3tLd1v)6uIOWvT7&1EK=OIf&GL1lGWjm}Ag{ld@29iK& zX)#;{A<#NU3$#GX1UkY&d%a#RgE=E}V1TNts|ye`c2DNmBrqr?M}d|Jv_Q)QTA&45 zCeQ*c3l#M1>}+0Mp5hoz3{c#E7u?JX)ZodlvzqsWqS(4>j0^ z`$_y2=s-YIF0*9GBEL2%z2_G$Y@MTsPV=eNc%WiBIwGO&KzB57{nY6Ee?N3gis<12 z4L%z#*BVnqXOE+ZPN^6&EptHA_j|c!H;U*KiSepHI|UsZyOd^V8+nFeq$1 ze0cSu@0F)3$#EBv_K2A zOrQliVoY*u0==;8`)qMm_U4L;eS?Fie7<7;n3^i`dXu-rt?>B-IutfNUF<#*|7oI_ zF+6g%s`^lV;V$OChrJ}wAp-5m`~&*K$1_dMIR@8;CsUxq0GiR)pYO>OXn|fZpnLoB zXs3gz=EzmN77HEMMXzAVwv_OXwG(SlZCN)L$+}sUF84Gk+KvROA zn7DFsBA2GCn>P}*qr>Pr`g*gDA776yI?*ha=o`~H(b$k|9fsCaakVyq;(mb+)<)5@ivI0#TuT3mY$29z#H*x;9z1BeE9^2>XxfBU*@A*iQbZ3iXw=S$$ui;U z1nwl`i60xc8h$Oqlp+8P}ly;kPH0M*jc5+G=7Qs&qsFeoKQftCrhK+6PLpaohc&=G3C Y01&T69A6$Y7Ak$|Q>-gF%TwF_L_MtIKeg#egzUmq{56VvsV?-IXg0Hpj(qbb*{%TllF~L&AV}Wn|>YN1@_meya>=qOyGs000eSNkl3Ku|!y2P*OkO)J4yDvBwYuTt~TL-Ud4 zJGDepucn3XM_RtEd}LlNGGD0qy!RKg)-3KH9cIow5a-O^Yu2nebLN|G<~MuxeDlq> z_kQ9(BGn>K2m%2uphX0aXqU~A2+K2&2fIRx|8*3w6pU+ng4txeft+(tino6o)oxlZM5}) zZr=Rqiu&@(D_$QhiQr`N@>*@&(0RxZ?}}%2@Ant@_w}NHigsrCvW~C5>N|T@2xZTm zO=HcPPdc&LyYGauym(Rg_}Bag4Qg5PRl?4agiqJSu<@gZ{rQVKX~~BX{Cxktup@_4 z($bnO_~3b}zH&Kz#qy4<|M5bf@6Pl}NN`4lhYj=DP#mj(o-sXmvKh_;0oPYQ3uIkFm|4#SuX}~o&u)kYMip%|bFF=s> zK|%J+fA~Jl!{eFTW!b>RQn7~Xv43B;pDy-EPj}~MTwIfzH?mk@vr{K~-o2B96=t8C zH^q%B^kaQk*t6dpNqzjNKecc?fPQV9KkNJUbvye*nze#P4Hqp8XQ6ZFCclWHUBq0hG$#=FjVZMtgbH=N93rq^7zkTW;GLKkxl8_@dV!i->UK z+GJ)nSN?6=wxP0c;e79zGlHq8e}7MYMn*oz#w_&e)$HJbWI(T76V1<;Uh<;2b%1{O zAh%(|+7&@he=GQ%xuMGGUHYl-o!dFBTQ^YuojN55vw%Sl4sKKtbcYUqaL1ltA`Tr) zW)5@|5cNb!mCK;&_=pj{m;&}pU04QnKU^e^hXv4cGkv+EYgU$rawHfNcE!ELys*%3 z+0sbX=j3>N^<~$qzh-cs)pXF9SDU14z85?jo)^y5gZJQfB! z^Wux1m|E=k?p=v!sA6+=?M!4>JRzus8v@X1_k{5Qr%(0dmMSP{^VXZ~e!H5<1)Do3 z1gnBYGQO?yJFKU!QWyYsZ*zJ-MV^< zAPp=e9B}>m^{ouFmzUS}?b}QBNPd3)$dMy;6&XH!_^@HaN-ZQ?w{G?E@v$<{9P#19 zhjkSxC@7dPVS=tA!$^QLytM~@y|vSi8T&6}@XyH>H@jT<+zxBdI~KYaMm#z9}ac(HNg#+NT&)&ly` zqena*^YZd8UAhz-8@p@QE-j$Pj~~Bs<;uHv?{bksLPFH_R9d@s?WClnEnBv9>C$EF z*s-<@dd{3V3l}b&K7G1I)3IpLqREpdvrtr2v}VnkNXU^m#gM)*$gHB6JLrFV!>`3uSLBnyyiWPr$R4QmBCME{GwoTC3 z&7nhwqDQ{Iz9&wc(52CI5;>|3YrHX1l6B6ZrphM__6vEbD@B~bLS3kp<~93u{F?~Es8c^zyRfOf9TL5 zsL?hs4f^)&+c`NoiHV5^4jh;}ckYxaQ?xt`r%s(pMLT!yT)lcVS5#fk@zknSD>O8e zhDT~@YF1Vj`?o>R*ox!Fk1L?hpFe-{sGdQplMzXV`=G<{h24v3ii)BwB&I`5hk&kWpiPYMR6%PmqJq{t8dg9HXiI>; za^=dvfh}={c<?5f79K>3GabGfur>r}+*PJc4dPVy?M>2XI#jnX zt_^NKdO_pHQoKG^H5%7`%QOn;9z9&~+`gMAC5*zNlkGE)eG9!w*lTn3T5sg zoPY5cGmu83S$Z`ZZ#eFKy`U2lowxogUUiDGx37KcX148vo-;dS%H$yBv>~qDM&n*+ zUaJGYuJ-L6e!u&IQryMmDMPe6aCD{B@xlBK_-*5=rLon4|174%JkW|aF4f_XOVfo` zhqR-m9W9`xuS5De1ax(QwzIQi@J4$*!k~*LO`7N}g0#1gpd1WGw=&S~?(Pc~EGW?< z>FMd&+1a{^WM*b&WMq_BNaoL<-=akeF&$z$On}zb05A<&-Pml<>K+8NfVL25h7J*= zicU*L*N3M0AEi&`S-66 zC2IjqV4qsG{>I#F&>t=ix3hbaP9;N_4q{E^=C)ApVt4naHJT1}W3xf45w?s1Iy$;B zan_WHa8{Z@(^D#-^?>f(+l{b+%7l^%=xQ(>uoKW#1Ddd+v9V5sZsdgdyi~8=KL~`W z1vFvv>ej8r+-%TPM1(^_fX0Jo53M6W;WBXO_`%EuiVSXJh7OgC=AfUn%8< zr3znIXjU>pl|Yb0AQaH@W+OEM2;k(bm=% zfrc(=Z#}};b;g0~D$=N2ye%XQ_a_34m<}-=0@^BD9r{7*jma#a1+*nVlWKxU@k)LG zV!;!?p1$cS1x;r^2{x#uVw83AEZDwAdwSOALkYve$By+Q9(^@vG+6@3IiP0LaCEH0 zCm!1e9T;d&jswP#GNinEOb0hmt5$Z(U{q2hUF*LozX;e<(P6B6R^?Ki1a3L1AgzUe5{k;{pPFe!;h zphf4nw4#Tzz(I?<%0H7BtO4@@$DlS2<`#8PM() z&;nXO3us-SD`r*YeOf?Q73iV~0St#Eqc90qiHsnir5!Dx1+;tx7tq!NnlVQN*v{4? zxHS9u`ROXsx?Hj?B!sqibab>b(9X`z1bLsQN8;k*I8|Lm$c7LfA3x7R!Y$?M>MEu~ zOoxE3`5ph#ysKHD&5jbLI!Kd?!6={^UlAJWpm=5RS+-@%`YV=qGz}U$ym=E~aLI=e zcsOq;j@1jABq1~;$hSgxc5;Ro0yLuxo_)5?pg}Da-5fKfjTVhQeylq&9F=RsAO&5Z z)eJ&pLQ|54o;!EW5TNmnVu%HEz|mWz1@wvIJrvNWb9T0e8K6nB1?cP7uagPRC>qW9 z3c5Y<0zdq9igwWL+BL$+Fz^LSZ60Xs1~Gg{cw->YN~m32j)^d=OQV&^Y>g(l+33-u z4FP)pzHZ96EG7b2KKEQ5EueSrO5_px|A6-Q_a`Zrl2?x$Qnn9z#`It!!18GdUdkX^ z=9RWOxHgPT+_E{&JkYc{$gV;@G$pa>?Af#L-@h;IXnA1~(9+i-parym7SIA(KnrLA zEugKS9c`kBfELg-1~jRGNTS4`P&)85f`0s{|DN4RbYbH?M>nY%p!trqZQC}wJLvZ? z1nApk**$x@;`xrh-^>}oqzkJQG?eiSq<@ty*MAn1n(6{;y`X2!nsw~hF?7J%R)5G`j%U)^`vLF&YRxjw>+}z^gVixGPGX&`P_$C+umD=FJ zt;pwS8ZR~`T&7t_1`i&Lqk@%z?%K60uVh2?NVjg? zQc_ZM6-h3aAr=x27{8k;11<8uBA^Adh=3N*0$M~s3uqAmU1Q|GBm31|w;%2Q00000 LNkvXXu0mjfO9jX3 literal 0 HcmV?d00001 diff --git a/tests/ref/par-show.png b/tests/ref/par-show.png new file mode 100644 index 0000000000000000000000000000000000000000..1ceb26f71142ca0b0edd35a4da93a41fb252e271 GIT binary patch literal 932 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 uG6l2(S^=$00j+>mrhryJE1;GAFZ&xQWdS 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ð3J!Q`ss(-k-mX;;$ zv2{brkd`B)S(g=Qm`x_p^xJf~q8wBXjjg5<$!(lR7Z%vHbRa+jVZIhc^TogS_0cvs@#ZjVpN*Ig!k_e=?3}O2k$!sn!oR-U0 zVUBh4=6L2If~bcGT{g)*cN&xP*#Pj=7~xy!?&ug2jnNGK90;3x3*2`JBYWgdHy|Dx zCex#1TU#fPY5dhP&p1_PQ{>*g(DplMrcU1_1W+^c?^3OIXDZtH11}2v?Y02PY(}x% z!~;pHrCV4s!A?uCxOtQ}iA}Ptq<5wU*c!(ZX);wDG32T=Dl`PfsZE|OD$kg=LfRSK zt9%L}tDiD^>mgdTJn^MjCW$}Cc(gV8pd4gvfgXN^qZIzCsve*+C&O#w+JC*T`5UX)+3_AbwP0;;IyyR5x8333eSExO zwieEF2i$MX_UQ|cZmbU5j5vZqyI?4LxRf!zxyrulAX~(O+&}Yb40X)9-EGx;y;A{R zu5?A_$=(b~LBGVC)1%&Tl+J}fD*;iURg4a!DdWEok=R?~m={FvQZHSS7gOqIy}@X(Jth(T6jOJT-XK zg!Sbbc#7Y!vcquSIHt7BG`JVoDgv-AK!qc!GymAvD+Uk!bRpLYnUk{PAP&Expd&1k zV_%B?H2H+V7Y?$#c5R5|>6;y!W{D8U@>?+5tX z73-1&$P%6}9mRkQ=S#d-eea9A>`uM~h*Li5z({GfvX@zqH~u3}|E9se-c$f}*{OaS zZ?Qo7uk*{F{94{(YOl{JMsWqT!UVq+6?IH`F-ctF%f!u1{#N z)t^r;*tAVSGZJZSFZTBGw1TTXy~{MJDpZ*Ls;}*1lttt9E=tjhnEjW08*b88dS&EJ zBvE$xc__>e2#VKP#X9t2Kon^wtaPU#?T`+(TR5vc^%Weh=)tGO%wSIGYKh&Pk&Z02 z&6?*6O;tP-!N+U&;lZcuw>X|bAME;fl$lsC<`V$B!dS{G(n?;sh`arabrem&wH;S zOcg|i(^RGH);&Bt;wxMdfpDU7g13~=O-f30Du|hvr>B|A!&3@>n}Q-=5lW6qC1cl? zNPDKD1b-|EZN&MpDb7t&HOh9rRJ!e6lTzcLMTsV6sFP%l3_Xf++#Ug(U~W!>POF!E zS;4;d`EE@dJo&(TlLa6K1OHFc2_zc4kGvAH(6Jo`a$6#G!(`g(`f4?*k7ND=UUYA0 literal 0 HcmV?d00001 diff --git a/tests/suite/layout/table.typ b/tests/suite/layout/table.typ index f59d8b42..5c2b0749 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 760910f4..8c761111 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 20eb8acd..6de44e24 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 288392d4..7176b04e 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 58ba2b2a..37fb4ecd 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 4e529fdf..4e04e5c5 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 96ddf3c1..9bed930b 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 a755151d..49fd7d7c 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 0c2b5cb5..84f2ec15 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 d0dcc55d..51c4bba5 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 23ac6e51..103a8033 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 19/79] 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 754e89aa..50685a96 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 20/79] 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 f2c7ebd1..34362a6c 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 cbc490ba..14cf2e3b 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 83ca82bf..f8a36368 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 e26c9b14..0344d433 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 5897c3c0..9a64992a 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 0bdbe4ea..cf31b519 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 9a2ed6aa..e197ff31 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 fa230451..e7669006 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 21/79] 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 1214f2b0..f413189b 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 22/79] 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 2060d25f..2bbc7e41 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 b92cbf55..ef8288f2 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 2c3730d5..2921481b 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 a476d6af..2001aca1 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 b51f8caa..99c9a37e 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 8d9f5933..d9902772 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 2ea77eaa..22f3a62a 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 669835d4..ec075463 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 014e8392..640138e7 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 95214db7..03e2efc6 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 00000000..9138f3c3 --- /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 23/79] 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 6c7a7b33..5e0f1555 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 24/79] 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 8aa7c0ec..3343c246 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 1be7816a..6b592cd3 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 f59235c7..2a2223e1 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 0f8abddb..24b76537 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 cb3eba16..a05deb1f 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 2921481b..a790da4f 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 2001aca1..3ee59c10 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 7dbdde8f..6c240844 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 d41261ed..cbc0f52d 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 99c9a37e..b7b4a6d9 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 d9902772..4fa380b4 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 0842980e..9feacc03 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 d34213fb..08a60fa3 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 25/79] 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 3343c246..ada3a3d4 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 6b592cd3..d03bfa6d 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 e521b993..503c3082 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 e3183e88..0a7b13c9 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 77f8426e..0e5c9e32 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 098843a2..d43b1548 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 089f0543..dcc55077 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 bff7bfef..550f60a4 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 27b03911..7425bdd2 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 41d35565..5416621e 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 ede4e76e..d7443202 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 fa471b2a..e6620a59 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 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 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 zcmZ^~byOU|w=Rl11oz-FXmFQcg9mpB5FCQL4X%T`y9alIySqEVA-GH6&hMUg-nr|p z_vVl8sjBW?)wOHe_w63uh91~~j-v)Spvp>$se7#bT~v%JHzpmvHWvM2YB*P{qtKA9 z(vn=?lqZQ#RaFwFmSd<8=L|Npk1_=$vbrweCg z-oqBHoAg-^db7Tj&RPy+TqCK0lECH-I=?qBu03pdGE1yjQ^(KU#$7W?beL1_y;yjy zGL~$44%)ps-{0Oyk>K`ACe8O;1d>(jv_T4zE(Y#=q=q$GU4Ptu-#&M2?B3osELx8a z?mA$`Z`!4fnxgdslI2YNXg)5e(-jztNio8!I;c}w#V!3hMk~6J_vI3nU#T8R9D8u6T^~{!V!a0k(^No3PiND{Pri(Z8xsu+65yHg8&4EDD@zF&e` zAG0o>vtQW~5)y81Z;7x&;h9-jdW$7)p4XaeqN1YyBK=g#5z;X<>}qOqeE|Jk^?&ju zCSAwSGH+>Y^hi*FlU)Cpqq)JM{{|QPa&sW>Fxb?@-D>))rlzI28KLR=WS$-&L6!4^ zXKgr^JS;Ty4IdYm4>B4X9gT;JjEsySCF$hEF>P;e|H<&*&B1s?M8tZF{f|mc$WNxe zqr*11uCDHExh6I?R`BB$^msWoHr8Ul?dIloAv&JVeA+up7N|%CP0H0{BjZwBT+FfZ z@Zi=1h%+-a?MbP`Ht_O#d3)0*#*l_$(UhG(z>6+313KJR9Sm@tuc*`jb&tHaCq!=y$#ue9OAMjCMLKzx<|*y z3zbfW+uH>iWP&m?GfQd->{TrI8k!3V3X+rYZdUPu($ZBQL(wFcTb*ae#|||Z2&6`}ue>{eD*4GW?e0cWy`T{$Xl9N|M4kM)4tb4MuNab(1Ce9oe zmzLzPv4KfQvu^&Np|Ds|{=T}ljW*|_i|uamo?tsPy9s;#95%7_>T1@m&vr#r&(?Z+ zB!XN4AGb$JKv&oM28(IJR8|9PYwH5iknU^vUm1U&uJ_;)E4RCRQFpxG-`ecJ8yg#o ziyCS85&5w81etaOhXqfAQ3Rmu1I!-_K?qo@hqp)5SNU9^R*&m~?X)z)d@{C*DHaBX zHn01NcYr^t%zl7Zloz&T>X+|yfAr0E-~(%MaBw=S-rwJ!o}Tb9#!R;Z8LT-Q4RCNm)$Z8L9V#$PWRK#;Hx4S`4R{^`;A>Y|tw>lOI?8?i_ zjXm9<&m1B^5v{hXN7K1A<>ii13yfNvce{wHtXQXU7)^xlV_Oj_TX6{qeU6BTi2WU1 zS~83+cAHL?ne|)?avfOUf{x_ZvOa3p*XzA7UxQYM0FZP9=1)YMn(y5u!YbwY`2m^N zfNDYp%xp^PfvM%1@6S1GNtY1`zuhmL{ATMOGgop!qM)nP{@I>FvjG9ndQv!f(A<&W5%F9D zDPdB!ogO`78A0P)+yc8YvG%Yd@571oiVDVNvs%(3Nu zbSDi^p@td%(8+x1@87?PwxsRtzwAwB@~*C~qO~IGTvDr(^4f2~aR?S*{)8JN7xnw= z^ArqcY=`!PQCLQNXueeG06Fv>Q|VckHaL0IG2S?k+DP{&jyrx z(A^AL`R$Bf*~N^qQx`!I4v-PdM=Ho8)rxTTf z0EAX_X-~+En4;1j$M7xWE$yMKl^GQupsF3mE#IRY!4Bu;Rg3M;2c6Gc*2+#>l)qQp zS8zV3ZM8GHlec(z-n>5AGh=$MYh#)3`luL9{(HHXr{qL>_yr2_#x1;NED)mpB(O&` zT6tP)!QAz8y*d5ihNg9Jet!O1R>><5^wXWVf4ma+$tyk`~$A&$=N!F4=^#WU3`vNu%Gevh7edwMXV=r_rTJBN$$X1i;? zifxqYXOr8*Nr*|~v4|i^U7R2)g1X4W=Q@~ZN;f=K=X^9K*qNUbtK&-h;!XMobjUG( zvvc*0F(_SGfNVZ?SQN*hb5qC(@*0MW9&z;9?;mfxasF1CjC&%T%Z1q}`aa;~4o;*e zL#1qig9wkeCr86*qn|vHErufj@x5E7>W&~`kE@;^O*<+}gwa%}1UsYC4g-VZ>XO3g zoy0XhgUV?m_l1Q0F~4hi+&E)E46Po#>SAX2Ov7REalWUbKa4nd_5$m&YoOFJPcr!g z3p~T3wl~q#)+>vdx5o9sUEF<;U%Sn1z481%^61n7GQ3 zDPbOj{NA4OCBhVF!5%ROOy*!UAa&U)l5wjWPRGUFwjfTCZA?GNddVI%42y`-ifOR_ zXE5rGgdgQ;)X-d&l)@`?TsGBJrANX1$gtDvej=Sy6!hLGC^zZ*LyqYwWB#}E(YHfM z;^q8tq`|p7*rU&#h;pT>NOwyAqMxm@%qMGAg=a&3IORpS%!fO@AK88|a=P4EE0u}Z zdIy>|g8yP8L*+=jPTaDAn(U@AXPruPt`lJ-EG)xDO`%gR0TW``Ie>D}luGm42_s+;GA+Jbj1pSh5 z<;-3#>S02%f7|DyDBR;QL+Q%IzaGy8kN!y+$tsf3uB|7eE9DKh#1WvYm!ugM`jH@0 zZ&{E+V3OHONKvG)VvxITLQ6px@uWvX-VkxPe#;cjCcJ$sINi$X;J}U|e7yj1!Zy>y zc#ASun3I};J@g<-P0gfM|J&zDjrh*d07+wf;bLz}fPn{Hl^t9+#87_Qu-~!AOu4j| zAuYo%@Fxy%U@2{(2>vDT@rJX)h78TB-}-f?oQ=d}KDerw6seVy>=>{h7o~7ZOR6!_ zsh8?4#mCgr2Wnb^e@3;_pmWw{&A>7GZL`c8V%ZYhMF&bpwbCSiGC2@eqb4ynh^W?r za$9-3oJB?3&mb#0|sJkN8r%oYRBwSh&51MF}QI&1q01C`V`63yBWiC z@cvZy+y%`5W~U(jd9Y`m61;|WP#u%EEf$xf02izfG4W_~V-`(O|L^W(-dI@Ro8yfn z@gK4XL^JvM4beSrU9HnaUR#;l@+FSyc%3eG0Q$~dxe&c=KxPo0)ClXakPKl#Cr}3$K^1Y{TvL;uEo#T0ySzIdQeGj4)f*t%gL1rjgTpw zKAEOKF&l^O$AjA{DBj6g&=UE<yeQ6B5}6Yon}Db}dW+qO!%fQNFnq4>1TTuDc?^-#T{4o^h88YDRZX zMC>L+7bMBlq4Oh>t`V7-vcj<7EHnO&25ZvDD>$9Z6Ki?u{<&2fgy~BC}|M<}| zfH(3i9Nns8I6k*i)U$B)efZ!+2ru2Y`d`zNmQ2O*7_m zF?^&dmw-ak-P#NwgJ=C_`XT*`CXAXINbN6FqWl-0G#D=qMpFTrgAgHA^-aRW&uq_Y z{!A1Ik+a!4{fA#$gJ%nACba`$XO46WR31FhhI)c$<>^b5kEfG_(vLAy`0deL_2Sem zlgu{^)JB*}s`Oeo#>`uO=nAtfKt0D{ez)cArlWrVBs1&9?h6H1W7t#BDVqJP{~F>b zt-!f|oQJ|wk(WIKNM!TGsdcl&y&cNryrDd#+2a`ATw&zOfblcckKOV=Fp2++kIGq4 zOCwzO%?|bxj(z7OguptH7v^(QF%gA`VYq1oqFXkHP`f# zqm_AN8AYmTB;Y(B{`drpxT>3E$Y_44k_Q8WcR5DyUEn<3SS!R6h{ z)Fd&~tu#tLdzIWicOpO2Z|ru0IHR)Z4?>^l?oq1yT(;U61c~TG#TUY{){*k5iR(6K zKg%EesJ`iJ#{-*%eVuv7B~w+SNwwnRYNk{p(K&lzk`CK^T_G4xRKjEgrFB!0enDE~dMki_BR4kvA-D5*Ed2SjQ}7E(3W)8+0cip3OuXF$jPEg}l& z4ECD*E)B{$v?Wfipjaz~o}zR-3}Bo1rGhpqf*l#p+sKP6Ri|sI9$c~*I_gK)Kx)jJ zVwF&zr*c@a;$1iu*n1Cfi@xAcKs`zGD*4Pv%j%zfLbT^YK@R9H* zhNN~0zyrDw;-%KiHSm=2Om6(v4`;^ghfv6hVMU9ltPgtul0Bk??~@TjYmvK6*|()*|nrRn68HBvYFsRvjw^!8fHdgUg?^JNra-(s++ z9VcF3d#U;)BsEDuR*2CuNmICjlG7cJvH%e~F7QcNO|Z=@P&pmT4t2Rb6O#Cbth^_<&Xx2tMd=tc@6RY6G= z+)6Vntl&X}fwZ?YBn&G#EUJ%4eX=np=Mh!grOW%6dPz!Ii(%xR28PjOcQr+pDNcm=;1=EcZXY5bD>Yu^*GRlcrK%oX>A5GWGIB)Nz*IET-3*8f-6oLwuMPNrM@x_S zDk{?WW;iX9`H~b|ATtj7cBS3iM0tL*&ohHx`GggQ-l7GssN#TY# zQYZ(+td{pjDV1N=#7(4!wP;@& z)Exv}J6KXKNUB^(jT4If2iGmm&HfbApon$3$tGYOG8zcA7vbR1h+4HL1Ov&4pF2iA z!t;KmGovgTn#wmn(JWe&>1O8WM>a!o=;8sr$!set zQz?Sk>FFvevlUykK? zAqMGY+~<;FHAkWxX2kS2^zv1V-QJH=CRJ9!_Rzzd zpmtZ(-?R5N(On|Z+VVtU97ZhN3ge(Z1IVMYvK7C=Sq3A5y^&IxQ4|y9%R!f0!lg+e z4Whm;MW0h|k7uc=15jPkS>qUf3coy56#$}HNutMM-5>tyz)RAZ)%PKp7XA`C>(H~y zsd_vZCpeK$P=4MUj(vkw+5RCQ6aq*_1dVv~VVlPJ{d>XNUUjAX;mLWsB%cxC6E+&ZX<)c}#`7;U3c($B) zI!s{OW4X;3Id@DBO^(hrCOl?}8ZD<;zy_nn^_${5jwiz<($!C6nz$;M$KuINXmvz`Da-PI5_!J;AK{tgVRm8zV zx*I{azdfaIXx}u<2u42VuZ5zMJjt$iLNbeX$vn0$nPw6+5=TuQ#v>dK=SS{Q$s1y6 zZe?J`^QhLMkNGz4*O0d)-!O@=!t;^Xlf@K{TA&zZbdd-R=zXUCcENT`cf1scYr9-u zlc&pLO~0Hx#&an5n-L`YU#j_kI}Bo$$O6FoG}kG5>)>IqkCy=%OQO;JF^7eOP$Hn% zSXxYKUt#Q40^GSm9g^(ds#Qw)4r<&4?p8)cyo_mRf1k!{s>GABx&!q?-ZV$Vmr)!k zGmZ^;sPP&C{W(WW&Q>BRI+l!m1%=C6m)BBYkO>_-(^9y|f-CZ7XWguzsSvWss||jM zWHX%ztKgdju$(*mM?kn&A+2}8fuXA9G&q#x#I%KApd~i1-Ns6jjn-0jsbIo8HG7ey zB9%nH&4OgnnNFupPsbZ|z6upB$V=AScL zqzZz_nmq1ON)9fwHF{k-Y`=Q2pq?&UwKlJDJc$zC$E<8la5ksD zYyERn0oMz8%g+11S9%;yr8$7-C(bzh_pxLqYT|p@2Rl}UCQt+r%S=_+_x+9kLT0l5 zohr+sjLuaCw^*e~)aNO#%a9TrdUI8S>{|3lTE2?9PM4|P3f9Lxc1-~w4O(>?xB~B={>$y0^9Cs8%RRpTDZoe9yZnf~{_>1?t z|Nb0P`G(*j7zrrs-aWZVbCHCTE}T^Zu9|v{Z)DM*MPYkxkOA;KX+$|9n}=ShrNb?O zh$Nm6zU_^dLykWkWRi08@QDM3{eUEPc*N4H@QxfI z2pab9H$WZrg%tsu^~8wfTM<7JIr;DIBdy?)`Qa;8TovFpPQ=OME1!M+Ty&#K+@HmT z6PL1%GTzj2TAMGnCCNlJ{I+6OGT-jk&fRp9`#j;w%|gGmNq@v6yEf@c)tbO7eo6M^ zx^?ln0>v4T=A?ap^b)4Kmti}k@CadRvdAa3z`2W|<|0PNO&6=u>{rKqE;R5@*=>Q) z7M41|F-jvlap~j~CJefy0`;bjD5U|}%SXW_nx`x$R(&}u-+B_^un|Z6Ne@ARm|uHg zfhty1P?#ouBy1ABJhWY!D{_>_`pGSY;T$hypdw7G@w6EFOdS)seBEyf;-;$uM<;F0 z2>woHok$vHxm@gpYJWqs4wOl=HlN1NNW-)MG*%Q8lgv!)nNv}u!SW_w`N1&tqW26S zXLcC}46-2Xr1p(+840MiiMG~5pPnq-dyBa7C>)f`9-T2BT7H;ImM`p%>|0S(W5+Fs zAX+nS<{v3wOZYr@KNhM2aAY{3kcw)oEIgcp@&7&lclUpv|DWOiH2-ti|8#f6^&CMM zCVx1rjr)J%l9ELKM3JZ|$s)tC{>YDd-JoJ&ncd}5C`5#tn>>XVQ2WM$v}8Ih6iT+P zJtVcy?!6K+x&ohlwyc|wj9Cc=AwDD1HQfZyS`>woMqy;kg62%@B}2QgiS3?_G++Au$D+44jY zBt$=SeAFy6mB)zsiCwjV{^Wu21GEL4uCyxgMn~H!4OcH8ljTBe|BcWFxbJVr6Gq-C z(BZt_dk975{@q-yoHD>gyyBQvJEYu$=b+)FrAk7gHPsA_(@&!cq2fqF#Z3)WAdris zGLso29Q^G5hUK)SIPZP?H){Une0%G)<)OCg?Dw)W$eNoNp2yFqF$sd{ zRJi1U)}%_hA;{Qinsj-^2uq%FcRcQ#}Fbd}Wt~L~7rUs~t(o@KV26(t66WIeN(NS$mH0gHHC?eMZO zx}2RuNCbIzhP?6hR%oKzQiXI?>n_m86!`+T-)`g(7(}G@*qw5Jw!C+Pe(Z6xFB_FU z(bW9~;#auMh_Q@-yh7jFX{u7L2tQDRt=(q18m92|@zNc|>?qay83IP3nePon zLjviy=i5=3@)dH6spr=LNZ5cJV-BU;?xRo(`gpHiA^2jbUS4ze(qKNB(8ABxYSbHO zHDA)U31LpUQGjMErK1H>QTSgBneC#2r_$K__23feR&aE_r|2hv?oStM&@{Z%0g#(B zAVQ$sYxLfLz+Ci-83WtOnWb&+mt?6R2v?8`aY{$+0>*uC*MHDScxvZ*u0v1>6tYYR z( zSPsX0_8*l*i_s*wthd_~ICHo=R*5~D$|e!^;@Uf+LubM+xEW2PPdZttD>MfEe1UA4 zFmw{P+e5{gU(Md5%|F+TI?Y^;2A#Qwc8;kpjIn-Y^5{H6ct=|ZkPZno4>hz@l)sHb zBy8k8_QO-i<0=pDY^h3*xWnVR&$aiL)-Ubrz2Q&vz4}j9O!gN6DmeN1QcLwuPTuX- zi;To5BM$9%`4tpBd167NP#|B14G1md1ir^>%$E&BeyG%_)T9Q2^LPTm0lN65jXjpy zA@GiG0L`FTV(`0;Pjp+e&GHn#9U-_qJYohbOE>u&%{aZk)FC9Wk>}W>Z}53OL(XUH zI^)8DgoBWcy9-wV{^8Th9dQX;2+Yvfh67_hx;dKgNeP*8^%y7D3Ewz95Vx^X} zMCSnXiJEyT1qZ=_vodzssvvjq*D`A9nAWeB3Q9eu7y5+^xTnKR5Qtwp2%>eB>>@-v z_&n`y=ek;Ond=2osCibxgK$$h!sAWKqZj&ehiTRZKRC#e`jb`I1bt`w`FAf%g>DK-} z@epf=kyfCD%rTM0@BH?34X4IQXA;Bjgbks<82raQ;qWJsS|)2vS#DW^a6@(tv~w2c zm)%yR5kBtEw31DdItZW#2%(*o?ht&L29!bnAhze6LK}{EmVSTP5}p_D_Qo=3gzrmg z33~-9*P?n3y5tii&A^;tXA3b(`4sXJf{PkT%;ImEMWMS`ETa7v&7m^Cvdhd3`S8Y6 z&Truc|2y5SDl7Scov{vSDI~Tm86rLRPLDC{wPUlcx@)q*E$g*J2a;%CML{&}5QN6x zs?jUiif^iLy-r)8f?T-k3!!#3BT9C4;DNJcPo7Tg)O3&ueA0tM>H9@!Qmq0}7q|Lr zio$?#m_lPiWM&S+WIs!)-!E-Y;^;xJ%-j}JS=abSEC@Xnqs8Guc{~tr@{R4=sQW)N zC&L4JTkTJ+sNhfwjlMFh3L`EM)VZ7N1O6o{c#cI3jW49dT(+v&hJk;ou%|3{WF#D~ ze@GP-OkCCKgWsM&sM?fP~hA&(iC|PMLQ2N)yJ8)NKmNx*u zY9+f@K~6Kf?gP*YhBDKE8#e*Lqc12kcTbTJjo=gBSKW;TwjvJ1t5<-Aks07HntZ}j z<-)%MM#r8aErF{HHmq@~Mvclll2%{4XEb>_>yg?lM{)LNU!o!xFbfe1Kk$osdY#tt ziq1?;ti#i&HC87Smta!|qpzAC2 z<hiM4x8Z7ukr7Ygl!B=y>z| z!NXxQ9UXf~c%ZIWjkdvwnmYOhR=Subnx6YD`Dh{!N$EW@Tc%{nVm_qVU(4rS@?R%v4!LFY-;OTqc%V>W?GtaA@WqL1UK2 ztWn>S$z0KA-eJ}nbk^^v!`fDLk1xu8IBJul(Opyn;o!TezxKk zjniUgY7*aCRC`GyFHK7S^@svh%|+-qTgdiui3r^qWc-Yh#OalaN7llcBo+>ZPnwnZ zy~}6z$8!8tk|`ULu{aCX&0#nkvV<|+fCT`IoE?6%MS9!J5fgxK@zequp76_&i~N+COJwpGf4 zvyy4L7%^3&&awJFWr377_%lXQWt9{5Uj$(K?yyhpTq%)oNJfxu#C%71vIyyjc~{!A zO!l+bk5JPbCJa9XoKbvysHnzf@pPhneMRm+L}j2_O;uro&F&fcbK49LT?G-DTtv8~ zT85(|{O7V*`bjh(M=q9(m+S4IEOqG}GhLATc(J@yfjXa#r=nUM^r8oY#rdT#QM&&x z-UD9-nX@)we#@2(JUA|MzDtdv>6ryj0l;61bMqyt78}4xwhDB&QetVi_*=xAf9HWv zwgAZTgH*47pCHp#O>V`pVz+1`&nNOqHIgi3_BNCCh-Ac{;{w)%^;*n~0;_mq{>v-y#)HQC@tXzZ) z=~ihnftReC{;LC}uxZ-{|I?HzNR#Ov!}=}CH0Hpn=|ewT8V8-d z@}HQg=Grhb2hJpI;y@d!0QNSm0+U#=AYGg-B4U0mCiU4ZA{RpiGk0bPjCUi;lkR%W zWYukKM|m^$G5;>M2^G)*v~=_SkkMy(j+jHmYUr?gN5q!2a$AKfTDx+?9kJ9qPr9BXGXsne5SOorbIZ=rv0F|%Lo8pM)eAVX0A~#d? zqxxN#h#T!iB^zj0oLZVy%Ip}IM;};(6Z+fUjsTxwg-dd)4tCBNByVM^fmcK16IQ!J zQiQUU1viVA1>(d-W_isOL@oME=VdUU4yOZLUL|bS$i&Y#^}so_SCEtOb1`Zlo6?4l zqIZx8YT3gD+KB#Tqm;RWcd$*e#f;9#D`5kfx(yqQnbn zhu*~L*w3}n8!sX2z#E!(;!5#@rX)1FPfS-%8H{9(>s6Z^PAR4ODS(*g6b)B`5MB+Z z3iCoS0+AY2m@4qiMN;(sw5)Sf^frNu;g~XCXm9q%FsZ^YE-RcFYCZnw)P0Eoc9^e1 zE@G5iL;_hTDg|lT0}8N?1u&F+C=xW7o|K0s%ob_QU=ZgQ999k-ReYzK-`lY_UZ9rF zp6(X7AT<4wIjlfxsIa7 zA}<8d9RdW4f)QkLAIB9V1WgxoDgemqhW8VmLd{6tiwgwg&&HTuCejGR`X zB0(w|igt|U72|C%8V6j;Fawj+G%3H(Sca&v(Tg3aoOy?&*r<(>C^gRwGq{N;<5pR~ z#Hb9MO|SnEXre5$35ZJFKRjs^+9A)}`#gOJV7vr2ah#S_lqz@TN9$6^X&n-2&_#~O52_MY zx#W?Ke!J+Pk0$FnTEty9;4~H}>gQ_GfJ4x5tsbLCrH3kb0SWd1U?+2YM*-3yUnHns zKE<=I7VSe@B7WgQTh>#Xp8aOG%>Z7qa}kD;bJGs|4ymAXr7@@av>1u{Ns@vt>qaUk zC}Praof013{12LsKh%`qvNzndi|w0w;6@Bi70*ec=L!*mWF-2|5LQa2^>K9o?69&3 zTGV(A(p9{09tihTTZG*xb<55WyC?c7M=t@&LBauv1ihy?;1eL-e&(sw7^+!y(K+?k zMw#U;w&Tz+IpQRJ=Er&HY~NFkUwR_^4C}M3(yYV-+BE4_v8~aSK`N;K&mP@Z9G(%9Y~e~~P(#-y zjKc0N>u0A|@@lFyw$&;{K$2Nb$|QNECA}f1&685I&fzM#F=QsabSbhj3NbidGDZwo zt5o3+-X5CW5X9hn>(n>m%9#e|e^zvS9OF+)oX{D4(Qeq<2drrvr;5_)l#nc4kY3AH zyaDiuOP~RwTIx9M)D;FoUB#eOehIoD7@2~io-+R|U*H2IB+Z~R!F0!WO(5pinb7X= zx2CZ?HHA%jRl0^`^hi32;FAFPRZ1hLt4Gr_uP2C$35;IH0Gx2@BNUcV# zem`l501(h3hnKG$pwTZc-4>AK3VY*cQ}U08t+1&wFel?l(!;tV zB}v4R%q2&@Pgj)npc;{+bbZ=ZDR-0-CiQnuH_C>`i%p!Dat@Q-1EoR6L`E*PqTU19 z#Ik%udcm(BaYEvtc1S?-%)*)PfHD!!5g;uZq|&YcmrFSnKHCg2?j?KgZG?g*+T!_EkauQj-v#6jDGQ4<7XX z3~%!FkVbpJV!nj|^EYbI)cF;X!idn{PqzgI!Rs()UPI}@=wJkPD>tK74D4+yrzJ;q zo+KEZq+`{zFJ|R3Q6^*K_Hqj)C-1Y74>hVVh2~@+NQvr>zgKvrihoIHONh#0ii&z* zs1X;veTWMAL7pB08uYt0q3EWEQ6Jnvs_0j+3_waOD20o;o`V<#+%AOP*Vlg0AuXUM}kDO#q zOn87y6hstGu7a~^fHcrV3cFbSVq@O>;T-?urjtw{Sqyz%8^R5Fj6#kj|@I0Ni`;7YtNz8o3LU zzWrMPr-Y6-t2alne+kwmW`q5-0e4~j2WYCPs1BBn zX9X`!nJXut=)X{Ms}6%P%DXYeYML{Kp^L(s+37-zSVN=@xOs^+l!wtUoYp_DaD(8E zC(e@|jl{Zcl`RiPq*IXbCzh=##2qoBCznBweMtdQ7XDhz=7|bkd07H`ZTveC)21P& za&QiDOj`LGWYJ4m^`xdzSj8jT?9p8TDwxdd>FNyBP-#3W7SkL~o@4`(!AWZtWN@Lx zWC+#~_+xHv3K0z7ysi6<>LwYJoS$@ zv6E8_H0|U^xT#e~>q@!e`7vsx5qFet-<+sG!xun4C-`r3*}8iMsb~(GPY&vJWk3~y z_V9ZZ)cw< z9c1mZ@`$WRks_cs40LJi4j1<~+BgY40JgQ!!j5c=q zcQdv_jy4~mMs?Svb}BveMTNX9f`GWpD2ri^+U2ghk-8oZCUv&_5WB@+p=(GHijq7T zY2sp+A=$YjlKeu#n83NbNz&aUC@}MGOa_SkGTE)Ar8PTN3}Yo(@LiTF(S>gRXKLAE z%;qg4%4&n~AAF$ext>{Re5Rf;*LtVzIi#|{xDdAR&N+f8AzX@bm-<}msQdtX9|3is z&+~d`?i&wmp`521S&O-wF{m$u&AhawC`P*&3bOW5c}Js_Wm4Z6Zl6C zzj9Je%%v)7wi92c3wAUy#sQk^@&NJ3x43@b=eSMo`T*UNa)G&F+Tm40mCDa%wcszU zu!RDtva)X&dgwuh-h^o}a9)Rt-+CIUTKAlkv@jR?$!abL ze4A}G?MD0prmoqgaW;|$PMS{aR>A7%sju<&;S)Sa&I9988)`ZgUP7UMv9U81oMTm&*(VOTHIeZZY>46isA{fU+sU_FepdP$WaK=R z_aV9*1!}5->7F|d<0m+p7+iT~lOk$iSTb>QYP1_7dvCmq#}dAks@+ZteV;0yZGZFw zF-JT?#ZZu_U4*5x_aZX2`rGh}ihgWuN4FPE7RI@6!?J}KIuj0(FxQUw%%xQI1$*}V ze54!Vil&Tg#<{GqU4?E8F*}~#_;)xb2z1u}xIcQNI!)uav1_Ee-?nYDFRQMHQ*vdizL6@ z3n8+UVqS~-IctN#tY05E{#QO~uY{Z2cw(-Sg2sP*Ra%8?P$3ci}*p93Z zT%bb98S!y_a}$W6JibmT<>gk_g<}WRf=mKJa46F3tF|did6_zLwtY)>vBV|}+#DAr z#UPO>F3-gcu@=ajT>94{qrSq~x)81jr5@5EhPqJJtas_-=BD{~u(Jkt$iMcSpmuL| zc2+BPf@`74JtPE)Wflh|-%PD$A1=nFUxks&_T(RSU~i1~N$KMBK}KFzfDkSXq3sH& zD#*dd%d4+X+dXc2db;5Eqx_`@In9NGTK}1WA|kPyvlrfJAO7@e;Er^j{`q6VMf|g`uCu-U zMuOr?&TMFt%5v+EYZoZy9aD~gFS_4`iRwd$s)yHVDq40dNJRdyFw*R;cMyF8FD={u zVo#40Do-pUBs+^_Q6ilV%1{x`JguI>!}34!@b!)EFhPwEvsmmy;DX*3vm#o}2N}Q4 z$GoWlG|Pm^5dF9M`z2tBBRz^)o@<009-lmbq(UO0E9I50&^!%l$}{!+%o`~Kv9X^p zC^(}VYqp@IS4Bib@TIzoBoKBgQY6OuANfT@4!xLT1r9vDy`Aw%Md3tEMKzS8Tvfvi z^7kAx8o5O6?CkWd&ZkN>3Awu4K~g)dgecYO>R+@5HXX7cUoz6LhptREMwLN(rJ8Y$ zAs(H#OARLM5bx`dx0n82^>SKSL00zu&Qr3umc1fbrQbVf_*(&MqoOMT)Z7NgZT?+2 zWl}fi>EPyOt{Vd%96Or?Hf<^R0spCdsJ>iwzZFO;*X!rwV_F&MDy!_$vQSg=411f$BPm)RCCRz6hSe$pCLeeb#j;}m@R_NKshKrYTNwOgQK zQ>!Mvqr48I@hwK;hsk(_ZFK)d>?fXQMo)xn8J2(e+=o!-D=uy9ayt z-y@+mV@Ng7TEbMew6vhG!OF3)*C0xk2z7xL>k;I93#H2D;6;4&iNr&+Ke?1m>J$6h zhF~#evn0)wIL&^QeJq)G?<|EEDR7d;As)|r+(9*A;~Q&*?Bp-~80Z(0#&|n@e9I5w zcyHkZJ5LuE-$XD#**4n71(E}E;F5qLw@KMXLU;`p?OiDt9jk={lz&Y2iz^KoA3ptH zw3%aOSrgt*kcl-I%qY`BuCP#?J6l_?<~S4SqLV?us@ll!YJ}&9hoP-d)XPj~ zmmSF|m8S-)&O+m{PM;3tbp8^h#Es~hqcd)8Z#x3iwm-#S;Mqd&Twgn?Zt}oDjZC#6 zuEC?lOnPgqqC)i-o%XG$B~etcmCvZ0K0FzfX7MRaj_f&9a`I`X`JTM?IpJUuR!20%xLfg!r1JH>8@(acwhj zzqmg$rjGb&mviRX&7AdNhlza)3`Wiqib@ZM@YEn1d4^~*bv*_n}Li7Wj0*?+6T|kC;rmTFR#NeYn99EhF68^bsEVF zHeW$%2+VNJp#@LLz$UB<6*Dr&qWJQ&vz=CmmOpBeM5kO~mwRR#G_MJdPxbF&p^4vw zwO{0k6CkerAuO79Zl}VRAVuWQOx4Oc*%2EKhN^A|F~C~#pK1q}jeP67mL@KCB+MD= zu{GX@we6k(nBsRZ#fVtTHD||}%;CBu@Pi10f26T+T6f6}oPTTPjDG(J8b{e1J-)qF z^INNu%c1oj_4Q~jq_t~<%|?Q_}k4GG8z=_`Y&89r*Z(@ojkP^L}gH zo)M|&fZie8C|>=9uhT03_%92;9&!pejh&;`*S0;sKU5AI8jO)tqN~a8hdBlxJgG)CafSw@xd-?a`DR>N#QvkohueYEz=9S2pCp?9x;5d z*#k*A^2BN9ovGGr&M#Xw$@m(M_(78elp^a2#B#K}{0p;N1!zq@!Pi4VOCFkY2(*~_ zI%=D(04;O<0fP^V!Y|kn%W>n_p~y&uudibA36IztpdFy*KlIp~!nO%gh#m@_n6+qm z{R<@TsV;Ygll3>>%n!CoKnNfx&4f?Kp?>)LQg{A_^$V6`i09U1zXVD<`lL}3Y!zcy zqv7UZTz|p+9jh{$^TGBFJ^?tCZ1!jLmmqX|Dlz5>+65tR%?Z~~*FfzPSJ!OtE*0^% zi|LRBCUZ_iXK5INGTJ%_I)&+9)1DEUHZ{m8RhFNm*hs`Z&7Yj=rqlJBnS5ea;eT~? zRzY z-nI6(MyJu^S$Z#uFrCd*;e}axN0F;z0|=2YIMwOplR) z4xo=zGx#B!&lYm+?biu4V1|nNVg7tX-(0$^FMRK2yfYnrXFdK`>gJ-4tdk1ZAcvwb z)*H!P&sZwSSWJLEQc$h9b{U}`onY5VL*XAu_18tD&~A7@>$X5CGGPw%u{8_Sz>#cw z>^aI`qD}>shy)Cb5S}DF=iiomOy_@*p`?02^C46vaqHbVoJ%CN!iKBTrZMXTgU@5oj(IXK}?#vHA#a*9%O?o3bZT3n! zk%9Vjck0c=OpZ*J ziDE4!k>rlyXU&B2(GR-wU6Z~zq?K90%hDYGLP!JCvGXAi$G37N3&7nntXqE8bY56) zfgVS(z-yG#)g@U;2u6iCx2suwgQ8Z6zqE?wxXhP+Lba8`<1FBCY)XAt(|dzM_GKO{ zXsFp7C#b}VXPPfDFI0otFV=b(D;zgX-IHXpNlRuNZ5JLlNj%oi=y8*2F_N!asDqfO zJWaf#FRa(7o`Nde|GFtx$Uqf;CsMQ^1yPUDmTdUTPv=F5>m-$+db@=mQ+ANrx^r`+ zbXBtA4dSEAAO~X|h7o2Ebr}DqO!FRliJ_}XJU}ovggQ>STh}CFf8Rzeu9##tD5+DD z8t`99QqXZ|^`*zv^ffij3Oet(6F_rQRcYp&XhHIBZ!;w~@va6vffnu%oc4z69Avi| zfDk@$g7U56UbkvHJ?K_eGm``>Y|w(HWR54@=GouJhT3xcRJo?uB0~ZdCd@`0$;Nt_)9z)S)9mCA>RO?sDI3%Z~T9g3<3%`Uzh%hye; zV2ri<5Bl1lR*6GrGq&Q%M`ae&&aJJvR3sScDjLx&`#D)F635eB$vNlp`AtvwVPgg( zG#k;!P;2TR#s-eHFem6KQkq_7JD4~<);ri4pf56#EH)i-9GIMb?VC)&|9q@>0b0?% ziZh>F%}?PGMX}>erA$m?%vVPTcOkm;4p(#PLvR|xR?FQVFXJj8T_xU7Hqw9kc{Fk% zbO%ye&{C7O2uJk96>q)mCPWnaCiCpil~0ogy4it}euTOWnxBuWSFNjF)a3}c8fIZh zs^H1m5EqluBa#D7nHw89BgcMO0ArBlxeXdTH+QMgU*#tqvlO=N-plkVX1TxhCt}wG zs=fp%%Y%$n?He5AvCb+I`xMTDHF)ZH*969-Z|=T42;(WKhm$S9b>+`e8liKuD^;@K z7Y0CIEGzC4r!L;|xb?>6z@8zl@rh40@Q;|RkN1~7pds*#x>HxBM|;x40p?kj)=MOe ziI;8aC6y-$e+~XDGlqtu+(rBLh85o!EotvI3(>aKi{~sLSoCP}SAdG87c2-kEFwn? znx(l>rAyaCVT|jE+1Af%SZ$dq`v#O3W&Zt%r8e16PnQeH!9wAnA8v+Z^OuWXUv=A; z?=92Q&ik&3(8sm(%G06>0a!T)mC@aio@ihH0e7dvC*f>Ya+vLqM;?fi+J{yw99Mv_ z&!$!E?s<|5^X_WNl^j3Qv@Vg?kQY!h8#UlnAmuUHHMa!Y=j%|wkZR9fSwQr1+n1e# z-oGwTf8)KK@c$u|XXH{>T}(HPiTLX?xUEE4B>9lhX66J6Qvaz~7a-IS3ybuwQpzUy z9Iz&usjCX*TY*>s2MKE(i;XpF8|NCL`n9hsCl=YT=z)1bQhG7DhpZvt)Qr{K1FLQk zE07QaYxGK1cpkcFxVpJ0-JMzWniyBnCpo^!n<#EB*W^6h_A?SOoochWB&AXrBk+NH)#68z3R9_rs7 zoE_p-qjW?&nk$Xha(I=9=~c5A`$t;N38JYTjY&>>5Xe_s)F<;<V5hSKLD=bpFD+EoqI-&baoHJIf>2@SZA`QVxq0cx)T%eO{^7TIXQv zoGPEL_}=8VjXMCxbaZZ=ykIU2e*jyk;(zWcF7(GIn-N%l_{zNun)qnA^hVF=I`Yun zvtMI;MM5;VW_I-hi`In3Ka7KBQge8SWFlG@qoA)~tBeSwnSMqTYmUZNWH@~1lrzhQ z7DkeQSC^qxDR4_(KuJJ}M5$Vo zHt-32eBOn5$NMlbmZ3N9TT6|;4QQF)etM!j4Cc#2YEhUY&HV8LHYZO>-X?Zk^l_Gu zUMQ}|*$Y2{@}&0@`DmCkCRoobHf5LUXwP9DV45&$?rthBtxN?9<$!{3xSKc85&NKoaQ(x@z^ z5F@(zkpH1)jX3FpKr5$1{akwXshom3r1f+rKxW#=fbMK(md~V@La>j%^=ugt^fS8~ ze@sqYSR4(ww^r{5tO6x!%wzs6H2x*mm=U9Z2-%G{?d%#dt+6_+8V~Y4h!G|0Yz4fF zJjq%5aBODjXG8TnP`IQ2sHn>Y!554M9JVq~YQrZTwCB{QyJ*{#Jf{%YC2*s*TPhT! zanDi9-N_Z5t}33uAP3kn#!omw=IS{rYy!_r z7;Rl+q$k3!XkF0nCC3p%-*A+wsFUUuZ~&_%)U!Ip1X?JfjfRUd(;B<*S zMMinJ)^PqI6Jp|H_Ed}J{4S88?pt!jz)Ja***8kyX9Br12Rq%@jj&eb-qMt<)=43z z%&QhHdK{PdmY9ds{e91Q_7e3yj-bizKx&Xt$;@aZ^jvl@1+q-3b|V3a;pTjy0{((_ zG#{$)KM|(;5jZmaPppY61P(g^@LlBsTeBC<4XatLu7mr6;Pgby9MO&VR8hB}4kGDc zTw;oQtc7P^-g{RyPjNLC?&A00YT2Sv?*)NdXv2-E)Z|xY4gA~7L=Cc) z@|6B@s1&kmQL^Xv*5o`I7@x|CGyY+|F)3BrDH<}U#2N~~dmY-OSv z#lMnO=!me?=8o&Hl%ZCcPWD@r6pBM}9klqgaSgfp$skV&$3 zUlUVV7GzOK&XR~J<&|}ts{UZiTSRM)?NjCWmIx$_NM6)aoQ*D|Xi$2T+ZF)fMW-3# zLI=|D?0Ho1m@q@a4x9k}col{dmc8tCCB%h6IBa)uP>=@i9R#_|mBR5QWqX$1@8Ifm zTMF5cw~4}AzYf_j=qcCFaPOrJA;%9i@nqXGVK`LOj}J(nY$DiO$83^dGUSr~bz|&5 z`ej_b#~w6?5WU4z`X63Dh^Osl@i1{!lhl6sgV43MbNAPO`+^uq)byc&RfS!{N615a z&ebE$m?<8p*0}FYsTlv&&uQ3=>y8YaOq-lE;xJ~Iia6o+!lSY~wZA#02Wp-5K&+~P zy{X}yO;a0amlP9Y%cK;*>vQF>Q42Y7bx(=9Q&A22bI4EvFSXD5ot)|d4u2neTqcY< zC3YZg*yX8VPDmHH_*OpCaA9yJ9T24EZUV`hTFS(>7QM)w<}TjnO`K9Z3fHoYAYFVt zUS0xsT}3*ezj%P?W!*(QCY7YENXTkQ$R|N;zU&o`IFYK|yoHV$f&{(q*+cWhG}`g0 zw+St!9KLYTT0Ux4jK7E zqfOTsKjQGRkY0}71di5%7>*2+j7l&hqnAo83Foq8$<8Qfs$Vns(v2NbTLYlL)L zwu0cBZ4c!64De`whwgF2XiX3v`sC*Z`<6=d6)NW9QSX4p+IB1I25y5JQ z3uW%2zw&>{Z54|Ev75G-RfO7RJKzU5Vd8 zy&zB1B4VZGmzM6e(Kf08O~OeaMJf_tg9PJXkd&;-1gtjWvxL%dA8X>xK-G=oR>Z4s z43jLEPc+Kpy|}>I>_j)pc`9T^h1VJp*bHN(EB`I^7uAUt{N2PQ)tWaP@E84KTzZih+)Rc3Xy?CWO5} zrb?V#R04}3y22%Rq+(ut+jzb)ovpaS-i2RgS|%$J->JV$vo1N^K64SC9*-;<7x!Lh z5*>}FSp!nLR4N%1eFI2t53QQAv0>ybH$GNemD_%83ltSIr}WUguT+6nOeXx;iI$uK@y`%|49rO}Pu2O`*`nq~3^(1<#N zJ!J_tqSYq{L-!KK=QVw5qla1a3pzaap}-)G6~wT~To0{f)3P=BS-Z(kYSC+bSe5Va z;X!a9*K^g`8sBph6HXkCmM)}xh$nXV2TU+b}Fmd>%XvQxbJYo1GJs= zJej=Z<%>T(1Q`%QusLxQu#WcX+iq!i(^jn%%#b(WqbP9NqV-f@y??@mar4#2`B~B& z<&d@>D=n4?BWLs3>Oh+WP0peLnJY@ZQZKoL`UiwiqwU{xs+!UPP|O2Xjo+MALQJ~Z zp3OR@>be`Ovfu#Bs35-ezHoK9`}`DYCFa8!cb5m}mu6U3ffP$`66uhM-1;u02v6{Pt(#u}_2uTK2AM&+Jm)r?4PIS?76coxht& zOX}GC^QE{7BTbdPf*I~~E(30leUhQkLvw!(Q2|3*MqEJPfwZHvO=bqd-W=63boXN8 z4CGJ?JP3slLS?e~7TPFu3)BfAUKzUxqf+CVF3ea0LF5xLj9R?G99z6*Rk$-&(8#Zp zL7|+BNTaDtbKXW3aLlrlGit4nm^cmK%;T167KYi0+z?9Fdd`$g0j6)66>ZU7CKOX* zq<>{UHg;AmAjhbw7F#?)zZfW#!ujzQA4@j3$_L_!)Lv2NXoB&F!5hSTV}4EpM2^w) zSf2_#*6++^>ZBz7hpGkcQ|jEHJ~{fF5-ChX*1@!(5lWrqXcwiJv0O^TGH;F50wi2L zQrhy^bI&IJu+g>>0K(AGh{=;7p?R=?S6HSt3wqFojlTA9i;^{uIb)GHAcMgViC;H72L4lir@WZ~`2V7=WpCQun z%9@PBXYA z({SJ3aBhhya~jmnx~ct(!gw0>^1r;DHlc_xqAh9*j2+9>$*b&8ppt85&B?0uL9y=8 zucAQd^m@UC0LcMsE#U_ivCJ)LrN5e1Kc}E6B#UBk_5wo1Mq7DMchiHXY@Jh-%6xX> zx>Z(my=69u9T7kJ^yv!dhgwHZ6@{{~V$=kCa2jw+*Av}P!)+G#f*g9xTfaamlDP`R ziGKz@-bNqqdZWGQqVdKss+>+T2%dZF+!w6&JO!uCfhcRPa;ll_MeC+0nd7?kAXy!k zOq&!oQJx`fT*hk8sN+_1BJh4Q?@8#9#eGTQ3peHhse1jccvr;GU0?3AxW+ibOwYI_gM<3TI_-pFw_t z=%wlc{F;;kH$qOHsrOtM?_)*gXbZg09x1%RaJq0&4cao1ga3rAUuG%qi2Of5Hy=$t z7V_@xtk3=M7 zt4C(pJIE5;@bCYXA#869G&FcwJMY4vN*Z5H;M2b-{hP3nbvGx$>ut=eR=#KQKW|Yt zX-ZpPaMkRF8I(g#TBgNT%JD1@FCPCZA==(Iv9_nv@aB`6yT??%A@NE!1Sf++%Hxc+ z4=(SLBn9YT0sV8wC)BQg)xKD$%L}TF5Jt;!eTxB#5#;agztVM?uJM8ShJK(PA9udx z1(&oQN@Z=!+Hh^%T6@YMcwMT1psleFh)yMBnC=?E|E?Ox1*kqRLm-xS!AqatVF!ru zdo)b)_m=s(I+t(+BEZRp(@UFA(b~FCxF4w^&HVt3n-JiZXwe&G;qv6%xtBFbk@SRQ zbfWx(&QMBH(&&z=)Dvu5Dn2uOS1H}Defo6G?zLBM3gHwf`#;5L)JW+lOa|LUN?7~m z@~=z99SQ+yo!#Ue{#(GqO;>p!TC})OSW1^_mR-MNu)m35<}8H@1EeNLX^p)Wq~^HTRqlk=MeJx{exC+XXjMxM z-kG8;^E;!Vyq0)kK&sWI`%HMu?*rh4nEI}cnqq(A5OcbohZw|jzv}%|H91SS2mG6w zT<_$>9(E!8rwnU&F82>njIA)rw-1h697-17plk)p1!QXxX`TsF94n0)T||^^ff`>@ zLOd~Hk-(8Z%N6&F>yU5FpT-tV7Frt1{BkMxYWu3|CC7-;ls$tHBu|Sq224y;S5`(z z7gAACftz9+TM@~+-W4@8G|U@U=)r@5K~t0K=L8oQm!F>>Sj(bN($LUA0<|MkQALma*f7b0sD3IY%3{{Ykfk0+hrgAaW0GodFz$%D%rE(ylS@v2)((1m2T&U&$x zbi)h2XR6;QTl%R<3wsQyf_bm^BE8$HolJ`Cl;}NMbG-e*B{u1RP6WFd!mn;?e+&$k zLeP+Rc@SWy%E^b=!IN_Sz@^<5rB>#cyjR|GOhO)44gZU@0ME&t#c=tznEAdB?5dl+ zPpE&3jdf2I>9{-Vy!>eJ-4h$WPPl*yLn$dL($Lf#ZK$%rj|zx}GzxA$zebI6xDmad zvlAkI?0bpCk30S)T>r~!c|l{ROGR)#lcxEpbBf};P}oj$TJ zl(g{aazM(I>h1dBRP|K*-|^^AAn2pK=OGnMeA9sntBse&qZ3y*#{TH{peY*|60N6a z$BD15u6}xY>L+X@NNlASuX<*M?qEfIRbuwAF*;ze0TfR1X_uIQ*Zek=1~P0+uW>|-VfyRm44Oq0PA=F(|X*Pp{AxL zFjKjEGgrp>7b<%sFs-Ej6P4L0=Nn*o80(z`-wzQBV3I0098yEez<=g|Lc|QHV`F2@ zLxbbTO&k&}0HdwzjiGFVIm_83BOcIbu?NU$r%h=?vhH;GPQei@z8?TePEI3iJ3#QR7cG`r<$`PbU(%8q-BBwVvhC*e3sgNTEyNTDWLm74S^ww#&9tl_)7;_wGBq=m+zj$J&mZ@s1S z_kUJx*>VQ$HnnxLgaS#M)*QMwYgK6XT-*D%X+cqp<9*Pd%&`D{q{!lW$NRb1yfjxkRBX6NiK9g`*2$yh{MpR-F&NOFIAG#cucoeLc=KY3FFT)`%kFNRTSf55 z(Ie7QN#XB()_1SLTx}+1=3na01|Nf&q8{JQFFxp<-JZ$H9On&q`0 zPl*>dj0_tjNe;}M-bY9V7SN^(|A-E}Wuf0Ltc^Hmp&`**i4d~G_Ti)>rJVc}yF0eu zFn6r4t(|`H@V#?Gy6{t!IvCnM8N9u6<8pctc35NoG^+rOSBrBZTx^~W#dY|-B}JM@tD@`I?qK5M_01KFAsO@%&Xu4vW1MS;4X z6)6kJ9P1AZW#s#)bEV!jC`Sq0m-Fy)7%JFt2*$89t}v#^Y0_)*5?N~x!NlmdlXte{; zxK$Cx#&s8Y$EmSl*HY0MnwSbThPWM9l$3tU|2{U+dTCVr8DV0s!OlHk572&BL__?( z9MwqIF=gZL<{O53yKy=3h}crtX^>U1bUJE2dknaPNi9e3l?(Kt1qIRx{!^Jj7p`l_ z&->q4Ca+t2b27LD^K4hP7!WHY0hfi+ycXk@!-cu@9li6_c^f zNyDI85P{%`yInGkK4x#|+c7)xjGVJyFb~;3m=O_dXfjaL3ii(R)1rz-ofRiOSB8Rg zAuw}zyR87h6?qmc@i>t!q#_8I(s8I|25WnTUqXNB(^cDl=NJ4!qG;glb2(Z%G=%!5 z*AZvky4J3bg7In+dGh#*7n)#!mwe*d4j;&hi)7iDp@JrbJ|2QEr2$h7@Pz`vfMIa& z(a}xwYs^CV;hBk}=|3iVFHTCcrq{fgWB0#~JYMRBGm_*9Y>^GxjL!3?_kpCwekz4B zaS2`H=6v#64RwObwJaCe1dp4tq5>j+N-}drHvX(nAG;sB<0Ep>%9lh)Lb{K-5Dw&E z{LG`R(5lp_)`}aZki}R;;EKEhEQ$Fk7zw?-o{OJ6Z~fjGu6e#&EhTD!eHXq?A7cr1 z2Gj(4E+HO~!~a4G{1pKk7;SLI%fjukWQg2iLLd8=*^|t$*mzDcW7tI9#*vT6kdjyc z*OnkB);1AS@{u)gg|v z^77IyoYBCMGD+(-W<`S!->Id6Er~P1?KD(2r)yAEQ(qk?vG4FXLoe_sNN_ogd6)d_ zW2P;(kn|H;3>tV!N}i^F#gQ*w@&Il zZ!C(LIk{}2^EVMTwgAmiKrVn^3NZ0cDxG8720ycox_7KX_1>_23eeQe*JH0 zCb*?@vQ+0aU>wbAo^)aTY%qDk$nx^?%L~7kpN3k4>TEY;!ZWsl!_FA)6(3_ENoUY2Z| zcO+ox&fUr8wDL)+7i@J>2PL~^xj&7;ncnsFK`yImxHZfKfV@VvE!AGQ)l0LtUXtag zLc~9%qxG?tJ)Hyb{oCNiI zl1WfNAgYerCj{hATmUB|A`<8C9;V~Fb=Ip85MA(e(-hadhjs4Z`_dkXN(E%+4&sN<7W7{ zC|f<_IB@GL^?)Je;^TwwT|5s>LvX&%I(a#Tv9ad#M@IUj+K(u9S6W(n3+pbt``^%y zo_W1)HsM^Z-5(H%0*DeH{k74s%WYLUg97hN``(G zzG{>)te6ygTbuhIW6gqPt8F}F7y&8wL`iaoW?KObdRW;(K}vM2=%;B|a);x%HPrUI zna=*guvel!z7>=>w~fxj?A!MoVkB64YeIjH<HEL0UboIZ9$=O-@%s8uG1c`C@?qf(X;CMv-i05y4 z6R#Q7Ye%PrL8Mx*Lx=4^gSw_a^;np>t|ZSGL#$iX2)p=gUvqqOIT(LSd8YF^PT)FV z=qQSt0j~_FB!BjS-1q@fi(n_|OU{h724Lrf6*VYH9zcW=781kOq{}2@^Le6y@uh?I zH2)*mj=ER@4;glU3Rx+S#J{;&@02mOdeiWtd4Jt4e~|=_?_$#pe6U6=oqMK`ie@RO zn3)~EyJAL2rk8!)5zS4OVMvMebXXg*z#cPiqAUCaAsaXDrfx?7b?*QEx~uT61?;|l zp+bE3&*dC9XZM-UGHMHcE?u#w7c0}Mgtk~qTJ{O0%TRDtnND^Awi+57b%4bO)194?Vj z=E}-CIvoiXK6#3q&LSJqGEXw^-mSN|nI};>DiWzHRR9sqI?RE`2s9+DV5B9@GI$NP z-mjIi_>Zf&nJ0wX9#%8n?o8u4Z@nYvBX^#}J39_v9Q02oQ(kC}l)T!|fk0e%-6pia zl|eU)iy?UORY^5o1e2EYC0vCqQd#9FnV9%ULfkShl{l|d@0UJTIl=xy(&0tx@vY^X`9}61-HoyfLqG4_Ge||wjuW|C zZj_&#xw~C0O-+)ZfKV0;>AfV~L`pTsyi!%H1Ou;}ugqx{&gAjW{;c)a`IdO++~JZz znBgZq_{F8UEu$b(TJdln9wb{)YledLYVVTaAHJO-3`i2ZvJMTN`+zIon{n3a^pAfL zQ(WWLkD@iGskl7LrIKkmn|&&nI-kyx?Ck6m9m}WH5v$0q-IjmMHHoGs$WHCirM ziTXj)1SB|FDsgz7K_FL-a2RZ=!)knmXN7m}mGB>Vc(qnJ-DTm){*42BhFtNEH*t2z z<*S>Un>iz{Yj$?F(A>>F?__Upz)TI-k_T7^TTW+!YkHT1`JgQ=V7S-9OZ2qWKeYbs z)SOxsD}Qp3K%SwM@6WoEg9F})=#T3kx)AFo?yP&7lakWXfHVjPryK?$r0?m8H`hIN zMk!(Hu-+|8_h4+O6MUd$Y9E?*j~pD71Rmf+YzzmZd2|{e3gD_pw`G=D7+ddZJ~0+% z>l#A08vINs9B!vsDx{mi8k-Z(kyPj*U;Hs0QNg>uImg+-UM@L+Qp)xDDMafy%a6ydwLn@p}K z<%-kqIO3GDeO9>^oQ>o-2feIFblCg-ts!4gN1>-bX{yWh%wV#Je5=tbyZuRuTwqJ@x+!Q^jm^S`}1_vxV>#_4OA4>S4@cH9E7nm|0QyGJq&%2CuZrpC}O3 z2Dx+eXQCG=7(buhuuE;zd6Qga0YQ?|?6)4(!YXmDVOjPb+z8cXL8|O z8mKkLUaUQ1y#lXOjL*4B-&tnMW zb~fT$8+#`IHk*5qzm1cSkjMnX6~C!h8meMF({9qG$gR4F@aC@RGxPBA*$5pG$Ba18 zvAv|}rb?3Mdkfu%&R=)t2s7JS<=P}i)_OX2mf7cWY83o4%GvP{UpPBETVB@Y{@Ltr zZ6i(qa&L<$L1##jd(u{IotxwFO_JIq##ERy_Qi+uJaYZ@UH+)O>sxG!ft?%mzQL8@isYc8Qnx|*65BmJ2Y=h| z`I^c1rz_JZx~)4?U*Eqk^&K5&uxeE0hS9OHq59Q}S+xy%@}fSu0gm$fKZ!%d@S|PDiV?E&Ue7|drhX|g>c*5 znd7tKL@1a8pb%%-X!8zE@k+jcbFmJmzJa@iBd2=#xh`suR008QQAEUNOt-=AoTGGF z%%2$-F@#jioRR2 zyu7^6LsKsoevCVRHUC_N@7K7~xLKv2j~Q)! z^^_|@4D1BQh5#`@3xQS$AsP*tB<3WMWa&hXbGOE#T}As11Y;l%0UokaN|H6=#)1C@ 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 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 literal 6111 zcmV<57a-_~P)0ssI2phCI_000-JNklJ$X}-M^TiLOUUJPkceCpM;INs=6Y}(_k%c!a?d@N z&dg*kTYIj1X7-$arlx7?98;$>Q|sTfTWjxcuQlKATWd`tFp)k)0zyEb2{eg76KDcW zBG3eyL^cKF4vZtE&$W5B3C6tJONENB$ko*qSD8!}7Z(SE=lPzVp8o#+jg1YsGcz;8 z!^0gN9k7i?BWfRIyT8A0c^xNob8|B_HFbS`4HrdPT3VWwl?59b92~^!z9UB`P7#Kj)z@C_x!0R|a*jrm$r>Cbdf`WpAgM*!~qVc^O8yl07 zl3rh5;kLK8W0Q-Eiygwv0loXq}kCk3tUIGWxJD^c$3WWkjb8~Z9SsC2;_;@rab_*Lj-`Uy8 z$jAUDgTW9U9=@`&!g1W=+2&(n|{vJWF24fxV za&mH#n3xz96*V_Ehu3d!Z_!~4!|d+vj*N_0N5BE#zLSlPp`oF?ygc+NY-|Z#sZyz` zs;cVi>rrcf3;Nh6`WkZ<`>(95Y;A4r?(W9)v;+aoqN1Y9%ggfeay&(|7Z(>}4QMts zHK8go?k$TE6Qil2p#ixt!Epzmam+%Y5H^HFB_$;|44iaDMFsi^4Z*T%3=MoV8XZ=i zot@Di7W%_NBG3eyM4$;YfhG}X0!<>&1e!pT2sDBI5b-9SGwPL`R;Abel3eXQT__X^ zxlFFnXn39{(ElqVZ`2w%C8tsAbq1r+<|z>CbUKD%q*5u%vIc{JK>uOa3~~|%qA)@(LYb(xl-KS8)e zWzh0M?$c;A(Cw8h2`kf*NpOC8c5-yA$eG8ZYFlSUDm5jUWiUnt>-RrJPj7a1|8+PjGFVPunAHHK%?nL z2%0;$tLM9dw{e|LC%4V&_xqM*ZSq1-Js1pfiwUaNg2oO>Dt~EHT+I%P*pX4Qw*AA-{j8=HL}36Yi#$VKB8zOY33-HQ@gIT|6mg@V6R8CL<0^Y=iWQc%suzaxijbZeH#%Qd5PUC z+AbEvwOXyhwdHa-OMPg#ZLth2m&<3O#%;m3(&KnME*6V_H($4RA4VTbZ)<>cySHeA z13M(ZAukq-ZNY;9o=;9ro~#Dy3=s|JxUFq%sZ?6W@_(9srt7^>v<tri(elN`MF=3e!XW>99$Evc-8c{F z5Bdok3cOBz-ELPVq19^Hb{h;7Kk^VUQgBjRd}9L)bxBP?V8Ii!1hvTR>OS?R*nIc? z=da&;p=dS*153pYu{g|TGo!56>kcI^#L~p%&d<+7r;z1Q&Eugy5c5W(0dfqSyb;a0 zyu1tn^d3C+Vb@yL+G@4ZOE6U=C7n*wBen(f2M!^8=~{QXpwj^CJI@aEPQ@G`@H0h& zl4}r|z-TnOyZ`a!`sVuU_x-Zya2oA)8ieEfr=CYrjlk_d8hbVX z^;EN|=tm0-vq{5LktV=DuV2#dd_EVb0?BvS@Ang`5VL18nVg=UM##|k320y7s3{ur zLNd;E(uCcovY1ss>4Q^%@hW5{!&)J;qnZ%D@AyMvRP#duq6DE%;QF((v!kP<^_Bzg zukb}8#Se2s2qenovOFOd6nadqayyLIyuqiCj4Id}yklwe`1m+##(9ekKOuYA!8SnN zqg0kvN3+>9?}98zQrQZGXEe%GD&;?W2Gii<{q+^86}Gu*WCE1StgUoX0x2kYgKgEawGtERi2aB^`20yIu2X# zL#*Ra4vIb~`k?4P?A=XhT}2cJ@D{oW5|U^ZU1X6EjIYTqge(IIAq0XDK_Or)2%3FF zaU&>Z6;VV%isGUemqr(*l9d#Sk~Hd~E-WJILIi0Azmqm;>9093y?vqYy`)Xwd+iM2 zadPk6nYm}q%$+&^bI#0lL(DxvzTGYXlCqF_obZk91D5MKpA@T}m0jbc3cr#-J3{yyy%Z8(F_-DFy*G?vFqH z_QH#=tghYEy^V~FB)ZIHigqp0;`@ezJ5-lSwx{3yAS4CsfJpTgoxL>}N~ZQ49UYa;b^1@mQn%95 zL0)t`?Nz`iKme*y?56fqj-b3A@hhUVK-di}(YPtxKJ6`KBb^*T1h|LS(4(+_?djr<{dSvW|eN0YH{`||YWe7!rS`_6G1f@RLAkYTAaCiz03=AN!H3Xch8kB{YnwkOy z=-Q|VD`|~^1~W4={?#fol35O0v0j7Sg)^u+wV`n(S`JZy@zkVYDugYyx3`mDqJ#?( zvN*Zfbar<7C5-+3{nY2;aJRL!-LB*??vD{;jN_omAwo+V@4I7I`&E(2s_em(N4>qh zy|c5k^r8`5T;r)gn9F)2BhK--!iel$LYzaKe@f$U$R>RW_~XLeGo*j#kW#W3*D z2QJ3a%3oSqfl7@G7W|YT0YD`}_VgAh|@&NjxiokPC(+&+_(SZ)T27E{8 zLiL6p3K2mAx{7Vc8o})9PbE*%csgE-s_qFP%o@k{v-g)=E!NI{}rAMcqc#;CMIO)@0 z7lf3P#CxYqZEj(UgvFCgjygnfEX|kT7$W?@UejAmE2u=wEm_7c;B@+~I;3Fw&ta)6>%qrR9tjRvBn4 zfIUK~J(gHC#CXJsyO$b6WzpbH4>B11l%Tg<$#4o%7Xn_q zYeEdjRewxOOayG|3Rdcah`NY(+&vNLaS$a3DS6Pk2gjZDt0y3Y{6vi&brmb(T1(!Y z6O2Lf%bXzH)BxQnoBlu~$GQl22kj*&S_mX#OyYHPBOFa5 z2N`2at$YpSpHntSbFeG#yBF6LB@)y%xLheD+ZG<9$!a8VSawzH;|nP zv^OdWM{^Swa}-T7UwIIC56Cym3B3vUcjUQ3AeqlUcf1L9hO9Lzgse-%?j<9p2W^ht zQ+zZNv{lgV24ER>-=7;6a5z&CqP^FJxfMl3$?(hKi)h#WI0sV$E7`Cybb^lOonhV? z=F|v`W{h(7?3WiV{7`1bc_+zY%c?6g`j&L*kH5Y+v-;A@uNIAQt&C6D?tJsZ%S_6^pifEYo1v%NbSHe9G|&^&M8DoCxpfZWvLj%k`9# z&6ws{8p`0VZA&y2t+lo`byoaey!7V>AAbDFr=O>Y#8&&Ze8oI=()Pj3N)b42t41|} zuq5z?c6T-%7P%JSZh7;kueWyxxlIH?6oyZc6Xh64IYRbXA(reR8zk5OOSXt5;u;)= zPx(>HB8hQuf(@>gWwpmWJ>%-`n)j-z%g>(a#}RZqyio#;tE(%pL02nZF^Y_{7^;=@ znba@~gNoDWI@r{TkE~|Hd0#r6equmzVe_1daWhJpi~H zKBVecLC59f26e@dquOqLQ|FQh@?uJw{@Rqaqrge+4M3sIQVbBfaWYy6&gubk!QxOG zA1Y()WnLf4 zpD6&jl8?AubAfH)m*wIhPK<+*U{)mTOD3bUvojQqh0o;jE@B)FqmfK5lW6TvQB-ux zsMJ9wDkF!^As|Z;neRwr>IHQyym)n@y>E5vEOJ!0396Q^sHKb2A+(Ayt6MjTj#_GN z@{p=S6s$?@MV+yhwNHx`Mt!j_nYh?X;|}6(JIErzh$GmS+9f`g9lI+Mgr)5Vdmfr) znIIU8O@9q6EEatECDDG<|}I zx+VH2a%ABB76*?VNBem#ZP~-_`}2u5p$v}Rx0pEAX}a}xVE{5=O}Y{EltT-;6gn%% zV!62rgYKBpYa1U4WkeG605-8rzy z%iIKwg8=}*CfUO;GV{9)*A3YD%9Gap)I-H0Lwvle!;3B#yfYQthOcy<#~nmbG16{W z{a8Z6AU11|eh@0>BNr!F$|I#Nr6&xOl>-Q~fpP&mA5ADoj$hG78)*rYlhh*M2Gu8? z?R0WMXGo0&nM{4<$FEC+S*Vb!8&sYY;EZPvWF|ziBlmXZR^UjC(dF<9&0t|>XvaBu zjSbG3Lx6q5b)5_FaHN5EN2EG%BsYY-5nrq~5|yCWN2qhvAX`$D?Hg5no?luTu=9de2k=`TZ)R0S%0t(E;wtJ z>agor4pJJx4+iYmjLcB+D{K@dJJUoNzT<3~X%KBQPhHVLJ?nwW(t}Y?KX7|f7pSIh zIeFW*U!3#CsO3yqLA&UFJG9@HHjGB|qQOpX^97vyLL7_A{JPgpDw zc!R8m84fd?atwm@=Ol06eR%cyjXDju`#+U<`ax;z*CXg%JuIKUe9bw#J^3>Fu<|_w zY%`UdMA_fN0-2GokWuVeupQh=px`#vR}3SUhYF616qMg1SPhxMZIBiQ&&rNC0BW!S zzQg%4yRx?V3?(@No{}1>joP5;e5}zKDd0=PUx~2cZ(yv7Jpo%t>NAM8uLK^@5L? zGk&w44w#eE;#&^A|6-8?qsgEr0k3Z=~f5P1?P{Awc0t;An8I_}49M zIOv!)@?Yd4$x+MP;dGXGG!jp-9Whs|f#D@6^i5-f^!=vX?pa`*FsPHj(fnBUZVGkX z(x$x6L`J3-JI1IE3lb=Yi*L!4p!1X3bS9f9M=e6{T%nN|5MIs=w%Mo1IcM2B&aZZ2 z6#V|eX(aHjbi_C}84)P2Hn%M29v5nPP5D?k19QCkh|3rc2ia2mUFv zJr`}k6-qIa|FtDf~oF_4( zSJ-Yj&r5krSjacUyMFMo{H2rQ#Gc} z6vb9$R{*t?cj9!*f|40QPZ>dv lphwVCM$jYZDI@4ZmLIRcXa)U1?Tr8c002ovPDHLkV1l%*cy<5) 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 zcmV+zE9caSP))SKH|xZ+d2S9Con=1h`m{5LSU841pj+kRJG?kRrs8 zLI@!!0g8Y^&;k$zYIiB_E@th_?#zzsaXmdfZL6xQ%iOB#P2QU~y*ZhOL;eALLlhtS z`=m*dbpdaJ`^aJPzc>w*NYpDP9WcatQ ze=|xBZ8zbOj`1|`?Zs-L7Y^NYxH9{qNJ|IJ&s60!3>Anufi3}P7K|LrGg;;%mT;`* zX8lQQ-Ms&E0ck9>bH$T}x+<}Go|UpJB4CV=9!Civw8U3qJYpHvat8fTGq0a$w>!4i z+}_`2sAOSbZvU`zc6k}G-rVfj`TUa|XXobTTYws$TYa>|7Z6vVAd7=|QOR=BkBfSSSm;MAO=6 z+Guv)p3QG;?R^MSpJU~axQNs1bC2)tz1QsaYesIRwj6^^3?c?En9kTsD1#-EB)FkV z34WHrM(4gWjYUozj$_MAMk8R4qqSvY;pEC_Jf2#eC;-0e{hL1<9S`&${Yi1XR$N>B z@^HEdr0H_ul_YZs#_isrz+|jY3KQG3+dQA+Da=yb-t;!dNUnXLAcMI5YN4psH|B~j zWsn;?BR}r*Eay6AoZ2}yzccxm=J>>EiefH^sDk!LPAtVyn=nF_NuJlt8#OAG;@D*w zjf0dIGm2a^!Egfz<)Anq+}^_;-02e zT_Wz7*L~bb;xr}Pso7@~t{O%yKIf6lr%>Wd5A$$B(Rx9D`KWoFrOt(k!29Y4cm7P( zZ~=XRgKUJmh#^o+JI-x^nE;Bd&wteq4n!)G8J$5AN7q=|=uWo7!~$d$d@cgIUU)#W zi=9CafIVG0J#a>tkto`;3CO4{Uz(w^WHPjUpOa7?5Wjc$*+O0SxB*L@PF--m_Vp9b zG2egddwirm`@|~)_vq~pKa&`tYu`V;uvD%tA}YU9gVj~pG0fd8v%4t~;EJEgr&+B~EmrLAisWaYIgrdYOB1%pYpm3$g8x(RY5v2y<0mxFl zw$fK_~QY_)W+w`K6lbF(Wkgo?Tt1>U4( z@9$0yn|le90) z#Ho|dr#WVQrgA=t{my7J;Bnvi|EY|b)8@~tK7V!RN3P#1D+^lb=}v3s{jGP_s;5fD za-(;5nK)SjkrV-p_2SZWtXr{j?ul}S($8-0{l*K&1bu38Y2X~p>9Z{FQ%+CdD-}gA z(o4&Eue&@NCY>}^4;mejoon}z!Z1iIQV1W6+Zcq8Y-qbp7N+?^k)Iw~td;-jH-G=} z#QEdD`0twLsH~X}ugsGOt1?UA?l9>4o*XApn9yNh<)VTt2-(bde0w5rlEfk-*O7QZ zLWJhxR=dqIWm*jH?Y}j)-5|0BA)YNG0MXA5zD=;$9y)n$>{cwrNJAf&tLGfRz-Zj< zgUC;B?A$##3<_enumJ5aKQir~zO!@ojH(p!$F{xu$s7M=gk1zB?0c!0xk*;m^`bh% zGKl5KI2^g5%OO_c%5KyUm_;|<9@-6x(#pzN97j7x{oSo@p~4OKn7ePq|LuSCyBe!* z?qAt#Z8!0 zlsFn;JkIIM6SFVNMy9Kd=b7YM205BublrZ>e{Z?;499u71u|c)%*fg&KYsgP{m=jX z6KmBBP_3plAoc+piY#X*a_SFTTVUhF^1pogm78}C)N%IpfAXbk@BH#cXPYX?)5$pD zVcCof-p-Wp21$=rD??T0)#w8LWtNTjCdOQiRjs3ng%JAs;bN|1V<4k2w@z8 zEK7;J_g3q88Xb{TLkdC($i|2&wt5Xo=9zFJDmjLcW1K27Pv{`0&qFF^X=&>23W`$T z7FyGLX&TpOaw3lu%0^`lZfN{HO!I2b9yXc>nn(b}{`l57OerT!A755hstVD{>Vp1c zDLOe#w9{YMy7~*nKf{7aj6oH=;^Q;<18BH_AJQIbH7Xm;-LSY`lW7~B=KnK|NqN$`x z1*D?tB0x0H&q>-Wol$_KQJmC_%P#JA#+&oS6^=E6G`(+qnBbu#SJ+ff(ty${D$lZY zzH_7Xi+A4VM7~rso;o2f6@-p?r(y`to-5Iu{_f4ag0}nU>a#-q+#D451^oC$^~J}3 z#Y=j=Z;q^d;8BE%B_#-45~Nh4)n;ggWk`1rQb-0#2mw}9(e~&7dsKiDieWjDqeRcA zyeL7CLJ(p=D8Nutheny8GgU=QB0LyGqA1WbJ~n8E*)$0P zp9%sI0xU=%%4m{i3rqoLgvkO5lhpNRN-85|iY6W#v@>E2$VNfU^#VprCa&lPZ2;)u z#I=Llqsj!a(6a3~99oLW@Q9)ukz3GJeLNnzE)sZ;OC)F0d+pyIq_oXY((OiPkSKhv z^3eJs^gEyK+mEg5-6<)ZdG@)l{??!UPY0ilq@8B(vp@Oc%fI&Wo0DH=9Cv2*A0;sC zPfbl(=?4wZ?Jrcz1fXJhCZ{Y!QDdXNC=1o2@lA!jB+?br+au|68euQWK$0BvTO7?N z6t&;$%c6)Q%+Yj062wZj+eUQ26hu){e(JC5%0h zqeM=>-{^)>q-wKSM%z)qurqPU5VEf*h+~DETumJX#P#u1h*6+&LY~i_d}#HDZ+xul zl@sTm`Tzdkzo@90g>zs3uYRXF4F2j{-+uG`4`=cWZ_HiYeDA5S`smy@esJeotcXl& zkP@0fpV1VZS8L|j+h|cdW1!+ zA>-}s0~TV6z>I>J2={kx*g2t~abG%Ld+S#F`4_%60?fvVS8|{HGWF~8}ke| z9(F#x{z_)>wF+_1^s#R&T5xKq+Te@1<4>c(E)s11C}@km8nztz6i zo^0x@Qq1Qe)4H^NOwc7g=S9gRYg7wsP=b(}Ox=-GD5T-=(j!&V>?+2*Ym2Un>vOVz z)N7x8I#*lOm8$D`h@~XI%y=A0J!?GJ263r$NiUr+53Xs%@LX%PUM!Ysy^&iHwbfa5 z^V8SQz4VViYDLBDU^Fn#UwV|_h!xb8w2~dPW=nz}rU0^0O1=K$`xiFm!YoL}?mPGH z8$~8JZ`5aN2`zr$2o4AAjS^86hmk$%ZU5wjWV5c_qYjl5xNDOzzP1ZJ$OS^)DW| z{Oa}l;WP_-mTibICkT_Y^Qm2swc)Ttm+0N>Cd9(gz_jfnS&>%eDuJ`zo3ssmMi9`> z;h5(UjU=22L{^Ut`o`v+AaPQPq_~lnR%E?+aMboa{Mjd)F^W`Su0I&)Y%V8PM!lhH zjmmTRaj?}N^aD?vFP`ENEyjRnSw^A+ zW09_2ID7HY)2A=|@caMru}97$igqVM*~rn@#TmU`HE!&67pesS(O%zLo1gK+;M;HC zr4aoGuUz_Qr+3&MM;*IR(4icjs)0RodaKF&53hXnN4JFQ;-}A^T^#jVkPcVpW-cy0 zetq|bhpk?FqG(DA2uJkJXt=%KIkvaprkit|Lgpw@AhD9v=?8uStf1-CD(2)r|I`1~3#ZS&_%fg&AsEtVG9DWOwR1F_D;OQq4SjE+q`!S* zpBIgP{`F@+FXvyscGJTsU#@DJAY>!#Hdvm&vN`?2BWK><-u=ms8mr#f^9v_9C7UVK z6d~yh@1C5WSzoBHEo)C)Ja^&b?ByrU&s6F4xx%qE`u5#~qL^Bxnl{`H*ZhPM zSgvXapX~R~tjzxTt5?n~*S~W4;UB+$#6FF$f-<>w#W z{Kb`<&e){-7oQuHz)DO&XN47Olp0q~|7I;sb{_?kLH*ZR7E9Di9D=E`>1}+72Rdij$qA z9dgh;dwNZmI2Tja#1e9|)zu48!ri&@#^Uq;Xnwvl+PmfxQ7#B;XHOpP?Ttr02@#oF zV@NV>-IZ|PP`S1_)aA0Dz@46Rav44I@Y26~>*hDV_{=$;q4~t68O5qac$}cMb?H^ZF<)8oNQ?+UVGW6Zuo_PdkPUZ=i%39X$^ye#O zFUrPFSjwN+>>l3Ty7|wJ3%Y2OScXL`S1AceGC|OhrQ*HDCXOPRx0V;Q6v+U28p#DI z(T0}5P_;^Ya&dmcOLq>ZNT!qyJY$sp=8fiFvzwPil}<7`GxBrY{(zGO`f*{OSwJWy{zxv{( zUwm})v->Sxkbd%`@18n!>G0^tG)D{-TfKm0#GCggZ+zr%4B`=qf>;oc2+>+~ew*|$ z7745ic$}|@aY9dRt2;DhC>|Shqt!dy-B#dGCQ0`x9)f0sg+te*WJ77#gw9I6SI#!Z*Ths z&2SH`&7ZHU3^vUv7@6SG`3oTSG_7p)@6ilP5&{T_;^@ZK!BdZ3`0krG4?E*O{PpMl z)wkcs8Ku_24JTS3o5Ku;Ihi$Nj-r5}h)>K_vn*0XQ5TsoiVRh9T|2dVd=p4{mRu*3B*{;+EJ*|8#SAB+DB(qxtvUVa5W51$#zVipJd?y@ z%NvDm=7gkD5dj|08il=mM=rPzon8&@9Ns&cSRuVupD`eqI>F8B*IS)|p~zo(@sbqx z9M^P0;F|px9(_n8ws-%R1_y_6kY#LuSxi!1U|RhN4WYoJ?Z)8OUw---+Q7 z*&DZRuPrWdFz)tRX@qzlAVLBe`F^mq-_J|J3zyF{T8Ew8&T4((X6MKw0mblHY9DK*TLa67Tdv2~)525_Q zwl(PXFQ1;(@ZsSH-`~Cc5lv$+!f71YQRZX1sH$Vv593IcrOn;__wF=jSJu-snO%B_ z;gzlXTV9yF_}mx%!Ixin_}tvdc|A_zD2g9mucjdS-p@WV$5EkF*;rWDrbZ6es4?`- zK5++{rq0bQEiarowno!P6KIg)45!5JIwKY^GR3(Gz(C@X&@;y=W*9>eWRXd_*2Ilf z0u&~*a=F58f0&mmG|4WWn%(~SjdyN0zxMKr1oGEz+}ztg@WU!cr`6ib`tqzO%isCw zyY}GV*PmWJSB3F(?kDh=w?2OT+RdBIc5iOBPBI^6(V4|cUgPiV^%iG~d5OMvXj&P^xB5ClOqpL% zjB%WSG?NKc5~Ls*AK$=|Q^Ft`yFsy15{VcQD+wr!BNC(x1qb79ifNW-y@G&!hhVnn z1;sqWA>ey4Mf3g?gEEV#;P?KEul~)CKYkUx|HaFfbWKfv@m9Ovn5)e$FU*}-S$XIE z>s&PctuH=<@Zjpb=E$@i*Zbmz#u8_%u?HwRq&Po?y^()3j$}!dWaHeq>T4g~7C5EZ zGg&2n|L*qXm9PEoZ~u0`wbN+z7RoG+DL>(E-0!b1%{+Q$;riyGwlY6c(Q%CWrprNT z&>m8Y5c_7iR7QaD{ef?V$F_77G_jnQE^I8Ts?fEYc~!~F2A#pwbK09jyFxW^ zyLZ%>HupzfZ_sW{hF!<(Talj%A$t3ho#r^vCHi-N^DD!{y?^_6Kj8)W*=H`3B#t7S zH;UUk&Gq@2FF*Hi8V-&cyLWaDcAMRmGM{ABPv5_)OZ@!YT;HKG$Z(uU)0~@RK2i(S z1wk&jVd7%|SxwgpjZX91-}_#@l7IYcY3H!Vv-EQpYp*;}fA`Znoq<)#36rS}Fr^rb zX*tzWp_~xjiYZiZ9DBAbss)>H?c+Npjm0oYEh!u+T%H2zVIA zE)D|2D5Yr(p-Te{2@5hB#^9O78NH^x_VL#J#&C6I>Cv;R*S6X}edm)itBWr`_wsDD zv@x%hcufPN(V%gy@hMO6GiyR`3Y@^Z*}yf8Q6)5SVlx1Xx+06xuGP29;lLwuRb)k# z!@QI$H+S!dYEDELbJV~2yVvKd%F_=my>{h#e<-gl8XSZB!;!2AEKPGfyLUJ`HD8qZ z*mE38%!LBk+Ozz0SSf~?0FDiMvvI(yIp|=8Rq`cu;*Py2!W8ANsLoMpHjKY^XkJd{PyDdr{Dk4Pw(yQomop?x;X1tn$TmF^5|mT*uIImfW7EhlWP~Zto(Pl3FyWoX@cQk&mGhTQl+}io&M|UL-4aaVGI4O&gA3DQ{bMnN}*3qyci5siAE7!Nw z*~g~)~5<4vPX7nSHqYjQf`N=IGU@MKi^*Om@D3YSj zARi$*4C#hRwuh9H3P}uVY)}&<#6m3OOSN3K*|%-0)9trM?r=0L8Z%xLy6!j_H^21E zBWW=C(Od88k}PxJ7uOnbnifTBw#-h(o~kH^1AAq5_N9mGyUl|xG3Z=<|Mo{B;&b`j z$iXau<4JgI&~!@E38wI{g9*!U6o`1C=uf5`hy)hI5JY~+XDNb=YkPs`5ga9PNK>r9 zz=iqKpB&*{`<*QIAfzdXc{*!!2Xi{JQma(*`kU9RtGkYu^7GTlY@Smko)y#-#EOK8 znPI@M)GI>7`AIr*vxA|(a{lSXRXhCA&vc_`IvxvPZGPS;@L&GIld)x9yScqmFVE(s zH$OZ)x%SXE|LZ@xefLf=uid)&p557ISYd6wGPhEC=kC_6?S{Pm>G+R4U1wOl3%vd67h6lx7UWq&Q9qp)*3lB+jxpNJ&aK z9z1p7r5D#XaQ|i;dw?Vm;EYg$AiAzq;^U%_tSv3tZu+m^yki)8QA*}Dyj+$QRU@>r z(;9zt5UNJ$R4qq)O+)7Y@7>%8XkUKmAGGdYO`3P}g*+_PBeQcjJW3dSlBRK(Kq%Qk z5ZY}KQu6#uQm#B-UATX6W6(LUI>Yh|oVwjfI1p)?ns5R~gEYk~#}h6?8ANpEPCZ$fKf8W8 zAhx#l1*&=59CZt`%e{LaL>Xjg#sLN@J%h9fjyqbuNHdgA_$i?=mxEHC=8H-99$=>w zP2U~%>UlX3a+Wk{4?Ev|^?$GA3k$h?ue&vBe9Q^T2Rpk1F9Jdaxz8gLhGH0ng*flM zdFATvV;5f@O}kS!Ru|RH!H&YqVH~%{gUCyCg~23rC@q!q5~9;Ib{xzi_ShQz2Y*rt zy(~@m!)DSx#EunvK`ihrtFc<1ff-Fw5eanc;n>C$FQy5GFpUChuCQEHXYzAvGrHp2 z!I2MZQs8yAhmG5(*G~yBm5_h#;tRKW$|Dya+B@uh{|A41;3b@qef_%b=8%kR1l9Vn}SvX8gqV z^g<~mAegop8gP=N8HOG4vF$~`_oGyzRWFDDNeN5$f3|r%(EsB9xWGYSIw=gT0^VRLnemG48nr)8TwV4eJqh5dNc^G?1Jaqt$b9q_PbV-mWU9i2m$!nC% z(DN#vlOda>OEZO%$}eC39aKD*rISDXoBw5;>?4%oz@1-si~^}O8Un~7KJr5ga+$=i z1OPT;5{$wWLWBe{2jgfmogfbRb`*qB;35iU1Of7;3x{m-cXT{?S`IDWJTmKfU}TA!l%(ls7|=AT*Jgq!3jCDiV1HuU!=98E484E}Z0+v=hRLZ3!{}k0 z`mu{Mf*?IHJ;dqbw+@eIjUEho39a*#)%C)**$07@uM~1d-k+F^l!wef;xZ);I)iJH z0F)e;FD};RakZ!w4PLJ3xxCFFp}3$T4Jj(Uv{Vq)LS7k7eWSw?vBsO(SeCc5skSmZ zFZ0kP8nh}zZg++q$e}_-4@t~qAY+PAY9&BUNfbDKt~>`|mJIuD3St}*uEcR{mLf@4 z(=>~d43mVDd4>rvrg%CL78ZlpZ47q8m<>Z$hm19i0d32I3ga*i3C+_8W(3B^_iM%9 z{_0V44`yoPz-Lv+vvione(W=n2XK^=L@%a3cTd8U6~(DN;6%El<`bWSoHv}NVGzY( zzEqZPeB8<9wS~o6LIWCvNt!k8?bH`UA44K!B1k!!3%oeUvdqslh7+F9ykvN>Ym|9S zAR;Ur_Doh_r|mw$tX|eb40_ED0KV%3Bc}lp=j4hHV@VgMBbP%`nmIW0Gbkp39T7eP zX}{IyXk%fOo51jGDQiB#woJl*?RwQpD9Vp2D9r~VT5KY=bWgeNy#&p&+Z=XQ%vYvkY{O@ zjVp8Yk?mTO?rdF|hMe#C3<&r_mY)q7h9;zt3Zm=*z$>!mOYiUZe*CBY>Lj3UaX`xLE&J`ibb)SF7WK~Ze% zQ>cf=0~AbSsgx$r+&FsJXXgR`jagJBku7C1Q zt_pYr{b?ZOWK05HVaCIu6Pbn_RK?PEV<;;uioN;*H|Z1+$%3G`e2HQBY&3Nw!-7=6 zT8eQ>;FP9ugFz~S-AVNVy*WzFl(^W>|*#CGc2!MNR(3vv`ae@J42^0j&b*E-qg@FEC zhvwu1bClx)&l==%93+TJ86@bgvm*#-asJ%UJ$UGmk~J|kWn-^%kLS~za&9W;jzoe0{h-HVlq@X5h6i6=DV4JB2L)N*KtVe@&z zn&5{|vjelB3bU@iZ;lyhzBaHr#|F(q26#%8`q&V%EZHcnG}{xG3O4Uf3OUKPTQlqH zFP{5GYt*M{AxXQ<;cGRi?3k0?MBrH9CRUIPjS@Hk6@jAX7H7NDGy_yYTCw9UhG~66 zn|MQ#4Sd|MOZnw`@Y!vDYBo86i2!c3($TPq6Fc?+CM-#B(xRFZ31HN;)Z~$#*Jg8K zvb}!~Ld~|s-GfmrXRL2j2v?{UHzAB|f0bi7jb>>!#Tk9;=xAd_Id}dsf0*gDW<_%; zxW}@=B;t2Am3oamHfWE~tSADP9*zcUvy0f5W-CSrhB4-XG?`y1Pr_}CT*3-j2COI+ zxV)q+l7!;uu`bmNN#2ik0A>nBLFTK&>8AdKuz;c@km!Q%@W_gv95AlF<9Je!RMTwtg?qO5ExG}?Xx}bU~UzNyQqcwc$ zyk4FuWc)tQc~be@WST6^EJ^Z{lj&|vU;p&l8wSs@3S)ag(O^k3mDJ4g6U#Q&YY!DJ zmWOWl*tU27+h1PriNbMt%efcAbVi@0z-XFLGQE%_VKOT7Z?f;+KrA&{Gfeiac$?|Ek+bmj37NJsq=2?iZTC(zxm7Kfo^rL zrG$o%!Kt4Cz#$HR1Oq@*JjoJ3;}mBUl`^!Lku**S%kUh72w~F9!&$^IGzAz6vIJ+4 zMifQGu?K0CftX|v;}ilWN%Ko}N6!f=e@hf^BTkRmjaDT*Xf7^NV~B8eCMZ~`Gs z2n8t2(p(Bsn88}<{NES!gKLc+T*vkR`avE*KY)G!{U8sZA3#6I1L*%;^X~zRRIVb? S7q5%}0000$&ec1VWYTvhaU(S2>wY%9Xn@v)rNSc&HQI-`Mwj)n8^4K%EvIZDr0(%x984Ttk zz|6(WU_5Ya%gzD|ff&=4rAU-4vZ*E|Hk-Yl?$c*K@4nW)Z%>8!4{{Tn1n6(w)I|XW zeDK4AdM@h0^E^kM_MbldDZx|dr_fJ(3jGxNDfH8xLO+Fm+EeKNLZSclyWfh_uI zE_e=C<%?KeKb-V$|KgW(^DDDi5PKt;4@FW9;dYQZnIkeOiJ=oNNusEX%EczT2Iq3e zcY506nQ4>Ql;Ii6KMC?A$t;ztCy35sl41LROlICOl2U7nFOz^>s-4Agb!H#;hCgqp znzqq!)(pHposPM5Is85L0wM3`zaFP*@EGiUW$eNk2#L^LKkJjEJM2b zzH6n-cr-O)+h8;ev9)MprA~7Td&9P64v&1`yL-R$)+tT&?Ry_>HcI?bnM8?%FxOYV z5NCElNMkhKSg4V7ajsI;>>fo&NZ#fEIkFyw3GZqh0h6FJZj{O*enu_UmRp;aYgH;L zMG~%~TmH68sBPmWMlrhfONLebATKM|6i_A;3L)8%_R#gZMWu;kbQ1Y8TTNrDERzgb z64=Jb?rO2sp5Ejnx)d`L+pgXl3M@c_-!G-v2}2RZ4rk6DtTVabli-2?0Bh>N}c z`k94VxmhUKAWfcGy3E(rJNN%IPbnPb@eDn+r31zyfRqzYM6vEb^U3sP2s=p(Awq4` zUY6(@6%9zm&Y_Ewh;8;Nga@!{RsG6I=O#rw8$y!u#g88Tktjt4{52eB!?=rKkY&-a zvm;PGz+rXiH(l>QCICaq6e+SKL7>XW*hyi40tp2{hRyt_gAog3-N@1tkqxJgo)R2} z1vY}DFfNvxAWuv!HUgBx)jaoy?YoQfDn`u+a3=GxOLO1Yc%69X`rAxWcd}BcchH9(uv5K%bb_;TAkiN$)YmZ*68LPXdWNGJBSV^n zS7oXgG73eN2l``yR(PU?0WOYXhEM{#Q!cI%92W!!NDig?T>B)QX@_!QWi&a#BDm1J zl;A4Oh&YZkOV4USx52GG>E4LcBqS9QeLirS5?Gyz_lx7 zmOd6)$`6qJ!8=$k>~--UelSotHb-3Bm?02(;DGIQu1gHf zu{DMn(iA(hANd#`<$g}4gTcO^*>36)C=+ITl6VF|oKlf>L*R#9S&A#n`C@S`2;Bbc z2|~vM^8+L!3Z%R`|J=>p9~kCIOXmV9CRWy>GmEO3<70OH&t{Qxu-LE7kHsgWfvVie{!h3{K)u>~sfJ zWm(gi*x_)7qN(&m>vAme#x-H*h{|JDY4OI9LAlCr{N}e`PvhVJzyHzS-Lh+ve{E$x z3lNW{P+~Lm#fBeHBK*xGoq7A#`Q~fZGVo6!kVhqMp!(kpjN$xz^>2#8k5K5~j zradz)>qqZ9TW18JD4*IyXhkj*S*BhSSF)-TXs=yep^1_e z>{EErO%9R7S}QD;=QmLZ4hH?5N8L)Tpv||gzkU3ZpS@of)&KU~=eN5%a)zphsDx2> z+wS6g?bW&YJt_LD{_S4h{=@(359UjY|KtzAfdQh5(p+Zx(V*MCc6MdY_NT+p!IjNfi zW{Q++_Ew4M4YL~`j2kU*YS1&s%rZR5Vk1sB$UX+}SeH!&tHYRb>Nkx#& zb|1&*78m~4i!U83eQ$evFf*8$_)D8VCB_e1>k;f7oO5tvh zPOP*M$+41$3HRB30>D)7Wf_sd&&qHVdMOMXcQ}lsLjK%&#ZU48I$0=QziG}@vW<;$ zZ|K-|WIW|VN{nF6$;a-<1anev~(-OX~XvwHJxhSXb1&5>RGc_Sw-f_!@>T@m^Os` zxt8^PXa++mn1vM0gkgjru-#|x-G6`~34)`doRt*Bu#%5I)sigAVTvH9_<09N zEGHOB$PkT2tNH}e94?@_BCY^H#Ysrh42lC;T1rx#qvmycpAtWV6*cP-LGi6x0mmZ> z3CoQFby>mhwA~1?9DO*MZML}ehVYj^?;|MsY1>;XC5t5nD9!aN%bUz>u^f`;UcP}nNqdd`ryH_66_3ROocJ+CpI2oAwtl+RK-Qd$-OY8a12B91VM6$ z;%-V1Il;iP$ft2K(_)69Nh&)vXp&H6MoBXxiR~Daf-tPgb;r_Jv!7bG0Nx9Xl4dq7@-i%X3{zRek@+vFM&7hkn zF2j~ELJ$b7AT9>c;d*_IB-PsD0}i7EyrKt3I5;1Bz83{7N_6!J1#pO?d&52sFeJ%H zK-#fKP^4-5IGN&gfxsDFCW$OMHRz+Ez1!aIjt!;2G%JJ`r5LINkt>R{NC}#(!|wsb1*vOI6|ZwkxvW6oaSN(osl>j2gXa!p1pPNK$eZ>^7B8r@r#y< zOk?FAeDfcD`0<^8diVc0Io|pDi}N91QMoh?q?fmT|4)AQKYL&rh9QbEUGxEklQLiG z4K0=w&>YTY_i`-M1w026j%HyJK@?(S4Il{4lT}$Qu&wH1l=-8wd}-0RZ_XZ;`86z8 zj?70mjv(2}QTst>(iemR0n#wgPYpUlVH%+l&E_8Z;8E|&rPT)yx_M6a(zGC%wlgX% zEGCFM8ipyr7w27)aErp+bY^B)JB$<}^ycaXt<%;#P_MP_{9?Pc+Q5joC`LCwdXFd4 zEW@L~Jw7?o&Dr{5Q#xh}WcKd8;cvY8<|K?4s+Ts|OMmgn_i0k5XrJYIl8lEF+i~*( zOUOK%!Z1i2o*hUy#}x`RCHdZia!G-iPvAI5#(7ej8oLrLq+ut{agHp>{6Y{;7SxuN zyM689g}Jkvtrrm*(gY5};MAZCYPxuKD+_^Tg%3O9Mx6vm(;6F-X=1sl7Zvty90Jg# zXr$RJyLN}9AeIlK$fZCTQ5+hpgi($F2B(DP(mFJE+r~@~4)a8PZ z2_(Vfi94HF6mE$aqIYKw;*khc1i2mcvWzyV1R^JfHQ_mNX1$l@3Cx+}_92-k+{$vA z8^ARYL1cycsX?zU%wbGWtkq$Hx7NQ%<1WLv!nvgo2@dw`Vc%a^0)~c2f?BCZt?I%- zXDCUEETtq_;w4Op*C}7nt-Zl;@b>qA^!bY`jcivY^%IwM&DnB8MM;KN>U%a=Us%$Q zK3rL>ZJqfl-8?%sLjtD|1oG;_1!nW8KVWGw*Xhla0!~uwK;N&bs-zZeFA1%*Ra?{C3B%6`2--8oePbU@5mn|e z?BJzyrvxprlI}Q%)~&h91=qFQ-16+iPc)1Sm)5vq%_vnGu1#vDx3VzrI5|JF=Ns!Z zD_go9r&*E!kms^QC81kgYZVu^JU=}-x}8CkCsCG`=jIj$V|{^|%I>V^9y68y`on#r z;v8<@d+@DqejO$e$#4x!c80er#fYP6Ex;}`YVY3f&$sGP;xjz<$zgs!(OVk;t5Hxc zcFhw3Q0@cym2-{#Kzoc>F|uFdl#dT>-?tT6qKIrbeH=v^NmWcVAhA>?mnZ(jHi1kt zr#8{Y?R_gWBVZe@okk1}Vi*&{!@cFz>XVbcz*IDEoCYyTs+tXV585lW4FU^}hK~}5 zrGO$bOp^M|=G>&hEyZRvNu{q?{7 z>mS~j{kU0gBuPvW1TE4Gi{K1uMecm9(AJVKUt8Dxurtc{c4oi-E7!IM+O54I7&-$w z;Od1s$?Xr@$P#B9kzak`rCa{3V|!vbap1UiI6k|yRpJT{#yvrDvLv4BCP@MkXA_hP zXzKK!hh`93S(-NH-Bx9u!l>Qj6BLnt@$-Y~Tro0``Bs4eyoeXv*_c2CNadP;q)%pP zrdAczPPELWCd27-&t1ED^Y&l;?Y~>Euyaao2P{rdJSS!l14ypQG@vDis9bzz{qXVb z&wl#uuYUEdEX@L2$BO0Do#q+B6R2gU$D>(MDeQK~BCmYm>ekyIKK#y4Z-4crv#(#< ze0Vqknkl5Ih96zt5O3^EBD}FttKR%!SzhqJ{F~409o{7XT&h)8tIw>g%#ZD(eot>S zN9kP7=s)_;@;7Ei-G2 z6+^7rUi2q_@<(f1s~0X^rUgEO362wv||$ZvDMiFW|of>Z;~EU09fqT=$!Q-hvNvnYjjP@Wms$+q@rI1^Oy!eR{t*LTg( z7N|Ifd6W{vw&liCj3{zA1bL-D^R1?TW^>~gH-Di`rfRW4sueUg>+_3KBa&F!%`lo3 zZQs`-3Z(d-{n>ZE^w#UO`dpr+04GUWaAp&UfMefO3d%qaAb^|A!cO1Fay|jxKmPyz z=J(!u>Dqbx_LFv!(%Nh|nA(lT+>4hlZf-38i|>D+P3_?VuBM9*XgQL`=K$@lT?w~Em^DkUJyMJ_I7(Iaza<)`oIXk_1Z!#Dm zYGrL{#UF1o)s08TvujtM*;?Odx7)*Mm&UUC+}hra@1xcpLlI#VL@D9M$o1{vx8A(^ zPyh1fYHRLGFR=gb_x|q13u|XL&c?Cz;CQ+`zy8f{e8cqJfA;4;X%^V8yt4V;?cGlw zbx0t*Rjv)#ZCwj;m>r)WmEy2(?iDz3sXX-@5l2e}u5oJ6rZc2S29pRy$YimAVnULI zpS!R*nvLAJete`sVpdfokD@}~N>V$=$fZ`*w49NhL5KuVu4~nO`~s0wZ&}0+Ch}=3#eA zv$dkgbhPk;2kpQArOW^4UtWLp{OWJNdF{R1dtE((iCHaGU%0&f@uR&@9<+b`%@>E$ z!N2lvWOlwYuI|VX{9Db=WIl}8FDc0as+vPor+_kGvP)vn4(}A z3T&LRu)^R{s_sM z$#^G42$sql3O#g#B*_Ja{^g_YZ?&42HWqH&dEBas^R10bpL^qv|NsB}!rJ=LEV6y? zORv7TI~*K5?ES-Ey;?5PNrr}Lde9EpREk8`G`N~U< z9iagrK=$d)_eq?7_Duce8A&c31n)9@*G7FmL5IB{_wfFB{kKC{# z6iy9#th;gGvMko0S`yFbAS3Y*04IV3qX8I=Tk8xLr3i0|RU9T!4haM!W^P~NL%9I` zDKeON!$|*?s~c~>fB)vr=<1n;8jW`ze?;=KD)TIj?QY+u;ZP4;0_3kw4Dmewu&2FzaplH?&bLz&-j#yf;2|DESn_} zCNWgki1}jt+SRikK5XAVHkTLb5}A0$)Yi1l@v%PB``ym-S1+wKc`BVn06Q3JbCsoP za}kcWD@w6#v=J1CfJ?F*#lcap|FtvgV7vEVfAHoDmx6>ingnrTKiIx2v8-)sgO1)P zmlUNU3*2X~(saX#;MRO?WwFv7j~?A=qYP$O%^)3T8D7S{AcOmz8HTV}!krp42eQx1 z!oZ`jh*u>6!JMfcu{_1nF+xgw6UMIXo?sN`X%p3t2`tmF9+I%*XcxQha=~Mm1wc@!3XZz+yC>*+W3HK}ui=Rusyo1`RMC zBXI>M^{(YxgZZ^8#blA|KvDsD1W7c?NrF<*oDy-deKJ;MhG&apo+%uC)X`UJD#xHN zU0D8$cWZVv0w;qwCn=_&$d3=&-+B9oZ@u;PJWq8aEVtG_{OHGb?o$`e zo;$m_Dk=z`MIuX`^roUHT)VWS&5Yp)uoUZfaTSxW677Xo_5w8nh&; zS(?mdmQoNogm+B`BTyX8(FDjWu1`89az!1$yj-Y59 z{`zarfA0r3{=skm)|bEVg}e9e{`CEirh2N1e5+BscIEt|{qFXiPd|Tt?#hZ7gwrRw zc{1{94AN@Jhfj`zB=!+DhJ@kRyJJ_DilstnYm>jb*N$C(d#{7C+$W!W@WSWcytq)< z+uM$#VA5-kbu)+}mFIrt+J@r<-Jx}5YoR@yxGors4I)Rq$SJJJhCbjJ4o~we9UFS* z)SxR06@*BEoA+X98&*>hf*|V!I-Xgh0kmTeVM#o4^*D=74@6mxWnsC>;SmP(6vfS9 zaC0<%X07tVl{1t)_xAVRe))6Hp4-^OQ1YE0{&j(0USC@7^bLKYf3}&PudwZtot=Zj z(agHED6Om%9^Si)q*=XE+uieH1b4zb#AF(IvPY=rN80Mo<@B9brT_++YUIdK2} zn=e0j@@PD^zWTyirNrMq7{C3~+rRn7Wz)91Ba4TW+(x3+x3WCLJ*ilaU9wl;2@p>G)PT5p$SN2 z34%PgS%tmsdpCCs7tAlLec`$DH$HjbxJgNsmzU3!%e87jUcus}OG+NxKE6lq3G`(Xd=(qd1~)c4E0DfvFWKec)A4RhPuwcK;0Hh@x$I z?x{iVb*F%)Qq$)!u2Naj91Ej(f@EM0g(?n%7$X=rGO;XWc@aw^h+t@rW+{>-SdAh- zKVP`|>u>#=?_dAH2e+6<$7eU!o;|lVn~gWt)vy2FmsB!h5sgBeNN*nvJ4|fF7?~!q z>qmx{ssfMG^u4{^1TTdtUa2p}Ni6C3?%>JegHdziY7FoUXCY2&nl@LP%l*NdufB-o z?hihAbYZ<#RlrGiHq{LlEPhu-pHS6Q(4aZ z1fCl7WHw|4m8N)%48j~xBsTMA5MW+tax|Z22}Dwyz%hy@F$B#q*UkU}vm_7G=yOf! zY?Wp>;n`;|tkqdfpB1H~S#ZJ-7}}=?vy4o&JhJC1oWOA`ORrQjhycrl5V9Q`7zsDD z>?-MRE;9s)QiTRjQuwhIM#;|Z;pJCy!?I|cw>?jrO)jsmU%YT}Hty}Wy(?SuGtIjF zWQ?JZ2Jq}kt*<*AD-28@&52)oQohu{4AQKDKm-V|N7EJsyDN=neohpO`1V3omn)8J7a$3%9wt`I-5>@_p2`lXiD;FTv&*MsOKbmw+`bJSy-D7ns1!B`nkock#3Dc zULEc3sf*$;wg2Mdzk^;tkoa_V$MpIMD#>09?KsPznOJB}oEr4v3WZZ#9=e5LEvG?B zWC=)7khyLY#x6;-6b|AvB?y9IF*Ij0GV&c?kn3N3`I``0;_@|ju&ZlhK`Ggz!xX_# zKzfktpj%ZesZ_Ytr69+Ej{qZuRaD@q0?SsC$$pAD5=HlIt0Bm^R3sb8?zsKkzxgu} zPp)3s@+OZ??mgsK;ojqiBQNvuG|N#zDnJ4Q1R<0$j(zgozx=@$U;B;2PR??4$Dw9;$Lh;Cfi~{I3E0M@T#~j8Q7DiHonI zmFfQW(e7w_ZBG1byNIopuND_mikNutBmjC2O-DZ-_j#G|)8n2KV36p~+D@r)crsw) zKpUN;FgkX_C`)A?2OJxuW)j61j#_S}WiwPV-n;o{qFfmoy<)CT%^`-OBt_e!vB=bT zmGkW$fhIu)1i-OamXa#Wvr~fxf-RwpW1DL&m^+PuFF)#>)aOd`3u@c$rf$mBpO%Y7R+3dw4Q%>5Km0bqCUl<6OH{endHBgYkDBFHMg7|E zd|PQf`!Wdr@c;TB|M}nkh`v^NV0DwYyJc)u+gFdr(E{q`}yK=EeVN7{!TmAA#vmA`*!zZO*7O+ zm=BpW$MJ~YJG6K;$54>MJn?v#Wu7bJ5R$~$C|8?N zn3-0DVsgD)iIUWHWs&D*nLQm1S(Q-aW}5S(_Hmp8FFYc1h0Gy|MJY%z6eNaE;HS5A zn3|E`3QvS%$LvgYVrN=ulx4Z#TVthCM3re0q%j6`_Y;mzl?vCGn{UqNi!HILRvzwK z%{gq;FWT0OuhTqFwOUnHC|6X|aN+P6;tl>F+Y^OPom|>jZj3n6M*!v3dU|(kj9CgR zl!YM6@SK2XA2{DKI>(4;?57bzGAWTK2nJ6L8X<6P_+%VW-To9~0ZU2y zdtsa&5o`eAu}qUeigFB}MjTGiGx@xYa9&LZv7{ zBJraf!;~bOVwgumDhQdP5+!g(F+q~%A(DlJRSvdDvm$NI-0?&MSdwHOz&VK8F#X*vC0SomdzqfvdtL^*m|{m)IF+}=XeRz+>j}7 z(2sKjlLX4o@YJ8iX{gN{G>8DOVuGY05JV0q!q_KCI*oHVhiPD&03q@OgK-M|{M4XH z3RH^K-BGu>Ha8uZ7@e(drlu_bzev}XkoCCrjI<$QZ*Wm%dH&SvQ(4U1FFr5I&6 zHc)_na^y$?Ve1jkxmkop5kLryBn90<8B8`N2FbV56we|A9bv{44b7A(#z{Omd;VOK z>oK$)J+9Y8o_7eMc=E`XBHT1>mSz~7Z`6VaB{5$J&>&3F8X4 zQO|N+D+L~cfXKsv$1sY5Z$BgMy{42|vaTF<$ERjJWQfyh8)BGS;0!=?FVy&Ra1KOG_}LqHs3 z1b`;)tm~O}Op7bVFk#Bfrzb{|m>{QAAroZM_6f_vWt1?@fRVOm(${ub?7>qOGOnp@C)yTI819bO>;C^GZW^JuMSBllA&S8gR{8Aws56LIhcSZnl0qQv#wkNdyts%YCnT6DTq9wHqi`=z z(Q<|5sj6n(N?|7Qat07fBMi;!kow#cpRA4q^e%{;uwYpPM5}lN~x8Z zUUlUnoujtv49$L$25IITcjQZ36^2$B!ON&BkF-wu)9260)kZ02PiV5o*3KAax>#T3 zxR*@ppeAkHx&1Q{ACm$RL~*e|AgL|Lndv9CJ6o?`EU*>LJH3fk3BreHQJFKH{kxq< zb-4-hnF)b_&u6(ljC98hG<&zgZ}^EbnIz@%GE8%Zzvu+p!{Nt;0$)_O2&$Y|VNA*_ zhuOoO00a@Z2Qfkw@v3m4KkLv1UXYcMjdYCzhLSNfV6dd2Hfr@+9^01X;xw!9DT-Hc zWUDvJ4i2}I1gcFg4ciHz8bvNnJLOcy7}AW6kT4<3Oa!I1r7KU4ZeZw4U}L~1&wc*d z;!1VsAzsk0HrMw~0)k~Rj26WNVi4yK7RctQLH`f`-Cg8A!`?vtBm31$fAIA1s;7rn yJ?$y)SKH|xZ+d2S9Con=1h`m{5LSU841pj+kRJG?kRrs8 zLI@!!0g8Y^&;k$zYIiB_E@th_?#zzsaXmdfZL6xQ%iOB#P2QU~y*ZhOL;eALLlhtS z`=m*dbpdaJ`^aJPzc>w*NYpDP9WcatQ ze=|xBZ8zbOj`1|`?Zs-L7Y^NYxH9{qNJ|IJ&s60!3>Anufi3}P7K|LrGg;;%mT;`* zX8lQQ-Ms&E0ck9>bH$T}x+<}Go|UpJB4CV=9!Civw8U3qJYpHvat8fTGq0a$w>!4i z+}_`2sAOSbZvU`zc6k}G-rVfj`TUa|XXobTTYws$TYa>|7Z6vVAd7=|QOR=BkBfSSSm;MAO=6 z+Guv)p3QG;?R^MSpJU~axQNs1bC2)tz1QsaYesIRwj6^^3?c?En9kTsD1#-EB)FkV z34WHrM(4gWjYUozj$_MAMk8R4qqSvY;pEC_Jf2#eC;-0e{hL1<9S`&${Yi1XR$N>B z@^HEdr0H_ul_YZs#_isrz+|jY3KQG3+dQA+Da=yb-t;!dNUnXLAcMI5YN4psH|B~j zWsn;?BR}r*Eay6AoZ2}yzccxm=J>>EiefH^sDk!LPAtVyn=nF_NuJlt8#OAG;@D*w zjf0dIGm2a^!Egfz<)Anq+}^_;-02e zT_Wz7*L~bb;xr}Pso7@~t{O%yKIf6lr%>Wd5A$$B(Rx9D`KWoFrOt(k!29Y4cm7P( zZ~=XRgKUJmh#^o+JI-x^nE;Bd&wteq4n!)G8J$5AN7q=|=uWo7!~$d$d@cgIUU)#W zi=9CafIVG0J#a>tkto`;3CO4{Uz(w^WHPjUpOa7?5Wjc$*+O0SxB*L@PF--m_Vp9b zG2egddwirm`@|~)_vq~pKa&`tYu`V;uvD%tA}YU9gVj~pG0fd8v%4t~;EJEgr&+B~EmrLAisWaYIgrdYOB1%pYpm3$g8x(RY5v2y<0mxFl zw$fK_~QY_)W+w`K6lbF(Wkgo?Tt1>U4( z@9$0yn|le90) z#Ho|dr#WVQrgA=t{my7J;Bnvi|EY|b)8@~tK7V!RN3P#1D+^lb=}v3s{jGP_s;5fD za-(;5nK)SjkrV-p_2SZWtXr{j?ul}S($8-0{l*K&1bu38Y2X~p>9Z{FQ%+CdD-}gA z(o4&Eue&@NCY>}^4;mejoon}z!Z1iIQV1W6+Zcq8Y-qbp7N+?^k)Iw~td;-jH-G=} z#QEdD`0twLsH~X}ugsGOt1?UA?l9>4o*XApn9yNh<)VTt2-(bde0w5rlEfk-*O7QZ zLWJhxR=dqIWm*jH?Y}j)-5|0BA)YNG0MXA5zD=;$9y)n$>{cwrNJAf&tLGfRz-Zj< zgUC;B?A$##3<_enumJ5aKQir~zO!@ojH(p!$F{xu$s7M=gk1zB?0c!0xk*;m^`bh% zGKl5KI2^g5%OO_c%5KyUm_;|<9@-6x(#pzN97j7x{oSo@p~4OKn7ePq|LuSCyBe!* z?qAt#Z8!0 zlsFn;JkIIM6SFVNMy9Kd=b7YM205BublrZ>e{Z?;499u71u|c)%*fg&KYsgP{m=jX z6KmBBP_3plAoc+piY#X*a_SFTTVUhF^1pogm78}C)N%IpfAXbk@BH#cXPYX?)5$pD zVcCof-p-Wp21$=rD??T0)#w8LWtNTjCdOQiRjs3ng%JAs;bN|1V<4k2w@z8 zEK7;J_g3q88Xb{TLkdC($i|2&wt5Xo=9zFJDmjLcW1K27Pv{`0&qFF^X=&>23W`$T z7FyGLX&TpOaw3lu%0^`lZfN{HO!I2b9yXc>nn(b}{`l57OerT!A755hstVD{>Vp1c zDLOe#w9{YMy7~*nKf{7aj6oH=;^Q;<18BH_AJQIbH7Xm;-LSY`lW7~B=KnK|NqN$`x z1*D?tB0x0H&q>-Wol$_KQJmC_%P#JA#+&oS6^=E6G`(+qnBbu#SJ+ff(ty${D$lZY zzH_7Xi+A4VM7~rso;o2f6@-p?r(y`to-5Iu{_f4ag0}nU>a#-q+#D451^oC$^~J}3 z#Y=j=Z;q^d;8BE%B_#-45~Nh4)n;ggWk`1rQb-0#2mw}9(e~&7dsKiDieWjDqeRcA zyeL7CLJ(p=D8Nutheny8GgU=QB0LyGqA1WbJ~n8E*)$0P zp9%sI0xU=%%4m{i3rqoLgvkO5lhpNRN-85|iY6W#v@>E2$VNfU^#VprCa&lPZ2;)u z#I=Llqsj!a(6a3~99oLW@Q9)ukz3GJeLNnzE)sZ;OC)F0d+pyIq_oXY((OiPkSKhv z^3eJs^gEyK+mEg5-6<)ZdG@)l{??!UPY0ilq@8B(vp@Oc%fI&Wo0DH=9Cv2*A0;sC zPfbl(=?4wZ?Jrcz1fXJhCZ{Y!QDdXNC=1o2@lA!jB+?br+au|68euQWK$0BvTO7?N z6t&;$%c6)Q%+Yj062wZj+eUQ26hu){e(JC5%0h zqeM=>-{^)>q-wKSM%z)qurqPU5VEf*h+~DETumJX#P#u1h*6+&LY~i_d}#HDZ+xul zl@sTm`Tzdkzo@90g>zs3uYRXF4F2j{-+uG`4`=cWZ_HiYeDA5S`smy@esJeotcXl& zkP@0fpV1VZS8L|j+h|cdW1!+ zA>-}s0~TV6z>I>J2={kx*g2t~abG%Ld+S#F`4_%60?fvVS8|{HGWF~8}ke| z9(F#x{z_)>wF+_1^s#R&T5xKq+Te@1<4>c(E)s11C}@km8nztz6i zo^0x@Qq1Qe)4H^NOwc7g=S9gRYg7wsP=b(}Ox=-GD5T-=(j!&V>?+2*Ym2Un>vOVz z)N7x8I#*lOm8$D`h@~XI%y=A0J!?GJ263r$NiUr+53Xs%@LX%PUM!Ysy^&iHwbfa5 z^V8SQz4VViYDLBDU^Fn#UwV|_h!xb8w2~dPW=nz}rU0^0O1=K$`xiFm!YoL}?mPGH z8$~8JZ`5aN2`zr$2o4AAjS^86hmk$%ZU5wjWV5c_qYjl5xNDOzzP1ZJ$OS^)DW| z{Oa}l;WP_-mTibICkT_Y^Qm2swc)Ttm+0N>Cd9(gz_jfnS&>%eDuJ`zo3ssmMi9`> z;h5(UjU=22L{^Ut`o`v+AaPQPq_~lnR%E?+aMboa{Mjd)F^W`Su0I&)Y%V8PM!lhH zjmmTRaj?}N^aD?vFP`ENEyjRnSw^A+ zW09_2ID7HY)2A=|@caMru}97$igqVM*~rn@#TmU`HE!&67pesS(O%zLo1gK+;M;HC zr4aoGuUz_Qr+3&MM;*IR(4icjs)0RodaKF&53hXnN4JFQ;-}A^T^#jVkPcVpW-cy0 zetq|bhpk?FqG(DA2uJkJXt=%KIkvaprkit|Lgpw@AhD9v=?8uStf1-CD(2)r|I`1~3#ZS&_%fg&AsEtVG9DWOwR1F_D;OQq4SjE+q`!S* zpBIgP{`F@+FXvyscGJTsU#@DJAY>!#Hdvm&vN`?2BWK><-u=ms8mr#f^9v_9C7UVK z6d~yh@1C5WSzoBHEo)C)Ja^&b?ByrU&s6F4xx%qE`u5#~qL^Bxnl{`H*ZhPM zSgvXapX~R~tjzxTt5?n~*S~W4;UB+$#6FF$f-<>w#W z{Kb`<&e){-7oQuHz)DO&XN47Olp0q~|7I;sb{_?kLH*ZR7E9Di9D=E`>1}+72Rdij$qA z9dgh;dwNZmI2Tja#1e9|)zu48!ri&@#^Uq;Xnwvl+PmfxQ7#B;XHOpP?Ttr02@#oF zV@NV>-IZ|PP`S1_)aA0Dz@46Rav44I@Y26~>*hDV_{=$;q4~t68O5qac$}cMb?H^ZF<)8oNQ?+UVGW6Zuo_PdkPUZ=i%39X$^ye#O zFUrPFSjwN+>>l3Ty7|wJ3%Y2OScXL`S1AceGC|OhrQ*HDCXOPRx0V;Q6v+U28p#DI z(T0}5P_;^Ya&dmcOLq>ZNT!qyJY$sp=8fiFvzwPil}<7`GxBrY{(zGO`f*{OSwJWy{zxv{( zUwm})v->Sxkbd%`@18n!>G0^tG)D{-TfKm0#GCggZ+zr%4B`=qf>;oc2+>+~ew*|$ z7745ic$}|@aY9dRt2;DhC>|Shqt!dy-B#dGCQ0`x9)f0sg+te*WJ77#gw9I6SI#!Z*Ths z&2SH`&7ZHU3^vUv7@6SG`3oTSG_7p)@6ilP5&{T_;^@ZK!BdZ3`0krG4?E*O{PpMl z)wkcs8Ku_24JTS3o5Ku;Ihi$Nj-r5}h)>K_vn*0XQ5TsoiVRh9T|2dVd=p4{mRu*3B*{;+EJ*|8#SAB+DB(qxtvUVa5W51$#zVipJd?y@ z%NvDm=7gkD5dj|08il=mM=rPzon8&@9Ns&cSRuVupD`eqI>F8B*IS)|p~zo(@sbqx z9M^P0;F|px9(_n8ws-%R1_y_6kY#LuSxi!1U|RhN4WYoJ?Z)8OUw---+Q7 z*&DZRuPrWdFz)tRX@qzlAVLBe`F^mq-_J|J3zyF{T8Ew8&T4((X6MKw0mblHY9DK*TLa67Tdv2~)525_Q zwl(PXFQ1;(@ZsSH-`~Cc5lv$+!f71YQRZX1sH$Vv593IcrOn;__wF=jSJu-snO%B_ z;gzlXTV9yF_}mx%!Ixin_}tvdc|A_zD2g9mucjdS-p@WV$5EkF*;rWDrbZ6es4?`- zK5++{rq0bQEiarowno!P6KIg)45!5JIwKY^GR3(Gz(C@X&@;y=W*9>eWRXd_*2Ilf z0u&~*a=F58f0&mmG|4WWn%(~SjdyN0zxMKr1oGEz+}ztg@WU!cr`6ib`tqzO%isCw zyY}GV*PmWJSB3F(?kDh=w?2OT+RdBIc5iOBPBI^6(V4|cUgPiV^%iG~d5OMvXj&P^xB5ClOqpL% zjB%WSG?NKc5~Ls*AK$=|Q^Ft`yFsy15{VcQD+wr!BNC(x1qb79ifNW-y@G&!hhVnn z1;sqWA>ey4Mf3g?gEEV#;P?KEul~)CKYkUx|HaFfbWKfv@m9Ovn5)e$FU*}-S$XIE z>s&PctuH=<@Zjpb=E$@i*Zbmz#u8_%u?HwRq&Po?y^()3j$}!dWaHeq>T4g~7C5EZ zGg&2n|L*qXm9PEoZ~u0`wbN+z7RoG+DL>(E-0!b1%{+Q$;riyGwlY6c(Q%CWrprNT z&>m8Y5c_7iR7QaD{ef?V$F_77G_jnQE^I8Ts?fEYc~!~F2A#pwbK09jyFxW^ zyLZ%>HupzfZ_sW{hF!<(Talj%A$t3ho#r^vCHi-N^DD!{y?^_6Kj8)W*=H`3B#t7S zH;UUk&Gq@2FF*Hi8V-&cyLWaDcAMRmGM{ABPv5_)OZ@!YT;HKG$Z(uU)0~@RK2i(S z1wk&jVd7%|SxwgpjZX91-}_#@l7IYcY3H!Vv-EQpYp*;}fA`Znoq<)#36rS}Fr^rb zX*tzWp_~xjiYZiZ9DBAbss)>H?c+Npjm0oYEh!u+T%H2zVIA zE)D|2D5Yr(p-Te{2@5hB#^9O78NH^x_VL#J#&C6I>Cv;R*S6X}edm)itBWr`_wsDD zv@x%hcufPN(V%gy@hMO6GiyR`3Y@^Z*}yf8Q6)5SVlx1Xx+06xuGP29;lLwuRb)k# z!@QI$H+S!dYEDELbJV~2yVvKd%F_=my>{h#e<-gl8XSZB!;!2AEKPGfyLUJ`HD8qZ z*mE38%!LBk+Ozz0SSf~?0FDiMvvI(yIp|=8Rq`cu;*Py2!W8ANsLoMpHjKY^XkJd{PyDdr{Dk4Pw(yQomop?x;X1tn$TmF^5|mT*uIImfW7EhlWP~Zto(Pl3FyWoX@cQk&mGhTQl+}io&M|UL-4aaVGI4O&gA3DQ{bMnN}*3qyci5siAE7!Nw z*~g~)~5<4vPX7nSHqYjQf`N=IGU@MKi^*Om@D3YSj zARi$*4C#hRwuh9H3P}uVY)}&<#6m3OOSN3K*|%-0)9trM?r=0L8Z%xLy6!j_H^21E zBWW=C(Od88k}PxJ7uOnbnifTBw#-h(o~kH^1AAq5_N9mGyUl|xG3Z=<|Mo{B;&b`j z$iXau<4JgI&~!@E38wI{g9*!U6o`1C=uf5`hy)hI5JY~+XDNb=YkPs`5ga9PNK>r9 zz=iqKpB&*{`<*QIAfzdXc{*!!2Xi{JQma(*`kU9RtGkYu^7GTlY@Smko)y#-#EOK8 znPI@M)GI>7`AIr*vxA|(a{lSXRXhCA&vc_`IvxvPZGPS;@L&GIld)x9yScqmFVE(s zH$OZ)x%SXE|LZ@xefLf=uid)&p557ISYd6wGPhEC=kC_6?S{Pm>G+R4U1wOl3%vd67h6lx7UWq&Q9qp)*3lB+jxpNJ&aK z9z1p7r5D#XaQ|i;dw?Vm;EYg$AiAzq;^U%_tSv3tZu+m^yki)8QA*}Dyj+$QRU@>r z(;9zt5UNJ$R4qq)O+)7Y@7>%8XkUKmAGGdYO`3P}g*+_PBeQcjJW3dSlBRK(Kq%Qk z5ZY}KQu6#uQm#B-UATX6W6(LUI>Yh|oVwjfI1p)?ns5R~gEYk~#}h6?8ANpEPCZ$fKf8W8 zAhx#l1*&=59CZt`%e{LaL>Xjg#sLN@J%h9fjyqbuNHdgA_$i?=mxEHC=8H-99$=>w zP2U~%>UlX3a+Wk{4?Ev|^?$GA3k$h?ue&vBe9Q^T2Rpk1F9Jdaxz8gLhGH0ng*flM zdFATvV;5f@O}kS!Ru|RH!H&YqVH~%{gUCyCg~23rC@q!q5~9;Ib{xzi_ShQz2Y*rt zy(~@m!)DSx#EunvK`ihrtFc<1ff-Fw5eanc;n>C$FQy5GFpUChuCQEHXYzAvGrHp2 z!I2MZQs8yAhmG5(*G~yBm5_h#;tRKW$|Dya+B@uh{|A41;3b@qef_%b=8%kR1l9Vn}SvX8gqV z^g<~mAegop8gP=N8HOG4vF$~`_oGyzRWFDDNeN5$f3|r%(EsB9xWGYSIw=gT0^VRLnemG48nr)8TwV4eJqh5dNc^G?1Jaqt$b9q_PbV-mWU9i2m$!nC% z(DN#vlOda>OEZO%$}eC39aKD*rISDXoBw5;>?4%oz@1-si~^}O8Un~7KJr5ga+$=i z1OPT;5{$wWLWBe{2jgfmogfbRb`*qB;35iU1Of7;3x{m-cXT{?S`IDWJTmKfU}TA!l%(ls7|=AT*Jgq!3jCDiV1HuU!=98E484E}Z0+v=hRLZ3!{}k0 z`mu{Mf*?IHJ;dqbw+@eIjUEho39a*#)%C)**$07@uM~1d-k+F^l!wef;xZ);I)iJH z0F)e;FD};RakZ!w4PLJ3xxCFFp}3$T4Jj(Uv{Vq)LS7k7eWSw?vBsO(SeCc5skSmZ zFZ0kP8nh}zZg++q$e}_-4@t~qAY+PAY9&BUNfbDKt~>`|mJIuD3St}*uEcR{mLf@4 z(=>~d43mVDd4>rvrg%CL78ZlpZ47q8m<>Z$hm19i0d32I3ga*i3C+_8W(3B^_iM%9 z{_0V44`yoPz-Lv+vvione(W=n2XK^=L@%a3cTd8U6~(DN;6%El<`bWSoHv}NVGzY( zzEqZPeB8<9wS~o6LIWCvNt!k8?bH`UA44K!B1k!!3%oeUvdqslh7+F9ykvN>Ym|9S zAR;Ur_Doh_r|mw$tX|eb40_ED0KV%3Bc}lp=j4hHV@VgMBbP%`nmIW0Gbkp39T7eP zX}{IyXk%fOo51jGDQiB#woJl*?RwQpD9Vp2D9r~VT5KY=bWgeNy#&p&+Z=XQ%vYvkY{O@ zjVp8Yk?mTO?rdF|hMe#C3<&r_mY)q7h9;zt3Zm=*z$>!mOYiUZe*CBY>Lj3UaX`xLE&J`ibb)SF7WK~Ze% zQ>cf=0~AbSsgx$r+&FsJXXgR`jagJBku7C1Q zt_pYr{b?ZOWK05HVaCIu6Pbn_RK?PEV<;;uioN;*H|Z1+$%3G`e2HQBY&3Nw!-7=6 zT8eQ>;FP9ugFz~S-AVNVy*WzFl(^W>|*#CGc2!MNR(3vv`ae@J42^0j&b*E-qg@FEC zhvwu1bClx)&l==%93+TJ86@bgvm*#-asJ%UJ$UGmk~J|kWn-^%kLS~za&9W;jzoe0{h-HVlq@X5h6i6=DV4JB2L)N*KtVe@&z zn&5{|vjelB3bU@iZ;lyhzBaHr#|F(q26#%8`q&V%EZHcnG}{xG3O4Uf3OUKPTQlqH zFP{5GYt*M{AxXQ<;cGRi?3k0?MBrH9CRUIPjS@Hk6@jAX7H7NDGy_yYTCw9UhG~66 zn|MQ#4Sd|MOZnw`@Y!vDYBo86i2!c3($TPq6Fc?+CM-#B(xRFZ31HN;)Z~$#*Jg8K zvb}!~Ld~|s-GfmrXRL2j2v?{UHzAB|f0bi7jb>>!#Tk9;=xAd_Id}dsf0*gDW<_%; zxW}@=B;t2Am3oamHfWE~tSADP9*zcUvy0f5W-CSrhB4-XG?`y1Pr_}CT*3-j2COI+ zxV)q+l7!;uu`bmNN#2ik0A>nBLFTK&>8AdKuz;c@km!Q%@W_gv95AlF<9Je!RMTwtg?qO5ExG}?Xx}bU~UzNyQqcwc$ zyk4FuWc)tQc~be@WST6^EJ^Z{lj&|vU;p&l8wSs@3S)ag(O^k3mDJ4g6U#Q&YY!DJ zmWOWl*tU27+h1PriNbMt%efcAbVi@0z-XFLGQE%_VKOT7Z?f;+KrA&{Gfeiac$?|Ek+bmj37NJsq=2?iZTC(zxm7Kfo^rL zrG$o%!Kt4Cz#$HR1Oq@*JjoJ3;}mBUl`^!Lku**S%kUh72w~F9!&$^IGzAz6vIJ+4 zMifQGu?K0CftX|v;}ilWN%Ko}N6!f=e@hf^BTkRmjaDT*Xf7^NV~B8eCMZ~`Gs z2n8t2(p(Bsn88}<{NES!gKLc+T*vkR`avE*KY)G!{U8sZA3#6I1L*%;^X~zRRIVb? S7q5%}0000$&ec1VWYTvhaU(S2>wY%9Xn@v)rNSc&HQI-`Mwj)n8^4K%EvIZDr0(%x984Ttk zz|6(WU_5Ya%gzD|ff&=4rAU-4vZ*E|Hk-Yl?$c*K@4nW)Z%>8!4{{Tn1n6(w)I|XW zeDK4AdM@h0^E^kM_MbldDZx|dr_fJ(3jGxNDfH8xLO+Fm+EeKNLZSclyWfh_uI zE_e=C<%?KeKb-V$|KgW(^DDDi5PKt;4@FW9;dYQZnIkeOiJ=oNNusEX%EczT2Iq3e zcY506nQ4>Ql;Ii6KMC?A$t;ztCy35sl41LROlICOl2U7nFOz^>s-4Agb!H#;hCgqp znzqq!)(pHposPM5Is85L0wM3`zaFP*@EGiUW$eNk2#L^LKkJjEJM2b zzH6n-cr-O)+h8;ev9)MprA~7Td&9P64v&1`yL-R$)+tT&?Ry_>HcI?bnM8?%FxOYV z5NCElNMkhKSg4V7ajsI;>>fo&NZ#fEIkFyw3GZqh0h6FJZj{O*enu_UmRp;aYgH;L zMG~%~TmH68sBPmWMlrhfONLebATKM|6i_A;3L)8%_R#gZMWu;kbQ1Y8TTNrDERzgb z64=Jb?rO2sp5Ejnx)d`L+pgXl3M@c_-!G-v2}2RZ4rk6DtTVabli-2?0Bh>N}c z`k94VxmhUKAWfcGy3E(rJNN%IPbnPb@eDn+r31zyfRqzYM6vEb^U3sP2s=p(Awq4` zUY6(@6%9zm&Y_Ewh;8;Nga@!{RsG6I=O#rw8$y!u#g88Tktjt4{52eB!?=rKkY&-a zvm;PGz+rXiH(l>QCICaq6e+SKL7>XW*hyi40tp2{hRyt_gAog3-N@1tkqxJgo)R2} z1vY}DFfNvxAWuv!HUgBx)jaoy?YoQfDn`u+a3=GxOLO1Yc%69X`rAxWcd}BcchH9(uv5K%bb_;TAkiN$)YmZ*68LPXdWNGJBSV^n zS7oXgG73eN2l``yR(PU?0WOYXhEM{#Q!cI%92W!!NDig?T>B)QX@_!QWi&a#BDm1J zl;A4Oh&YZkOV4USx52GG>E4LcBqS9QeLirS5?Gyz_lx7 zmOd6)$`6qJ!8=$k>~--UelSotHb-3Bm?02(;DGIQu1gHf zu{DMn(iA(hANd#`<$g}4gTcO^*>36)C=+ITl6VF|oKlf>L*R#9S&A#n`C@S`2;Bbc z2|~vM^8+L!3Z%R`|J=>p9~kCIOXmV9CRWy>GmEO3<70OH&t{Qxu-LE7kHsgWfvVie{!h3{K)u>~sfJ zWm(gi*x_)7qN(&m>vAme#x-H*h{|JDY4OI9LAlCr{N}e`PvhVJzyHzS-Lh+ve{E$x z3lNW{P+~Lm#fBeHBK*xGoq7A#`Q~fZGVo6!kVhqMp!(kpjN$xz^>2#8k5K5~j zradz)>qqZ9TW18JD4*IyXhkj*S*BhSSF)-TXs=yep^1_e z>{EErO%9R7S}QD;=QmLZ4hH?5N8L)Tpv||gzkU3ZpS@of)&KU~=eN5%a)zphsDx2> z+wS6g?bW&YJt_LD{_S4h{=@(359UjY|KtzAfdQh5(p+Zx(V*MCc6MdY_NT+p!IjNfi zW{Q++_Ew4M4YL~`j2kU*YS1&s%rZR5Vk1sB$UX+}SeH!&tHYRb>Nkx#& zb|1&*78m~4i!U83eQ$evFf*8$_)D8VCB_e1>k;f7oO5tvh zPOP*M$+41$3HRB30>D)7Wf_sd&&qHVdMOMXcQ}lsLjK%&#ZU48I$0=QziG}@vW<;$ zZ|K-|WIW|VN{nF6$;a-<1anev~(-OX~XvwHJxhSXb1&5>RGc_Sw-f_!@>T@m^Os` zxt8^PXa++mn1vM0gkgjru-#|x-G6`~34)`doRt*Bu#%5I)sigAVTvH9_<09N zEGHOB$PkT2tNH}e94?@_BCY^H#Ysrh42lC;T1rx#qvmycpAtWV6*cP-LGi6x0mmZ> z3CoQFby>mhwA~1?9DO*MZML}ehVYj^?;|MsY1>;XC5t5nD9!aN%bUz>u^f`;UcP}nNqdd`ryH_66_3ROocJ+CpI2oAwtl+RK-Qd$-OY8a12B91VM6$ z;%-V1Il;iP$ft2K(_)69Nh&)vXp&H6MoBXxiR~Daf-tPgb;r_Jv!7bG0Nx9Xl4dq7@-i%X3{zRek@+vFM&7hkn zF2j~ELJ$b7AT9>c;d*_IB-PsD0}i7EyrKt3I5;1Bz83{7N_6!J1#pO?d&52sFeJ%H zK-#fKP^4-5IGN&gfxsDFCW$OMHRz+Ez1!aIjt!;2G%JJ`r5LINkt>R{NC}#(!|wsb1*vOI6|ZwkxvW6oaSN(osl>j2gXa!p1pPNK$eZ>^7B8r@r#y< zOk?FAeDfcD`0<^8diVc0Io|pDi}N91QMoh?q?fmT|4)AQKYL&rh9QbEUGxEklQLiG z4K0=w&>YTY_i`-M1w026j%HyJK@?(S4Il{4lT}$Qu&wH1l=-8wd}-0RZ_XZ;`86z8 zj?70mjv(2}QTst>(iemR0n#wgPYpUlVH%+l&E_8Z;8E|&rPT)yx_M6a(zGC%wlgX% zEGCFM8ipyr7w27)aErp+bY^B)JB$<}^ycaXt<%;#P_MP_{9?Pc+Q5joC`LCwdXFd4 zEW@L~Jw7?o&Dr{5Q#xh}WcKd8;cvY8<|K?4s+Ts|OMmgn_i0k5XrJYIl8lEF+i~*( zOUOK%!Z1i2o*hUy#}x`RCHdZia!G-iPvAI5#(7ej8oLrLq+ut{agHp>{6Y{;7SxuN zyM689g}Jkvtrrm*(gY5};MAZCYPxuKD+_^Tg%3O9Mx6vm(;6F-X=1sl7Zvty90Jg# zXr$RJyLN}9AeIlK$fZCTQ5+hpgi($F2B(DP(mFJE+r~@~4)a8PZ z2_(Vfi94HF6mE$aqIYKw;*khc1i2mcvWzyV1R^JfHQ_mNX1$l@3Cx+}_92-k+{$vA z8^ARYL1cycsX?zU%wbGWtkq$Hx7NQ%<1WLv!nvgo2@dw`Vc%a^0)~c2f?BCZt?I%- zXDCUEETtq_;w4Op*C}7nt-Zl;@b>qA^!bY`jcivY^%IwM&DnB8MM;KN>U%a=Us%$Q zK3rL>ZJqfl-8?%sLjtD|1oG;_1!nW8KVWGw*Xhla0!~uwK;N&bs-zZeFA1%*Ra?{C3B%6`2--8oePbU@5mn|e z?BJzyrvxprlI}Q%)~&h91=qFQ-16+iPc)1Sm)5vq%_vnGu1#vDx3VzrI5|JF=Ns!Z zD_go9r&*E!kms^QC81kgYZVu^JU=}-x}8CkCsCG`=jIj$V|{^|%I>V^9y68y`on#r z;v8<@d+@DqejO$e$#4x!c80er#fYP6Ex;}`YVY3f&$sGP;xjz<$zgs!(OVk;t5Hxc zcFhw3Q0@cym2-{#Kzoc>F|uFdl#dT>-?tT6qKIrbeH=v^NmWcVAhA>?mnZ(jHi1kt zr#8{Y?R_gWBVZe@okk1}Vi*&{!@cFz>XVbcz*IDEoCYyTs+tXV585lW4FU^}hK~}5 zrGO$bOp^M|=G>&hEyZRvNu{q?{7 z>mS~j{kU0gBuPvW1TE4Gi{K1uMecm9(AJVKUt8Dxurtc{c4oi-E7!IM+O54I7&-$w z;Od1s$?Xr@$P#B9kzak`rCa{3V|!vbap1UiI6k|yRpJT{#yvrDvLv4BCP@MkXA_hP zXzKK!hh`93S(-NH-Bx9u!l>Qj6BLnt@$-Y~Tro0``Bs4eyoeXv*_c2CNadP;q)%pP zrdAczPPELWCd27-&t1ED^Y&l;?Y~>Euyaao2P{rdJSS!l14ypQG@vDis9bzz{qXVb z&wl#uuYUEdEX@L2$BO0Do#q+B6R2gU$D>(MDeQK~BCmYm>ekyIKK#y4Z-4crv#(#< ze0Vqknkl5Ih96zt5O3^EBD}FttKR%!SzhqJ{F~409o{7XT&h)8tIw>g%#ZD(eot>S zN9kP7=s)_;@;7Ei-G2 z6+^7rUi2q_@<(f1s~0X^rUgEO362wv||$ZvDMiFW|of>Z;~EU09fqT=$!Q-hvNvnYjjP@Wms$+q@rI1^Oy!eR{t*LTg( z7N|Ifd6W{vw&liCj3{zA1bL-D^R1?TW^>~gH-Di`rfRW4sueUg>+_3KBa&F!%`lo3 zZQs`-3Z(d-{n>ZE^w#UO`dpr+04GUWaAp&UfMefO3d%qaAb^|A!cO1Fay|jxKmPyz z=J(!u>Dqbx_LFv!(%Nh|nA(lT+>4hlZf-38i|>D+P3_?VuBM9*XgQL`=K$@lT?w~Em^DkUJyMJ_I7(Iaza<)`oIXk_1Z!#Dm zYGrL{#UF1o)s08TvujtM*;?Odx7)*Mm&UUC+}hra@1xcpLlI#VL@D9M$o1{vx8A(^ zPyh1fYHRLGFR=gb_x|q13u|XL&c?Cz;CQ+`zy8f{e8cqJfA;4;X%^V8yt4V;?cGlw zbx0t*Rjv)#ZCwj;m>r)WmEy2(?iDz3sXX-@5l2e}u5oJ6rZc2S29pRy$YimAVnULI zpS!R*nvLAJete`sVpdfokD@}~N>V$=$fZ`*w49NhL5KuVu4~nO`~s0wZ&}0+Ch}=3#eA zv$dkgbhPk;2kpQArOW^4UtWLp{OWJNdF{R1dtE((iCHaGU%0&f@uR&@9<+b`%@>E$ z!N2lvWOlwYuI|VX{9Db=WIl}8FDc0as+vPor+_kGvP)vn4(}A z3T&LRu)^R{s_sM z$#^G42$sql3O#g#B*_Ja{^g_YZ?&42HWqH&dEBas^R10bpL^qv|NsB}!rJ=LEV6y? zORv7TI~*K5?ES-Ey;?5PNrr}Lde9EpREk8`G`N~U< z9iagrK=$d)_eq?7_Duce8A&c31n)9@*G7FmL5IB{_wfFB{kKC{# z6iy9#th;gGvMko0S`yFbAS3Y*04IV3qX8I=Tk8xLr3i0|RU9T!4haM!W^P~NL%9I` zDKeON!$|*?s~c~>fB)vr=<1n;8jW`ze?;=KD)TIj?QY+u;ZP4;0_3kw4Dmewu&2FzaplH?&bLz&-j#yf;2|DESn_} zCNWgki1}jt+SRikK5XAVHkTLb5}A0$)Yi1l@v%PB``ym-S1+wKc`BVn06Q3JbCsoP za}kcWD@w6#v=J1CfJ?F*#lcap|FtvgV7vEVfAHoDmx6>ingnrTKiIx2v8-)sgO1)P zmlUNU3*2X~(saX#;MRO?WwFv7j~?A=qYP$O%^)3T8D7S{AcOmz8HTV}!krp42eQx1 z!oZ`jh*u>6!JMfcu{_1nF+xgw6UMIXo?sN`X%p3t2`tmF9+I%*XcxQha=~Mm1wc@!3XZz+yC>*+W3HK}ui=Rusyo1`RMC zBXI>M^{(YxgZZ^8#blA|KvDsD1W7c?NrF<*oDy-deKJ;MhG&apo+%uC)X`UJD#xHN zU0D8$cWZVv0w;qwCn=_&$d3=&-+B9oZ@u;PJWq8aEVtG_{OHGb?o$`e zo;$m_Dk=z`MIuX`^roUHT)VWS&5Yp)uoUZfaTSxW677Xo_5w8nh&; zS(?mdmQoNogm+B`BTyX8(FDjWu1`89az!1$yj-Y59 z{`zarfA0r3{=skm)|bEVg}e9e{`CEirh2N1e5+BscIEt|{qFXiPd|Tt?#hZ7gwrRw zc{1{94AN@Jhfj`zB=!+DhJ@kRyJJ_DilstnYm>jb*N$C(d#{7C+$W!W@WSWcytq)< z+uM$#VA5-kbu)+}mFIrt+J@r<-Jx}5YoR@yxGors4I)Rq$SJJJhCbjJ4o~we9UFS* z)SxR06@*BEoA+X98&*>hf*|V!I-Xgh0kmTeVM#o4^*D=74@6mxWnsC>;SmP(6vfS9 zaC0<%X07tVl{1t)_xAVRe))6Hp4-^OQ1YE0{&j(0USC@7^bLKYf3}&PudwZtot=Zj z(agHED6Om%9^Si)q*=XE+uieH1b4zb#AF(IvPY=rN80Mo<@B9brT_++YUIdK2} zn=e0j@@PD^zWTyirNrMq7{C3~+rRn7Wz)91Ba4TW+(x3+x3WCLJ*ilaU9wl;2@p>G)PT5p$SN2 z34%PgS%tmsdpCCs7tAlLec`$DH$HjbxJgNsmzU3!%e87jUcus}OG+NxKE6lq3G`(Xd=(qd1~)c4E0DfvFWKec)A4RhPuwcK;0Hh@x$I z?x{iVb*F%)Qq$)!u2Naj91Ej(f@EM0g(?n%7$X=rGO;XWc@aw^h+t@rW+{>-SdAh- zKVP`|>u>#=?_dAH2e+6<$7eU!o;|lVn~gWt)vy2FmsB!h5sgBeNN*nvJ4|fF7?~!q z>qmx{ssfMG^u4{^1TTdtUa2p}Ni6C3?%>JegHdziY7FoUXCY2&nl@LP%l*NdufB-o z?hihAbYZ<#RlrGiHq{LlEPhu-pHS6Q(4aZ z1fCl7WHw|4m8N)%48j~xBsTMA5MW+tax|Z22}Dwyz%hy@F$B#q*UkU}vm_7G=yOf! zY?Wp>;n`;|tkqdfpB1H~S#ZJ-7}}=?vy4o&JhJC1oWOA`ORrQjhycrl5V9Q`7zsDD z>?-MRE;9s)QiTRjQuwhIM#;|Z;pJCy!?I|cw>?jrO)jsmU%YT}Hty}Wy(?SuGtIjF zWQ?JZ2Jq}kt*<*AD-28@&52)oQohu{4AQKDKm-V|N7EJsyDN=neohpO`1V3omn)8J7a$3%9wt`I-5>@_p2`lXiD;FTv&*MsOKbmw+`bJSy-D7ns1!B`nkock#3Dc zULEc3sf*$;wg2Mdzk^;tkoa_V$MpIMD#>09?KsPznOJB}oEr4v3WZZ#9=e5LEvG?B zWC=)7khyLY#x6;-6b|AvB?y9IF*Ij0GV&c?kn3N3`I``0;_@|ju&ZlhK`Ggz!xX_# zKzfktpj%ZesZ_Ytr69+Ej{qZuRaD@q0?SsC$$pAD5=HlIt0Bm^R3sb8?zsKkzxgu} zPp)3s@+OZ??mgsK;ojqiBQNvuG|N#zDnJ4Q1R<0$j(zgozx=@$U;B;2PR??4$Dw9;$Lh;Cfi~{I3E0M@T#~j8Q7DiHonI zmFfQW(e7w_ZBG1byNIopuND_mikNutBmjC2O-DZ-_j#G|)8n2KV36p~+D@r)crsw) zKpUN;FgkX_C`)A?2OJxuW)j61j#_S}WiwPV-n;o{qFfmoy<)CT%^`-OBt_e!vB=bT zmGkW$fhIu)1i-OamXa#Wvr~fxf-RwpW1DL&m^+PuFF)#>)aOd`3u@c$rf$mBpO%Y7R+3dw4Q%>5Km0bqCUl<6OH{endHBgYkDBFHMg7|E zd|PQf`!Wdr@c;TB|M}nkh`v^NV0DwYyJc)u+gFdr(E{q`}yK=EeVN7{!TmAA#vmA`*!zZO*7O+ zm=BpW$MJ~YJG6K;$54>MJn?v#Wu7bJ5R$~$C|8?N zn3-0DVsgD)iIUWHWs&D*nLQm1S(Q-aW}5S(_Hmp8FFYc1h0Gy|MJY%z6eNaE;HS5A zn3|E`3QvS%$LvgYVrN=ulx4Z#TVthCM3re0q%j6`_Y;mzl?vCGn{UqNi!HILRvzwK z%{gq;FWT0OuhTqFwOUnHC|6X|aN+P6;tl>F+Y^OPom|>jZj3n6M*!v3dU|(kj9CgR zl!YM6@SK2XA2{DKI>(4;?57bzGAWTK2nJ6L8X<6P_+%VW-To9~0ZU2y zdtsa&5o`eAu}qUeigFB}MjTGiGx@xYa9&LZv7{ zBJraf!;~bOVwgumDhQdP5+!g(F+q~%A(DlJRSvdDvm$NI-0?&MSdwHOz&VK8F#X*vC0SomdzqfvdtL^*m|{m)IF+}=XeRz+>j}7 z(2sKjlLX4o@YJ8iX{gN{G>8DOVuGY05JV0q!q_KCI*oHVhiPD&03q@OgK-M|{M4XH z3RH^K-BGu>Ha8uZ7@e(drlu_bzev}XkoCCrjI<$QZ*Wm%dH&SvQ(4U1FFr5I&6 zHc)_na^y$?Ve1jkxmkop5kLryBn90<8B8`N2FbV56we|A9bv{44b7A(#z{Omd;VOK z>oK$)J+9Y8o_7eMc=E`XBHT1>mSz~7Z`6VaB{5$J&>&3F8X4 zQO|N+D+L~cfXKsv$1sY5Z$BgMy{42|vaTF<$ERjJWQfyh8)BGS;0!=?FVy&Ra1KOG_}LqHs3 z1b`;)tm~O}Op7bVFk#Bfrzb{|m>{QAAroZM_6f_vWt1?@fRVOm(${ub?7>qOGOnp@C)yTI819bO>;C^GZW^JuMSBllA&S8gR{8Aws56LIhcSZnl0qQv#wkNdyts%YCnT6DTq9wHqi`=z z(Q<|5sj6n(N?|7Qat07fBMi;!kow#cpRA4q^e%{;uwYpPM5}lN~x8Z zUUlUnoujtv49$L$25IITcjQZ36^2$B!ON&BkF-wu)9260)kZ02PiV5o*3KAax>#T3 zxR*@ppeAkHx&1Q{ACm$RL~*e|AgL|Lndv9CJ6o?`EU*>LJH3fk3BreHQJFKH{kxq< zb-4-hnF)b_&u6(ljC98hG<&zgZ}^EbnIz@%GE8%Zzvu+p!{Nt;0$)_O2&$Y|VNA*_ zhuOoO00a@Z2Qfkw@v3m4KkLv1UXYcMjdYCzhLSNfV6dd2Hfr@+9^01X;xw!9DT-Hc zWUDvJ4i2}I1gcFg4ciHz8bvNnJLOcy7}AW6kT4<3Oa!I1r7KU4ZeZw4U}L~1&wc*d z;!1VsAzsk0HrMw~0)k~Rj26WNVi4yK7RctQLH`f`-Cg8A!`?vtBm31$fAIA1s;7rn 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 26/79] 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 a2395f2a..973c1cb6 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 36823e98..2f2c055a 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 27/79] 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 01d6d8f0..5bb21e43 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 1ba21630..a7f58a8d 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 28/79] =?UTF-8?q?Change=20the=20default=20math=20class=20o?= =?UTF-8?q?f=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 ada3a3d4..d2e410e1 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 81b726ba..1b508a34 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 c74eb8fa..b6c4654e 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 55d5550b..e187212d 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 5f828cff..360e07d8 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 b59fe2f7..34d6a943 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 29/79] 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 503c3082..d963ea50 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 0e5c9e32..07ebdabe 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 dcc55077..9bf1ead0 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 550f60a4..fa326e3e 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 30/79] 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 9bcac4d6..22a6b7f3 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 2a2223e1..6f0ec1fc 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 2baf4ea9..4ac48186 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 2bbc7e41..27b06af4 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 bfb54aa8..23b293f2 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 a5cbb6fa..52cfb4b5 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 24b76537..f68c925d 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 31fb9e34..69d702b3 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 ef8288f2..270d2f43 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 99ae0620..cfb97773 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 e4ab54e7..c93670c1 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 a05deb1f..741b6633 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 a790da4f..c335484f 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 3ee59c10..3259c17e 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 cbc0f52d..a33f1cb9 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 b7b4a6d9..e73afeac 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 973c1cb6..09f5efa1 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 ea248172..c412b460 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 b1ff2e08..d8184330 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 574a2830..57518fe7 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 22f3a62a..460321aa 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 171ae651..c645b691 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 3b4b133d..a97a19b0 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 7dad51c3..586e10ec 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 ec075463..3bd3b0c5 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 1617d3aa..aee7fb83 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 edbd2413..f506397e 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 43119149..b0e627af 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 2751500e..004c237c 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 375cc8c2..c7222b8e 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 31/79] 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 004c237c..ff745c9c 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 32/79] 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 6f0ec1fc..c68bef96 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 4ac48186..a7b6b6f9 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 23b293f2..0e271a08 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 f68c925d..c1f08cf0 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 bd4c90a1..49cbd02c 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 741b6633..3ed1562f 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 3259c17e..8d9626a1 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 e73afeac..d6c5a8d0 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 09f5efa1..40f7003c 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 4fa380b4..854c2486 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 2bdeb80e..bd65e844 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 1cf656ae..d01d687b 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 035c5e4a..52c87371 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 402207b0..45611246 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 ca467c23..0172071b 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 5767cb64..511c676c 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 07ebdabe..9306eb6f 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 b0e627af..76849ac8 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 6aacb319..5d3439c0 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 8a2f1ce6..392ab1a5 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 c7222b8e..c55261b8 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 00000000..4b50bb9c --- /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 93545fc4..6f57ec45 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 3ebeaf2f..c8df1ff6 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 855ca995..a4318a01 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 41cd20e7..933f3c48 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 bbfea41c..a8089052 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 6f6e1a15..e37932f2 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 55c0f534..e44b2270 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 5e61aa43..90413341 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 33/79] 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 68fd8974..0964dccd 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 34/79] 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 d2e410e1..44006cd1 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 35/79] 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 44006cd1..21573128 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 d03bfa6d..3550963e 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 aee7fb83..777f8172 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 36/79] 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 21573128..8b7754ae 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 3550963e..d91827ae 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 37/79] 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 e5a3d94c..708a4443 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 38/79] 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 8b7754ae..e5daf731 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 d91827ae..469439d3 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 39/79] 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 e5daf731..140dccf7 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 c335484f..8e3aa060 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 d6c5a8d0..e1ce61b8 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 5841552e..2a21fd42 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 c412b460..1d88781c 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 d8184330..995fbd7b 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 57518fe7..ef1ecdb3 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 460321aa..c39024f7 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 bd65e844..801ca617 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 d01d687b..6fdec445 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 52c87371..185bac14 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 c645b691..c57e0288 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 45611246..2660e7e7 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 0172071b..32ed6f24 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 511c676c..4eeec28f 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 a97a19b0..2e6d42b1 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 586e10ec..9bdbf001 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 3bd3b0c5..786a3637 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 777f8172..0588ace9 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 f506397e..12f4e4c5 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 9306eb6f..18d40caa 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 76849ac8..72a42065 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 5d3439c0..c1cfde94 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 26ec879c..00000000 --- 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 578389c7..82e63ddc 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 41a5645e..acc55175 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 743afa5a..5137ae1a 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 00000000..330c2e13 --- /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 00000000..b220ae94 --- /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 00000000..fe122f4d --- /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 00000000..630ab845 --- /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 00000000..659a8ccc --- /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 00000000..738c3789 --- /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 00000000..f48a9937 --- /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 00000000..450058d4 --- /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 00000000..61f2bb58 --- /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 00000000..e433ed53 --- /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 00000000..2e6f48cd --- /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 00000000..239c0b26 --- /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 00000000..9259401f --- /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 bfd1ef58..00000000 --- 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 4eb3954c..9077d5c4 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 ff745c9c..f9ee05bb 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 c55261b8..2e836b6c 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 b222322a..801c60c7 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 40/79] 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 00000000..50819f65 --- /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 12b6b896..bb245eb0 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 f9ee05bb..fae74e0f 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 56d8188c61de95e4fb6b8e77a175e72e20dba99e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 5 Feb 2025 14:59:13 +0100 Subject: [PATCH 41/79] Release Candidate 1 --- Cargo.lock | 43 ++++++++++++++++++++------------------- Cargo.toml | 36 ++++++++++++++++---------------- docs/changelog/0.13.0.md | 7 +++++-- docs/changelog/welcome.md | 2 +- 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 140dccf7..8ac32df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "comemo", "ecow", @@ -2752,12 +2752,13 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577" +version = "0.13.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e364df2dd61caf35f959a879e55654922a8cea77d4886103ed735c45c888445" [[package]] name = "typst-cli" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "chrono", "clap", @@ -2807,7 +2808,7 @@ source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907 [[package]] name = "typst-docs" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "clap", "ecow", @@ -2830,7 +2831,7 @@ dependencies = [ [[package]] name = "typst-eval" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "comemo", "ecow", @@ -2848,7 +2849,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "comemo", "libfuzzer-sys", @@ -2860,7 +2861,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "comemo", "ecow", @@ -2874,7 +2875,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "comemo", "ecow", @@ -2891,7 +2892,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "dirs", "ecow", @@ -2912,7 +2913,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "az", "bumpalo", @@ -2942,7 +2943,7 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "az", "bitflags 2.8.0", @@ -3001,7 +3002,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "heck", "proc-macro2", @@ -3011,7 +3012,7 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "arrayvec", "base64", @@ -3037,7 +3038,7 @@ dependencies = [ [[package]] name = "typst-realize" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "arrayvec", "bumpalo", @@ -3053,7 +3054,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "bytemuck", "comemo", @@ -3069,7 +3070,7 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "base64", "comemo", @@ -3087,7 +3088,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "ecow", "serde", @@ -3103,7 +3104,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "clap", "comemo", @@ -3128,7 +3129,7 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "parking_lot", "serde", @@ -3138,7 +3139,7 @@ dependencies = [ [[package]] name = "typst-utils" -version = "0.12.0" +version = "0.13.0-rc1" dependencies = [ "once_cell", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index 469439d3..fc9878c3 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-rc1" rust-version = "1.80" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" @@ -16,23 +16,23 @@ 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 = "8cccef9" } +typst = { path = "crates/typst", version = "0.13.0-rc1" } +typst-cli = { path = "crates/typst-cli", version = "0.13.0-rc1" } +typst-eval = { path = "crates/typst-eval", version = "0.13.0-rc1" } +typst-html = { path = "crates/typst-html", version = "0.13.0-rc1" } +typst-ide = { path = "crates/typst-ide", version = "0.13.0-rc1" } +typst-kit = { path = "crates/typst-kit", version = "0.13.0-rc1" } +typst-layout = { path = "crates/typst-layout", version = "0.13.0-rc1" } +typst-library = { path = "crates/typst-library", version = "0.13.0-rc1" } +typst-macros = { path = "crates/typst-macros", version = "0.13.0-rc1" } +typst-pdf = { path = "crates/typst-pdf", version = "0.13.0-rc1" } +typst-realize = { path = "crates/typst-realize", version = "0.13.0-rc1" } +typst-render = { path = "crates/typst-render", version = "0.13.0-rc1" } +typst-svg = { path = "crates/typst-svg", version = "0.13.0-rc1" } +typst-syntax = { path = "crates/typst-syntax", version = "0.13.0-rc1" } +typst-timing = { path = "crates/typst-timing", version = "0.13.0-rc1" } +typst-utils = { path = "crates/typst-utils", version = "0.13.0-rc1" } +typst-assets = "0.13.0-rc1" typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } arrayvec = "0.7.4" az = "1.2" diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 50819f65..0d6f915d 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 +title: 0.13.0 description: Changes slated to appear in Typst 0.13.0 --- -# Unreleased +# Version 0.13.0, Release Candidate 1 (February 5, 2025) { #v0.13.0-rc1 } ## Highlights - There is now a distinction between [proper paragraphs]($par) and just @@ -322,3 +322,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 bb245eb0..eca5c254 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 (Release Candidate 1)]($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 d8b79b5b9bddeff4a9cb5b5978a1dc124a761901 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 10:34:28 +0100 Subject: [PATCH 42/79] 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 c1f08cf0..7df788dc 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 c2316b9a3ec6f3eaa009a88d2b9e17ae03e85c29 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 43/79] 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 eb665f06..21d0a518 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 a33f1cb9..31f8cd73 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 801ca617..aa14c5c7 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 6fdec445..6afb5bae 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 185bac14..aa908cca 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 2660e7e7..f04b2e74 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 32ed6f24..daccd02f 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 4eeec28f..3f48113e 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 f413189b..7ceb530f 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 f9ca3ca0..001078e5 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 607d92ab..fb5151e8 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 18d40caa..97189e22 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 c1cfde94..968146cd 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 3c62b210..439b4cd9 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 0d6f915d..7779d390 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 fe122f4d..0e817e0f 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 c417b17442501d4146a897468fe296067696cefe Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 11:18:35 +0100 Subject: [PATCH 44/79] 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 fae74e0f..e9771738 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 f64d029fe6ff486f8cb4f3be43f9ce04e43c1386 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 21:57:46 +0100 Subject: [PATCH 45/79] 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 7779d390..78a8f897 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 20dd19c64ef03619c49c1a2dfb011988c82d9e2a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 6 Feb 2025 22:10:43 +0100 Subject: [PATCH 46/79] 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 27b06af4..1b164148 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 03e2efc6..49b66ee5 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 72060d0142244740c2a544ed4f4f992f47d9ed07 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 47/79] 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 d963ea50..3e5b7d8b 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 d43b1548..0883fe71 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 88f88016e0f9f54edebaf8dfc565a1280338f1c2 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 48/79] 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 001078e5..f902e7f1 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 ab5e356d8121318df5ecff9733a501f9741ac55a 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 49/79] 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 2f89fe29..f457a637 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 9c3ecf43a0359cf6f0ff20f05530421f546d72b0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 15:37:19 +0100 Subject: [PATCH 50/79] 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 34362a6c..2c14f7a3 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 2acbbcef..cba228bc 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 14cf2e3b..5a1b7b4f 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 7ad287c4..c9de0085 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 9f697380..bd08f30e 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 87113c68..a9f21188 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 f8a36368..5ef820d0 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 0344d433..5d7fcd7c 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 9a64992a..59ac5b08 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 24b746b7..ea85aa94 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 12f4e4c5..30c2ea1d 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 5bb21e43..b330c01e 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 93fe02b45746a6de1a713bcbace0918fc611dce9 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 16:36:30 +0100 Subject: [PATCH 51/79] Bump `typst-assets` --- Cargo.lock | 3 +-- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ac32df5..115ae539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2753,8 +2753,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.0-rc1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e364df2dd61caf35f959a879e55654922a8cea77d4886103ed735c45c888445" +source = "git+https://github.com/typst/typst-assets?rev=7eb87f5#7eb87f5496aff556ace09cf574d11d90d90543ca" [[package]] name = "typst-cli" diff --git a/Cargo.toml b/Cargo.toml index fc9878c3..8fefe7cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.0-rc1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.0-rc1" } typst-timing = { path = "crates/typst-timing", version = "0.13.0-rc1" } typst-utils = { path = "crates/typst-utils", version = "0.13.0-rc1" } -typst-assets = "0.13.0-rc1" +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "7eb87f5" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } arrayvec = "0.7.4" az = "1.2" From 024bbb2b46135c82eede69dc69dc24e5937eadb5 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 11 Feb 2025 11:30:30 +0100 Subject: [PATCH 52/79] 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 7df788dc..564b97bd 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 ed74df22..42833542 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 e470ccff19e3c3ffc177ce79645e24ef2f339498 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:35:03 -0500 Subject: [PATCH 53/79] 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 fcc46b03..21d0a8d8 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 c259545c6e628c35cde82c0cb22f432323cb6df3 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:38:40 -0500 Subject: [PATCH 54/79] `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 431f07dd..d6530dd0 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 55/79] 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 115ae539..51ea226e 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 266eba0b..52aa407c 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 e7eb71ee..172d8740 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 2f1a5ab91444f6d45166d61aef33770bbe86953c Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sun, 16 Feb 2025 14:18:39 +0100 Subject: [PATCH 56/79] 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 30c2ea1d..3aac15ba 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 9e5c0150..60a1cd94 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 e294fe85a591f01aef1c2466aa0618dda3c58095 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 17 Feb 2025 11:52:11 +0100 Subject: [PATCH 57/79] Bring back type/str compatibility for 0.13, with warnings and hints (#5877) --- crates/typst-eval/src/code.rs | 2 +- crates/typst-eval/src/flow.rs | 7 +- crates/typst-eval/src/ops.rs | 46 +++++- crates/typst-library/src/diag.rs | 32 ++++- crates/typst-library/src/foundations/array.rs | 41 ++++-- crates/typst-library/src/foundations/ops.rs | 132 ++++++++++++++++-- crates/typst-library/src/foundations/scope.rs | 2 +- crates/typst-library/src/foundations/ty.rs | 18 +++ crates/typst-library/src/foundations/value.rs | 3 +- docs/changelog/0.13.0.md | 6 +- tests/src/world.rs | 2 +- tests/suite/foundations/type.typ | 54 +++++++ 12 files changed, 302 insertions(+), 43 deletions(-) diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index a7b6b6f9..6768ccdc 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -55,7 +55,7 @@ fn eval_code<'a>( _ => expr.eval(vm)?, }; - output = ops::join(output, value).at(span)?; + output = ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?; if let Some(event) = &vm.flow { warn_for_discarded_content(&mut vm.engine, event, &output); diff --git a/crates/typst-eval/src/flow.rs b/crates/typst-eval/src/flow.rs index b5ba487f..0e7d3e63 100644 --- a/crates/typst-eval/src/flow.rs +++ b/crates/typst-eval/src/flow.rs @@ -83,7 +83,8 @@ impl Eval for ast::WhileLoop<'_> { } let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; + let span = body.span(); + output = ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?; match vm.flow { Some(FlowEvent::Break(_)) => { @@ -129,7 +130,9 @@ impl Eval for ast::ForLoop<'_> { let body = self.body(); let value = body.eval(vm)?; - output = ops::join(output, value).at(body.span())?; + let span = body.span(); + output = + ops::join(output, value, &mut (&mut vm.engine, span)).at(span)?; match vm.flow { Some(FlowEvent::Break(_)) => { diff --git a/crates/typst-eval/src/ops.rs b/crates/typst-eval/src/ops.rs index ebbd6743..f2a8f6c6 100644 --- a/crates/typst-eval/src/ops.rs +++ b/crates/typst-eval/src/ops.rs @@ -1,4 +1,4 @@ -use typst_library::diag::{At, HintedStrResult, SourceResult}; +use typst_library::diag::{At, DeprecationSink, HintedStrResult, SourceResult}; use typst_library::foundations::{ops, IntoValue, Value}; use typst_syntax::ast::{self, AstNode}; @@ -23,22 +23,22 @@ impl Eval for ast::Binary<'_> { fn eval(self, vm: &mut Vm) -> SourceResult { match self.op() { - ast::BinOp::Add => apply_binary(self, vm, ops::add), + ast::BinOp::Add => apply_binary_with_sink(self, vm, ops::add), ast::BinOp::Sub => apply_binary(self, vm, ops::sub), ast::BinOp::Mul => apply_binary(self, vm, ops::mul), ast::BinOp::Div => apply_binary(self, vm, ops::div), ast::BinOp::And => apply_binary(self, vm, ops::and), ast::BinOp::Or => apply_binary(self, vm, ops::or), - ast::BinOp::Eq => apply_binary(self, vm, ops::eq), - ast::BinOp::Neq => apply_binary(self, vm, ops::neq), + ast::BinOp::Eq => apply_binary_with_sink(self, vm, ops::eq), + ast::BinOp::Neq => apply_binary_with_sink(self, vm, ops::neq), ast::BinOp::Lt => apply_binary(self, vm, ops::lt), ast::BinOp::Leq => apply_binary(self, vm, ops::leq), ast::BinOp::Gt => apply_binary(self, vm, ops::gt), ast::BinOp::Geq => apply_binary(self, vm, ops::geq), - ast::BinOp::In => apply_binary(self, vm, ops::in_), - ast::BinOp::NotIn => apply_binary(self, vm, ops::not_in), + ast::BinOp::In => apply_binary_with_sink(self, vm, ops::in_), + ast::BinOp::NotIn => apply_binary_with_sink(self, vm, ops::not_in), ast::BinOp::Assign => apply_assignment(self, vm, |_, b| Ok(b)), - ast::BinOp::AddAssign => apply_assignment(self, vm, ops::add), + ast::BinOp::AddAssign => apply_assignment_with_sink(self, vm, ops::add), ast::BinOp::SubAssign => apply_assignment(self, vm, ops::sub), ast::BinOp::MulAssign => apply_assignment(self, vm, ops::mul), ast::BinOp::DivAssign => apply_assignment(self, vm, ops::div), @@ -65,6 +65,18 @@ fn apply_binary( op(lhs, rhs).at(binary.span()) } +/// Apply a basic binary operation, with the possiblity of deprecations. +fn apply_binary_with_sink( + binary: ast::Binary, + vm: &mut Vm, + op: impl Fn(Value, Value, &mut dyn DeprecationSink) -> HintedStrResult, +) -> SourceResult { + let span = binary.span(); + let lhs = binary.lhs().eval(vm)?; + let rhs = binary.rhs().eval(vm)?; + op(lhs, rhs, &mut (&mut vm.engine, span)).at(span) +} + /// Apply an assignment operation. fn apply_assignment( binary: ast::Binary, @@ -89,3 +101,23 @@ fn apply_assignment( *location = op(lhs, rhs).at(binary.span())?; Ok(Value::None) } + +/// Apply an assignment operation, with the possiblity of deprecations. +fn apply_assignment_with_sink( + binary: ast::Binary, + vm: &mut Vm, + op: fn(Value, Value, &mut dyn DeprecationSink) -> HintedStrResult, +) -> SourceResult { + let rhs = binary.rhs().eval(vm)?; + let location = binary.lhs().access(vm)?; + let lhs = std::mem::take(&mut *location); + let mut sink = vec![]; + let span = binary.span(); + *location = op(lhs, rhs, &mut (&mut sink, span)).at(span)?; + if !sink.is_empty() { + for warning in sink { + vm.engine.sink.warn(warning); + } + } + Ok(Value::None) +} diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index 49cbd02c..bf8dc0e4 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -232,18 +232,42 @@ 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); + fn emit(&mut self, message: &str); + + /// Emits the given deprecation message into this sink, with the given + /// hints. + fn emit_with_hints(&mut self, message: &str, hints: &[&str]); } impl DeprecationSink for () { - fn emit(self, _: &str) {} + fn emit(&mut self, _: &str) {} + fn emit_with_hints(&mut self, _: &str, _: &[&str]) {} +} + +impl DeprecationSink for (&mut Vec, Span) { + fn emit(&mut self, message: &str) { + self.0.push(SourceDiagnostic::warning(self.1, message)); + } + + fn emit_with_hints(&mut self, message: &str, hints: &[&str]) { + self.0.push( + SourceDiagnostic::warning(self.1, message) + .with_hints(hints.iter().copied().map(Into::into)), + ); + } } impl DeprecationSink for (&mut Engine<'_>, Span) { - /// Emits the deprecation message as a warning. - fn emit(self, message: &str) { + fn emit(&mut self, message: &str) { self.0.sink.warn(SourceDiagnostic::warning(self.1, message)); } + + fn emit_with_hints(&mut self, message: &str, hints: &[&str]) { + self.0.sink.warn( + SourceDiagnostic::warning(self.1, message) + .with_hints(hints.iter().copied().map(Into::into)), + ); + } } /// A part of a diagnostic's [trace](SourceDiagnostic::trace). diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index aad7266b..8c921a13 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -9,7 +9,9 @@ use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use typst_syntax::{Span, Spanned}; -use crate::diag::{bail, At, HintedStrResult, SourceDiagnostic, SourceResult, StrResult}; +use crate::diag::{ + bail, At, DeprecationSink, HintedStrResult, SourceDiagnostic, SourceResult, StrResult, +}; use crate::engine::Engine; use crate::foundations::{ cast, func, ops, repr, scope, ty, Args, Bytes, CastInfo, Context, Dict, FromValue, @@ -143,6 +145,11 @@ impl Array { Ok(self.iter().cloned().cycle().take(count).collect()) } + + /// The internal implementation of [`Array::contains`]. + pub fn contains_impl(&self, value: &Value, sink: &mut dyn DeprecationSink) -> bool { + self.0.iter().any(|v| ops::equal(v, value, sink)) + } } #[scope] @@ -290,10 +297,12 @@ impl Array { #[func] pub fn contains( &self, + engine: &mut Engine, + span: Span, /// The value to search for. value: Value, ) -> bool { - self.0.contains(&value) + self.contains_impl(&value, &mut (engine, span)) } /// Searches for an item for which the given function returns `{true}` and @@ -576,6 +585,8 @@ impl Array { #[func] pub fn sum( self, + engine: &mut Engine, + span: Span, /// What to return if the array is empty. Must be set if the array can /// be empty. #[named] @@ -587,7 +598,7 @@ impl Array { .or(default) .ok_or("cannot calculate sum of empty array with no default")?; for item in iter { - acc = ops::add(acc, item)?; + acc = ops::add(acc, item, &mut (&mut *engine, span))?; } Ok(acc) } @@ -686,6 +697,8 @@ impl Array { #[func] pub fn join( self, + engine: &mut Engine, + span: Span, /// A value to insert between each item of the array. #[default] separator: Option, @@ -701,13 +714,18 @@ impl Array { for (i, value) in self.into_iter().enumerate() { if i > 0 { if i + 1 == len && last.is_some() { - result = ops::join(result, last.take().unwrap())?; + result = ops::join( + result, + last.take().unwrap(), + &mut (&mut *engine, span), + )?; } else { - result = ops::join(result, separator.clone())?; + result = + ops::join(result, separator.clone(), &mut (&mut *engine, span))?; } } - result = ops::join(result, value)?; + result = ops::join(result, value, &mut (&mut *engine, span))?; } Ok(result) @@ -862,13 +880,14 @@ impl Array { self, engine: &mut Engine, context: Tracked, + span: Span, /// If given, applies this function to the elements in the array to /// determine the keys to deduplicate by. #[named] key: Option, ) -> SourceResult { let mut out = EcoVec::with_capacity(self.0.len()); - let mut key_of = |x: Value| match &key { + let key_of = |engine: &mut Engine, x: Value| match &key { // NOTE: We are relying on `comemo`'s memoization of function // evaluation to not excessively reevaluate the `key`. Some(f) => f.call(engine, context, [x]), @@ -879,14 +898,18 @@ impl Array { // 1. We would like to preserve the order of the elements. // 2. We cannot hash arbitrary `Value`. 'outer: for value in self { - let key = key_of(value.clone())?; + let key = key_of(&mut *engine, value.clone())?; if out.is_empty() { out.push(value); continue; } for second in out.iter() { - if ops::equal(&key, &key_of(second.clone())?) { + if ops::equal( + &key, + &key_of(&mut *engine, second.clone())?, + &mut (&mut *engine, span), + ) { continue 'outer; } } diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 6c240844..4fa3c99e 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -5,7 +5,7 @@ use std::cmp::Ordering; use ecow::eco_format; use typst_utils::Numeric; -use crate::diag::{bail, HintedStrResult, StrResult}; +use crate::diag::{bail, DeprecationSink, HintedStrResult, StrResult}; use crate::foundations::{ format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value, }; @@ -21,7 +21,7 @@ macro_rules! mismatch { } /// Join a value with another value. -pub fn join(lhs: Value, rhs: Value) -> StrResult { +pub fn join(lhs: Value, rhs: Value, sink: &mut dyn DeprecationSink) -> StrResult { use Value::*; Ok(match (lhs, rhs) { (a, None) => a, @@ -39,6 +39,17 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), (Args(a), Args(b)) => Args(a + b), + + // Type compatibility. + (Type(a), Str(b)) => { + warn_type_str_join(sink); + Str(format_str!("{a}{b}")) + } + (Str(a), Type(b)) => { + warn_type_str_join(sink); + Str(format_str!("{a}{b}")) + } + (a, b) => mismatch!("cannot join {} with {}", a, b), }) } @@ -88,7 +99,11 @@ pub fn neg(value: Value) -> HintedStrResult { } /// Compute the sum of two values. -pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { +pub fn add( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { use Value::*; Ok(match (lhs, rhs) { (a, None) => a, @@ -156,6 +171,16 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { (Datetime(a), Duration(b)) => Datetime(a + b), (Duration(a), Datetime(b)) => Datetime(b + a), + // Type compatibility. + (Type(a), Str(b)) => { + warn_type_str_add(sink); + Str(format_str!("{a}{b}")) + } + (Str(a), Type(b)) => { + warn_type_str_add(sink); + Str(format_str!("{a}{b}")) + } + (Dyn(a), Dyn(b)) => { // Alignments can be summed. if let (Some(&a), Some(&b)) = @@ -394,13 +419,21 @@ pub fn or(lhs: Value, rhs: Value) -> HintedStrResult { } /// Compute whether two values are equal. -pub fn eq(lhs: Value, rhs: Value) -> HintedStrResult { - Ok(Value::Bool(equal(&lhs, &rhs))) +pub fn eq( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + Ok(Value::Bool(equal(&lhs, &rhs, sink))) } /// Compute whether two values are unequal. -pub fn neq(lhs: Value, rhs: Value) -> HintedStrResult { - Ok(Value::Bool(!equal(&lhs, &rhs))) +pub fn neq( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + Ok(Value::Bool(!equal(&lhs, &rhs, sink))) } macro_rules! comparison { @@ -419,7 +452,7 @@ comparison!(gt, ">", Ordering::Greater); comparison!(geq, ">=", Ordering::Greater | Ordering::Equal); /// Determine whether two values are equal. -pub fn equal(lhs: &Value, rhs: &Value) -> bool { +pub fn equal(lhs: &Value, rhs: &Value, sink: &mut dyn DeprecationSink) -> bool { use Value::*; match (lhs, rhs) { // Compare reflexively. @@ -463,6 +496,12 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { rat == rel.rel && rel.abs.is_zero() } + // Type compatibility. + (Type(ty), Str(str)) | (Str(str), Type(ty)) => { + warn_type_str_equal(sink); + ty.compat_name() == str.as_str() + } + _ => false, } } @@ -534,8 +573,12 @@ fn try_cmp_arrays(a: &[Value], b: &[Value]) -> StrResult { } /// Test whether one value is "in" another one. -pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult { - if let Some(b) = contains(&lhs, &rhs) { +pub fn in_( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + if let Some(b) = contains(&lhs, &rhs, sink) { Ok(Value::Bool(b)) } else { mismatch!("cannot apply 'in' to {} and {}", lhs, rhs) @@ -543,8 +586,12 @@ pub fn in_(lhs: Value, rhs: Value) -> HintedStrResult { } /// Test whether one value is "not in" another one. -pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult { - if let Some(b) = contains(&lhs, &rhs) { +pub fn not_in( + lhs: Value, + rhs: Value, + sink: &mut dyn DeprecationSink, +) -> HintedStrResult { + if let Some(b) = contains(&lhs, &rhs, sink) { Ok(Value::Bool(!b)) } else { mismatch!("cannot apply 'not in' to {} and {}", lhs, rhs) @@ -552,13 +599,27 @@ pub fn not_in(lhs: Value, rhs: Value) -> HintedStrResult { } /// Test for containment. -pub fn contains(lhs: &Value, rhs: &Value) -> Option { +pub fn contains( + lhs: &Value, + rhs: &Value, + sink: &mut dyn DeprecationSink, +) -> Option { use Value::*; match (lhs, rhs) { (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Dyn(a), Str(b)) => a.downcast::().map(|regex| regex.is_match(b)), (Str(a), Dict(b)) => Some(b.contains(a)), - (a, Array(b)) => Some(b.contains(a.clone())), + (a, Array(b)) => Some(b.contains_impl(a, sink)), + + // Type compatibility. + (Type(a), Str(b)) => { + warn_type_in_str(sink); + Some(b.as_str().contains(a.compat_name())) + } + (Type(a), Dict(b)) => { + warn_type_in_dict(sink); + Some(b.contains(a.compat_name())) + } _ => Option::None, } @@ -568,3 +629,46 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option { fn too_large() -> &'static str { "value is too large" } + +#[cold] +fn warn_type_str_add(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "adding strings and types is deprecated", + &["convert the type to a string with `str` first"], + ); +} + +#[cold] +fn warn_type_str_join(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "joining strings and types is deprecated", + &["convert the type to a string with `str` first"], + ); +} + +#[cold] +fn warn_type_str_equal(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "comparing strings with types is deprecated", + &[ + "compare with the literal type instead", + "this comparison will always return `false` in future Typst releases", + ], + ); +} + +#[cold] +fn warn_type_in_str(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "checking whether a type is contained in a string is deprecated", + &["this compatibility behavior only exists because `type` used to return a string"], + ); +} + +#[cold] +fn warn_type_in_dict(sink: &mut dyn DeprecationSink) { + sink.emit_with_hints( + "checking whether a type is contained in a dictionary is deprecated", + &["this compatibility behavior only exists because `type` used to return a string"], + ); +} diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index e1ce61b8..ad6da323 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -300,7 +300,7 @@ impl Binding { /// 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 { + pub fn read_checked(&self, mut sink: impl DeprecationSink) -> &Value { if let Some(message) = self.deprecation { sink.emit(message); } diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index 40f7003c..4747775a 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -44,6 +44,16 @@ use crate::foundations::{ /// #type(int) \ /// #type(type) /// ``` +/// +/// # Compatibility +/// In Typst 0.7 and lower, the `type` function returned a string instead of a +/// type. Compatibility with the old way will remain until Typst 0.14 to give +/// package authors time to upgrade. +/// +/// - Checks like `{int == "integer"}` evaluate to `{true}` +/// - Adding/joining a type and string will yield a string +/// - The `{in}` operator on a type and a dictionary will evaluate to `{true}` +/// if the dictionary has a string key matching the type's name #[ty(scope, cast)] #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Type(Static); @@ -106,6 +116,14 @@ impl Type { } } +// Type compatibility. +impl Type { + /// The type's backward-compatible name. + pub fn compat_name(&self) -> &str { + self.long_name() + } +} + #[scope] impl Type { /// Determines a value's type. diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 854c2486..ba5b1473 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -292,7 +292,8 @@ impl Repr for Value { impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { - ops::equal(self, other) + // No way to emit deprecation warnings here :( + ops::equal(self, other, &mut ()) } } diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 78a8f897..5639f95b 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -313,6 +313,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] @@ -324,9 +327,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 diff --git a/tests/src/world.rs b/tests/src/world.rs index 5c267832..56ebc868 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -251,6 +251,6 @@ fn lines( (1..=count) .map(|n| numbering.apply(engine, context, &[n])) .collect::>()? - .join(Some('\n'.into_value()), None) + .join(engine, span, Some('\n'.into_value()), None) .at(span) } diff --git a/tests/suite/foundations/type.typ b/tests/suite/foundations/type.typ index 068b858d..60f9d0ef 100644 --- a/tests/suite/foundations/type.typ +++ b/tests/suite/foundations/type.typ @@ -2,6 +2,60 @@ #test(type(1), int) #test(type(ltr), direction) #test(type(10 / 3), float) +#test(type(10) == int, true) +#test(type(10) != int, false) + +--- type-string-compatibility-add --- +// Warning: 7-23 adding strings and types is deprecated +// Hint: 7-23 convert the type to a string with `str` first +#test("is " + type(10), "is integer") +// Warning: 7-23 adding strings and types is deprecated +// Hint: 7-23 convert the type to a string with `str` first +#test(type(10) + " is", "integer is") + +--- type-string-compatibility-join --- +// Warning: 16-24 joining strings and types is deprecated +// Hint: 16-24 convert the type to a string with `str` first +#test({ "is "; type(10) }, "is integer") +// Warning: 19-24 joining strings and types is deprecated +// Hint: 19-24 convert the type to a string with `str` first +#test({ type(10); " is" }, "integer is") + +--- type-string-compatibility-equal --- +// Warning: 7-28 comparing strings with types is deprecated +// Hint: 7-28 compare with the literal type instead +// Hint: 7-28 this comparison will always return `false` in future Typst releases +#test(type(10) == "integer", true) +// Warning: 7-26 comparing strings with types is deprecated +// Hint: 7-26 compare with the literal type instead +// Hint: 7-26 this comparison will always return `false` in future Typst releases +#test(type(10) != "float", true) + +--- type-string-compatibility-in-array --- +// Warning: 7-35 comparing strings with types is deprecated +// Hint: 7-35 compare with the literal type instead +// Hint: 7-35 this comparison will always return `false` in future Typst releases +#test(int in ("integer", "string"), true) +// Warning: 7-37 comparing strings with types is deprecated +// Hint: 7-37 compare with the literal type instead +// Hint: 7-37 this comparison will always return `false` in future Typst releases +#test(float in ("integer", "string"), false) + +--- type-string-compatibility-in-str --- +// Warning: 7-35 checking whether a type is contained in a string is deprecated +// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string +#test(int in "integers or strings", true) +// Warning: 7-35 checking whether a type is contained in a string is deprecated +// Hint: 7-35 this compatibility behavior only exists because `type` used to return a string +#test(str in "integers or strings", true) +// Warning: 7-37 checking whether a type is contained in a string is deprecated +// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string +#test(float in "integers or strings", false) + +--- type-string-compatibility-in-dict --- +// Warning: 7-37 checking whether a type is contained in a dictionary is deprecated +// Hint: 7-37 this compatibility behavior only exists because `type` used to return a string +#test(int in (integer: 1, string: 2), true) --- issue-3110-type-constructor --- // Let the error message report the type name. From d48708c5d55a5d56e4cd3a3b0de38425a6fd8380 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 17 Feb 2025 11:56:00 +0100 Subject: [PATCH 58/79] 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 51ea226e..f8499bd3 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 8fefe7cc..d1733a98 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 cc5e2671..fb45ec86 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 97189e22..258eb96f 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 5639f95b..e6ae88f0 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 e37932f2..7ce0c8c0 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 de16a2ced1c177f9a4c275d05e73bcc5402057d3 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 59/79] 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 b330c01e..1ce8bfc6 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 0a534f2c0e97be07958c14dc74bc510274f6ac9c Mon Sep 17 00:00:00 2001 From: Matthew Toohey Date: Tue, 18 Feb 2025 13:04:40 -0500 Subject: [PATCH 60/79] --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 515a777a..2b6a7d82 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 c02cb70f279cecb0185788e3a10fe1169ff1f1ec Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 19 Feb 2025 10:59:27 +0100 Subject: [PATCH 61/79] Update changelog (#5894) --- docs/changelog/0.13.0.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index e6ae88f0..fa9f18da 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 From 8dce676dcd691f75696719e0480cd619829846a9 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 19 Feb 2025 11:13:25 +0100 Subject: [PATCH 62/79] Version bump --- Cargo.lock | 47 ++++++++++++++++++++------------------- Cargo.toml | 38 +++++++++++++++---------------- docs/changelog/0.13.0.md | 6 ++--- docs/changelog/welcome.md | 2 +- 4 files changed, 47 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8499bd3..f8c958a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2752,12 +2752,13 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.13.0-rc1" -source = "git+https://github.com/typst/typst-assets?rev=7eb87f5#7eb87f5496aff556ace09cf574d11d90d90543ca" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1051c56bbbf74d31ea6c6b1661e62fa0ebb8104403ee53f6dcd321600426e0b6" [[package]] name = "typst-cli" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "chrono", "clap", @@ -2802,12 +2803,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?tag=v0.13.0#61aebe9575a5abff889f76d73c7b01dc8e17e340" [[package]] name = "typst-docs" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "clap", "ecow", @@ -2830,7 +2831,7 @@ dependencies = [ [[package]] name = "typst-eval" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2848,7 +2849,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "comemo", "libfuzzer-sys", @@ -2860,7 +2861,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2874,7 +2875,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2891,7 +2892,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "dirs", "ecow", @@ -2914,7 +2915,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "az", "bumpalo", @@ -2944,7 +2945,7 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "az", "bitflags 2.8.0", @@ -3004,7 +3005,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "heck", "proc-macro2", @@ -3014,7 +3015,7 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "arrayvec", "base64", @@ -3040,7 +3041,7 @@ dependencies = [ [[package]] name = "typst-realize" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "arrayvec", "bumpalo", @@ -3056,7 +3057,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "bytemuck", "comemo", @@ -3072,7 +3073,7 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "base64", "comemo", @@ -3090,7 +3091,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "ecow", "serde", @@ -3106,7 +3107,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "clap", "comemo", @@ -3131,7 +3132,7 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "parking_lot", "serde", @@ -3141,7 +3142,7 @@ dependencies = [ [[package]] name = "typst-utils" -version = "0.13.0-rc1" +version = "0.13.0" dependencies = [ "once_cell", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index d1733a98..bf8d1560 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"] resolver = "2" [workspace.package] -version = "0.13.0-rc1" +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.13.0-rc1" } -typst-cli = { path = "crates/typst-cli", version = "0.13.0-rc1" } -typst-eval = { path = "crates/typst-eval", version = "0.13.0-rc1" } -typst-html = { path = "crates/typst-html", version = "0.13.0-rc1" } -typst-ide = { path = "crates/typst-ide", version = "0.13.0-rc1" } -typst-kit = { path = "crates/typst-kit", version = "0.13.0-rc1" } -typst-layout = { path = "crates/typst-layout", version = "0.13.0-rc1" } -typst-library = { path = "crates/typst-library", version = "0.13.0-rc1" } -typst-macros = { path = "crates/typst-macros", version = "0.13.0-rc1" } -typst-pdf = { path = "crates/typst-pdf", version = "0.13.0-rc1" } -typst-realize = { path = "crates/typst-realize", version = "0.13.0-rc1" } -typst-render = { path = "crates/typst-render", version = "0.13.0-rc1" } -typst-svg = { path = "crates/typst-svg", version = "0.13.0-rc1" } -typst-syntax = { path = "crates/typst-syntax", version = "0.13.0-rc1" } -typst-timing = { path = "crates/typst-timing", version = "0.13.0-rc1" } -typst-utils = { path = "crates/typst-utils", version = "0.13.0-rc1" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "7eb87f5" } -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 = "0.13.0" +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.13.0" } 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 fa9f18da..6c2fe427 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -1,9 +1,9 @@ --- title: 0.13.0 -description: Changes slated to appear in Typst 0.13.0 +description: Changes in Typst 0.13.0 --- -# Version 0.13.0, Release Candidate 1 (February 5, 2025) { #v0.13.0-rc1 } +# Version 0.13.0 (February 19, 2025) ## Highlights - There is now a distinction between [proper paragraphs]($par) and just @@ -341,4 +341,4 @@ feature flag. - Fixed linux/arm64 Docker image ## Contributors - + diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index eca5c254..8fb85f87 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 -- [Typst 0.13.0 (Release Candidate 1)]($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 a998775edcb16308fd9f2a71bb20497414c113c4 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 63/79] 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 f6df57a3..762f94ed 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 82c1cc08..6f4461bd 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 00000000..54170f53 --- /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 00000000..54170f53 --- /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 00000000..54170f53 --- /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 2a7dfc2c..10345cb0 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 20d4f8135abe580f99d5061680ba606d1b68f5e0 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 24 Feb 2025 12:17:31 +0100 Subject: [PATCH 64/79] 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 3ed1562f..66c6b70a 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 4893eb501ecb6d27606460cbb0962742378c394d Mon Sep 17 00:00:00 2001 From: Sharzy Date: Tue, 25 Feb 2025 00:35:13 +0800 Subject: [PATCH 65/79] 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 25d0cd5d..236a3254 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 00000000..69e9da41 --- /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 00000000..81ab9457 --- /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 7d4010afadbf5d8797a044d790161b79f8dc4f5f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 12:31:15 +0100 Subject: [PATCH 66/79] 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 236a3254..aa769976 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 8cbaea89..9751dfcb 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 00000000..c37a7d2e --- /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 81ab9457..b416fdf9 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 a754be513dd784149006bbbc8596177cdf8aa5ea 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 67/79] 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 91132fc3..cc727f0f 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 4a78a7d082aea2477411caea628d61b0b75be926 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 17:00:21 +0100 Subject: [PATCH 68/79] Fix false positive for type/str comparison warning (#5957) --- crates/typst-library/src/foundations/ops.rs | 62 ++++++++++++++++++--- tests/suite/foundations/type.typ | 2 + 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 4fa3c99e..c2cd5f5a 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -498,7 +498,7 @@ pub fn equal(lhs: &Value, rhs: &Value, sink: &mut dyn DeprecationSink) -> bool { // Type compatibility. (Type(ty), Str(str)) | (Str(str), Type(ty)) => { - warn_type_str_equal(sink); + warn_type_str_equal(sink, str); ty.compat_name() == str.as_str() } @@ -647,14 +647,17 @@ fn warn_type_str_join(sink: &mut dyn DeprecationSink) { } #[cold] -fn warn_type_str_equal(sink: &mut dyn DeprecationSink) { - sink.emit_with_hints( - "comparing strings with types is deprecated", - &[ - "compare with the literal type instead", - "this comparison will always return `false` in future Typst releases", - ], - ); +fn warn_type_str_equal(sink: &mut dyn DeprecationSink, s: &str) { + // Only warn if `s` looks like a type name to prevent false positives. + if is_compat_type_name(s) { + sink.emit_with_hints( + "comparing strings with types is deprecated", + &[ + "compare with the literal type instead", + "this comparison will always return `false` in future Typst releases", + ], + ); + } } #[cold] @@ -672,3 +675,44 @@ fn warn_type_in_dict(sink: &mut dyn DeprecationSink) { &["this compatibility behavior only exists because `type` used to return a string"], ); } + +fn is_compat_type_name(s: &str) -> bool { + matches!( + s, + "boolean" + | "alignment" + | "angle" + | "arguments" + | "array" + | "bytes" + | "color" + | "content" + | "counter" + | "datetime" + | "decimal" + | "dictionary" + | "direction" + | "duration" + | "float" + | "fraction" + | "function" + | "gradient" + | "integer" + | "label" + | "length" + | "location" + | "module" + | "pattern" + | "ratio" + | "regex" + | "relative length" + | "selector" + | "state" + | "string" + | "stroke" + | "symbol" + | "tiling" + | "type" + | "version" + ) +} diff --git a/tests/suite/foundations/type.typ b/tests/suite/foundations/type.typ index 60f9d0ef..8f3dbea7 100644 --- a/tests/suite/foundations/type.typ +++ b/tests/suite/foundations/type.typ @@ -30,6 +30,8 @@ // Hint: 7-26 compare with the literal type instead // Hint: 7-26 this comparison will always return `false` in future Typst releases #test(type(10) != "float", true) +// This is not a warning. +#test(type(10) in ("any", str, int), true) --- type-string-compatibility-in-array --- // Warning: 7-35 comparing strings with types is deprecated From d04f014fc6fef4a1a4031d1f1f00b5cbd4599c05 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 25 Feb 2025 14:00:22 +0100 Subject: [PATCH 69/79] 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 c93a778e..36ed0fa2 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 59569cbf6172ae8f2159e794c2ee26d3d36713df 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 70/79] 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 21d0a518..7ab41e9d 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 f98f634a..14a1c0cc 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 9c4123457469d7771c76938493944fec54eb64fa Mon Sep 17 00:00:00 2001 From: Tijme <68817281+7ijme@users.noreply.github.com> Date: Mon, 3 Mar 2025 10:32:06 +0100 Subject: [PATCH 71/79] Fix docs example with type/string comparison (#5987) --- crates/typst-library/src/loading/xml.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index daccd02f..e76c4e9c 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -34,14 +34,14 @@ use crate::loading::{DataSource, Load, Readable}; /// let author = find-child(elem, "author") /// let pars = find-child(elem, "content") /// -/// heading(title.children.first()) +/// [= #title.children.first()] /// text(10pt, weight: "medium")[ /// Published by /// #author.children.first() /// ] /// /// for p in pars.children { -/// if (type(p) == "dictionary") { +/// if type(p) == dictionary { /// parbreak() /// p.children.first() /// } @@ -50,7 +50,7 @@ use crate::loading::{DataSource, Load, Readable}; /// /// #let data = xml("example.xml") /// #for elem in data.first().children { -/// if (type(elem) == "dictionary") { +/// if type(elem) == dictionary { /// article(elem) /// } /// } From d97967dd408efd43fbff86d6a3de6d25348e3093 Mon Sep 17 00:00:00 2001 From: F2011 <110890521+F2011@users.noreply.github.com> Date: Mon, 3 Mar 2025 21:23:29 +1000 Subject: [PATCH 72/79] Correct typo (#5971) --- docs/guides/guide-for-latex-users.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md index 5137ae1a..fffa6c52 100644 --- a/docs/guides/guide-for-latex-users.md +++ b/docs/guides/guide-for-latex-users.md @@ -447,7 +447,7 @@ document. To let a function style your whole document, the show rule processes everything that comes after it and calls the function specified after the colon with the result as an argument. The `.with` part is a _method_ that takes the `conf` -function and pre-configures some if its arguments before passing it on to the +function and pre-configures some of its arguments before passing it on to the show rule. From e0074dfc01d635c5c14225cab3b81962947351d4 Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Mon, 3 Mar 2025 14:31:39 +0300 Subject: [PATCH 73/79] Make `array.chunks` example more readable (#5975) --- crates/typst-library/src/foundations/array.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index 8c921a13..aab6fa90 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -769,7 +769,7 @@ impl Array { /// /// ```example /// #let array = (1, 2, 3, 4, 5, 6, 7, 8) - /// #array.chunks(3) + /// #array.chunks(3) \ /// #array.chunks(3, exact: true) /// ``` #[func] From fe94b2b54fd8836ee5d4a5fdec000fd50755f44b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 09:22:42 +0100 Subject: [PATCH 74/79] Hotfix for labels on symbols (#6015) --- crates/typst-realize/src/lib.rs | 5 ++++- tests/ref/issue-5930-symbol-label.png | Bin 0 -> 243 bytes tests/suite/symbols/symbol.typ | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 tests/ref/issue-5930-symbol-label.png diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 50685a96..151ae76b 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -326,7 +326,10 @@ fn visit_math_rules<'a>( // 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()); + let mut text = TextElem::packed(elem.text).spanned(elem.span()); + if let Some(label) = elem.label() { + text.set_label(label); + } visit(s, s.store(text), styles)?; return Ok(true); } diff --git a/tests/ref/issue-5930-symbol-label.png b/tests/ref/issue-5930-symbol-label.png new file mode 100644 index 0000000000000000000000000000000000000000..e8127aa0cc494f76def5a178a1985e2ceb294f94 GIT binary patch literal 243 zcmV@g#X!k|JZr|%uUb4ul2)7>$^n8xuD{*KiR4}*s40knJ~PO zEV76vgJ>{#SQT(i1!hG6U}$ZP0001HNklEz3` t-N$in$PcO^<-AY_xoBX~AE|Thn+MaJ1Fvqfmev3O002ovPDHLkV1n1FcTE5Q literal 0 HcmV?d00001 diff --git a/tests/suite/symbols/symbol.typ b/tests/suite/symbols/symbol.typ index 6d2513c1..5bc2cafa 100644 --- a/tests/suite/symbols/symbol.typ +++ b/tests/suite/symbols/symbol.typ @@ -151,3 +151,7 @@ --- symbol-sect-deprecated --- // Warning: 5-9 `sect` is deprecated, use `inter` instead $ A sect B = A inter B $ + +--- issue-5930-symbol-label --- +#emoji.face +#context test(query().first().text, "😀") From 74826fc6ec96ff0b5f7698e2eea04fe6d47bbd3e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 09:47:56 +0100 Subject: [PATCH 75/79] Replace `par` function call in tutorial (#6023) --- docs/tutorial/2-formatting.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/tutorial/2-formatting.md b/docs/tutorial/2-formatting.md index fabb544f..a8c72cef 100644 --- a/docs/tutorial/2-formatting.md +++ b/docs/tutorial/2-formatting.md @@ -13,11 +13,11 @@ your report using Typst's styling system. As we have seen in the previous chapter, Typst has functions that _insert_ content (e.g. the [`image`] function) and others that _manipulate_ content that they received as arguments (e.g. the [`align`] function). The first impulse you -might have when you want, for example, to justify the report, could be to look +might have when you want, for example, to change the font, could be to look for a function that does that and wrap the complete document in it. ```example -#par(justify: true)[ +#text(font: "New Computer Modern")[ = Background In the case of glaciers, fluid dynamics principles can be used @@ -37,9 +37,9 @@ do in Typst, there is special syntax for it: Instead of putting the content inside of the argument list, you can write it in square brackets directly after the normal arguments, saving on punctuation. -As seen above, that works. The [`par`] function justifies all paragraphs within -it. However, wrapping the document in countless functions and applying styles -selectively and in-situ can quickly become cumbersome. +As seen above, that works. With the [`text`] function, we can adjust the font +for all text within it. However, wrapping the document in countless functions +and applying styles selectively and in-situ can quickly become cumbersome. Fortunately, Typst has a more elegant solution. With _set rules,_ you can apply style properties to all occurrences of some kind of content. You write a set @@ -47,7 +47,9 @@ rule by entering the `{set}` keyword, followed by the name of the function whose properties you want to set, and a list of arguments in parentheses. ```example -#set par(justify: true) +#set text( + font: "New Computer Modern" +) = Background In the case of glaciers, fluid From 393be881f8c149eb86ce85aa2264b1861c439091 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Fri, 7 Mar 2025 10:05:16 +0100 Subject: [PATCH 76/79] Mention that `sym.ohm` was removed in the 0.13.0 changelog (#6017) Co-authored-by: Laurenz --- docs/changelog/0.13.0.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 6c2fe427..50e7fca7 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -294,7 +294,6 @@ feature flag. `errorbar.diamond.stroked`, `errorbar.diamond.filled`, `errorbar.circle.stroked`, `errorbar.circle.filled` - `numero` - - `Omega.inv` - Renamed - `ohm.inv` to `Omega.inv` - Changed codepoint @@ -308,6 +307,7 @@ feature flag. - `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) + - `ohm` in favor of `Omega` ## Deprecations - The [`path`] function in favor of the [`curve`] function From 381ff0cc2cf05f3d830f7ed96e9e6819b3ad2d7d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 10:17:11 +0100 Subject: [PATCH 77/79] Mark breaking symbol changes as breaking in 0.13.0 changelog (#6024) --- docs/changelog/0.13.0.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md index 50e7fca7..1cca48aa 100644 --- a/docs/changelog/0.13.0.md +++ b/docs/changelog/0.13.0.md @@ -294,16 +294,16 @@ feature flag. `errorbar.diamond.stroked`, `errorbar.diamond.filled`, `errorbar.circle.stroked`, `errorbar.circle.filled` - `numero` -- Renamed +- Renamed **(Breaking change)** - `ohm.inv` to `Omega.inv` -- Changed codepoint +- Changed codepoint **(Breaking change)** - `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 +- Removed **(Breaking change)** - `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) From 81e9bc7c8febc460ef470c0f555aa3d21b78c0c5 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 11:03:52 +0100 Subject: [PATCH 78/79] 0.13.1 changelog (#6025) --- docs/changelog/0.13.1.md | 26 ++++++++++++++++++++++++++ docs/changelog/welcome.md | 1 + docs/src/lib.rs | 1 + 3 files changed, 28 insertions(+) create mode 100644 docs/changelog/0.13.1.md diff --git a/docs/changelog/0.13.1.md b/docs/changelog/0.13.1.md new file mode 100644 index 00000000..15bd9f6d --- /dev/null +++ b/docs/changelog/0.13.1.md @@ -0,0 +1,26 @@ +--- +title: 0.13.1 +description: Changes in Typst 0.13.1 +--- + +# Version 0.13.1 + +## Command Line Interface +- Fixed high CPU usage for `typst watch` on Linux. Depending on the project + size, CPU usage would spike for varying amounts of time. This bug appeared + with 0.13.0 due to a behavioral change in the inotify file watching backend. + +## HTML export +- Fixed export of tables with [gutters]($table.gutter) +- Fixed usage of `` and `` element within [context] +- Fixed querying of [metadata] next to `` and `` element + +## Visualization +- Fixed [curves]($curve) with multiple non-closed components + +## Introspection +- Fixed a regression where labelled [symbols]($symbol) could not be + [queried]($query) by label + +## Deprecations +- Fixed false positives in deprecation warnings for type/str comparisons diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index 8fb85f87..7611f1c4 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 +- [Typst 0.13.1]($changelog/0.13.1) - [Typst 0.13.0]($changelog/0.13.0) - [Typst 0.12.0]($changelog/0.12.0) - [Typst 0.11.1]($changelog/0.11.1) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index e9771738..091bb1b2 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.1.md")), 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")), From 8ace67d942a4b8c6b9d95b73b3a39f5d0259c7b2 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 7 Mar 2025 11:13:08 +0100 Subject: [PATCH 79/79] Version bump --- Cargo.lock | 46 ++++++++++++++++++++-------------------- Cargo.toml | 38 ++++++++++++++++----------------- docs/changelog/0.13.1.md | 5 ++++- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8c958a7..7455949a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2735,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2752,13 +2752,13 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1051c56bbbf74d31ea6c6b1661e62fa0ebb8104403ee53f6dcd321600426e0b6" +checksum = "b5bf0cc3c2265502b51fcb73147cc7c951ceb694507195b93c2ab0b901abb902" [[package]] name = "typst-cli" -version = "0.13.0" +version = "0.13.1" dependencies = [ "chrono", "clap", @@ -2803,12 +2803,12 @@ dependencies = [ [[package]] name = "typst-dev-assets" -version = "0.13.0" -source = "git+https://github.com/typst/typst-dev-assets?tag=v0.13.0#61aebe9575a5abff889f76d73c7b01dc8e17e340" +version = "0.13.1" +source = "git+https://github.com/typst/typst-dev-assets?tag=v0.13.1#9879589f4b3247b12c5e694d0d7fa86d4d8a198e" [[package]] name = "typst-docs" -version = "0.13.0" +version = "0.13.1" dependencies = [ "clap", "ecow", @@ -2831,7 +2831,7 @@ dependencies = [ [[package]] name = "typst-eval" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2849,7 +2849,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "libfuzzer-sys", @@ -2861,7 +2861,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2875,7 +2875,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.13.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2892,7 +2892,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.13.0" +version = "0.13.1" dependencies = [ "dirs", "ecow", @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.13.0" +version = "0.13.1" dependencies = [ "az", "bumpalo", @@ -2945,7 +2945,7 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.13.0" +version = "0.13.1" dependencies = [ "az", "bitflags 2.8.0", @@ -3005,7 +3005,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.13.0" +version = "0.13.1" dependencies = [ "heck", "proc-macro2", @@ -3015,7 +3015,7 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.13.0" +version = "0.13.1" dependencies = [ "arrayvec", "base64", @@ -3041,7 +3041,7 @@ dependencies = [ [[package]] name = "typst-realize" -version = "0.13.0" +version = "0.13.1" dependencies = [ "arrayvec", "bumpalo", @@ -3057,7 +3057,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.13.0" +version = "0.13.1" dependencies = [ "bytemuck", "comemo", @@ -3073,7 +3073,7 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.13.0" +version = "0.13.1" dependencies = [ "base64", "comemo", @@ -3091,7 +3091,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.13.0" +version = "0.13.1" dependencies = [ "ecow", "serde", @@ -3107,7 +3107,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.13.0" +version = "0.13.1" dependencies = [ "clap", "comemo", @@ -3132,7 +3132,7 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.13.0" +version = "0.13.1" dependencies = [ "parking_lot", "serde", @@ -3142,7 +3142,7 @@ dependencies = [ [[package]] name = "typst-utils" -version = "0.13.0" +version = "0.13.1" dependencies = [ "once_cell", "portable-atomic", diff --git a/Cargo.toml b/Cargo.toml index bf8d1560..296164d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"] resolver = "2" [workspace.package] -version = "0.13.0" +version = "0.13.1" 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.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 = "0.13.0" -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.13.0" } +typst = { path = "crates/typst", version = "0.13.1" } +typst-cli = { path = "crates/typst-cli", version = "0.13.1" } +typst-eval = { path = "crates/typst-eval", version = "0.13.1" } +typst-html = { path = "crates/typst-html", version = "0.13.1" } +typst-ide = { path = "crates/typst-ide", version = "0.13.1" } +typst-kit = { path = "crates/typst-kit", version = "0.13.1" } +typst-layout = { path = "crates/typst-layout", version = "0.13.1" } +typst-library = { path = "crates/typst-library", version = "0.13.1" } +typst-macros = { path = "crates/typst-macros", version = "0.13.1" } +typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" } +typst-realize = { path = "crates/typst-realize", version = "0.13.1" } +typst-render = { path = "crates/typst-render", version = "0.13.1" } +typst-svg = { path = "crates/typst-svg", version = "0.13.1" } +typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } +typst-timing = { path = "crates/typst-timing", version = "0.13.1" } +typst-utils = { path = "crates/typst-utils", version = "0.13.1" } +typst-assets = "0.13.1" +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.13.1" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/docs/changelog/0.13.1.md b/docs/changelog/0.13.1.md index 15bd9f6d..caf523e1 100644 --- a/docs/changelog/0.13.1.md +++ b/docs/changelog/0.13.1.md @@ -3,7 +3,7 @@ title: 0.13.1 description: Changes in Typst 0.13.1 --- -# Version 0.13.1 +# Version 0.13.1 (March 7, 2025) ## Command Line Interface - Fixed high CPU usage for `typst watch` on Linux. Depending on the project @@ -24,3 +24,6 @@ description: Changes in Typst 0.13.1 ## Deprecations - Fixed false positives in deprecation warnings for type/str comparisons + +## Contributors +