diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 877afb249e..b5737f937c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,9 +18,9 @@ Typst日本語ドキュメント翻訳プロジェクトにご興味をお持ち 1. このGitHubリポジトリをフォークします。 2. ドキュメントの実体は、主にMarkdownファイルおよびコンパイラのソースコード内のコメントの2種類から構成されています。それぞれ、下記の注意書きに従って翻訳作業をお願いします。 - 1. `./crates/typst/src/`内の`.rs`ファイル群は、Typstのコンパイラのソースコードです。ソースコード内に含まれている、**既存のコメントを直接書き換えて翻訳してください**。 - - 例1:[Reference > Foundations](https://typst.app/docs/reference/foundations/)を翻訳する際は、`./crates/typst/src/foundations/mod.rs`のコメントを編集してください。 - - 例2:[Reference > Foundations > Arguments](https://typst.app/docs/reference/foundations/arguments/)を翻訳する際は、`./crates/typst/src/foundations/args.rs`のコメントを編集してください。 + 1. `./crates/typst-library/src/`内の`.rs`ファイル群は、Typstのコンパイラのソースコードです。ソースコード内に含まれている、**既存のコメントを直接書き換えて翻訳してください**。 + - 例1:[Reference > Foundations](https://typst.app/docs/reference/foundations/)を翻訳する際は、`./crates/typst-library/src/foundations/mod.rs`のコメントを編集してください。 + - 例2:[Reference > Foundations > Arguments](https://typst.app/docs/reference/foundations/arguments/)を翻訳する際は、`./crates/typst-library/src/foundations/args.rs`のコメントを編集してください。 2. `./docs`内のMarkdownファイル群は、Typstのチュートリアルや入門ガイドなど、言語リファレンス以外のページの本体です。**既存のMarkdownファイルを直接書き換えて翻訳してください**。 3. 翻訳の際の文体や表記は[翻訳ガイドライン](./TRANSLATING_GUIDELINES.md)を参照してください。ドキュメントの最新バージョンへの追従は管理者が一括で行っているため、日本語ドキュメントと公式ドキュメントのバージョンが異なる場合でも、日本語ドキュメントで管理されている原文を優先してください。 4. 翻訳作業の途中でも、Draft Pull Requestを作成して、翻訳の進捗状況を共有することができます。 diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index a391e58040..a0db6fd64a 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -43,39 +43,33 @@ use crate::text::{ }; use crate::World; -/// A bibliography / reference listing. +/// 参考文献 / 引用文献リスト。 /// -/// You can create a new bibliography by calling this function with a path -/// to a bibliography file in either one of two formats: +/// 次の2つの形式のどちらかの参考文献ファイルへのパスを指定してこの関数を呼び出すと、新しい引用文献リストを作成できます。 /// -/// - A Hayagriva `.yml` file. Hayagriva is a new bibliography file format -/// designed for use with Typst. Visit its -/// [documentation](https://github.com/typst/hayagriva/blob/main/docs/file-format.md) -/// for more details. -/// - A BibLaTeX `.bib` file. +/// - Hayagriva `.yml` ファイル。 +/// HayagrivaはTypstで使用するためにデザインされた新しい書誌ファイルフォーマットです。 +/// 詳しくは[ドキュメント](https://github.com/typst/hayagriva/blob/main/docs/file-format.md)をご覧ください。 +/// - BibLaTeX `.bib` ファイル。 /// -/// As soon as you add a bibliography somewhere in your document, you can start -/// citing things with reference syntax (`[@key]`) or explicit calls to the -/// [citation]($cite) function (`[#cite()]`). The bibliography will only -/// show entries for works that were referenced in the document. +/// 文書内に参考文献を追加すると、参照構文(`[@key]`)や引用関数の明示的な呼び出し(`[#cite()]`)を使って[引用]($cite)を始めることができます。 +/// 参考文献リストには、文書内で参照された作品の文献だけが表示されます。 /// -/// # Styles -/// Typst offers a wide selection of built-in -/// [citation and bibliography styles]($bibliography.style). Beyond those, you -/// can add and use custom [CSL](https://citationstyles.org/) (Citation Style -/// Language) files. Wondering which style to use? Here are some good defaults -/// based on what discipline you're working in: +/// # スタイル +/// Typstは、内蔵の[引用と文献スタイル]($bibliography.style)を幅広く取り揃えています。 +/// さらに、独自の[CSL](https://citationstyles.org/)(Citation Style Language)ファイルを追加して使用することもできます。 +/// どのスタイルを使えばいいか迷う方のために、分野ごとによく使われるスタイルを以下の表にまとめています。 /// -/// | Fields | Typical Styles | +/// | 分野 | Typical Styles | /// |-----------------|--------------------------------------------------------| -/// | Engineering, IT | `{"ieee"}` | -/// | Psychology, Life Sciences | `{"apa"}` | -/// | Social sciences | `{"chicago-author-date"}` | -/// | Humanities | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` | -/// | Economics | `{"harvard-cite-them-right"}` | -/// | Physics | `{"american-physics-society"}` | +/// | 工学、IT | `{"ieee"}` | +/// | 心理学、ライフサイエンス | `{"apa"}` | +/// | 社会科学 | `{"chicago-author-date"}` | +/// | 人文学 | `{"mla"}`, `{"chicago-notes"}`, `{"harvard-cite-them-right"}` | +/// | 経済学 | `{"harvard-cite-them-right"}` | +/// | 物理学 | `{"american-physics-society"}` | /// -/// # Example +/// # 例 /// ```example /// This was already noted by /// pirates long ago. @arrgh @@ -102,35 +96,30 @@ pub struct BibliographyElem { )] pub sources: Derived, Bibliography>, - /// The title of the bibliography. + /// 参考文献のタイトル。 /// - /// - When set to `{auto}`, an appropriate title for the - /// [text language]($text.lang) will be used. This is the default. - /// - When set to `{none}`, the bibliography will not have a title. - /// - A custom title can be set by passing content. + /// - `{auto}`に設定すると、[テキストの言語]($text.lang)に適したタイトルが表示されます。これがデフォルトです。 + /// - `{none}`に設定すると、参考文献のタイトルは何も表示されません。 + /// - 独自のタイトルはコンテンツで渡します。 /// - /// The bibliography's heading will not be numbered by default, but you can - /// force it to be with a show-set rule: + /// 参考文献の見出しはデフォルトでは番号が振られませんが、show-setルールで強制的に見出し番号をつけることも可能です。 /// `{show bibliography: set heading(numbering: "1.")}` pub title: Smart>, - /// Whether to include all works from the given bibliography files, even - /// those that weren't cited in the document. + /// 文書内で引用されていないものも含めて、参考文献ファイルにあるすべての文献を出力するかどうか。 /// - /// To selectively add individual cited works without showing them, you can - /// also use the `cite` function with [`form`]($cite.form) set to `{none}`. + /// 個々の引用文献を表示させずに追加するには、 [`form`]($cite.form) を `{none}`として [`cite`]($cite) 関数を使用します。 #[default(false)] pub full: bool, - /// The bibliography style. + /// 参考文献スタイル。 /// - /// This can be: - /// - A string with the name of one of the built-in styles (see below). Some - /// of the styles listed below appear twice, once with their full name and - /// once with a short alias. - /// - A path string to a [CSL file](https://citationstyles.org/). For more - /// details about paths, see the [Paths section]($syntax/#paths). - /// - Raw bytes from which a CSL style should be decoded. + /// 以下のいずれかの方法で指定します。 + /// - 組み込みスタイル(下記参照)のいずれかの名前を持つ文字列。 + /// 以下に挙げるスタイルのいくつかは、フルネームと短いエイリアスの2回表示されています。 + /// - [CSL ファイル](https://citationstyles.org/)へのパスを示す文字列。 + /// パスに関する詳細は[Pathセクション]($syntax/#paths)を参照してください。 + /// - CSLスタイルがデコードされるべき生バイト。 #[parse(match args.named::>("style")? { Some(source) => Some(CslStyle::load(engine.world, source)?), None => None, diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 78a79a8e26..2a3b21cb7a 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -25,14 +25,13 @@ use crate::model::{ use crate::text::{Lang, Region, TextElem}; use crate::visualize::ImageElem; -/// A figure with an optional caption. +/// 任意でキャプションを持つ図表。 /// -/// Automatically detects its kind to select the correct counting track. For -/// example, figures containing images will be numbered separately from figures -/// containing tables. +/// 自動的にその種類を検出し、それぞれに応じて番号付けします。 +/// 例えば、画像を含む図表は表を含む図表とは別々に番号が付けられます。 /// -/// # Examples -/// The example below shows a basic figure with an image: +/// # 例 +/// 以下の例は、画像を含む基本的な図表を示しています。 /// ```example /// @glacier shows a glacier. Glaciers /// are complex systems. @@ -43,8 +42,8 @@ use crate::visualize::ImageElem; /// ) /// ``` /// -/// You can also insert [tables]($table) into figures to give them a caption. -/// The figure will detect this and automatically use a separate counter. +/// 図表に [tables]($table) を挿入してキャプションを付けることもできます。 +/// 図表は表を含むこと検出し、自動的に別のカウンターを使用します。 /// /// ```example /// #figure( @@ -57,28 +56,23 @@ use crate::visualize::ImageElem; /// ) /// ``` /// -/// This behaviour can be overridden by explicitly specifying the figure's -/// `kind`. All figures of the same kind share a common counter. +/// この動作は、図表の種類である `kind` を明示的に指定することで上書き可能です。 +/// 同じ種類の図表はすべて共通のカウンターを共有します。 /// -/// # Figure behaviour -/// By default, figures are placed within the flow of content. To make them -/// float to the top or bottom of the page, you can use the -/// [`placement`]($figure.placement) argument. +/// # 図表の動作 +/// デフォルトでは、図表はコンテンツの流れの中に配置されます。 +/// 図表をページの上部または下部に配置するには、[`placement`]($figure.placement)引数を使用します。 /// -/// If your figure is too large and its contents are breakable across pages -/// (e.g. if it contains a large table), then you can make the figure itself -/// breakable across pages as well with this show rule: +/// 図表が大きすぎてその内容がページをまたいで分割可能な場合(例えば大きな表が含まれている場合)、このshowルールで図表自体もページをまたいで分割可能です。 /// ```typ /// #show figure: set block(breakable: true) /// ``` /// -/// See the [block]($block.breakable) documentation for more information about -/// breakable and non-breakable blocks. +/// 分割できるブロックと分割できないブロックの詳細については、[block]($block.breakable)のドキュメントを参照してください。 /// -/// # Caption customization -/// You can modify the appearance of the figure's caption with its associated -/// [`caption`]($figure.caption) function. In the example below, we emphasize -/// all captions: +/// # キャプションの改変 +/// 図表のキャプションの外観は、関連するキャプション機能で改変できます。 +/// 以下の例では、すべてのキャプションを斜体で強調しています。 /// /// ```example /// #show figure.caption: emph @@ -89,10 +83,8 @@ use crate::visualize::ImageElem; /// ) /// ``` /// -/// By using a [`where`]($function.where) selector, we can scope such rules to -/// specific kinds of figures. For example, to position the caption above -/// tables, but keep it below for all other kinds of figures, we could write the -/// following show-set rule: +/// [`where`]($function.where)セレクターを使うことで、このようなルールを特定の種類の図表に適用可能です。 +/// 例えば、図表の種類が表の場合はキャプションを表の上に配置し、他の種類ではキャプションを下に配置するには、次のようなshow-setルールを記述します。 /// /// ```example /// #show figure.where( @@ -110,18 +102,14 @@ pub struct FigureElem { #[required] pub body: Content, - /// The figure's placement on the page. + /// ページ上における図表の配置。 /// - /// - `{none}`: The figure stays in-flow exactly where it was specified - /// like other content. - /// - `{auto}`: The figure picks `{top}` or `{bottom}` depending on which - /// is closer. - /// - `{top}`: The figure floats to the top of the page. - /// - `{bottom}`: The figure floats to the bottom of the page. + /// - `{none}`: 他のコンテンツと同様に書かれた場所に置かれる。 + /// - `{auto}`: `{top}` か `{bottom}` の近い方に置かれる。 + /// - `{top}`: ページの上部に置かれる。 + /// - `{bottom}`: ページの下部に置かれる。 /// - /// The gap between the main flow content and the floating figure is - /// controlled by the [`clearance`]($place.clearance) argument on the - /// `place` function. + /// 本文のコンテンツと図表の間隔は`place`関数の [`clearance`]($place.clearance) 引数によって制御します。 /// /// ```example /// #set page(height: 200pt) @@ -136,12 +124,11 @@ pub struct FigureElem { /// ``` pub placement: Option>, - /// Relative to which containing scope the figure is placed. + /// どの包含スコープに対して図を配置するか。 /// - /// Set this to `{"parent"}` to create a full-width figure in a two-column - /// document. + /// これを`{"parent"}`に設定すると、段組みをまたいで、ページの幅をすべて使用した図表を作成します。 /// - /// Has no effect if `placement` is `{none}`. + /// もし`placement`を`{none}`とした場合には、何の効果もありません。 /// /// ```example /// #set page(height: 250pt, columns: 2) @@ -157,30 +144,25 @@ pub struct FigureElem { /// ``` pub scope: PlacementScope, - /// The figure's caption. + /// 図表のキャプション。 #[borrowed] pub caption: Option>, - /// The kind of figure this is. + /// 図表の種類。 /// - /// All figures of the same kind share a common counter. + /// 同じ種類のすべての図表は共通のカウンターを共有します。 /// - /// If set to `{auto}`, the figure will try to automatically determine its - /// kind based on the type of its body. Automatically detected kinds are - /// [tables]($table) and [code]($raw). In other cases, the inferred kind is - /// that of an [image]. + /// `{auto}` に設定された場合、図形はその中で記述されているものの種類に基づいて、自動的にその種類の決定を試みます。 + /// 自動的に検出される種類は、[table]($table)と[code]($raw)です。 + /// それ以外の場合は[image]と推測されます。 /// - /// Setting this to something other than `{auto}` will override the - /// automatic detection. This can be useful if - /// - you wish to create a custom figure type that is not an - /// [image], a [table] or [code]($raw), - /// - you want to force the figure to use a specific counter regardless of - /// its content. + /// これを `{auto}` 以外に設定すると、自動検出が上書きされます。 + /// 以下のような場合に便利です。 + /// - [image]や[table]、[code]($raw)以外のカスタム図表を作りたい場合 + /// - コンテンツに関わらず特定のカウンターを強制的に使用したい場合 /// - /// You can set the kind to be an element function or a string. If you set - /// it to an element function other than [`{table}`]($table), [`{raw}`](raw) - /// or [`{image}`](image), you will need to manually specify the figure's - /// supplement. + /// 種類は、エレメント関数または文字列に設定できます。 + /// [`{table}`]($table)、[`{raw}`](raw)、[`{image}`](image)以外のエレメント関数に設定した場合は、図表の補足(supplement)を手動で指定する必要があります。 /// /// ```example /// #figure( @@ -192,16 +174,12 @@ pub struct FigureElem { /// ``` pub kind: Smart, - /// The figure's supplement. + /// 図表の補足。 /// - /// If set to `{auto}`, the figure will try to automatically determine the - /// correct supplement based on the `kind` and the active - /// [text language]($text.lang). If you are using a custom figure type, you - /// will need to manually specify the supplement. + /// `{auto}` に設定すると、図表は、種類や[テキスト言語]($text.lang)に基づいて、正しい補足を自動的に決定しようとします。 + /// 独自の図表タイプを使用している場合は、補足を手動で指定する必要があります。 /// - /// If a function is specified, it is passed the first descendant of the - /// specified `kind` (typically, the figure's body) and should return - /// content. + /// 関数が指定された場合、その関数は指定された種類の最初の子孫要素(通常は図の本体)に渡され、内容を返す必要があります。 /// /// ```example /// #figure( @@ -214,29 +192,27 @@ pub struct FigureElem { #[borrowed] pub supplement: Smart>, - /// How to number the figure. Accepts a - /// [numbering pattern or function]($numbering). + /// 番号の付け方。[番号付けのパターンや関数]($numbering)を受け付けます。 #[default(Some(NumberingPattern::from_str("1").unwrap().into()))] #[borrowed] pub numbering: Option, - /// The vertical gap between the body and caption. + /// 本文とキャプションの間の垂直方向の隙間。 #[default(Em::new(0.65).into())] pub gap: Length, - /// Whether the figure should appear in an [`outline`] of figures. + /// 図表を[`outline`]に表示するかどうか。 #[default(true)] pub outlined: bool, - /// Convenience field to get access to the counter for this figure. + /// この図表のカウンターにアクセスするための便利なフィールド。 /// - /// The counter only depends on the `kind`: - /// - For (tables)[@table]: `{counter(figure.where(kind: table))}` - /// - For (images)[@image]: `{counter(figure.where(kind: image))}` - /// - For a custom kind: `{counter(figure.where(kind: kind))}` + /// カウンターは図表の種類 `kind` にのみ依存します。 + /// - (table)[@table]に対して: `{counter(figure.where(kind: table))}` + /// - (image)[@image]に対して: `{counter(figure.where(kind: image))}` + /// - 独自の図表kindに対して: `{counter(figure.where(kind: kind))}` /// - /// These are the counters you'll need to modify if you want to skip a - /// number or reset the counter. + /// 数字をスキップしたり、カウンターをリセットしたい場合は、これらのカウンターを修正する必要があります。 #[synthesized] pub counter: Option, } @@ -453,14 +429,12 @@ impl Outlinable for Packed { } } -/// The caption of a figure. This element can be used in set and show rules to -/// customize the appearance of captions for all figures or figures of a -/// specific kind. +/// 図のキャプション。 +/// この要素は、すべての図や特定の種類の図のキャプションの外観を改変するために、 +/// setルールやshowルールで使用可能です。 /// -/// In addition to its `pos` and `body`, the `caption` also provides the -/// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These -/// parts can be used in [`where`]($function.where) selectors and show rules to -/// build a completely custom caption. +/// キャプションは、`pos`と`body`に加えて、図の`kind`や`supplement`、`counter`、`numbering`もフィールドとして提供します。 +/// これらの要素を[`where`]($function.where)セレクターやshowルールで使用することで、独自のキャプションを構築できます。 /// /// ```example /// #show figure.caption: emph @@ -472,7 +446,7 @@ impl Outlinable for Packed { /// ``` #[elem(name = "caption", Synthesize, Show)] pub struct FigureCaption { - /// The caption's position in the figure. Either `{top}` or `{bottom}`. + /// 図表の仲のキャプションの位置。`{top}`や`{bottom}`を入力してください。 /// /// ```example /// #show figure.where( @@ -500,10 +474,10 @@ pub struct FigureCaption { #[default(OuterVAlignment::Bottom)] pub position: OuterVAlignment, - /// The separator which will appear between the number and body. + /// 番号とキャプション名の間に表示する区切り文字。 /// - /// If set to `{auto}`, the separator will be adapted to the current - /// [language]($text.lang) and [region]($text.region). + /// `{auto}`に設定すると、区切り文字は + /// [language]($text.lang)と[region]($text.region)に応じて決まります。 /// /// ```example /// #set figure.caption(separator: [ --- ]) @@ -515,10 +489,10 @@ pub struct FigureCaption { /// ``` pub separator: Smart, - /// The caption's body. + /// キャプション名。 /// - /// Can be used alongside `kind`, `supplement`, `counter`, `numbering`, and - /// `location` to completely customize the caption. + /// 独自のキャプションに改変するために + /// `kind`、`supplement`、`counter`、`numbering`、`location`が同時に使えます。 /// /// ```example /// #show figure.caption: it => [ diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 316617688f..9525142e27 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -14,7 +14,7 @@ use crate::model::{ }; use crate::text::TextElem; -/// A reference to a label or bibliography. +/// ラベルや参考文献への参照。 /// /// Takes a label and cross-references it. There are two kind of references, /// determined by its [`form`]($ref.form): `{"normal"}` and `{"page"}`. @@ -33,16 +33,7 @@ use crate::text::TextElem; /// write a show rule for it. In the future, there might be a more direct way /// to define a custom referenceable element. /// -/// If you just want to link to a labelled element and not get an automatic -/// textual reference, consider using the [`link`] function instead. -/// -/// A `{"page"}` reference produces a page reference to a label, displaying the -/// page number at its location. You can use the -/// [page's supplement]($page.supplement) to modify the text before the page -/// number. Unlike a `{"normal"}` reference, the label can be attached to any -/// element. -/// -/// # Example +/// # 例 /// ```example /// #set page(numbering: "1") /// #set heading(numbering: "1.") @@ -74,14 +65,14 @@ use crate::text::TextElem; /// label can be created by typing an `@` followed by the name of the label /// (e.g. `[= Introduction ]` can be referenced by typing `[@intro]`). /// -/// To customize the supplement, add content in square brackets after the -/// reference: `[@intro[Chapter]]`. +/// 補足をカスタマイズするには、 +/// `[@intro[Chapter]]`のように、参照の後に角括弧でコンテンツを追加します。 /// -/// # Customization -/// If you write a show rule for references, you can access the referenced -/// element through the `element` field of the reference. The `element` may -/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you -/// always need to handle that case in your code. +/// # カスタム +/// 参照のshowルールを書く場合、 +/// 参照の`element`フィールドを通じて参照先の要素にアクセスできます。 +/// ただし、Typstがまだそれを発見していない場合、`element`は存在していても`{none}`になる可能性があるため、 +/// 常にコード内でそのケースを処理する必要があります。 /// /// ```example /// #set heading(numbering: "1.") @@ -108,15 +99,14 @@ use crate::text::TextElem; /// ``` #[elem(title = "Reference", Synthesize, Locatable, Show)] pub struct RefElem { - /// The target label that should be referenced. + /// 参照されるべき対象ラベル。 /// - /// Can be a label that is defined in the document or, if the - /// [`form`]($ref.form) is set to `["normal"]`, an entry from the - /// [`bibliography`]. + /// これは、ドキュメント内で定義されたラベルや、 + /// [`参考文献リスト`]($bibliography)の参照キーである場合があります。 #[required] pub target: Label, - /// A supplement for the reference. + /// 参照の補足。 /// /// If the [`form`]($ref.form) is set to `{"normal"}`: /// - For references to headings or figures, this is added before the diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 258eb96f34..04b1aabd31 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -26,15 +26,14 @@ use crate::loading::{DataSource, Load, Readable}; use crate::model::Figurable; use crate::text::LocalName; -/// A raster or vector graphic. +/// ラスターまたはベクター画像。 /// -/// You can wrap the image in a [`figure`] to give it a number and caption. +/// 画像を[`figure`]で囲むことで、番号とキャプションを与えることができます。 /// -/// Like most elements, images are _block-level_ by default and thus do not -/// integrate themselves into adjacent paragraphs. To force an image to become -/// inline, put it into a [`box`]. +/// ほとんどの要素と同様に、画像はデフォルトでは _ブロックレベル_ であるため、隣接する段落に統合されることはありません。 +/// 画像を強制的にインラインにするには、[`box`]の中に入れてください。 /// -/// # Example +/// # 例 /// ```example /// #figure( /// image("molecular.jpg", width: 80%), @@ -70,16 +69,15 @@ pub struct ImageElem { )] pub source: Derived, - /// The image's format. + /// 画像のフォーマット。 /// /// 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). + /// 生のピクセルデータと同様にサポートされている拡張子は`{"png"}`、`{"jpg"}`、`{"gif"}`、`{"svg"}`です。 + /// [PDFの画像はまだサポートされていません。](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`: @@ -116,19 +114,18 @@ pub struct ImageElem { /// ``` pub format: Smart, - /// The width of the image. + /// 画像の幅。 pub width: Smart>, - /// The height of the image. + /// 画像の高さ。 pub height: Sizing, - /// A text describing the image. + /// 画像の説明文。 pub alt: Option, - /// How the image should adjust itself to a given area (the area is defined - /// by the `width` and `height` fields). Note that `fit` doesn't visually - /// change anything if the area's aspect ratio is the same as the image's - /// one. + /// 与えられた領域に対して、画像をどのように調整するか。 + /// 領域は `width` や `height` フィールドで定義します。 + /// 領域の縦横比が画像の縦横比と同じであれば、`fit` で見た目が変わらないことに注意してください。 /// /// ```example /// #set page(width: 300pt, height: 50pt, margin: 10pt) @@ -172,21 +169,21 @@ impl ImageElem { #[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. + /// 画像としてデコードするデータ。SVGの場合は文字列です。 data: Readable, - /// The image's format. Detected automatically by default. + /// 画像のフォーマット。デフォルトでは自動的に検出されます。 #[named] format: Option>, - /// The width of the image. + /// 画像の幅。 #[named] width: Option>>, - /// The height of the image. + /// 画像の高さ。 #[named] height: Option, - /// A text describing the image. + /// 画像の説明文。 #[named] alt: Option>, - /// How the image should adjust itself to a given area. + /// 与えられた領域に対して、画像をどのように調整するか。 #[named] fit: Option, /// A hint to viewers how they should scale the image. @@ -237,17 +234,15 @@ impl Figurable for Packed {} /// How an image should adjust itself to a given area, #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum ImageFit { - /// The image should completely cover the area (preserves aspect ratio by - /// cropping the image only horizontally or vertically). This is the - /// default. + /// 領域を完全にカバーします。 + /// 水平または垂直方向にのみ画像をトリミングすることで、アスペクト比を保持します。 + /// これがデフォルトです。 Cover, - /// The image should be fully contained in the area (preserves aspect - /// ratio; doesn't crop the image; one dimension can be narrower than - /// specified). + /// 画像は領域内に完全に収まるようにします。 + /// アスペクト比を維持して、画像を切り取らず、1つの寸法は指定より狭くします。 Contain, - /// The image should be stretched so that it exactly fills the area, even if - /// this means that the image will be distorted (doesn't preserve aspect - /// ratio and doesn't crop the image). + /// たとえ画像が歪むことになっても、その領域を正確に埋めるように引き伸ばします。 + /// アスペクト比は保たれず、画像は切り取られません。 Stretch, } @@ -424,7 +419,7 @@ fn is_svg(data: &[u8]) -> bool { /// A vector graphics format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum VectorFormat { - /// The vector graphics format of the web. + /// Webサイトに用いられるベクターフォーマット。 Svg, } diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 0883fe71d8..be4d013d1b 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -220,12 +220,12 @@ cast! { /// 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. Typst can - /// load GIFs, but they will become static. + /// 短いアニメーションクリップによく使われるラスターフォーマット。 + /// TypstはGIFを読み込めるが、静止画となってしまう。 Gif, } diff --git a/crates/typst/assets/cj_linebreak_data.postcard b/crates/typst/assets/cj_linebreak_data.postcard deleted file mode 100644 index 910dd1676a..0000000000 Binary files a/crates/typst/assets/cj_linebreak_data.postcard and /dev/null differ diff --git a/crates/typst/assets/icudata.postcard b/crates/typst/assets/icudata.postcard deleted file mode 100644 index a1fdbd4804..0000000000 Binary files a/crates/typst/assets/icudata.postcard and /dev/null differ diff --git a/crates/typst/assets/syntect.bin b/crates/typst/assets/syntect.bin deleted file mode 100644 index 043602a47c..0000000000 Binary files a/crates/typst/assets/syntect.bin and /dev/null differ diff --git a/crates/typst/src/introspection/introspector.rs b/crates/typst/src/introspection/introspector.rs deleted file mode 100644 index 113f9afe07..0000000000 --- a/crates/typst/src/introspection/introspector.rs +++ /dev/null @@ -1,453 +0,0 @@ -use std::collections::{BTreeSet, HashMap, HashSet}; -use std::fmt::{self, Debug, Formatter}; -use std::hash::Hash; -use std::num::NonZeroUsize; -use std::sync::RwLock; - -use ecow::EcoVec; -use smallvec::SmallVec; - -use crate::diag::{bail, StrResult}; -use crate::foundations::{Content, Label, Repr, Selector}; -use crate::introspection::{Location, Tag}; -use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; -use crate::model::Numbering; -use crate::utils::NonZeroExt; - -/// Can be queried for elements and their positions. -#[derive(Default, Clone)] -pub struct Introspector { - /// The number of pages in the document. - pages: usize, - /// The page numberings, indexed by page number minus 1. - page_numberings: Vec>, - - /// All introspectable elements. - elems: Vec, - /// Lists all elements with a specific hash key. This is used for - /// introspector-assisted location assignment during measurement. - keys: MultiMap, - - /// Accelerates lookup of elements by location. - locations: HashMap, - /// Accelerates lookup of elements by label. - labels: MultiMap, - - /// Caches queries done on the introspector. This is important because - /// even if all top-level queries are distinct, they often have shared - /// subqueries. Example: Individual counter queries with `before` that - /// all depend on a global counter query. - queries: QueryCache, -} - -/// A pair of content and its position. -type Pair = (Content, Position); - -impl Introspector { - /// Creates an introspector for a page list. - #[typst_macros::time(name = "introspect")] - pub fn new(pages: &[Page]) -> Self { - IntrospectorBuilder::new().build(pages) - } - - /// Iterates over all locatable elements. - pub fn all(&self) -> impl Iterator + '_ { - self.elems.iter().map(|(c, _)| c) - } - - /// Retrieves the element with the given index. - #[track_caller] - fn get_by_idx(&self, idx: usize) -> &Content { - &self.elems[idx].0 - } - - /// Retrieves the position of the element with the given index. - #[track_caller] - fn get_pos_by_idx(&self, idx: usize) -> Position { - self.elems[idx].1 - } - - /// Retrieves an element by its location. - fn get_by_loc(&self, location: &Location) -> Option<&Content> { - self.locations.get(location).map(|&idx| self.get_by_idx(idx)) - } - - /// Retrieves the position of the element with the given index. - fn get_pos_by_loc(&self, location: &Location) -> Option { - self.locations.get(location).map(|&idx| self.get_pos_by_idx(idx)) - } - - /// Performs a binary search for `elem` among the `list`. - fn binary_search(&self, list: &[Content], elem: &Content) -> Result { - list.binary_search_by_key(&self.elem_index(elem), |elem| self.elem_index(elem)) - } - - /// Gets the index of this element. - fn elem_index(&self, elem: &Content) -> usize { - self.loc_index(&elem.location().unwrap()) - } - - /// Gets the index of the element with this location among all. - fn loc_index(&self, location: &Location) -> usize { - self.locations.get(location).copied().unwrap_or(usize::MAX) - } -} - -#[comemo::track] -impl Introspector { - /// Query for all matching elements. - pub fn query(&self, selector: &Selector) -> EcoVec { - let hash = crate::utils::hash128(selector); - if let Some(output) = self.queries.get(hash) { - return output; - } - - let output = match selector { - Selector::Elem(..) => self - .all() - .filter(|elem| selector.matches(elem, None)) - .cloned() - .collect(), - Selector::Location(location) => { - self.get_by_loc(location).cloned().into_iter().collect() - } - Selector::Label(label) => self - .labels - .get(label) - .iter() - .map(|&idx| self.get_by_idx(idx).clone()) - .collect(), - Selector::Or(selectors) => selectors - .iter() - .flat_map(|sel| self.query(sel)) - .map(|elem| self.elem_index(&elem)) - .collect::>() - .into_iter() - .map(|idx| self.get_by_idx(idx).clone()) - .collect(), - Selector::And(selectors) => { - let mut results: Vec<_> = - selectors.iter().map(|sel| self.query(sel)).collect(); - - // Extract the smallest result list and then keep only those - // elements in the smallest list that are also in all other - // lists. - results - .iter() - .enumerate() - .min_by_key(|(_, vec)| vec.len()) - .map(|(i, _)| i) - .map(|i| results.swap_remove(i)) - .iter() - .flatten() - .filter(|candidate| { - results - .iter() - .all(|other| self.binary_search(other, candidate).is_ok()) - }) - .cloned() - .collect() - } - Selector::Before { selector, end, inclusive } => { - let mut list = self.query(selector); - if let Some(end) = self.query_first(end) { - // Determine which elements are before `end`. - let split = match self.binary_search(&list, &end) { - // Element itself is contained. - Ok(i) => i + *inclusive as usize, - // Element itself is not contained. - Err(i) => i, - }; - list = list[..split].into(); - } - list - } - Selector::After { selector, start, inclusive } => { - let mut list = self.query(selector); - if let Some(start) = self.query_first(start) { - // Determine which elements are after `start`. - let split = match self.binary_search(&list, &start) { - // Element itself is contained. - Ok(i) => i + !*inclusive as usize, - // Element itself is not contained. - Err(i) => i, - }; - list = list[split..].into(); - } - list - } - // Not supported here. - Selector::Can(_) | Selector::Regex(_) => EcoVec::new(), - }; - - self.queries.insert(hash, output.clone()); - output - } - - /// Query for the first element that matches the selector. - pub fn query_first(&self, selector: &Selector) -> Option { - match selector { - Selector::Location(location) => self.get_by_loc(location).cloned(), - Selector::Label(label) => self - .labels - .get(label) - .first() - .map(|&idx| self.get_by_idx(idx).clone()), - _ => self.query(selector).first().cloned(), - } - } - - /// Query for the first element that matches the selector. - pub fn query_unique(&self, selector: &Selector) -> StrResult { - match selector { - Selector::Location(location) => self - .get_by_loc(location) - .cloned() - .ok_or_else(|| "element does not exist in the document".into()), - Selector::Label(label) => self.query_label(*label).cloned(), - _ => { - let elems = self.query(selector); - if elems.len() > 1 { - bail!("selector matches multiple elements",); - } - elems - .into_iter() - .next() - .ok_or_else(|| "selector does not match any element".into()) - } - } - } - - /// Query for a unique element with the label. - pub fn query_label(&self, label: Label) -> StrResult<&Content> { - match *self.labels.get(&label) { - [idx] => Ok(self.get_by_idx(idx)), - [] => bail!("label `{}` does not exist in the document", label.repr()), - _ => bail!("label `{}` occurs multiple times in the document", label.repr()), - } - } - - /// This is an optimized version of - /// `query(selector.before(end, true).len()` used by counters and state. - pub fn query_count_before(&self, selector: &Selector, end: Location) -> usize { - // See `query()` for details. - let list = self.query(selector); - if let Some(end) = self.get_by_loc(&end) { - match self.binary_search(&list, end) { - Ok(i) => i + 1, - Err(i) => i, - } - } else { - list.len() - } - } - - /// The total number pages. - pub fn pages(&self) -> NonZeroUsize { - NonZeroUsize::new(self.pages).unwrap_or(NonZeroUsize::ONE) - } - - /// Find the page number for the given location. - pub fn page(&self, location: Location) -> NonZeroUsize { - self.position(location).page - } - - /// Find the position for the given location. - pub fn position(&self, location: Location) -> Position { - self.get_pos_by_loc(&location) - .unwrap_or(Position { page: NonZeroUsize::ONE, point: Point::zero() }) - } - - /// Gets the page numbering for the given location, if any. - pub fn page_numbering(&self, location: Location) -> Option<&Numbering> { - let page = self.page(location); - self.page_numberings - .get(page.get() - 1) - .and_then(|slot| slot.as_ref()) - } - - /// Try to find a location for an element with the given `key` hash - /// that is closest after the `anchor`. - /// - /// This is used for introspector-assisted location assignment during - /// measurement. See the "Dealing with Measurement" section of the - /// [`Locator`](crate::introspection::Locator) docs for more details. - pub fn locator(&self, key: u128, anchor: Location) -> Option { - let anchor = self.loc_index(&anchor); - self.keys - .get(&key) - .iter() - .copied() - .min_by_key(|loc| self.loc_index(loc).wrapping_sub(anchor)) - } -} - -impl Debug for Introspector { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("Introspector(..)") - } -} - -/// A map from one keys to multiple elements. -#[derive(Clone)] -struct MultiMap(HashMap>); - -impl MultiMap -where - K: Hash + Eq, -{ - fn get(&self, key: &K) -> &[V] { - self.0.get(key).map_or(&[], |vec| vec.as_slice()) - } - - fn insert(&mut self, key: K, value: V) { - self.0.entry(key).or_default().push(value); - } - - fn take(&mut self, key: &K) -> Option> { - self.0.remove(key).map(|vec| vec.into_iter()) - } -} - -impl Default for MultiMap { - fn default() -> Self { - Self(HashMap::new()) - } -} - -/// Caches queries. -#[derive(Default)] -struct QueryCache(RwLock>>); - -impl QueryCache { - fn get(&self, hash: u128) -> Option> { - self.0.read().unwrap().get(&hash).cloned() - } - - fn insert(&self, hash: u128, output: EcoVec) { - self.0.write().unwrap().insert(hash, output); - } -} - -impl Clone for QueryCache { - fn clone(&self) -> Self { - Self(RwLock::new(self.0.read().unwrap().clone())) - } -} - -/// Builds the introspector. -#[derive(Default)] -struct IntrospectorBuilder { - page_numberings: Vec>, - seen: HashSet, - insertions: MultiMap>, - keys: MultiMap, - locations: HashMap, - labels: MultiMap, -} - -impl IntrospectorBuilder { - /// Create an empty builder. - fn new() -> Self { - Self::default() - } - - /// Build the introspector. - fn build(mut self, pages: &[Page]) -> Introspector { - self.page_numberings.reserve(pages.len()); - - // Discover all elements. - let mut root = Vec::new(); - for (i, page) in pages.iter().enumerate() { - self.page_numberings.push(page.numbering.clone()); - self.discover( - &mut root, - &page.frame, - NonZeroUsize::new(1 + i).unwrap(), - Transform::identity(), - ); - } - - self.locations.reserve(self.seen.len()); - - // Save all pairs and their descendants in the correct order. - let mut elems = Vec::with_capacity(self.seen.len()); - for pair in root { - self.visit(&mut elems, pair); - } - - Introspector { - pages: pages.len(), - page_numberings: self.page_numberings, - elems, - keys: self.keys, - locations: self.locations, - labels: self.labels, - queries: QueryCache::default(), - } - } - - /// Processes the tags in the frame. - fn discover( - &mut self, - sink: &mut Vec, - frame: &Frame, - page: NonZeroUsize, - ts: Transform, - ) { - for (pos, item) in frame.items() { - match item { - FrameItem::Group(group) => { - let ts = ts - .pre_concat(Transform::translate(pos.x, pos.y)) - .pre_concat(group.transform); - - if let Some(parent) = group.parent { - let mut nested = vec![]; - self.discover(&mut nested, &group.frame, page, ts); - self.insertions.insert(parent, nested); - } else { - self.discover(sink, &group.frame, page, ts); - } - } - FrameItem::Tag(Tag::Start(elem)) => { - let loc = elem.location().unwrap(); - if self.seen.insert(loc) { - let point = pos.transform(ts); - sink.push((elem.clone(), Position { page, point })); - } - } - FrameItem::Tag(Tag::End(loc, key)) => { - self.keys.insert(*key, *loc); - } - _ => {} - } - } - } - - /// Saves a pair and all its descendants into `elems` and populates the - /// acceleration structures. - fn visit(&mut self, elems: &mut Vec, pair: Pair) { - let elem = &pair.0; - let loc = elem.location().unwrap(); - let idx = elems.len(); - - // Populate the location acceleration map. - self.locations.insert(loc, idx); - - // Populate the label acceleration map. - if let Some(label) = elem.label() { - self.labels.insert(label, idx); - } - - // Save the element. - elems.push(pair); - - // Process potential descendants. - if let Some(insertions) = self.insertions.take(&loc) { - for pair in insertions.flatten() { - self.visit(elems, pair); - } - } - } -} diff --git a/crates/typst/src/introspection/tag.rs b/crates/typst/src/introspection/tag.rs deleted file mode 100644 index b2bae28e40..0000000000 --- a/crates/typst/src/introspection/tag.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::fmt::{self, Debug, Formatter}; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - elem, Args, Construct, Content, NativeElement, Packed, Unlabellable, -}; -use crate::introspection::Location; - -/// Marks the start or end of a locatable element. -#[derive(Clone, PartialEq, Hash)] -pub enum Tag { - /// The stored element starts here. - /// - /// Content placed in a tag **must** have a [`Location`] or there will be - /// panics. - Start(Content), - /// The element with the given location and key hash ends here. - /// - /// Note: The key hash is stored here instead of in `Start` simply to make - /// the two enum variants more balanced in size, keeping a `Tag`'s memory - /// size down. There are no semantic reasons for this. - End(Location, u128), -} - -impl Tag { - /// Access the location of the tag. - pub fn location(&self) -> Location { - match self { - Tag::Start(elem) => elem.location().unwrap(), - Tag::End(loc, _) => *loc, - } - } -} - -impl Debug for Tag { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Tag::Start(elem) => write!(f, "Start({:?})", elem.elem().name()), - Tag::End(..) => f.pad("End"), - } - } -} - -/// Holds a tag for a locatable element that was realized. -/// -/// The `TagElem` is handled by all layouters. The held element becomes -/// available for introspection in the next compiler iteration. -#[elem(Construct, Unlabellable)] -pub struct TagElem { - /// The introspectable element. - #[required] - #[internal] - pub tag: Tag, -} - -impl TagElem { - /// Create a packed tag element. - pub fn packed(tag: Tag) -> Content { - let mut content = Self::new(tag).pack(); - // We can skip preparation for the `TagElem`. - content.mark_prepared(); - content - } -} - -impl Construct for TagElem { - fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { - bail!(args.span, "cannot be constructed manually") - } -} - -impl Unlabellable for Packed {} diff --git a/crates/typst/src/layout/container.rs b/crates/typst/src/layout/container.rs deleted file mode 100644 index d97edd5a97..0000000000 --- a/crates/typst/src/layout/container.rs +++ /dev/null @@ -1,1046 +0,0 @@ -use once_cell::unsync::Lazy; -use smallvec::SmallVec; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, Args, AutoValue, Construct, Content, NativeElement, Packed, Resolve, - Smart, StyleChain, Value, -}; -use crate::introspection::Locator; -use crate::layout::{ - layout_fragment, layout_frame, Abs, Axes, Corners, Em, Fr, Fragment, Frame, - FrameKind, Length, Region, Regions, Rel, Sides, Size, Spacing, -}; -use crate::utils::Numeric; -use crate::visualize::{clip_rect, 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. -/// -/// # Example -/// ```example -/// Refer to the docs -/// #box( -/// height: 9pt, -/// image("docs.svg") -/// ) -/// for more information. -/// ``` -#[elem] -pub struct BoxElem { - /// The width of the box. - /// - /// Boxes can have [fractional]($fraction) widths, as the example below - /// demonstrates. - /// - /// _Note:_ Currently, only boxes and only their widths might be fractionally - /// sized within paragraphs. Support for fractionally sized images, shapes, - /// and more might be added in the future. - /// - /// ```example - /// Line in #box(width: 1fr, line(length: 100%)) between. - /// ``` - pub width: Sizing, - - /// The height of the box. - pub height: Smart>, - - /// An amount to shift the box's baseline by. - /// - /// ```example - /// Image: #box(baseline: 40%, image("tiger.jpg", width: 2cm)). - /// ``` - #[resolve] - pub baseline: Rel, - - /// The box's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option, - - /// The box's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides>>, - - /// How much to round the box's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners>>, - - /// How much to pad the box's content. - /// - /// _Note:_ When the box contains text, its exact size depends on the - /// current [text edges]($text.top-edge). - /// - /// ```example - /// #rect(inset: 0pt)[Tight] - /// ``` - #[resolve] - #[fold] - pub inset: Sides>>, - - /// How much to expand the box's size without affecting the layout. - /// - /// This is useful to prevent padding from affecting line layout. For a - /// generalized version of the example below, see the documentation for the - /// [raw text's block parameter]($raw.block). - /// - /// ```example - /// An inline - /// #box( - /// fill: luma(235), - /// inset: (x: 3pt, y: 0pt), - /// outset: (y: 3pt), - /// radius: 2pt, - /// )[rectangle]. - /// ``` - #[resolve] - #[fold] - pub outset: Sides>>, - - /// Whether to clip the content inside the box. - /// - /// Clipping is useful when the box's content is larger than the box itself, - /// as any content that exceeds the box's bounds will be hidden. - /// - /// ```example - /// #box( - /// width: 50pt, - /// height: 50pt, - /// clip: true, - /// image("tiger.jpg", width: 100pt, height: 100pt) - /// ) - /// ``` - #[default(false)] - pub clip: bool, - - /// The contents of the box. - #[positional] - #[borrowed] - pub body: Option, -} - -impl Packed { - /// Layout this box as part of a paragraph. - #[typst_macros::time(name = "box", span = self.span())] - pub fn layout( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult { - // Fetch sizing properties. - let width = self.width(styles); - let height = self.height(styles); - let inset = self.inset(styles).unwrap_or_default(); - - // Build the pod region. - let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region); - - // Layout the body. - let mut frame = match self.body(styles) { - // If we have no body, just create an empty frame. If necessary, - // its size will be adjusted below. - None => Frame::hard(Size::zero()), - - // If we have a child, layout it into the body. Boxes are boundaries - // for gradient relativeness, so we set the `FrameKind` to `Hard`. - Some(body) => layout_frame(engine, body, locator, styles, pod)? - .with_kind(FrameKind::Hard), - }; - - // Enforce a correct frame size on the expanded axes. Do this before - // applying the inset, since the pod shrunk. - frame.set_size(pod.expand.select(pod.size, frame.size())); - - // Apply the inset. - if !inset.is_zero() { - crate::layout::grow(&mut frame, &inset); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self - .stroke(styles) - .unwrap_or_default() - .map(|s| s.map(Stroke::unwrap_or_default)); - - // Only fetch these if necessary (for clipping or filling/stroking). - let outset = Lazy::new(|| self.outset(styles).unwrap_or_default()); - let radius = Lazy::new(|| self.radius(styles).unwrap_or_default()); - - // Clip the contents, if requested. - if self.clip(styles) { - let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); - frame.clip(clip_rect(size, &radius, &stroke)); - } - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span()); - } - - // Assign label to the frame. - if let Some(label) = self.label() { - frame.label(label); - } - - // Apply baseline shift. Do this after setting the size and applying the - // inset, so that a relative shift is resolved relative to the final - // height. - let shift = self.baseline(styles).relative_to(frame.height()); - if !shift.is_zero() { - frame.set_baseline(frame.baseline() - shift); - } - - Ok(frame) - } -} - -/// An inline-level container that can produce arbitrary items that can break -/// across lines. -#[elem(Construct)] -pub struct InlineElem { - /// A callback that is invoked with the regions to produce arbitrary - /// inline items. - #[required] - #[internal] - body: callbacks::InlineCallback, -} - -impl Construct for InlineElem { - fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { - bail!(args.span, "cannot be constructed manually"); - } -} - -impl InlineElem { - /// Create an inline-level item with a custom layouter. - #[allow(clippy::type_complexity)] - pub fn layouter( - captured: Packed, - callback: fn( - content: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult>, - ) -> Self { - Self::new(callbacks::InlineCallback::new(captured, callback)) - } -} - -impl Packed { - /// Layout the element. - pub fn layout( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult> { - self.body().call(engine, locator, styles, region) - } -} - -/// Layouted items suitable for placing in a paragraph. -#[derive(Debug, Clone)] -pub enum InlineItem { - /// Absolute spacing between other items, and whether it is weak. - Space(Abs, bool), - /// Layouted inline-level content. - Frame(Frame), -} - -/// A block-level container. -/// -/// Such a container can be used to separate content, size it, and give it a -/// background or border. -/// -/// # Examples -/// With a block, you can give a background to content while still allowing it -/// to break across multiple pages. -/// ```example -/// #set page(height: 100pt) -/// #block( -/// fill: luma(230), -/// inset: 8pt, -/// radius: 4pt, -/// lorem(30), -/// ) -/// ``` -/// -/// Blocks are also useful to force elements that would otherwise be inline to -/// become block-level, especially when writing show rules. -/// ```example -/// #show heading: it => it.body -/// = Blockless -/// More text. -/// -/// #show heading: it => block(it.body) -/// = Blocky -/// More text. -/// ``` -#[elem] -pub struct BlockElem { - /// The block's width. - /// - /// ```example - /// #set align(center) - /// #block( - /// width: 60%, - /// inset: 8pt, - /// fill: silver, - /// lorem(10), - /// ) - /// ``` - pub width: Smart>, - - /// The block's height. When the height is larger than the remaining space - /// on a page and [`breakable`]($block.breakable) is `{true}`, the - /// block will continue on the next page with the remaining height. - /// - /// ```example - /// #set page(height: 80pt) - /// #set align(center) - /// #block( - /// width: 80%, - /// height: 150%, - /// fill: aqua, - /// ) - /// ``` - pub height: Sizing, - - /// Whether the block can be broken and continue on the next page. - /// - /// ```example - /// #set page(height: 80pt) - /// The following block will - /// jump to its own page. - /// #block( - /// breakable: false, - /// lorem(15), - /// ) - /// ``` - #[default(true)] - pub breakable: bool, - - /// The block's background color. See the - /// [rectangle's documentation]($rect.fill) for more details. - pub fill: Option, - - /// The block's border color. See the - /// [rectangle's documentation]($rect.stroke) for more details. - #[resolve] - #[fold] - pub stroke: Sides>>, - - /// How much to round the block's corners. See the - /// [rectangle's documentation]($rect.radius) for more details. - #[resolve] - #[fold] - pub radius: Corners>>, - - /// How much to pad the block's content. See the - /// [box's documentation]($box.inset) for more details. - #[resolve] - #[fold] - pub inset: Sides>>, - - /// How much to expand the block's size without affecting the layout. See - /// the [box's documentation]($box.outset) for more details. - #[resolve] - #[fold] - pub outset: Sides>>, - - /// The spacing around the block. When `{auto}`, inherits the paragraph - /// [`spacing`]($par.spacing). - /// - /// For two adjacent blocks, the larger of the first block's `above` and the - /// second block's `below` spacing wins. Moreover, block spacing takes - /// precedence over paragraph [`spacing`]($par.spacing). - /// - /// Note that this is only a shorthand to set `above` and `below` to the - /// same value. Since the values for `above` and `below` might differ, a - /// [context] block only provides access to `{block.above}` and - /// `{block.below}`, not to `{block.spacing}` directly. - /// - /// This property can be used in combination with a show rule to adjust the - /// spacing around arbitrary block-level elements. - /// - /// ```example - /// #set align(center) - /// #show math.equation: set block(above: 8pt, below: 16pt) - /// - /// This sum of $x$ and $y$: - /// $ x + y = z $ - /// A second paragraph. - /// ``` - #[external] - #[default(Em::new(1.2).into())] - pub spacing: Spacing, - - /// The spacing between this block and its predecessor. - #[parse( - let spacing = args.named("spacing")?; - args.named("above")?.or(spacing) - )] - pub above: Smart, - - /// The spacing between this block and its successor. - #[parse(args.named("below")?.or(spacing))] - pub below: Smart, - - /// Whether to clip the content inside the block. - /// - /// Clipping is useful when the block's content is larger than the block itself, - /// as any content that exceeds the block's bounds will be hidden. - /// - /// ```example - /// #block( - /// width: 50pt, - /// height: 50pt, - /// clip: true, - /// image("tiger.jpg", width: 100pt, height: 100pt) - /// ) - /// ``` - #[default(false)] - pub clip: bool, - - /// Whether this block must stick to the following one, with no break in - /// between. - /// - /// This is, by default, set on heading blocks to prevent orphaned headings - /// at the bottom of the page. - /// - /// ```example - /// >>> #set page(height: 140pt) - /// // Disable stickiness of headings. - /// #show heading: set block(sticky: false) - /// #lorem(20) - /// - /// = Chapter - /// #lorem(10) - /// ``` - #[default(false)] - pub sticky: bool, - - /// The contents of the block. - #[positional] - #[borrowed] - pub body: Option, -} - -impl BlockElem { - /// Create a block with a custom single-region layouter. - /// - /// Such a block must have `breakable: false` (which is set by this - /// constructor). - pub fn single_layouter( - captured: Packed, - f: fn( - content: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult, - ) -> Self { - Self::new() - .with_breakable(false) - .with_body(Some(BlockBody::SingleLayouter( - callbacks::BlockSingleCallback::new(captured, f), - ))) - } - - /// Create a block with a custom multi-region layouter. - pub fn multi_layouter( - captured: Packed, - f: fn( - content: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult, - ) -> Self { - Self::new().with_body(Some(BlockBody::MultiLayouter( - callbacks::BlockMultiCallback::new(captured, f), - ))) - } -} - -impl Packed { - /// Lay this out as an unbreakable block. - #[typst_macros::time(name = "block", span = self.span())] - pub fn layout_single( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult { - // Fetch sizing properties. - let width = self.width(styles); - let height = self.height(styles); - let inset = self.inset(styles).unwrap_or_default(); - - // Build the pod regions. - let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size); - - // Layout the body. - let body = self.body(styles); - let mut frame = match body { - // If we have no body, just create one frame. Its size will be - // adjusted below. - None => Frame::hard(Size::zero()), - - // If we have content as our body, just layout it. - Some(BlockBody::Content(body)) => { - layout_frame(engine, body, locator.relayout(), styles, pod)? - } - - // If we have a child that wants to layout with just access to the - // base region, give it that. - Some(BlockBody::SingleLayouter(callback)) => { - callback.call(engine, locator, styles, pod)? - } - - // If we have a child that wants to layout with full region access, - // we layout it. - Some(BlockBody::MultiLayouter(callback)) => { - let expand = (pod.expand | region.expand) & pod.size.map(Abs::is_finite); - let pod = Region { expand, ..pod }; - callback.call(engine, locator, styles, pod.into())?.into_frame() - } - }; - - // Explicit blocks are boundaries for gradient relativeness. - if matches!(body, None | Some(BlockBody::Content(_))) { - frame.set_kind(FrameKind::Hard); - } - - // Enforce a correct frame size on the expanded axes. Do this before - // applying the inset, since the pod shrunk. - frame.set_size(pod.expand.select(pod.size, frame.size())); - - // Apply the inset. - if !inset.is_zero() { - crate::layout::grow(&mut frame, &inset); - } - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self - .stroke(styles) - .unwrap_or_default() - .map(|s| s.map(Stroke::unwrap_or_default)); - - // Only fetch these if necessary (for clipping or filling/stroking). - let outset = Lazy::new(|| self.outset(styles).unwrap_or_default()); - let radius = Lazy::new(|| self.radius(styles).unwrap_or_default()); - - // Clip the contents, if requested. - if self.clip(styles) { - let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); - frame.clip(clip_rect(size, &radius, &stroke)); - } - - // Add fill and/or stroke. - if fill.is_some() || stroke.iter().any(Option::is_some) { - frame.fill_and_stroke(fill, &stroke, &outset, &radius, self.span()); - } - - // Assign label to each frame in the fragment. - if let Some(label) = self.label() { - frame.label(label); - } - - Ok(frame) - } -} - -impl Packed { - /// Lay this out as a breakable block. - #[typst_macros::time(name = "block", span = self.span())] - pub fn layout_multiple( - &self, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - // Fetch sizing properties. - let width = self.width(styles); - let height = self.height(styles); - let inset = self.inset(styles).unwrap_or_default(); - - // Allocate a small vector for backlogs. - let mut buf = SmallVec::<[Abs; 2]>::new(); - - // Build the pod regions. - let pod = - breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf); - - // Layout the body. - let body = self.body(styles); - let mut fragment = match body { - // If we have no body, just create one frame plus one per backlog - // region. We create them zero-sized; if necessary, their size will - // be adjusted below. - None => { - let mut frames = vec![]; - frames.push(Frame::hard(Size::zero())); - if pod.expand.y { - let mut iter = pod; - while !iter.backlog.is_empty() { - frames.push(Frame::hard(Size::zero())); - iter.next(); - } - } - Fragment::frames(frames) - } - - // If we have content as our body, just layout it. - Some(BlockBody::Content(body)) => { - let mut fragment = - layout_fragment(engine, body, locator.relayout(), styles, pod)?; - - // If the body is automatically sized and produced more than one - // fragment, ensure that the width was consistent across all - // regions. If it wasn't, we need to relayout with expansion. - if !pod.expand.x - && fragment - .as_slice() - .windows(2) - .any(|w| !w[0].width().approx_eq(w[1].width())) - { - let max_width = fragment - .iter() - .map(|frame| frame.width()) - .max() - .unwrap_or_default(); - let pod = Regions { - size: Size::new(max_width, pod.size.y), - expand: Axes::new(true, pod.expand.y), - ..pod - }; - fragment = layout_fragment(engine, body, locator, styles, pod)?; - } - - fragment - } - - // If we have a child that wants to layout with just access to the - // base region, give it that. - Some(BlockBody::SingleLayouter(callback)) => { - let pod = Region::new(pod.base(), pod.expand); - callback.call(engine, locator, styles, pod).map(Fragment::frame)? - } - - // If we have a child that wants to layout with full region access, - // we layout it. - // - // For auto-sized multi-layouters, we propagate the outer expansion - // so that they can decide for themselves. We also ensure again to - // only expand if the size is finite. - Some(BlockBody::MultiLayouter(callback)) => { - let expand = (pod.expand | regions.expand) & pod.size.map(Abs::is_finite); - let pod = Regions { expand, ..pod }; - callback.call(engine, locator, styles, pod)? - } - }; - - // Prepare fill and stroke. - let fill = self.fill(styles); - let stroke = self - .stroke(styles) - .unwrap_or_default() - .map(|s| s.map(Stroke::unwrap_or_default)); - - // Only fetch these if necessary (for clipping or filling/stroking). - let outset = Lazy::new(|| self.outset(styles).unwrap_or_default()); - let radius = Lazy::new(|| self.radius(styles).unwrap_or_default()); - - // Fetch/compute these outside of the loop. - let clip = self.clip(styles); - let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some); - let has_inset = !inset.is_zero(); - let is_explicit = matches!(body, None | Some(BlockBody::Content(_))); - - // Skip filling/stroking the first frame if it is empty and a non-empty - // one follows. - let mut skip_first = false; - if let [first, rest @ ..] = fragment.as_slice() { - skip_first = has_fill_or_stroke - && first.is_empty() - && rest.iter().any(|frame| !frame.is_empty()); - } - - // Post-process to apply insets, clipping, fills, and strokes. - for (i, (frame, region)) in fragment.iter_mut().zip(pod.iter()).enumerate() { - // Explicit blocks are boundaries for gradient relativeness. - if is_explicit { - frame.set_kind(FrameKind::Hard); - } - - // Enforce a correct frame size on the expanded axes. Do this before - // applying the inset, since the pod shrunk. - frame.set_size(pod.expand.select(region, frame.size())); - - // Apply the inset. - if has_inset { - crate::layout::grow(frame, &inset); - } - - // Clip the contents, if requested. - if clip { - let size = frame.size() + outset.relative_to(frame.size()).sum_by_axis(); - frame.clip(clip_rect(size, &radius, &stroke)); - } - - // Add fill and/or stroke. - if has_fill_or_stroke && (i > 0 || !skip_first) { - frame.fill_and_stroke( - fill.clone(), - &stroke, - &outset, - &radius, - self.span(), - ); - } - } - - // Assign label to each frame in the fragment. - if let Some(label) = self.label() { - for frame in fragment.iter_mut() { - frame.label(label); - } - } - - Ok(fragment) - } -} - -/// The contents of a block. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum BlockBody { - /// The block contains normal content. - Content(Content), - /// The block contains a layout callback that needs access to just one - /// base region. - SingleLayouter(callbacks::BlockSingleCallback), - /// The block contains a layout callback that needs access to the exact - /// regions. - MultiLayouter(callbacks::BlockMultiCallback), -} - -impl Default for BlockBody { - fn default() -> Self { - Self::Content(Content::default()) - } -} - -cast! { - BlockBody, - self => match self { - Self::Content(content) => content.into_value(), - _ => Value::Auto, - }, - v: Content => Self::Content(v), -} - -/// Defines how to size something along an axis. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum Sizing { - /// A track that fits its item's contents. - Auto, - /// A size specified in absolute terms and relative to the parent's size. - Rel(Rel), - /// A size specified as a fraction of the remaining free space in the - /// parent. - Fr(Fr), -} - -impl Sizing { - /// Whether this is an automatic sizing. - pub fn is_auto(self) -> bool { - matches!(self, Self::Auto) - } - - /// Whether this is fractional sizing. - pub fn is_fractional(self) -> bool { - matches!(self, Self::Fr(_)) - } -} - -impl Default for Sizing { - fn default() -> Self { - Self::Auto - } -} - -impl From> for Sizing { - fn from(smart: Smart) -> Self { - match smart { - Smart::Auto => Self::Auto, - Smart::Custom(rel) => Self::Rel(rel), - } - } -} - -impl> From for Sizing { - fn from(spacing: T) -> Self { - match spacing.into() { - Spacing::Rel(rel) => Self::Rel(rel), - Spacing::Fr(fr) => Self::Fr(fr), - } - } -} - -cast! { - Sizing, - self => match self { - Self::Auto => Value::Auto, - Self::Rel(rel) => rel.into_value(), - Self::Fr(fr) => fr.into_value(), - }, - _: AutoValue => Self::Auto, - v: Rel => Self::Rel(v), - v: Fr => Self::Fr(v), -} - -/// Builds the pod region for an unbreakable sized container. -fn unbreakable_pod( - width: &Sizing, - height: &Sizing, - inset: &Sides>, - styles: StyleChain, - base: Size, -) -> Region { - // Resolve the size. - let mut size = Size::new( - match width { - // - For auto, the whole region is available. - // - Fr is handled outside and already factored into the `region`, - // so we can treat it equivalently to 100%. - Sizing::Auto | Sizing::Fr(_) => base.x, - // Resolve the relative sizing. - Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), - }, - match height { - Sizing::Auto | Sizing::Fr(_) => base.y, - Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.y), - }, - ); - - // Take the inset, if any, into account. - if !inset.is_zero() { - size = crate::layout::shrink(size, inset); - } - - // If the child is manually, the size is forced and we should enable - // expansion. - let expand = Axes::new( - *width != Sizing::Auto && size.x.is_finite(), - *height != Sizing::Auto && size.y.is_finite(), - ); - - Region::new(size, expand) -} - -/// Builds the pod regions for a breakable sized container. -fn breakable_pod<'a>( - width: &Sizing, - height: &Sizing, - inset: &Sides>, - styles: StyleChain, - regions: Regions, - buf: &'a mut SmallVec<[Abs; 2]>, -) -> Regions<'a> { - let base = regions.base(); - - // The vertical region sizes we're about to build. - let first; - let full; - let backlog: &mut [Abs]; - let last; - - // If the block has a fixed height, things are very different, so we - // handle that case completely separately. - match height { - Sizing::Auto | Sizing::Fr(_) => { - // If the block is automatically sized, we can just inherit the - // regions. - first = regions.size.y; - full = regions.full; - buf.extend_from_slice(regions.backlog); - backlog = buf; - last = regions.last; - } - - Sizing::Rel(rel) => { - // Resolve the sizing to a concrete size. - let resolved = rel.resolve(styles).relative_to(base.y); - - // Since we're manually sized, the resolved size is the base height. - full = resolved; - - // Distribute the fixed height across a start region and a backlog. - (first, backlog) = distribute(resolved, regions, buf); - - // If the height is manually sized, we don't want a final repeatable - // region. - last = None; - } - }; - - // Resolve the horizontal sizing to a concrete width and combine - // `width` and `first` into `size`. - let mut size = Size::new( - match width { - Sizing::Auto | Sizing::Fr(_) => regions.size.x, - Sizing::Rel(rel) => rel.resolve(styles).relative_to(base.x), - }, - first, - ); - - // Take the inset, if any, into account, applying it to the - // individual region components. - let (mut full, mut last) = (full, last); - if !inset.is_zero() { - crate::layout::shrink_multiple(&mut size, &mut full, backlog, &mut last, inset); - } - - // If the child is manually, the size is forced and we should enable - // expansion. - let expand = Axes::new( - *width != Sizing::Auto && size.x.is_finite(), - *height != Sizing::Auto && size.y.is_finite(), - ); - - Regions { size, full, backlog, last, expand } -} - -/// Distribute a fixed height spread over existing regions into a new first -/// height and a new backlog. -fn distribute<'a>( - height: Abs, - mut regions: Regions, - buf: &'a mut SmallVec<[Abs; 2]>, -) -> (Abs, &'a mut [Abs]) { - // Build new region heights from old regions. - let mut remaining = height; - loop { - let limited = regions.size.y.clamp(Abs::zero(), remaining); - buf.push(limited); - remaining -= limited; - if remaining.approx_empty() - || !regions.may_break() - || (!regions.may_progress() && limited.approx_empty()) - { - break; - } - regions.next(); - } - - // If there is still something remaining, apply it to the - // last region (it will overflow, but there's nothing else - // we can do). - if !remaining.approx_empty() { - if let Some(last) = buf.last_mut() { - *last += remaining; - } - } - - // Distribute the heights to the first region and the - // backlog. There is no last region, since the height is - // fixed. - (buf[0], &mut buf[1..]) -} - -/// Manual closure implementations for layout callbacks. -/// -/// Normal closures are not `Hash`, so we can't use them. -mod callbacks { - use super::*; - - macro_rules! callback { - ($name:ident = ($($param:ident: $param_ty:ty),* $(,)?) -> $ret:ty) => { - #[derive(Debug, Clone, PartialEq, Hash)] - pub struct $name { - captured: Content, - f: fn(&Content, $($param_ty),*) -> $ret, - } - - impl $name { - pub fn new( - captured: Packed, - f: fn(&Packed, $($param_ty),*) -> $ret, - ) -> Self { - Self { - // Type-erased the content. - captured: captured.pack(), - // Safety: The only difference between the two function - // pointer types is the type of the first parameter, - // which changes from `&Packed` to `&Content`. This - // is safe because: - // - `Packed` is a transparent wrapper around - // `Content`, so for any `T` it has the same memory - // representation as `Content`. - // - While `Packed` imposes the additional constraint - // that the content is of type `T`, this constraint is - // upheld: It is initially the case because we store a - // `Packed` above. It keeps being the case over the - // lifetime of the closure because `capture` is a - // private field and `Content`'s `Clone` impl is - // guaranteed to retain the type (if it didn't, - // literally everything would break). - #[allow(clippy::missing_transmute_annotations)] - f: unsafe { std::mem::transmute(f) }, - } - } - - pub fn call(&self, $($param: $param_ty),*) -> $ret { - (self.f)(&self.captured, $($param),*) - } - } - }; - } - - callback! { - InlineCallback = ( - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult> - } - - callback! { - BlockSingleCallback = ( - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - } - - callback! { - BlockMultiCallback = ( - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - } -} diff --git a/crates/typst/src/math/cancel.rs b/crates/typst/src/math/cancel.rs deleted file mode 100644 index 0bc28f0788..0000000000 --- a/crates/typst/src/math/cancel.rs +++ /dev/null @@ -1,254 +0,0 @@ -use comemo::Track; - -use crate::diag::{At, SourceResult}; -use crate::foundations::{cast, elem, Content, Context, Func, Packed, Smart, StyleChain}; -use crate::layout::{ - Abs, Angle, Frame, FrameItem, Length, Point, Ratio, Rel, Size, Transform, -}; -use crate::math::{scaled_font_size, FrameFragment, LayoutMath, MathContext}; -use crate::syntax::Span; -use crate::text::TextElem; -use crate::visualize::{FixedStroke, Geometry, Stroke}; - -/// Displays a diagonal line over a part of an equation. -/// -/// This is commonly used to show the elimination of a term. -/// -/// # Example -/// ```example -/// >>> #set page(width: 140pt) -/// Here, we can simplify: -/// $ (a dot b dot cancel(x)) / -/// cancel(x) $ -/// ``` -#[elem(LayoutMath)] -pub struct CancelElem { - /// The content over which the line should be placed. - #[required] - pub body: Content, - - /// The length of the line, relative to the length of the diagonal spanning - /// the whole element being "cancelled". A value of `{100%}` would then have - /// the line span precisely the element's diagonal. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ a + cancel(x, length: #200%) - /// - cancel(x, length: #200%) $ - /// ``` - #[default(Rel::new(Ratio::one(), Abs::pt(3.0).into()))] - pub length: Rel, - - /// Whether the cancel line should be inverted (flipped along the y-axis). - /// For the default angle setting, inverted means the cancel line - /// points to the top left instead of top right. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ (a cancel((b + c), inverted: #true)) / - /// cancel(b + c, inverted: #true) $ - /// ``` - #[default(false)] - pub inverted: bool, - - /// Whether two opposing cancel lines should be drawn, forming a cross over - /// the element. Overrides `inverted`. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi, cross: #true) $ - /// ``` - #[default(false)] - pub cross: bool, - - /// How much to rotate the cancel line. - /// - /// - If given an angle, the line is rotated by that angle clockwise with - /// respect to the y-axis. - /// - If `{auto}`, the line assumes the default angle; that is, along the - /// rising diagonal of the content box. - /// - If given a function `angle => angle`, the line is rotated, with - /// respect to the y-axis, by the angle returned by that function. The - /// function receives the default angle as its input. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel(Pi) - /// cancel(Pi, angle: #0deg) - /// cancel(Pi, angle: #45deg) - /// cancel(Pi, angle: #90deg) - /// cancel(1/(1+x), angle: #(a => a + 45deg)) - /// cancel(1/(1+x), angle: #(a => a + 90deg)) $ - /// ``` - pub angle: Smart, - - /// How to [stroke]($stroke) the cancel line. - /// - /// ```example - /// >>> #set page(width: 140pt) - /// $ cancel( - /// sum x, - /// stroke: #( - /// paint: red, - /// thickness: 1.5pt, - /// dash: "dashed", - /// ), - /// ) $ - /// ``` - #[resolve] - #[fold] - #[default(Stroke { - // Default stroke has 0.5pt for better visuals. - thickness: Smart::Custom(Abs::pt(0.5).into()), - ..Default::default() - })] - pub stroke: Stroke, -} - -impl LayoutMath for Packed { - #[typst_macros::time(name = "math.cancel", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - let body = ctx.layout_into_fragment(self.body(), styles)?; - // Preserve properties of body. - let body_class = body.class(); - let body_italics = body.italics_correction(); - let body_attach = body.accent_attach(); - let body_text_like = body.is_text_like(); - - let mut body = body.into_frame(); - let body_size = body.size(); - let span = self.span(); - let length = self.length(styles).at(scaled_font_size(ctx, styles)); - - let stroke = self.stroke(styles).unwrap_or(FixedStroke { - paint: TextElem::fill_in(styles).as_decoration(), - ..Default::default() - }); - - let invert = self.inverted(styles); - let cross = self.cross(styles); - let angle = self.angle(styles); - - let invert_first_line = !cross && invert; - let first_line = draw_cancel_line( - ctx, - length, - stroke.clone(), - invert_first_line, - &angle, - body_size, - styles, - span, - )?; - - // The origin of our line is the very middle of the element. - let center = body_size.to_point() / 2.0; - body.push_frame(center, first_line); - - if cross { - // Draw the second line. - let second_line = draw_cancel_line( - ctx, length, stroke, true, &angle, body_size, styles, span, - )?; - - body.push_frame(center, second_line); - } - - ctx.push( - FrameFragment::new(ctx, styles, body) - .with_class(body_class) - .with_italics_correction(body_italics) - .with_accent_attach(body_attach) - .with_text_like(body_text_like), - ); - - Ok(()) - } -} - -/// Defines the cancel line. -#[derive(Debug, Clone, PartialEq, Hash)] -pub enum CancelAngle { - Angle(Angle), - Func(Func), -} - -cast! { - CancelAngle, - self => match self { - Self::Angle(v) => v.into_value(), - Self::Func(v) => v.into_value() - }, - v: Angle => CancelAngle::Angle(v), - v: Func => CancelAngle::Func(v), -} - -/// Draws a cancel line. -#[allow(clippy::too_many_arguments)] -fn draw_cancel_line( - ctx: &mut MathContext, - length_scale: Rel, - stroke: FixedStroke, - invert: bool, - angle: &Smart, - body_size: Size, - styles: StyleChain, - span: Span, -) -> SourceResult { - let default = default_angle(body_size); - let mut angle = match angle { - // Non specified angle defaults to the diagonal - Smart::Auto => default, - Smart::Custom(angle) => match angle { - // This specifies the absolute angle w.r.t y-axis clockwise. - CancelAngle::Angle(v) => *v, - // This specifies a function that takes the default angle as input. - CancelAngle::Func(func) => func - .call(ctx.engine, Context::new(None, Some(styles)).track(), [default])? - .cast() - .at(span)?, - }, - }; - - // invert means flipping along the y-axis - if invert { - angle *= -1.0; - } - - // same as above, the default length is the diagonal of the body box. - let default_length = body_size.to_point().hypot(); - let length = length_scale.relative_to(default_length); - - // Draw a vertical line of length and rotate it by angle - let start = Point::new(Abs::zero(), length / 2.0); - let delta = Point::new(Abs::zero(), -length); - - let mut frame = Frame::soft(body_size); - frame.push(start, FrameItem::Shape(Geometry::Line(delta).stroked(stroke), span)); - - // Having the middle of the line at the origin is convenient here. - frame.transform(Transform::rotate(angle)); - Ok(frame) -} - -/// The default line angle for a body of the given size. -fn default_angle(body: Size) -> Angle { - // The default cancel line is the diagonal. - // We infer the default angle from - // the diagonal w.r.t to the body box. - // - // The returned angle is in the range of [0, Pi/2] - // - // Note that the angle is computed w.r.t to the y-axis - // - // B - // /| - // diagonal / | height - // / | - // / | - // O ---- - // width - let (width, height) = (body.x, body.y); - let default_angle = (width / height).atan(); // arctangent (in the range [0, Pi/2]) - Angle::rad(default_angle) -} diff --git a/crates/typst/src/math/ctx.rs b/crates/typst/src/math/ctx.rs deleted file mode 100644 index 35eb665c6b..0000000000 --- a/crates/typst/src/math/ctx.rs +++ /dev/null @@ -1,469 +0,0 @@ -use std::f64::consts::SQRT_2; - -use ecow::{eco_vec, EcoString}; -use rustybuzz::Feature; -use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; -use ttf_parser::math::MathValue; -use ttf_parser::opentype_layout::LayoutTable; -use ttf_parser::GlyphId; -use unicode_math_class::MathClass; -use unicode_segmentation::UnicodeSegmentation; - -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{Content, Packed, StyleChain, StyleVec}; -use crate::introspection::{SplitLocator, TagElem}; -use crate::layout::{ - layout_frame, Abs, Axes, BoxElem, Em, Frame, HElem, PlaceElem, Region, Size, Spacing, -}; -use crate::math::{ - scaled_font_size, styled_char, EquationElem, FrameFragment, GlyphFragment, - LayoutMath, MathFragment, MathRun, MathSize, THICK, -}; -use crate::realize::{realize, Arenas, RealizationKind}; -use crate::syntax::{is_newline, Span}; -use crate::text::{ - features, BottomEdge, BottomEdgeMetric, Font, LinebreakElem, SpaceElem, TextElem, - TextSize, TopEdge, TopEdgeMetric, -}; - -macro_rules! scaled { - ($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { - match $crate::math::EquationElem::size_in($styles) { - $crate::math::MathSize::Display => scaled!($ctx, $styles, $display), - _ => scaled!($ctx, $styles, $text), - } - }; - ($ctx:expr, $styles:expr, $name:ident) => { - $ctx.constants - .$name() - .scaled($ctx, $crate::math::scaled_font_size($ctx, $styles)) - }; -} - -macro_rules! percent { - ($ctx:expr, $name:ident) => { - $ctx.constants.$name() as f64 / 100.0 - }; -} - -/// The context for math layout. -pub struct MathContext<'a, 'v, 'e> { - // External. - pub engine: &'v mut Engine<'e>, - pub locator: &'v mut SplitLocator<'a>, - pub region: Region, - // Font-related. - pub font: &'a Font, - pub ttf: &'a ttf_parser::Face<'a>, - pub table: ttf_parser::math::Table<'a>, - pub constants: ttf_parser::math::Constants<'a>, - pub ssty_table: Option>, - pub glyphwise_tables: Option>>, - pub space_width: Em, - // Mutable. - pub fragments: Vec, -} - -impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { - /// Create a new math context. - pub fn new( - engine: &'v mut Engine<'e>, - locator: &'v mut SplitLocator<'a>, - styles: StyleChain<'a>, - base: Size, - font: &'a Font, - ) -> Self { - let math_table = font.ttf().tables().math.unwrap(); - let gsub_table = font.ttf().tables().gsub; - let constants = math_table.constants.unwrap(); - - let ssty_table = gsub_table - .and_then(|gsub| { - gsub.features - .find(ttf_parser::Tag::from_bytes(b"ssty")) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index)) - }) - .and_then(|ssty| ssty.subtables.get::(0)) - .and_then(|ssty| match ssty { - SubstitutionSubtable::Alternate(alt_glyphs) => Some(alt_glyphs), - _ => None, - }); - - let features = features(styles); - let glyphwise_tables = gsub_table.map(|gsub| { - features - .into_iter() - .filter_map(|feature| GlyphwiseSubsts::new(gsub, feature)) - .collect() - }); - - let ttf = font.ttf(); - let space_width = ttf - .glyph_index(' ') - .and_then(|id| ttf.glyph_hor_advance(id)) - .map(|advance| font.to_em(advance)) - .unwrap_or(THICK); - - Self { - engine, - locator, - region: Region::new(base, Axes::splat(false)), - font, - ttf: font.ttf(), - table: math_table, - constants, - ssty_table, - glyphwise_tables, - space_width, - fragments: vec![], - } - } - - /// Push a fragment. - pub fn push(&mut self, fragment: impl Into) { - self.fragments.push(fragment.into()); - } - - /// Push multiple fragments. - pub fn extend(&mut self, fragments: impl IntoIterator) { - self.fragments.extend(fragments); - } - - /// Layout the given element and return the result as a [`MathRun`]. - pub fn layout_into_run( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult { - Ok(MathRun::new(self.layout_into_fragments(elem, styles)?)) - } - - /// Layout the given element and return the resulting [`MathFragment`]s. - pub fn layout_into_fragments( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult> { - // The element's layout_math() changes the fragments held in this - // MathContext object, but for convenience this function shouldn't change - // them, so we restore the MathContext's fragments after obtaining the - // layout result. - let prev = std::mem::take(&mut self.fragments); - self.layout(elem, styles)?; - Ok(std::mem::replace(&mut self.fragments, prev)) - } - - /// Layout the given element and return the result as a - /// unified [`MathFragment`]. - pub fn layout_into_fragment( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult { - Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles)) - } - - /// Layout the given element and return the result as a [`Frame`]. - pub fn layout_into_frame( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult { - Ok(self.layout_into_fragment(elem, styles)?.into_frame()) - } -} - -impl MathContext<'_, '_, '_> { - /// Layout arbitrary content. - fn layout(&mut self, content: &Content, styles: StyleChain) -> SourceResult<()> { - let arenas = Arenas::default(); - let pairs = realize( - RealizationKind::Math, - self.engine, - self.locator, - &arenas, - content, - styles, - )?; - - let outer = styles; - for (elem, styles) in pairs { - // Hack because the font is fixed in math. - if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { - let frame = self.layout_external(elem, styles)?; - self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); - continue; - } - - self.layout_realized(elem, styles)?; - } - - Ok(()) - } - - /// Layout an element resulting from realization. - fn layout_realized( - &mut self, - elem: &Content, - styles: StyleChain, - ) -> SourceResult<()> { - if let Some(elem) = elem.to_packed::() { - self.push(MathFragment::Tag(elem.tag.clone())); - } else if elem.is::() { - let font_size = scaled_font_size(self, styles); - self.push(MathFragment::Space(self.space_width.at(font_size))); - } else if elem.is::() { - self.push(MathFragment::Linebreak); - } else if let Some(elem) = elem.to_packed::() { - if let Spacing::Rel(rel) = elem.amount() { - if rel.rel.is_zero() { - self.push(MathFragment::Spacing( - rel.abs.at(scaled_font_size(self, styles)), - elem.weak(styles), - )); - } - } - } else if let Some(elem) = elem.to_packed::() { - let fragment = self.layout_text(elem, styles)?; - self.push(fragment); - } else if let Some(boxed) = elem.to_packed::() { - let frame = self.layout_box(boxed, styles)?; - self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); - } else if let Some(elem) = elem.with::() { - elem.layout_math(self, styles)?; - } else { - let mut frame = self.layout_external(elem, styles)?; - if !frame.has_baseline() { - let axis = scaled!(self, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - } - self.push( - FrameFragment::new(self, styles, frame) - .with_spaced(true) - .with_ignorant(elem.is::()), - ); - } - - Ok(()) - } - - /// Layout a box into a frame. - fn layout_box( - &mut self, - boxed: &Packed, - styles: StyleChain, - ) -> SourceResult { - let local = - TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap(); - boxed.layout( - self.engine, - self.locator.next(&boxed.span()), - styles.chain(&local), - self.region.size, - ) - } - - /// Layout into a frame with normal layout. - fn layout_external( - &mut self, - content: &Content, - styles: StyleChain, - ) -> SourceResult { - let local = - TextElem::set_size(TextSize(scaled_font_size(self, styles).into())).wrap(); - layout_frame( - self.engine, - content, - self.locator.next(&content.span()), - styles.chain(&local), - self.region, - ) - } - - /// Layout the given [`TextElem`] into a [`MathFragment`]. - fn layout_text( - &mut self, - elem: &Packed, - styles: StyleChain, - ) -> SourceResult { - let text = elem.text(); - let span = elem.span(); - let mut chars = text.chars(); - let math_size = EquationElem::size_in(styles); - let fragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| styled_char(styles, c, true)) - .and_then(|c| GlyphFragment::try_new(self, styles, c, span)) - { - // A single letter that is available in the math font. - match math_size { - MathSize::Script => { - glyph.make_scriptsize(self); - } - MathSize::ScriptScript => { - glyph.make_scriptscriptsize(self); - } - _ => (), - } - - if glyph.class == MathClass::Large { - let mut variant = if math_size == MathSize::Display { - let height = scaled!(self, styles, display_operator_min_height) - .max(SQRT_2 * glyph.height()); - glyph.stretch_vertical(self, height, Abs::zero()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(self); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. - let mut fragments = vec![]; - for c in text.chars() { - let c = styled_char(styles, c, false); - fragments.push(GlyphFragment::new(self, styles, c, span).into()); - } - let frame = MathRun::new(fragments).into_frame(self, styles); - FrameFragment::new(self, styles, frame).with_text_like(true).into() - } else { - let local = [ - TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), - TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)), - TextElem::set_size(TextSize(scaled_font_size(self, styles).into())), - ] - .map(|p| p.wrap()); - - // Anything else is handled by Typst's standard text layout. - let styles = styles.chain(&local); - let 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(self.layout_complex_text(piece, span, styles)?.into()); - } - } - let mut frame = MathRun::new(fragments).into_frame(self, styles); - let axis = scaled!(self, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(self, styles, frame).into() - } else { - self.layout_complex_text(&text, span, styles)?.into() - } - }; - Ok(fragment) - } - - /// Layout the given text string into a [`FrameFragment`]. - fn layout_complex_text( - &mut self, - text: &str, - 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 = crate::layout::layout_inline( - self.engine, - &StyleVec::wrap(eco_vec![elem]), - self.locator.next(&span), - styles, - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); - - Ok(FrameFragment::new(self, styles, frame) - .with_class(MathClass::Alphabetic) - .with_text_like(true) - .with_spaced(spaced)) - } -} - -/// Converts some unit to an absolute length with the current font & font size. -pub(super) trait Scaled { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs; -} - -impl Scaled for i16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) - } -} - -impl Scaled for u16 { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - ctx.font.to_em(self).at(font_size) - } -} - -impl Scaled for MathValue<'_> { - fn scaled(self, ctx: &MathContext, font_size: Abs) -> Abs { - self.value.scaled(ctx, font_size) - } -} - -/// An OpenType substitution table that is applicable to glyph-wise substitutions. -pub enum GlyphwiseSubsts<'a> { - Single(SingleSubstitution<'a>), - Alternate(AlternateSubstitution<'a>, u32), -} - -impl<'a> GlyphwiseSubsts<'a> { - pub fn new(gsub: LayoutTable<'a>, feature: Feature) -> Option { - let table = gsub - .features - .find(ttf_parser::Tag(feature.tag.0)) - .and_then(|feature| feature.lookup_indices.get(0)) - .and_then(|index| gsub.lookups.get(index))?; - let table = table.subtables.get::(0)?; - match table { - SubstitutionSubtable::Single(single_glyphs) => { - Some(Self::Single(single_glyphs)) - } - SubstitutionSubtable::Alternate(alt_glyphs) => { - Some(Self::Alternate(alt_glyphs, feature.value)) - } - _ => None, - } - } - - pub fn try_apply(&self, glyph_id: GlyphId) -> Option { - match self { - Self::Single(single) => match single { - SingleSubstitution::Format1 { coverage, delta } => coverage - .get(glyph_id) - .map(|_| GlyphId(glyph_id.0.wrapping_add(*delta as u16))), - SingleSubstitution::Format2 { coverage, substitutes } => { - coverage.get(glyph_id).and_then(|idx| substitutes.get(idx)) - } - }, - Self::Alternate(alternate, value) => alternate - .coverage - .get(glyph_id) - .and_then(|idx| alternate.alternate_sets.get(idx)) - .and_then(|set| set.alternates.get(*value as u16)), - } - } - - pub fn apply(&self, glyph_id: GlyphId) -> GlyphId { - self.try_apply(glyph_id).unwrap_or(glyph_id) - } -} diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs deleted file mode 100644 index 19e3ce575e..0000000000 --- a/crates/typst/src/math/equation.rs +++ /dev/null @@ -1,576 +0,0 @@ -use std::num::NonZeroUsize; - -use unicode_math_class::MathClass; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, - Styles, Synthesize, -}; -use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Locator}; -use crate::layout::{ - layout_frame, Abs, AlignElem, Alignment, Axes, BlockElem, Em, FixedAlignment, - Fragment, Frame, InlineElem, InlineItem, OuterHAlignment, Point, Region, Regions, - Size, SpecificAlignment, VAlignment, -}; -use crate::math::{ - scaled_font_size, MathContext, MathRunFrameBuilder, MathSize, MathVariant, -}; -use crate::model::{Numbering, Outlinable, ParElem, ParLine, Refable, Supplement}; -use crate::syntax::Span; -use crate::text::{ - families, variant, Font, FontFamily, FontList, FontWeight, LocalName, TextEdgeBounds, - TextElem, -}; -use crate::utils::{NonZeroExt, Numeric}; -use crate::World; - -/// A mathematical equation. -/// -/// Can be displayed inline with text or as a separate block. -/// -/// # Example -/// ```example -/// #set text(font: "New Computer Modern") -/// -/// Let $a$, $b$, and $c$ be the side -/// lengths of right-angled triangle. -/// Then, we know that: -/// $ a^2 + b^2 = c^2 $ -/// -/// Prove by induction: -/// $ sum_(k=1)^n k = (n(n+1)) / 2 $ -/// ``` -/// -/// By default, block-level equations will not break across pages. This can be -/// changed through `{show math.equation: set block(breakable: true)}`. -/// -/// # Syntax -/// This function also has dedicated syntax: Write mathematical markup within -/// dollar signs to create an equation. Starting and ending the equation with at -/// least one space lifts it into a separate block that is centered -/// horizontally. For more details about math syntax, see the -/// [main math page]($category/math). -#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] -pub struct EquationElem { - /// Whether the equation is displayed as a separate block. - #[default(false)] - pub block: bool, - - /// How to [number]($numbering) block-level equations. - /// - /// ```example - /// #set math.equation(numbering: "(1)") - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - #[borrowed] - pub numbering: Option, - - /// The alignment of the equation numbering. - /// - /// By default, the alignment is `{end + horizon}`. For the horizontal - /// component, you can use `{right}`, `{left}`, or `{start}` and `{end}` - /// of the text direction; for the vertical component, you can use - /// `{top}`, `{horizon}`, or `{bottom}`. - /// - /// ```example - /// #set math.equation(numbering: "(1)", number-align: bottom) - /// - /// We can calculate: - /// $ E &= sqrt(m_0^2 + p^2) \ - /// &approx 125 "GeV" $ - /// ``` - #[default(SpecificAlignment::Both(OuterHAlignment::End, VAlignment::Horizon))] - pub number_align: SpecificAlignment, - - /// A supplement for the equation. - /// - /// For references to equations, this is added before the referenced number. - /// - /// If a function is specified, it is passed the referenced equation and - /// should return content. - /// - /// ```example - /// #set math.equation(numbering: "(1)", supplement: [Eq.]) - /// - /// We define: - /// $ phi.alt := (1 + sqrt(5)) / 2 $ - /// - /// With @ratio, we get: - /// $ F_n = floor(1 / sqrt(5) phi.alt^n) $ - /// ``` - pub supplement: Smart>, - - /// The contents of the equation. - #[required] - pub body: Content, - - /// The size of the glyphs. - #[internal] - #[default(MathSize::Text)] - #[ghost] - pub size: MathSize, - - /// The style variant to select. - #[internal] - #[ghost] - pub variant: MathVariant, - - /// Affects the height of exponents. - #[internal] - #[default(false)] - #[ghost] - pub cramped: bool, - - /// Whether to use bold glyphs. - #[internal] - #[default(false)] - #[ghost] - pub bold: bool, - - /// Whether to use italic glyphs. - #[internal] - #[ghost] - pub italic: Smart, - - /// A forced class to use for all fragment. - #[internal] - #[ghost] - pub class: Option, -} - -impl Synthesize for Packed { - fn synthesize( - &mut self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult<()> { - let supplement = match self.as_ref().supplement(styles) { - Smart::Auto => TextElem::packed(Self::local_name_in(styles)), - Smart::Custom(None) => Content::empty(), - Smart::Custom(Some(supplement)) => { - supplement.resolve(engine, styles, [self.clone().pack()])? - } - }; - - self.push_supplement(Smart::Custom(Some(Supplement::Content(supplement)))); - Ok(()) - } -} - -impl Show for Packed { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if self.block(styles) { - Ok(BlockElem::multi_layouter(self.clone(), layout_equation_block) - .pack() - .spanned(self.span())) - } else { - Ok(InlineElem::layouter(self.clone(), layout_equation_inline) - .pack() - .spanned(self.span())) - } - } -} - -impl ShowSet for Packed { - fn show_set(&self, styles: StyleChain) -> Styles { - let mut out = Styles::new(); - if self.block(styles) { - out.set(AlignElem::set_alignment(Alignment::CENTER)); - out.set(BlockElem::set_breakable(false)); - out.set(ParLine::set_numbering(None)); - out.set(EquationElem::set_size(MathSize::Display)); - } else { - out.set(EquationElem::set_size(MathSize::Text)); - } - out.set(TextElem::set_weight(FontWeight::from_number(450))); - out.set(TextElem::set_font(FontList(vec![FontFamily::new( - "New Computer Modern Math", - )]))); - out - } -} - -impl Count for Packed { - fn update(&self) -> Option { - (self.block(StyleChain::default()) && self.numbering().is_some()) - .then(|| CounterUpdate::Step(NonZeroUsize::ONE)) - } -} - -impl LocalName for Packed { - const KEY: &'static str = "equation"; -} - -impl Refable for Packed { - fn supplement(&self) -> Content { - // After synthesis, this should always be custom content. - match (**self).supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - } - } - - fn counter(&self) -> Counter { - Counter::of(EquationElem::elem()) - } - - fn numbering(&self) -> Option<&Numbering> { - (**self).numbering(StyleChain::default()).as_ref() - } -} - -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(), - }; - - if !supplement.is_empty() { - supplement += TextElem::packed("\u{a0}"); - } - - let numbers = self.counter().display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - Ok(Some(supplement + numbers)) - } -} - -/// Layout an inline equation (in a paragraph). -#[typst_macros::time(span = elem.span())] -fn layout_equation_inline( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, -) -> SourceResult> { - assert!(!elem.block(styles)); - - let font = find_math_font(engine, styles, elem.span())?; - - let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font); - let run = ctx.layout_into_run(&elem.body, styles)?; - - let mut items = if run.row_count() == 1 { - run.into_par_items() - } else { - vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())] - }; - - // An empty equation should have a height, so we still create a frame - // (which is then resized in the loop). - if items.is_empty() { - items.push(InlineItem::Frame(Frame::soft(Size::zero()))); - } - - for item in &mut items { - let InlineItem::Frame(frame) = item else { continue }; - - let font_size = scaled_font_size(&ctx, styles); - let slack = ParElem::leading_in(styles) * 0.7; - - let (t, b) = font.edges( - TextElem::top_edge_in(styles), - TextElem::bottom_edge_in(styles), - font_size, - TextEdgeBounds::Frame(frame), - ); - - let ascent = t.max(frame.ascent() - slack); - let descent = b.max(frame.descent() - slack); - frame.translate(Point::with_y(ascent - frame.baseline())); - frame.size_mut().y = ascent + descent; - } - - Ok(items) -} - -/// Layout a block-level equation (in a flow). -#[typst_macros::time(span = elem.span())] -fn layout_equation_block( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, -) -> SourceResult { - assert!(elem.block(styles)); - - let span = elem.span(); - let font = find_math_font(engine, styles, span)?; - - let mut locator = locator.split(); - let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font); - let full_equation_builder = ctx - .layout_into_run(&elem.body, styles)? - .multiline_frame_builder(&ctx, styles); - let width = full_equation_builder.size.x; - - let equation_builders = if BlockElem::breakable_in(styles) { - let mut rows = full_equation_builder.frames.into_iter().peekable(); - let mut equation_builders = vec![]; - let mut last_first_pos = Point::zero(); - let mut regions = regions; - - loop { - // Keep track of the position of the first row in this region, - // so that the offset can be reverted later. - let Some(&(_, first_pos)) = rows.peek() else { break }; - last_first_pos = first_pos; - - let mut frames = vec![]; - let mut height = Abs::zero(); - while let Some((sub, pos)) = rows.peek() { - let mut pos = *pos; - pos.y -= first_pos.y; - - // Finish this region if the line doesn't fit. Only do it if - // we placed at least one line _or_ we still have non-last - // regions. Crucially, we don't want to infinitely create - // new regions which are too small. - if !regions.size.y.fits(sub.height() + pos.y) - && (regions.may_progress() - || (regions.may_break() && !frames.is_empty())) - { - break; - } - - let (sub, _) = rows.next().unwrap(); - height = height.max(pos.y + sub.height()); - frames.push((sub, pos)); - } - - equation_builders - .push(MathRunFrameBuilder { frames, size: Size::new(width, height) }); - regions.next(); - } - - // Append remaining rows to the equation builder of the last region. - if let Some(equation_builder) = equation_builders.last_mut() { - equation_builder.frames.extend(rows.map(|(frame, mut pos)| { - pos.y -= last_first_pos.y; - (frame, pos) - })); - - let height = equation_builder - .frames - .iter() - .map(|(frame, pos)| frame.height() + pos.y) - .max() - .unwrap_or(equation_builder.size.y); - - equation_builder.size.y = height; - } - - // Ensure that there is at least one frame, even for empty equations. - if equation_builders.is_empty() { - equation_builders - .push(MathRunFrameBuilder { frames: vec![], size: Size::zero() }); - } - - equation_builders - } else { - vec![full_equation_builder] - }; - - let Some(numbering) = (**elem).numbering(styles) else { - let frames = equation_builders - .into_iter() - .map(MathRunFrameBuilder::build) - .collect(); - return Ok(Fragment::frames(frames)); - }; - - let pod = Region::new(regions.base(), Axes::splat(false)); - let counter = Counter::of(EquationElem::elem()) - .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? - .spanned(span); - let number = 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); - - let number_align = match elem.number_align(styles) { - SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon), - SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v), - SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v), - }; - - // Add equation numbers to each equation region. - let region_count = equation_builders.len(); - let frames = equation_builders - .into_iter() - .map(|builder| { - if builder.frames.is_empty() && region_count > 1 { - // Don't number empty regions, but do number empty equations. - return builder.build(); - } - add_equation_number( - builder, - number.clone(), - number_align.resolve(styles), - AlignElem::alignment_in(styles).resolve(styles).x, - regions.size.x, - full_number_width, - ) - }) - .collect(); - - Ok(Fragment::frames(frames)) -} - -fn find_math_font( - engine: &mut Engine<'_>, - styles: StyleChain, - span: Span, -) -> SourceResult { - let variant = variant(styles); - let world = engine.world; - let Some(font) = families(styles).find_map(|family| { - let id = world.book().select(family, variant)?; - let font = world.font(id)?; - let _ = font.ttf().tables().math?.constants?; - Some(font) - }) else { - bail!(span, "current font does not support math"); - }; - Ok(font) -} - -fn add_equation_number( - equation_builder: MathRunFrameBuilder, - number: Frame, - number_align: Axes, - equation_align: FixedAlignment, - region_size_x: Abs, - full_number_width: Abs, -) -> Frame { - let first = - equation_builder.frames.first().map_or( - (equation_builder.size, Point::zero(), Abs::zero()), - |(frame, pos)| (frame.size(), *pos, frame.baseline()), - ); - let last = - equation_builder.frames.last().map_or( - (equation_builder.size, Point::zero(), Abs::zero()), - |(frame, pos)| (frame.size(), *pos, frame.baseline()), - ); - let line_count = equation_builder.frames.len(); - let mut equation = equation_builder.build(); - - let width = if region_size_x.is_finite() { - region_size_x - } else { - equation.width() + 2.0 * full_number_width - }; - - let is_multiline = line_count >= 2; - let resizing_offset = resize_equation( - &mut equation, - &number, - number_align, - equation_align, - width, - is_multiline, - [first, last], - ); - equation.translate(Point::with_x(match (equation_align, number_align.x) { - (FixedAlignment::Start, FixedAlignment::Start) => full_number_width, - (FixedAlignment::End, FixedAlignment::End) => -full_number_width, - _ => Abs::zero(), - })); - - let x = match number_align.x { - FixedAlignment::Start => Abs::zero(), - FixedAlignment::End => equation.width() - number.width(), - _ => unreachable!(), - }; - let y = { - let align_baselines = |(_, pos, baseline): (_, Point, Abs), number: &Frame| { - resizing_offset.y + pos.y + baseline - number.baseline() - }; - match number_align.y { - FixedAlignment::Start => align_baselines(first, &number), - FixedAlignment::Center if !is_multiline => align_baselines(first, &number), - // In this case, the center lines (not baselines) of the number frame - // and the equation frame shall be aligned. - FixedAlignment::Center => (equation.height() - number.height()) / 2.0, - FixedAlignment::End => align_baselines(last, &number), - } - }; - - equation.push_frame(Point::new(x, y), number); - equation -} - -/// Resize the equation's frame accordingly so that it encompasses the number. -fn resize_equation( - equation: &mut Frame, - number: &Frame, - number_align: Axes, - equation_align: FixedAlignment, - width: Abs, - is_multiline: bool, - [first, last]: [(Axes, Point, Abs); 2], -) -> Point { - if matches!(number_align.y, FixedAlignment::Center if is_multiline) { - // In this case, the center lines (not baselines) of the number frame - // and the equation frame shall be aligned. - return equation.resize( - Size::new(width, equation.height().max(number.height())), - Axes::::new(equation_align, FixedAlignment::Center), - ); - } - - let excess_above = Abs::zero().max({ - if !is_multiline || matches!(number_align.y, FixedAlignment::Start) { - let (.., baseline) = first; - number.baseline() - baseline - } else { - Abs::zero() - } - }); - let excess_below = Abs::zero().max({ - if !is_multiline || matches!(number_align.y, FixedAlignment::End) { - let (size, .., baseline) = last; - (number.height() - number.baseline()) - (size.y - baseline) - } else { - Abs::zero() - } - }); - - // The vertical expansion is asymmetric on the top and bottom edges, so we - // first align at the top then translate the content downward later. - let resizing_offset = equation.resize( - Size::new(width, equation.height() + excess_above + excess_below), - Axes::::new(equation_align, FixedAlignment::Start), - ); - equation.translate(Point::with_y(excess_above)); - resizing_offset + Point::with_y(excess_above) -} diff --git a/crates/typst/src/math/frac.rs b/crates/typst/src/math/frac.rs deleted file mode 100644 index 744a15c5e8..0000000000 --- a/crates/typst/src/math/frac.rs +++ /dev/null @@ -1,179 +0,0 @@ -use crate::diag::{bail, SourceResult}; -use crate::foundations::{elem, Content, Packed, StyleChain, Value}; -use crate::layout::{Em, Frame, FrameItem, Point, Size}; -use crate::math::{ - scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment, - GlyphFragment, LayoutMath, MathContext, Scaled, DELIM_SHORT_FALL, -}; -use crate::syntax::{Span, Spanned}; -use crate::text::TextElem; -use crate::visualize::{FixedStroke, Geometry}; - -const FRAC_AROUND: Em = Em::new(0.1); - -/// A mathematical fraction. -/// -/// # Example -/// ```example -/// $ 1/2 < (x+1)/2 $ -/// $ ((x+1)) / 2 = frac(a, b) $ -/// ``` -/// -/// # Syntax -/// This function also has dedicated syntax: Use a slash to turn neighbouring -/// expressions into a fraction. Multiple atoms can be grouped into a single -/// expression using round grouping parenthesis. Such parentheses are removed -/// from the output, but you can nest multiple to force them. -#[elem(title = "Fraction", LayoutMath)] -pub struct FracElem { - /// The fraction's numerator. - #[required] - pub num: Content, - - /// The fraction's denominator. - #[required] - pub denom: Content, -} - -impl LayoutMath for Packed { - #[typst_macros::time(name = "math.frac", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout( - ctx, - styles, - self.num(), - std::slice::from_ref(self.denom()), - false, - self.span(), - ) - } -} - -/// A binomial expression. -/// -/// # Example -/// ```example -/// $ binom(n, k) $ -/// $ binom(n, k_1, k_2, k_3, ..., k_m) $ -/// ``` -#[elem(title = "Binomial", LayoutMath)] -pub struct BinomElem { - /// The binomial's upper index. - #[required] - pub upper: Content, - - /// The binomial's lower index. - #[required] - #[variadic] - #[parse( - let values = args.all::>()?; - if values.is_empty() { - // Prevents one element binomials - bail!(args.span, "missing argument: lower"); - } - values.into_iter().map(|spanned| spanned.v.display()).collect() - )] - pub lower: Vec, -} - -impl LayoutMath for Packed { - #[typst_macros::time(name = "math.binom", span = self.span())] - fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - layout(ctx, styles, self.upper(), self.lower(), true, self.span()) - } -} - -/// Layout a fraction or binomial. -fn layout( - ctx: &mut MathContext, - styles: StyleChain, - num: &Content, - denom: &[Content], - binom: bool, - span: Span, -) -> SourceResult<()> { - let font_size = scaled_font_size(ctx, styles); - let short_fall = DELIM_SHORT_FALL.at(font_size); - let axis = scaled!(ctx, styles, axis_height); - let thickness = scaled!(ctx, styles, fraction_rule_thickness); - let shift_up = scaled!( - ctx, styles, - text: fraction_numerator_shift_up, - display: fraction_numerator_display_style_shift_up, - ); - let shift_down = scaled!( - ctx, styles, - text: fraction_denominator_shift_down, - display: fraction_denominator_display_style_shift_down, - ); - let num_min = scaled!( - ctx, styles, - text: fraction_numerator_gap_min, - display: fraction_num_display_style_gap_min, - ); - let denom_min = scaled!( - ctx, styles, - text: fraction_denominator_gap_min, - display: fraction_denom_display_style_gap_min, - ); - - let num_style = style_for_numerator(styles); - let num = ctx.layout_into_frame(num, styles.chain(&num_style))?; - - let denom_style = style_for_denominator(styles); - 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), - ), - styles.chain(&denom_style), - )?; - - let around = FRAC_AROUND.at(font_size); - let num_gap = (shift_up - (axis + thickness / 2.0) - num.descent()).max(num_min); - let denom_gap = - (shift_down + (axis - thickness / 2.0) - denom.ascent()).max(denom_min); - - let line_width = num.width().max(denom.width()); - let width = line_width + 2.0 * around; - let height = num.height() + num_gap + thickness + denom_gap + denom.height(); - let size = Size::new(width, height); - let num_pos = Point::with_x((width - num.width()) / 2.0); - let line_pos = - Point::new((width - line_width) / 2.0, num.height() + num_gap + thickness / 2.0); - let denom_pos = Point::new((width - denom.width()) / 2.0, height - denom.height()); - let baseline = line_pos.y + axis; - - let mut frame = Frame::soft(size); - frame.set_baseline(baseline); - frame.push_frame(num_pos, num); - frame.push_frame(denom_pos, denom); - - if binom { - let mut left = GlyphFragment::new(ctx, styles, '(', span) - .stretch_vertical(ctx, height, short_fall); - left.center_on_axis(ctx); - ctx.push(left); - ctx.push(FrameFragment::new(ctx, styles, frame)); - let mut right = GlyphFragment::new(ctx, styles, ')', span) - .stretch_vertical(ctx, height, short_fall); - right.center_on_axis(ctx); - ctx.push(right); - } else { - frame.push( - line_pos, - FrameItem::Shape( - Geometry::Line(Point::with_x(line_width)).stroked( - FixedStroke::from_pair( - TextElem::fill_in(styles).as_decoration(), - thickness, - ), - ), - span, - ), - ); - ctx.push(FrameFragment::new(ctx, styles, frame)); - } - - Ok(()) -} diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs deleted file mode 100644 index b89a152431..0000000000 --- a/crates/typst/src/model/outline.rs +++ /dev/null @@ -1,547 +0,0 @@ -use std::num::NonZeroUsize; -use std::str::FromStr; - -use comemo::Track; - -use crate::diag::{bail, At, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, scope, select_where, Content, Context, Func, LocatableSelector, - NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, -}; -use crate::introspection::{Counter, CounterKey, Locatable}; -use crate::layout::{ - BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing, -}; -use crate::model::{ - Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, -}; -use crate::syntax::Span; -use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; -use crate::utils::NonZeroExt; - -/// 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. -/// -/// # Example -/// ```example -/// #outline() -/// -/// = Introduction -/// #lorem(5) -/// -/// = Prior work -/// #lorem(10) -/// ``` -/// -/// # Alternative outlines -/// 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). -/// -/// ```example -/// #outline( -/// title: [List of Figures], -/// target: figure.where(kind: image), -/// ) -/// -/// #figure( -/// image("tiger.jpg"), -/// caption: [A nice figure!], -/// ) -/// ``` -/// -/// # 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)] -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. - /// - When set to `{none}`, the outline will not have a title. - /// - A custom title can be set by passing content. - /// - /// The outline's heading will not be numbered by default, but you can - /// force it to be with a show-set rule: - /// `{show outline: set heading(numbering: "1.")}` - pub title: Smart>, - - /// 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)}`. - /// - /// ```example - /// #outline( - /// title: [List of Tables], - /// target: figure.where(kind: table), - /// ) - /// - /// #figure( - /// table( - /// columns: 4, - /// [t], [1], [2], [3], - /// [y], [0.3], [0.7], [0.5], - /// ), - /// caption: [Experiment results], - /// ) - /// ``` - #[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))] - #[borrowed] - pub target: LocatableSelector, - - /// The maximum level up to which elements are included in the outline. When - /// this argument is `{none}`, all elements are included. - /// - /// ```example - /// #set heading(numbering: "1.") - /// #outline(depth: 2) - /// - /// = Yes - /// Top-level section. - /// - /// == Still - /// Subsection. - /// - /// === Nope - /// Not included. - /// ``` - pub depth: Option, - - /// 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 - /// 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. - /// - /// *Migration hints:* Specifying `{true}` (equivalent to `{auto}`) or - /// `{false}` (equivalent to `{none}`) for this option is deprecated and - /// will be removed in a future release. - /// - /// ```example - /// #set heading(numbering: "1.a.") - /// - /// #outline( - /// title: [Contents (Automatic)], - /// indent: auto, - /// ) - /// - /// #outline( - /// title: [Contents (Length)], - /// indent: 2em, - /// ) - /// - /// #outline( - /// title: [Contents (Function)], - /// indent: n => [→ ] * n, - /// ) - /// - /// = About ACME Corp. - /// == History - /// === Origins - /// #lorem(10) - /// - /// == 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, -} - -#[scope] -impl OutlineElem { - #[elem] - type OutlineEntry; -} - -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()]; - // Build the outline title. - if let Some(title) = self.title(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) - }) { - seq.push( - HeadingElem::new(title) - .with_depth(NonZeroUsize::ONE) - .pack() - .spanned(self.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); - - for elem in &elems { - let Some(entry) = OutlineEntry::from_outlinable( - engine, - self.span(), - elem.clone(), - self.fill(styles), - styles, - )? - else { - continue; - }; - - let level = entry.level(); - if depth < *level { - continue; - } - - // 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() >= *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()); - 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 { - 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 - } -} - -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 { - Bool(bool), - Rel(Rel), - Func(Func), -} - -impl OutlineIndent { - fn apply( - indent: &Option>, - engine: &mut Engine, - ancestors: &Vec<&Content>, - seq: &mut Vec, - styles: StyleChain, - span: Span, - ) -> SourceResult<()> { - match indent { - // 'none' | 'false' => no indenting - None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {} - - // 'auto' | 'true' => use numbering alignment for indenting - Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => { - // 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()); - seq.push(SpaceElem::shared().clone()); - } - } - - // Length => indent with some fixed spacing per level - Some(Smart::Custom(OutlineIndent::Rel(length))) => { - seq.push( - HElem::new(Spacing::Rel(*length)).pack().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(()) - } -} - -cast! { - OutlineIndent, - self => match self { - Self::Bool(v) => v.into_value(), - Self::Rel(v) => v.into_value(), - Self::Func(v) => v.into_value() - }, - v: bool => OutlineIndent::Bool(v), - v: Rel => OutlineIndent::Rel(v), - v: Func => OutlineIndent::Func(v), -} - -struct LengthOrContent(Content); - -cast! { - LengthOrContent, - v: Rel => Self(HElem::new(Spacing::Rel(v)).pack()), - v: Content => Self(v), -} - -/// Represents each entry line in an outline, including the reference to the -/// outlined element, its page number, and the filler content between both. -/// -/// 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)] -pub struct OutlineEntry { - /// The nesting level of this outline entry. Starts at `{1}` for top-level - /// entries. - #[required] - pub level: NonZeroUsize, - - /// The element this entry refers to. Its location will be available - /// through the [`location`]($content.location) method on 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. - /// - /// 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] - 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))) - } -} - -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(); - - // 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()) - } - }; - - // 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 - // pipeline does tend to choke a bit on default ignorables (in - // particular the CJK-Latin spacing). - // - // See also: - // - https://github.com/typst/typst/issues/4476 - // - https://github.com/typst/typst/issues/5176 - let rtl = TextElem::dir_in(styles) == Dir::RTL; - if rtl { - // "Right-to-Left Embedding" - seq.push(TextElem::packed("\u{202B}")); - } - - seq.push(self.body().clone().linked(Destination::Location(location))); - - 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() { - seq.push(SpaceElem::shared().clone()); - seq.push( - BoxElem::new() - .with_body(Some(filler.clone())) - .with_width(Fr::one().into()) - .pack() - .spanned(self.span()), - ); - seq.push(SpaceElem::shared().clone()); - } else { - seq.push(HElem::new(Fr::one().into()).pack()); - } - - // Add the page number. - let page = self.page().clone().linked(Destination::Location(location)); - seq.push(page); - - Ok(Content::sequence(seq)) - } -} diff --git a/crates/typst/src/realize/behave.rs b/crates/typst/src/realize/behave.rs deleted file mode 100644 index bc90099488..0000000000 --- a/crates/typst/src/realize/behave.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Element interaction. - -use std::borrow::Cow; - -use crate::foundations::{ - Behave, Behaviour, Content, StyleChain, StyleVec, StyleVecBuilder, -}; - -/// A wrapper around a [`StyleVecBuilder`] that allows elements to interact. -#[derive(Debug)] -pub struct BehavedBuilder<'a> { - /// The internal builder. - builder: StyleVecBuilder<'a, Cow<'a, Content>>, - /// Staged weak and ignorant elements that we can't yet commit to the - /// builder. The option is `Some(_)` for weak elements and `None` for - /// ignorant elements. - staged: Vec<(Cow<'a, Content>, Behaviour, StyleChain<'a>)>, - /// What the last non-ignorant item was. - last: Behaviour, -} - -impl<'a> BehavedBuilder<'a> { - /// Create a new style-vec builder. - pub fn new() -> Self { - Self { - builder: StyleVecBuilder::new(), - staged: vec![], - last: Behaviour::Destructive, - } - } - - /// Whether the builder is totally empty. - pub fn is_empty(&self) -> bool { - self.builder.is_empty() && self.staged.is_empty() - } - - /// Whether the builder is empty except for some weak elements that will - /// probably collapse. - pub fn has_strong_elements(&self, last: bool) -> bool { - !self.builder.is_empty() - || self.staged.iter().any(|(_, behaviour, _)| { - !matches!(behaviour, Behaviour::Weak(_) | Behaviour::Invisible) - || (last && *behaviour == Behaviour::Invisible) - }) - } - - /// Push an item into the sequence. - pub fn push(&mut self, elem: Cow<'a, Content>, styles: StyleChain<'a>) { - let interaction = elem - .with::() - .map_or(Behaviour::Supportive, Behave::behaviour); - - match interaction { - Behaviour::Weak(level) => { - if matches!(self.last, Behaviour::Weak(_)) { - let item = elem.with::().unwrap(); - let i = self.staged.iter().position(|prev| { - let Behaviour::Weak(prev_level) = prev.1 else { return false }; - level < prev_level - || (level == prev_level && item.larger(prev, styles)) - }); - let Some(i) = i else { return }; - self.staged.remove(i); - } - - if self.last != Behaviour::Destructive { - self.staged.push((elem, interaction, styles)); - self.last = interaction; - } - } - Behaviour::Supportive => { - self.flush(true); - self.builder.push(elem, styles); - self.last = interaction; - } - Behaviour::Destructive => { - self.flush(false); - self.builder.push(elem, styles); - self.last = interaction; - } - Behaviour::Ignorant | Behaviour::Invisible => { - self.staged.push((elem, interaction, styles)); - } - } - } - - /// Return the finish style vec and the common prefix chain. - pub fn finish(mut self) -> (StyleVec>, StyleChain<'a>) { - self.flush(false); - self.builder.finish() - } - - /// Push the staged elements, filtering out weak elements if `supportive` is - /// false. - fn flush(&mut self, supportive: bool) { - for (item, interaction, styles) in self.staged.drain(..) { - if supportive - || interaction == Behaviour::Ignorant - || interaction == Behaviour::Invisible - { - self.builder.push(item, styles); - } - } - } -} - -impl<'a> Default for BehavedBuilder<'a> { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/typst/src/visualize/image/mod.rs b/crates/typst/src/visualize/image/mod.rs deleted file mode 100644 index 14af297838..0000000000 --- a/crates/typst/src/visualize/image/mod.rs +++ /dev/null @@ -1,490 +0,0 @@ -//! Image handling. - -mod raster; -mod svg; - -pub use self::raster::{RasterFormat, RasterImage}; -pub use self::svg::SvgImage; - -use std::ffi::OsStr; -use std::fmt::{self, Debug, Formatter}; -use std::sync::Arc; - -use comemo::Tracked; -use ecow::EcoString; - -use crate::diag::{bail, warning, At, SourceResult, StrResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart, - StyleChain, -}; -use crate::introspection::Locator; -use crate::layout::{ - Abs, Axes, BlockElem, FixedAlignment, Frame, FrameItem, Length, Point, Region, Rel, - Size, Sizing, -}; -use crate::loading::Readable; -use crate::model::Figurable; -use crate::syntax::{Span, Spanned}; -use crate::text::{families, LocalName}; -use crate::utils::LazyHash; -use crate::visualize::Path; -use crate::World; - -/// ラスターまたはベクター画像。 -/// -/// 画像を[`figure`]で囲むことで、番号とキャプションを与えることができます。 -/// -/// ほとんどの要素と同様に、画像はデフォルトでは _ブロックレベル_ であるため、隣接する段落に統合されることはありません。 -/// 画像を強制的にインラインにするには、[`box`]の中に入れてください。 -/// -/// # 例 -/// ```example -/// #figure( -/// image("molecular.jpg", width: 80%), -/// caption: [ -/// A step in the molecular testing -/// pipeline of our lab. -/// ], -/// ) -/// ``` -#[elem(scope, Show, LocalName, Figurable)] -pub struct ImageElem { - /// 画像ファイルのパス。 - /// - /// より詳細な情報は[パスの章]($syntax/#paths)を参照してください。 - #[required] - #[parse( - let Spanned { v: path, span } = - args.expect::>("path to image file")?; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - path - )] - #[borrowed] - pub path: EcoString, - - /// The raw file data. - #[internal] - #[required] - #[parse(Readable::Bytes(data))] - pub data: Readable, - - /// 画像のフォーマット。デフォルトでは自動的に検出されます。 - /// - /// サポートされている拡張子はPNG, JPEG, GIF, SVGです。 - /// [PDFの画像はまだサポートされていません。](https://github.com/typst/typst/issues/145) - pub format: Smart, - - /// 画像の幅。 - pub width: Smart>, - - /// 画像の高さ。 - pub height: Sizing, - - /// 画像の説明文。 - pub alt: Option, - - /// 与えられた領域に対して、画像をどのように調整するか。 - /// 領域は `width` や `height` フィールドで定義します。 - /// 領域の縦横比が画像の縦横比と同じであれば、`fit` で見た目が変わらないことに注意してください。 - /// - /// ```example - /// #set page(width: 300pt, height: 50pt, margin: 10pt) - /// #image("tiger.jpg", width: 100%, fit: "cover") - /// #image("tiger.jpg", width: 100%, fit: "contain") - /// #image("tiger.jpg", width: 100%, fit: "stretch") - /// ``` - #[default(ImageFit::Cover)] - pub fit: ImageFit, -} - -#[scope] -impl ImageElem { - /// バイトまたは文字列からラスターまたはベクトル図形をデコードします。 - /// - /// ```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")] - pub fn decode( - /// The call span of this function. - span: Span, - /// 画像としてデコードするデータ。SVGの場合は文字列です。 - data: Readable, - /// 画像のフォーマット。デフォルトでは自動的に検出されます。 - #[named] - format: Option>, - /// 画像の幅。 - #[named] - width: Option>>, - /// 画像の高さ。 - #[named] - height: Option, - /// 画像の説明文。 - #[named] - alt: Option>, - /// 与えられた領域に対して、画像をどのように調整するか。 - #[named] - fit: Option, - ) -> StrResult { - let mut elem = ImageElem::new(EcoString::new(), data); - if let Some(format) = format { - elem.push_format(format); - } - if let Some(width) = width { - elem.push_width(width); - } - if let Some(height) = height { - elem.push_height(height); - } - if let Some(alt) = alt { - elem.push_alt(alt); - } - if let Some(fit) = fit { - elem.push_fit(fit); - } - Ok(elem.pack().spanned(span)) - } -} - -impl Show for Packed { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_image) - .with_width(self.width(styles)) - .with_height(self.height(styles)) - .pack() - .spanned(self.span())) - } -} - -impl LocalName for Packed { - const KEY: &'static str = "figure"; -} - -impl Figurable for Packed {} - -/// Layout the image. -#[typst_macros::time(span = elem.span())] -fn layout_image( - elem: &Packed, - engine: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult { - let span = elem.span(); - - // Take the format that was explicitly defined, or parse the extension, - // or try to detect the format. - let data = elem.data(); - let format = match elem.format(styles) { - Smart::Custom(v) => v, - Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?, - }; - - // Warn the user if the image contains a foreign object. Not perfect - // because the svg could also be encoded, but that's an edge case. - if format == ImageFormat::Vector(VectorFormat::Svg) { - let has_foreign_object = - data.as_str().is_some_and(|s| s.contains(">(), - ) - .at(span)?; - - // Determine the image's pixel aspect ratio. - let pxw = image.width(); - let pxh = image.height(); - let px_ratio = pxw / pxh; - - // Determine the region's aspect ratio. - let region_ratio = region.size.x / region.size.y; - - // Find out whether the image is wider or taller than the region. - let wide = px_ratio > region_ratio; - - // The space into which the image will be placed according to its fit. - let target = if region.expand.x && region.expand.y { - // If both width and height are forced, take them. - region.size - } else if region.expand.x { - // If just width is forced, take it. - Size::new(region.size.x, region.size.y.min(region.size.x / px_ratio)) - } else if region.expand.y { - // If just height is forced, take it. - Size::new(region.size.x.min(region.size.y * px_ratio), region.size.y) - } else { - // If neither is forced, take the natural image size at the image's - // DPI bounded by the available space. - let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); - let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); - Size::new( - natural.x.min(region.size.x).min(region.size.y * px_ratio), - natural.y.min(region.size.y).min(region.size.x / px_ratio), - ) - }; - - // Compute the actual size of the fitted image. - let fit = elem.fit(styles); - let fitted = match fit { - ImageFit::Cover | ImageFit::Contain => { - if wide == (fit == ImageFit::Contain) { - Size::new(target.x, target.x / px_ratio) - } else { - Size::new(target.y * px_ratio, target.y) - } - } - ImageFit::Stretch => target, - }; - - // First, place the image in a frame of exactly its size and then resize - // the frame to the target size, center aligning the image in the - // process. - let mut frame = Frame::soft(fitted); - frame.push(Point::zero(), FrameItem::Image(image, fitted, span)); - frame.resize(target, Axes::splat(FixedAlignment::Center)); - - // Create a clipping group if only part of the image should be visible. - if fit == ImageFit::Cover && !target.fits(fitted) { - frame.clip(Path::rect(frame.size())); - } - - Ok(frame) -} - -/// Determine the image format based on path and data. -fn determine_format(path: &str, data: &Readable) -> StrResult { - let ext = std::path::Path::new(path) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - Ok(match ext.as_str() { - "png" => ImageFormat::Raster(RasterFormat::Png), - "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), - "gif" => ImageFormat::Raster(RasterFormat::Gif), - "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), - _ => match &data { - Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), - Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { - Some(f) => ImageFormat::Raster(f), - None => bail!("unknown image format"), - }, - }, - }) -} - -/// How an image should adjust itself to a given area, -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum ImageFit { - /// 領域を完全にカバーします。 - /// 水平または垂直方向にのみ画像をトリミングすることで、アスペクト比を保持します。 - /// これがデフォルトです。 - Cover, - /// 画像は領域内に完全に収まるようにします。 - /// アスペクト比を維持して、画像を切り取らず、1つの寸法は指定より狭くします。 - Contain, - /// たとえ画像が歪むことになっても、その領域を正確に埋めるように引き伸ばします。 - /// アスペクト比は保たれず、画像は切り取られません。 - Stretch, -} - -/// A loaded raster or vector image. -/// -/// Values of this type are cheap to clone and hash. -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Image(Arc>); - -/// The internal representation. -#[derive(Hash)] -struct Repr { - /// The raw, undecoded image data. - 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), -} - -impl Image { - /// When scaling an image to it's natural size, we default to this DPI - /// if the image doesn't contain DPI metadata. - pub const DEFAULT_DPI: f64 = 72.0; - - /// 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")] - pub fn new( - data: Bytes, - format: ImageFormat, - 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 })))) - } - - /// Create a possibly font-dependent image from a buffer and a format. - #[comemo::memoize] - #[typst_macros::time(name = "load image")] - pub fn with_fonts( - data: Bytes, - format: ImageFormat, - alt: Option, - world: Tracked, - families: &[&str], - ) -> 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, 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(), - } - } - - /// The format of the image. - pub fn format(&self) -> ImageFormat { - match &self.0.kind { - ImageKind::Raster(raster) => raster.format().into(), - ImageKind::Svg(_) => VectorFormat::Svg.into(), - } - } - - /// The width of the image in pixels. - pub fn width(&self) -> f64 { - match &self.0.kind { - ImageKind::Raster(raster) => raster.width() as f64, - ImageKind::Svg(svg) => svg.width(), - } - } - - /// The height of the image in pixels. - pub fn height(&self) -> f64 { - match &self.0.kind { - ImageKind::Raster(raster) => raster.height() as f64, - ImageKind::Svg(svg) => svg.height(), - } - } - - /// The image's pixel density in pixels per inch, if known. - pub fn dpi(&self) -> Option { - match &self.0.kind { - ImageKind::Raster(raster) => raster.dpi(), - ImageKind::Svg(_) => Some(Image::USVG_DEFAULT_DPI), - } - } - - /// A text describing the image. - pub fn alt(&self) -> Option<&str> { - self.0.alt.as_deref() - } - - /// The decoded image. - pub fn kind(&self) -> &ImageKind { - &self.0.kind - } -} - -impl Debug for Image { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("Image") - .field("format", &self.format()) - .field("width", &self.width()) - .field("height", &self.height()) - .field("alt", &self.alt()) - .finish() - } -} - -/// A raster or vector image format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub enum ImageFormat { - /// A raster graphics format. - Raster(RasterFormat), - /// A vector graphics format. - Vector(VectorFormat), -} - -/// A vector graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum VectorFormat { - /// Webサイトに用いられるベクターフォーマット。 - Svg, -} - -impl From for ImageFormat { - fn from(format: RasterFormat) -> Self { - Self::Raster(format) - } -} - -impl From for ImageFormat { - fn from(format: VectorFormat) -> Self { - Self::Vector(format) - } -} - -cast! { - ImageFormat, - self => match self { - Self::Raster(v) => v.into_value(), - Self::Vector(v) => v.into_value() - }, - v: RasterFormat => Self::Raster(v), - v: VectorFormat => Self::Vector(v), -} diff --git a/crates/typst/src/visualize/image/raster.rs b/crates/typst/src/visualize/image/raster.rs deleted file mode 100644 index 43c6d89094..0000000000 --- a/crates/typst/src/visualize/image/raster.rs +++ /dev/null @@ -1,287 +0,0 @@ -use std::cmp::Ordering; -use std::hash::{Hash, Hasher}; -use std::io; -use std::sync::Arc; - -use ecow::{eco_format, EcoString}; -use image::codecs::gif::GifDecoder; -use image::codecs::jpeg::JpegDecoder; -use image::codecs::png::PngDecoder; -use image::Limits; -use image::{guess_format, DynamicImage, ImageDecoder, ImageResult}; - -use crate::diag::{bail, StrResult}; -use crate::foundations::{Bytes, Cast}; - -/// A decoded raster image. -#[derive(Clone, Hash)] -pub struct RasterImage(Arc); - -/// The internal representation. -struct Repr { - data: Bytes, - format: RasterFormat, - dynamic: image::DynamicImage, - icc: Option>, - dpi: Option, -} - -impl RasterImage { - /// Decode a raster image. - #[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)) - } - - 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 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); - } - - // Extract pixel density. - let dpi = determine_dpi(&data, exif.as_ref()); - - Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi }))) - } - - /// The raw image data. - pub fn data(&self) -> &Bytes { - &self.0.data - } - - /// The image's format. - pub fn format(&self) -> RasterFormat { - self.0.format - } - - /// The image's pixel width. - pub fn width(&self) -> u32 { - self.dynamic().width() - } - - /// The image's pixel height. - pub fn height(&self) -> u32 { - self.dynamic().height() - } - - /// The image's pixel density in pixels per inch, if known. - pub fn dpi(&self) -> Option { - self.0.dpi - } - - /// Access the underlying dynamic image. - pub fn dynamic(&self) -> &image::DynamicImage { - &self.0.dynamic - } - - /// Access the ICC profile, if any. - pub fn icc(&self) -> Option<&[u8]> { - self.0.icc.as_deref() - } -} - -impl Hash for Repr { - fn hash(&self, state: &mut H) { - // The image is fully defined by data and format. - self.data.hash(state); - self.format.hash(state); - } -} - -/// A raster graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] -pub enum RasterFormat { - /// イラストや透明グラフィック用のラスターフォーマット。 - Png, - /// 写真に適した非可逆ラスターフォーマット。 - Jpg, - /// 短いアニメーションクリップによく使われるラスターフォーマット。 - Gif, -} - -impl RasterFormat { - /// 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 { - match format { - RasterFormat::Png => image::ImageFormat::Png, - RasterFormat::Jpg => image::ImageFormat::Jpeg, - RasterFormat::Gif => image::ImageFormat::Gif, - } - } -} - -impl TryFrom for RasterFormat { - 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."), - }) - } -} - -/// 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)? - .value - .get_uint(0) -} - -/// Apply an EXIF rotation to a dynamic image. -fn apply_rotation(image: &mut DynamicImage, rotation: u32) { - use image::imageops as ops; - match rotation { - 2 => ops::flip_horizontal_in_place(image), - 3 => ops::rotate180_in_place(image), - 4 => ops::flip_vertical_in_place(image), - 5 => { - ops::flip_horizontal_in_place(image); - *image = image.rotate270(); - } - 6 => *image = image.rotate90(), - 7 => { - ops::flip_horizontal_in_place(image); - *image = image.rotate90(); - } - 8 => *image = image.rotate270(), - _ => {} - } -} - -/// Try to determine the DPI (dots per inch) of the image. -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 - // DPI metadata. GIF does not have any. - exif.and_then(exif_dpi) - .or_else(|| jpeg_dpi(data)) - .or_else(|| png_dpi(data)) -} - -/// Try to get the DPI from the EXIF metadata. -fn exif_dpi(exif: &exif::Exif) -> Option { - let axis = |tag| { - let dpi = exif.get_field(tag, exif::In::PRIMARY)?; - let exif::Value::Rational(rational) = &dpi.value else { return None }; - Some(rational.first()?.to_f64()) - }; - - [axis(exif::Tag::XResolution), axis(exif::Tag::YResolution)] - .into_iter() - .flatten() - .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)) -} - -/// Tries to extract the DPI from raw JPEG data (by inspecting the JFIF APP0 -/// section). -fn jpeg_dpi(data: &[u8]) -> Option { - let validate_at = |index: usize, expect: &[u8]| -> Option<()> { - data.get(index..)?.starts_with(expect).then_some(()) - }; - let u16_at = |index: usize| -> Option { - data.get(index..index + 2)?.try_into().ok().map(u16::from_be_bytes) - }; - - validate_at(0, b"\xFF\xD8\xFF\xE0\0")?; - validate_at(6, b"JFIF\0")?; - validate_at(11, b"\x01")?; - - let len = u16_at(4)?; - if len < 16 { - return None; - } - - let units = *data.get(13)?; - let x = u16_at(14)?; - let y = u16_at(16)?; - let dpu = x.max(y) as f64; - - Some(match units { - 1 => dpu, // already inches - 2 => dpu * 2.54, // cm -> inches - _ => return None, - }) -} - -/// Tries to extract the DPI from raw PNG data. -fn png_dpi(mut data: &[u8]) -> Option { - let mut decoder = png::StreamingDecoder::new(); - let dims = loop { - let (consumed, event) = decoder.update(data, &mut Vec::new()).ok()?; - match event { - png::Decoded::PixelDimensions(dims) => break dims, - // Bail as soon as there is anything data-like. - png::Decoded::ChunkBegin(_, png::chunk::IDAT) - | png::Decoded::ImageData - | png::Decoded::ImageEnd => return None, - _ => {} - } - data = data.get(consumed..)?; - if consumed == 0 { - return None; - } - }; - - let dpu = dims.xppu.max(dims.yppu) as f64; - match dims.unit { - png::Unit::Meter => Some(dpu * 0.0254), // meter -> inches - png::Unit::Unspecified => None, - } -} - -/// Format the user-facing raster graphic decoding error message. -fn format_image_error(error: image::ImageError) -> EcoString { - match error { - image::ImageError::Limits(_) => "file is too large".into(), - err => eco_format!("failed to decode image ({err})"), - } -} - -#[cfg(test)] -mod tests { - use super::{RasterFormat, RasterImage}; - use crate::foundations::Bytes; - - #[test] - fn test_image_dpi() { - #[track_caller] - fn test(path: &str, format: RasterFormat, dpi: f64) { - let data = typst_dev_assets::get(path).unwrap(); - let bytes = Bytes::from_static(data); - let image = RasterImage::new(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); - } -} diff --git a/crates/typst/src/visualize/line.rs b/crates/typst/src/visualize/line.rs deleted file mode 100644 index e00bf22014..0000000000 --- a/crates/typst/src/visualize/line.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::introspection::Locator; -use crate::layout::{ - Abs, Angle, Axes, BlockElem, Frame, FrameItem, Length, Region, Rel, Size, -}; -use crate::utils::Numeric; -use crate::visualize::{Geometry, Stroke}; - -/// A line from one point to another. -/// -/// # Example -/// ```example -/// #set page(height: 100pt) -/// -/// #line(length: 100%) -/// #line(end: (50%, 50%)) -/// #line( -/// length: 4cm, -/// stroke: 2pt + maroon, -/// ) -/// ``` -#[elem(Show)] -pub struct LineElem { - /// The start point of the line. - /// - /// Must be an array of exactly two relative lengths. - #[resolve] - pub start: Axes>, - - /// The offset from `start` where the line ends. - #[resolve] - pub end: Option>>, - - /// The line's length. This is only respected if `end` is `{none}`. - #[resolve] - #[default(Abs::pt(30.0).into())] - pub length: Rel, - - /// The angle at which the line points away from the origin. This is only - /// respected if `end` is `{none}`. - pub angle: Angle, - - /// How to [stroke] the line. - /// - /// ```example - /// #set line(length: 100%) - /// #stack( - /// spacing: 1em, - /// line(stroke: 2pt + red), - /// line(stroke: (paint: blue, thickness: 4pt, cap: "round")), - /// line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")), - /// line(stroke: (paint: blue, thickness: 1pt, dash: ("dot", 2pt, 4pt, 2pt))), - /// ) - /// ``` - #[resolve] - #[fold] - pub stroke: Stroke, -} - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), layout_line) - .pack() - .spanned(self.span())) - } -} - -/// Layout the line. -#[typst_macros::time(span = elem.span())] -fn layout_line( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, -) -> SourceResult { - let resolve = |axes: Axes>| axes.zip_map(region.size, Rel::relative_to); - let start = resolve(elem.start(styles)); - let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| { - let length = elem.length(styles); - let angle = elem.angle(styles); - let x = angle.cos() * length; - let y = angle.sin() * length; - resolve(Axes::new(x, y)) - }); - - let stroke = elem.stroke(styles).unwrap_or_default(); - let size = start.max(start + delta).max(Size::zero()); - - if !size.is_finite() { - bail!(elem.span(), "cannot create line with infinite length"); - } - - let mut frame = Frame::soft(size); - let shape = Geometry::Line(delta.to_point()).stroked(stroke); - frame.push(start.to_point(), FrameItem::Shape(shape, elem.span())); - Ok(frame) -}