Merge branch 'main' into krilla-port

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	crates/typst-cli/src/args.rs
#	crates/typst-cli/src/compile.rs
#	crates/typst-library/src/visualize/image/raster.rs
#	crates/typst-pdf/src/catalog.rs
#	crates/typst-pdf/src/content.rs
#	crates/typst-pdf/src/image.rs
#	crates/typst-pdf/src/lib.rs
#	crates/typst-pdf/src/outline.rs
This commit is contained in:
Laurenz Stampfl 2025-03-10 08:31:45 +01:00
commit 38fba0a02c
491 changed files with 13074 additions and 6608 deletions

View File

@ -5,6 +5,7 @@ env:
RUSTFLAGS: "-Dwarnings" RUSTFLAGS: "-Dwarnings"
RUSTDOCFLAGS: "-Dwarnings" RUSTDOCFLAGS: "-Dwarnings"
TYPST_TESTS_EXTENDED: true TYPST_TESTS_EXTENDED: true
PKG_CONFIG_i686-unknown-linux-gnu: /usr/bin/i686-linux-gnu-pkgconf
jobs: jobs:
# This allows us to have one branch protection rule for the full test matrix. # This allows us to have one branch protection rule for the full test matrix.
@ -27,30 +28,43 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
bits: [64]
include:
- os: ubuntu-latest
bits: 32
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0 - if: startsWith(matrix.os, 'ubuntu-') && matrix.bits == 32
run: |
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386
- uses: dtolnay/rust-toolchain@1.85.0
with:
targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }}
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test --workspace --no-run with:
- run: cargo test --workspace --no-fail-fast key: ${{ matrix.bits }}
- run: cargo test --workspace --no-run ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }}
- run: cargo test --workspace --no-fail-fast ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }}
- name: Upload rendered test output - name: Upload rendered test output
if: failure() if: failure()
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: tests-rendered-${{ matrix.os }} name: tests-rendered-${{ matrix.os }}-${{ matrix.bits }}
path: tests/store/render/** path: tests/store/render/**
retention-days: 3 retention-days: 3
- name: Update test artifacts - name: Update test artifacts
if: failure() if: failure()
run: | run: |
cargo test --workspace --test tests -- --update cargo test --workspace --test tests ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} -- --update
echo 'updated_artifacts=1' >> "$GITHUB_ENV" echo 'updated_artifacts=1' >> "$GITHUB_ENV"
- name: Upload updated reference output (for use if the test changes are desired) - name: Upload updated reference output (for use if the test changes are desired)
if: failure() && env.updated_artifacts if: failure() && env.updated_artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: tests-updated-${{ matrix.os }} name: tests-updated-${{ matrix.os }}-${{ matrix.bits }}
path: tests/ref/** path: tests/ref/**
retention-days: 3 retention-days: 3
@ -59,7 +73,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0 - uses: dtolnay/rust-toolchain@1.85.0
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@ -73,7 +87,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.80.0 - uses: dtolnay/rust-toolchain@1.83.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo check --workspace - run: cargo check --workspace

View File

@ -44,7 +44,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0 - uses: dtolnay/rust-toolchain@1.85.0
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}

View File

@ -37,8 +37,8 @@ Below are some signs of a good PR:
- Adds/changes as little code and as few interfaces as possible. Should changes - Adds/changes as little code and as few interfaces as possible. Should changes
to larger-scale abstractions be necessary, these should be discussed to larger-scale abstractions be necessary, these should be discussed
throughout the implementation process. throughout the implementation process.
- Adds tests if appropriate (with reference images for visual tests). See the - Adds tests if appropriate (with reference output for visual/HTML tests). See
[testing] readme for more details. the [testing] readme for more details.
- Contains documentation comments on all new Rust types. - Contains documentation comments on all new Rust types.
- Comes with brief documentation for all new Typst definitions - Comes with brief documentation for all new Typst definitions
(elements/functions), ideally with a concise example that fits into ~5-10 (elements/functions), ideally with a concise example that fits into ~5-10

1083
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@ default-members = ["crates/typst-cli"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.12.0" version = "0.13.1"
rust-version = "1.80" # also change in ci.yml rust-version = "1.83" # also change in ci.yml
authors = ["The Typst Project Developers"] authors = ["The Typst Project Developers"]
edition = "2021" edition = "2021"
homepage = "https://typst.app" homepage = "https://typst.app"
@ -16,24 +16,24 @@ keywords = ["typst"]
readme = "README.md" readme = "README.md"
[workspace.dependencies] [workspace.dependencies]
typst = { path = "crates/typst", version = "0.12.0" } typst = { path = "crates/typst", version = "0.13.1" }
typst-cli = { path = "crates/typst-cli", version = "0.12.0" } typst-cli = { path = "crates/typst-cli", version = "0.13.1" }
typst-eval = { path = "crates/typst-eval", version = "0.12.0" } typst-eval = { path = "crates/typst-eval", version = "0.13.1" }
typst-html = { path = "crates/typst-html", version = "0.12.0" } typst-html = { path = "crates/typst-html", version = "0.13.1" }
typst-ide = { path = "crates/typst-ide", version = "0.12.0" } typst-ide = { path = "crates/typst-ide", version = "0.13.1" }
typst-kit = { path = "crates/typst-kit", version = "0.12.0" } typst-kit = { path = "crates/typst-kit", version = "0.13.1" }
typst-layout = { path = "crates/typst-layout", version = "0.12.0" } typst-layout = { path = "crates/typst-layout", version = "0.13.1" }
typst-library = { path = "crates/typst-library", version = "0.12.0" } typst-library = { path = "crates/typst-library", version = "0.13.1" }
typst-macros = { path = "crates/typst-macros", version = "0.12.0" } typst-macros = { path = "crates/typst-macros", version = "0.13.1" }
typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" } typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" }
typst-realize = { path = "crates/typst-realize", version = "0.12.0" } typst-realize = { path = "crates/typst-realize", version = "0.13.1" }
typst-render = { path = "crates/typst-render", version = "0.12.0" } typst-render = { path = "crates/typst-render", version = "0.13.1" }
typst-svg = { path = "crates/typst-svg", version = "0.12.0" } typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.12.0" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.12.0" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "9879589" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -47,18 +47,18 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1" clap_complete = "4.2.1"
clap_mangen = "0.2.10" clap_mangen = "0.2.10"
codespan-reporting = "0.11" codespan-reporting = "0.11"
codex = { git = "https://github.com/typst/codex", rev = "343a9b1" } codex = "0.1.1"
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"
ctrlc = "3.4.1" ctrlc = "3.4.1"
dirs = "5" dirs = "6"
ecow = { version = "0.2", features = ["serde"] } ecow = { version = "0.2", features = ["serde"] }
env_proxy = "0.4" env_proxy = "0.4"
flate2 = "1" flate2 = "1"
fontdb = { version = "0.22", default-features = false } fontdb = { version = "0.21", default-features = false }
fs_extra = "1.3" fs_extra = "1.3"
hayagriva = "0.8" hayagriva = "0.8.1"
heck = "0.5" heck = "0.5"
hypher = "0.1.4" hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] } icu_properties = { version = "1.4", features = ["serde"] }
@ -67,16 +67,16 @@ icu_provider_adapters = "1.4"
icu_provider_blob = "1.4" icu_provider_blob = "1.4"
icu_segmenter = { version = "1.4", features = ["serde"] } icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1" if_chain = "1"
image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] } image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
kamadak-exif = "0.5" kamadak-exif = "0.6"
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "ffdd1aa", features = ["comemo", "rayon", "svg"] }
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"
memchr = "2"
miniz_oxide = "0.8" miniz_oxide = "0.8"
native-tls = "0.2" native-tls = "0.2"
notify = "6" notify = "8"
once_cell = "1" once_cell = "1"
open = "5.0.1" open = "5.0.1"
openssl = "0.10" openssl = "0.10"
@ -84,7 +84,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
parking_lot = "0.12.1" parking_lot = "0.12.1"
pathdiff = "0.2" pathdiff = "0.2"
pdf-writer = "0.12" pdf-writer = "0.12.1"
phf = { version = "0.11", features = ["macros"] } phf = { version = "0.11", features = ["macros"] }
pixglyph = "0.5.1" pixglyph = "0.5.1"
png = "0.17" png = "0.17"
@ -96,7 +96,7 @@ quote = "1"
rayon = "1.7.0" rayon = "1.7.0"
regex = "1" regex = "1"
regex-syntax = "0.8" regex-syntax = "0.8"
resvg = { version = "0.44", default-features = false, features = ["raster-images"] } resvg = { version = "0.43", default-features = false, features = ["raster-images"] }
roxmltree = "0.20" roxmltree = "0.20"
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] } rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
rustybuzz = "0.18" rustybuzz = "0.18"
@ -123,21 +123,23 @@ tiny_http = "0.12"
tiny-skia = "0.11" tiny-skia = "0.11"
toml = { version = "0.8", default-features = false, features = ["parse", "display"] } toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
ttf-parser = "0.24.1" ttf-parser = "0.24.1"
two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] } two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] }
typed-arena = "2" typed-arena = "2"
unicode-bidi = "0.3.13" unicode-bidi = "0.3.18"
unicode-ident = "1.0" unicode-ident = "1.0"
unicode-math-class = "0.1" unicode-math-class = "0.1"
unicode-script = "0.5" unicode-script = "0.5"
unicode-normalization = "0.1.24"
unicode-segmentation = "1" unicode-segmentation = "1"
unscanny = "0.1" unscanny = "0.1"
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
usvg = { version = "0.44", default-features = false, features = ["text"] } usvg = { version = "0.43", default-features = false, features = ["text"] }
walkdir = "2" walkdir = "2"
wasmi = "0.39.0" wasmi = "0.40.0"
web-sys = "0.3"
xmlparser = "0.13.5" xmlparser = "0.13.5"
xmlwriter = "0.1.0" xmlwriter = "0.1.0"
xmp-writer = "0.3" xmp-writer = "0.3.1"
xz2 = { version = "0.1", features = ["static"] } xz2 = { version = "0.1", features = ["static"] }
yaml-front-matter = "0.1" yaml-front-matter = "0.1"
zip = { version = "2", default-features = false, features = ["deflate"] } zip = { version = "2", default-features = false, features = ["deflate"] }

View File

@ -5,19 +5,19 @@
<p align="center"> <p align="center">
<a href="https://typst.app/docs/"> <a href="https://typst.app/docs/">
<img alt="Documentation" src="https://img.shields.io/website?down_message=offline&label=docs&up_color=007aff&up_message=online&url=https%3A%2F%2Ftypst.app%2Fdocs" <img alt="Documentation" src="https://img.shields.io/website?down_message=offline&label=docs&up_color=007aff&up_message=online&url=https%3A%2F%2Ftypst.app%2Fdocs"
/></a> ></a>
<a href="https://typst.app/"> <a href="https://typst.app/">
<img alt="Typst App" src="https://img.shields.io/website?down_message=offline&label=typst.app&up_color=239dad&up_message=online&url=https%3A%2F%2Ftypst.app" <img alt="Typst App" src="https://img.shields.io/website?down_message=offline&label=typst.app&up_color=239dad&up_message=online&url=https%3A%2F%2Ftypst.app"
/></a> ></a>
<a href="https://discord.gg/2uDybryKPe"> <a href="https://discord.gg/2uDybryKPe">
<img alt="Discord Server" src="https://img.shields.io/discord/1054443721975922748?color=5865F2&label=discord&labelColor=555" <img alt="Discord Server" src="https://img.shields.io/discord/1054443721975922748?color=5865F2&label=discord&labelColor=555"
/></a> ></a>
<a href="https://github.com/typst/typst/blob/main/LICENSE"> <a href="https://github.com/typst/typst/blob/main/LICENSE">
<img alt="Apache-2 License" src="https://img.shields.io/badge/license-Apache%202-brightgreen" <img alt="Apache-2 License" src="https://img.shields.io/badge/license-Apache%202-brightgreen"
/></a> ></a>
<a href="https://typst.app/jobs/"> <a href="https://typst.app/jobs/">
<img alt="Jobs at Typst" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ftypst.app%2Fassets%2Fdata%2Fshields.json&query=%24.jobs.text&label=jobs&color=%23A561FF&cacheSeconds=1800" <img alt="Jobs at Typst" src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Ftypst.app%2Fassets%2Fdata%2Fshields.json&query=%24.jobs.text&label=jobs&color=%23A561FF&cacheSeconds=1800"
/></a> ></a>
</p> </p>
Typst is a new markup-based typesetting system that is designed to be as powerful Typst is a new markup-based typesetting system that is designed to be as powerful
@ -39,7 +39,7 @@ A [gentle introduction][tutorial] to Typst is available in our documentation.
However, if you want to see the power of Typst encapsulated in one image, here However, if you want to see the power of Typst encapsulated in one image, here
it is: it is:
<p align="center"> <p align="center">
<img alt="Example" width="900" src="https://user-images.githubusercontent.com/17899797/228031796-ced0e452-fcee-4ae9-92da-b9287764ff25.png"/> <img alt="Example" width="900" src="https://user-images.githubusercontent.com/17899797/228031796-ced0e452-fcee-4ae9-92da-b9287764ff25.png">
</p> </p>
@ -113,7 +113,9 @@ Typst's CLI is available from different sources:
- You can install Typst through different package managers. Note that the - You can install Typst through different package managers. Note that the
versions in the package managers might lag behind the latest release. versions in the package managers might lag behind the latest release.
- Linux: View [Typst on Repology][repology] - Linux:
- View [Typst on Repology][repology]
- View [Typst's Snap][snap]
- macOS: `brew install typst` - macOS: `brew install typst`
- Windows: `winget install --id Typst.Typst` - Windows: `winget install --id Typst.Typst`
@ -254,3 +256,4 @@ instant preview. To achieve these goals, we follow three core design principles:
[contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md [contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md
[packages]: https://github.com/typst/packages/ [packages]: https://github.com/typst/packages/
[`comemo`]: https://github.com/typst/comemo/ [`comemo`]: https://github.com/typst/comemo/
[snap]: https://snapcraft.io/typst

View File

@ -241,10 +241,6 @@ pub struct CompileArgs {
#[arg(long = "pages", value_delimiter = ',')] #[arg(long = "pages", value_delimiter = ',')]
pub pages: Option<Vec<Pages>>, pub pages: Option<Vec<Pages>>,
/// The version of the produced PDF.
#[arg(long = "pdf-version")]
pub pdf_version: Option<PdfVersion>,
/// One (or multiple comma-separated) PDF standards that Typst will enforce /// One (or multiple comma-separated) PDF standards that Typst will enforce
/// conformance with. /// conformance with.
#[arg(long = "pdf-standard", value_delimiter = ',')] #[arg(long = "pdf-standard", value_delimiter = ',')]
@ -467,45 +463,19 @@ pub enum Feature {
display_possible_values!(Feature); display_possible_values!(Feature);
/// A PDF version.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
#[allow(non_camel_case_types)]
pub enum PdfVersion {
/// PDF 1.4.
#[value(name = "1.4")]
V_1_4,
/// PDF 1.5.
#[value(name = "1.5")]
V_1_5,
/// PDF 1.5.
#[value(name = "1.6")]
V_1_6,
/// PDF 1.7.
#[value(name = "1.7")]
V_1_7,
}
display_possible_values!(PdfVersion);
/// A PDF standard that Typst can enforce conformance with. /// A PDF standard that Typst can enforce conformance with.
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum PdfStandard { pub enum PdfStandard {
/// PDF/A-1b. /// PDF 1.7.
#[value(name = "a-1b")] #[value(name = "1.7")]
A_1b, V_1_7,
/// PDF/A-2b. /// PDF/A-2b.
#[value(name = "a-2b")] #[value(name = "a-2b")]
A_2b, A_2b,
/// PDF/A-2u. /// PDF/A-3b.
#[value(name = "a-2u")]
A_2u,
/// PDF/A-3u.
#[value(name = "a-3b")] #[value(name = "a-3b")]
A_3b, A_3b,
/// PDF/A-2b.
#[value(name = "a-3u")]
A_3u,
} }
display_possible_values!(PdfStandard); display_possible_values!(PdfStandard);

View File

@ -6,8 +6,9 @@ use std::path::{Path, PathBuf};
use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono::{DateTime, Datelike, Timelike, Utc};
use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term; use codespan_reporting::term;
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use parking_lot::RwLock; use parking_lot::RwLock;
use pathdiff::diff_paths;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
@ -17,11 +18,11 @@ use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Source, Span}; use typst::syntax::{FileId, Source, Span};
use typst::WorldExt; use typst::WorldExt;
use typst_pdf::{PdfOptions, Timestamp, Validator}; use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
use crate::args::{ use crate::args::{
CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
PdfStandard, PdfVersion, WatchCommand, PdfStandard, WatchCommand,
}; };
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
use crate::server::HtmlServer; use crate::server::HtmlServer;
@ -62,10 +63,9 @@ pub struct CompileConfig {
/// Opens the output file with the default viewer or a specific program after /// Opens the output file with the default viewer or a specific program after
/// compilation. /// compilation.
pub open: Option<Option<String>>, pub open: Option<Option<String>>,
/// The version that should be used to export the PDF. /// One (or multiple comma-separated) PDF standards that Typst will enforce
pub pdf_version: Option<PdfVersion>, /// conformance with.
/// A list of standards the PDF should conform to. pub pdf_standards: PdfStandards,
pub pdf_standard: Vec<PdfStandard>,
/// A path to write a Makefile rule describing the current compilation. /// A path to write a Makefile rule describing the current compilation.
pub make_deps: Option<PathBuf>, pub make_deps: Option<PathBuf>,
/// The PPI (pixels per inch) to use for PNG export. /// The PPI (pixels per inch) to use for PNG export.
@ -130,6 +130,19 @@ impl CompileConfig {
PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
}); });
let pdf_standards = {
let list = args
.pdf_standard
.iter()
.map(|standard| match standard {
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
})
.collect::<Vec<_>>();
PdfStandards::new(&list)?
};
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
let server = match watch { let server = match watch {
Some(command) Some(command)
@ -146,16 +159,15 @@ impl CompileConfig {
output, output,
output_format, output_format,
pages, pages,
pdf_standards,
creation_timestamp: args.world.creation_timestamp, creation_timestamp: args.world.creation_timestamp,
make_deps: args.make_deps.clone(), make_deps: args.make_deps.clone(),
ppi: args.ppi, ppi: args.ppi,
diagnostic_format: args.process.diagnostic_format, diagnostic_format: args.process.diagnostic_format,
open: args.open.clone(), open: args.open.clone(),
pdf_version: args.pdf_version,
export_cache: ExportCache::new(), export_cache: ExportCache::new(),
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
server, server,
pdf_standard: args.pdf_standard.clone(),
}) })
} }
} }
@ -177,7 +189,7 @@ pub fn compile_once(
match output { match output {
// Export the PDF / PNG. // Export the PDF / PNG.
Ok(()) => { Ok(outputs) => {
let duration = start.elapsed(); let duration = start.elapsed();
if config.watching { if config.watching {
@ -191,7 +203,7 @@ pub fn compile_once(
print_diagnostics(world, &[], &warnings, config.diagnostic_format) print_diagnostics(world, &[], &warnings, config.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
write_make_deps(world, config)?; write_make_deps(world, config, outputs)?;
open_output(config)?; open_output(config)?;
} }
@ -215,12 +227,15 @@ pub fn compile_once(
fn compile_and_export( fn compile_and_export(
world: &mut SystemWorld, world: &mut SystemWorld,
config: &mut CompileConfig, config: &mut CompileConfig,
) -> Warned<SourceResult<()>> { ) -> Warned<SourceResult<Vec<Output>>> {
match config.output_format { match config.output_format {
OutputFormat::Html => { OutputFormat::Html => {
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world); let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
let result = output.and_then(|document| export_html(&document, config)); let result = output.and_then(|document| export_html(&document, config));
Warned { output: result, warnings } Warned {
output: result.map(|()| vec![config.output.clone()]),
warnings,
}
} }
_ => { _ => {
let Warned { output, warnings } = typst::compile::<PagedDocument>(world); let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
@ -246,9 +261,14 @@ fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<
} }
/// Export to a paged target format. /// Export to a paged target format.
fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> { fn export_paged(
document: &PagedDocument,
config: &CompileConfig,
) -> SourceResult<Vec<Output>> {
match config.output_format { match config.output_format {
OutputFormat::Pdf => export_pdf(document, config), OutputFormat::Pdf => {
export_pdf(document, config).map(|()| vec![config.output.clone()])
}
OutputFormat::Png => { OutputFormat::Png => {
export_image(document, config, ImageExportFormat::Png).at(Span::detached()) export_image(document, config, ImageExportFormat::Png).at(Span::detached())
} }
@ -275,37 +295,11 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<
}) })
} }
}; };
let validator = match config.pdf_standard.first() {
None => Validator::None,
Some(s) => {
if config.pdf_standard.len() > 1 {
bail!(Span::detached(), "cannot export using more than one PDF standard";
hint: "typst currently only supports export using \
one standard at the same time");
} else {
match s {
PdfStandard::A_1b => Validator::A1_B,
PdfStandard::A_2b => Validator::A2_B,
PdfStandard::A_2u => Validator::A2_U,
PdfStandard::A_3b => Validator::A3_B,
PdfStandard::A_3u => Validator::A3_U,
}
}
}
};
let options = PdfOptions { let options = PdfOptions {
ident: Smart::Auto, ident: Smart::Auto,
timestamp, timestamp,
page_ranges: config.pages.clone(), page_ranges: config.pages.clone(),
pdf_version: config.pdf_version.map(|v| match v { standards: config.pdf_standards.clone(),
PdfVersion::V_1_4 => typst_pdf::PdfVersion::Pdf14,
PdfVersion::V_1_5 => typst_pdf::PdfVersion::Pdf15,
PdfVersion::V_1_6 => typst_pdf::PdfVersion::Pdf16,
PdfVersion::V_1_7 => typst_pdf::PdfVersion::Pdf17,
}),
validator,
}; };
let buffer = typst_pdf::pdf(document, &options)?; let buffer = typst_pdf::pdf(document, &options)?;
config config
@ -342,7 +336,7 @@ fn export_image(
document: &PagedDocument, document: &PagedDocument,
config: &CompileConfig, config: &CompileConfig,
fmt: ImageExportFormat, fmt: ImageExportFormat,
) -> StrResult<()> { ) -> StrResult<Vec<Output>> {
// Determine whether we have indexable templates in output // Determine whether we have indexable templates in output
let can_handle_multiple = match config.output { let can_handle_multiple = match config.output {
Output::Stdout => false, Output::Stdout => false,
@ -356,7 +350,7 @@ fn export_image(
.iter() .iter()
.enumerate() .enumerate()
.filter(|(i, _)| { .filter(|(i, _)| {
config.pages.as_ref().map_or(true, |exported_page_ranges| { config.pages.as_ref().is_none_or(|exported_page_ranges| {
exported_page_ranges.includes_page_index(*i) exported_page_ranges.includes_page_index(*i)
}) })
}) })
@ -398,7 +392,7 @@ fn export_image(
&& config.export_cache.is_cached(*i, &page.frame) && config.export_cache.is_cached(*i, &page.frame)
&& path.exists() && path.exists()
{ {
return Ok(()); return Ok(Output::Path(path.to_path_buf()));
} }
Output::Path(path.to_owned()) Output::Path(path.to_owned())
@ -407,11 +401,9 @@ fn export_image(
}; };
export_image_page(config, page, &output, fmt)?; export_image_page(config, page, &output, fmt)?;
Ok(()) Ok(output)
}) })
.collect::<Result<Vec<()>, EcoString>>()?; .collect::<StrResult<Vec<Output>>>()
Ok(())
} }
mod output_template { mod output_template {
@ -516,14 +508,25 @@ impl ExportCache {
/// Writes a Makefile rule describing the relationship between the output and /// Writes a Makefile rule describing the relationship between the output and
/// its dependencies to the path specified by the --make-deps argument, if it /// its dependencies to the path specified by the --make-deps argument, if it
/// was provided. /// was provided.
fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> { fn write_make_deps(
world: &mut SystemWorld,
config: &CompileConfig,
outputs: Vec<Output>,
) -> StrResult<()> {
let Some(ref make_deps_path) = config.make_deps else { return Ok(()) }; let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
let Output::Path(output_path) = &config.output else { let Ok(output_paths) = outputs
bail!("failed to create make dependencies file because output was stdout") .into_iter()
}; .filter_map(|o| match o {
let Some(output_path) = output_path.as_os_str().to_str() else { Output::Path(path) => Some(path.into_os_string().into_string()),
Output::Stdout => None,
})
.collect::<Result<Vec<_>, _>>()
else {
bail!("failed to create make dependencies file because output path was not valid unicode") bail!("failed to create make dependencies file because output path was not valid unicode")
}; };
if output_paths.is_empty() {
bail!("failed to create make dependencies file because output was stdout")
}
// Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't // Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't
// perfect as some special characters can't be escaped. // perfect as some special characters can't be escaped.
@ -537,6 +540,10 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
res.push('$'); res.push('$');
slashes = 0; slashes = 0;
} }
':' => {
res.push('\\');
slashes = 0;
}
' ' | '\t' => { ' ' | '\t' => {
// `munge`'s source contains a comment here that says: "A // `munge`'s source contains a comment here that says: "A
// space or tab preceded by 2N+1 backslashes represents N // space or tab preceded by 2N+1 backslashes represents N
@ -559,18 +566,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
fn write( fn write(
make_deps_path: &Path, make_deps_path: &Path,
output_path: &str, output_paths: Vec<String>,
root: PathBuf, root: PathBuf,
dependencies: impl Iterator<Item = PathBuf>, dependencies: impl Iterator<Item = PathBuf>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut file = File::create(make_deps_path)?; let mut file = File::create(make_deps_path)?;
let current_dir = std::env::current_dir()?;
let relative_root = diff_paths(&root, &current_dir).unwrap_or(root.clone());
file.write_all(munge(output_path).as_bytes())?; for (i, output_path) in output_paths.into_iter().enumerate() {
if i != 0 {
file.write_all(b" ")?;
}
file.write_all(munge(&output_path).as_bytes())?;
}
file.write_all(b":")?; file.write_all(b":")?;
for dependency in dependencies { for dependency in dependencies {
let Some(dependency) = let relative_dependency = match dependency.strip_prefix(&root) {
dependency.strip_prefix(&root).unwrap_or(&dependency).to_str() Ok(root_relative_dependency) => {
else { relative_root.join(root_relative_dependency)
}
Err(_) => dependency,
};
let Some(relative_dependency) = relative_dependency.to_str() else {
// Silently skip paths that aren't valid unicode so we still // Silently skip paths that aren't valid unicode so we still
// produce a rule that will work for the other paths that can be // produce a rule that will work for the other paths that can be
// processed. // processed.
@ -578,14 +596,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
}; };
file.write_all(b" ")?; file.write_all(b" ")?;
file.write_all(munge(dependency).as_bytes())?; file.write_all(munge(relative_dependency).as_bytes())?;
} }
file.write_all(b"\n")?; file.write_all(b"\n")?;
Ok(()) Ok(())
} }
write(make_deps_path, output_path, world.root().to_owned(), world.dependencies()) write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies())
.map_err(|err| { .map_err(|err| {
eco_format!("failed to create make dependencies file due to IO error ({err})") eco_format!("failed to create make dependencies file due to IO error ({err})")
}) })

View File

@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Perform initial compilation. // Perform initial compilation.
timer.record(&mut world, |world| compile_once(world, &mut config))??; timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Watch all dependencies of the initial compilation.
watcher.update(world.dependencies())?;
// Recompile whenever something relevant happens. // Recompile whenever something relevant happens.
loop { loop {
// Watch all dependencies of the most recent compilation.
watcher.update(world.dependencies())?;
// Wait until anything relevant happens. // Wait until anything relevant happens.
watcher.wait()?; watcher.wait()?;
@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Evict the cache. // Evict the cache.
comemo::evict(10); comemo::evict(10);
// Adjust the file watching.
watcher.update(world.dependencies())?;
} }
} }
@ -204,6 +201,10 @@ impl Watcher {
let event = event let event = event
.map_err(|err| eco_format!("failed to watch dependencies ({err})"))?; .map_err(|err| eco_format!("failed to watch dependencies ({err})"))?;
if !is_relevant_event_kind(&event.kind) {
continue;
}
// Workaround for notify-rs' implicit unwatch on remove/rename // Workaround for notify-rs' implicit unwatch on remove/rename
// (triggered by some editors when saving files) with the // (triggered by some editors when saving files) with the
// inotify backend. By keeping track of the potentially // inotify backend. By keeping track of the potentially
@ -224,7 +225,17 @@ impl Watcher {
} }
} }
relevant |= self.is_event_relevant(&event); // Don't recompile because the output file changed.
// FIXME: This doesn't work properly for multifile image export.
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
continue;
}
relevant = true;
} }
// If we found a relevant event or if any of the missing files now // If we found a relevant event or if any of the missing files now
@ -234,32 +245,23 @@ impl Watcher {
} }
} }
} }
}
/// Whether a watch event is relevant for compilation. /// Whether a kind of watch event is relevant for compilation.
fn is_event_relevant(&self, event: &notify::Event) -> bool { fn is_relevant_event_kind(kind: &notify::EventKind) -> bool {
// Never recompile because the output file changed. match kind {
if event notify::EventKind::Any => true,
.paths notify::EventKind::Access(_) => false,
.iter() notify::EventKind::Create(_) => true,
.all(|path| is_same_file(path, &self.output).unwrap_or(false)) notify::EventKind::Modify(kind) => match kind {
{ notify::event::ModifyKind::Any => true,
return false; notify::event::ModifyKind::Data(_) => true,
} notify::event::ModifyKind::Metadata(_) => false,
notify::event::ModifyKind::Name(_) => true,
match &event.kind { notify::event::ModifyKind::Other => false,
notify::EventKind::Any => true, },
notify::EventKind::Access(_) => false, notify::EventKind::Remove(_) => true,
notify::EventKind::Create(_) => true, notify::EventKind::Other => false,
notify::EventKind::Modify(kind) => match kind {
notify::event::ModifyKind::Any => true,
notify::event::ModifyKind::Data(_) => true,
notify::event::ModifyKind::Metadata(_) => false,
notify::event::ModifyKind::Name(_) => true,
notify::event::ModifyKind::Other => false,
},
notify::EventKind::Remove(_) => true,
notify::EventKind::Other => false,
}
} }
} }

View File

@ -305,7 +305,7 @@ impl FileSlot {
) -> FileResult<Bytes> { ) -> FileResult<Bytes> {
self.file.get_or_init( self.file.get_or_init(
|| read(self.id, project_root, package_storage), || read(self.id, project_root, package_storage),
|data, _| Ok(data.into()), |data, _| Ok(Bytes::new(data)),
) )
} }
} }

View File

@ -30,12 +30,14 @@ impl Access for ast::Ident<'_> {
fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> {
let span = self.span(); let span = self.span();
if vm.inspected == Some(span) { if vm.inspected == Some(span) {
if let Ok(value) = vm.scopes.get(&self).cloned() { if let Ok(binding) = vm.scopes.get(&self) {
vm.trace(value); vm.trace(binding.read().clone());
} }
} }
let value = vm.scopes.get_mut(&self).at(span)?; vm.scopes
Ok(value) .get_mut(&self)
.and_then(|b| b.write().map_err(Into::into))
.at(span)
} }
} }

View File

@ -6,13 +6,12 @@ use typst_library::diag::{
}; };
use typst_library::engine::{Engine, Sink, Traced}; use typst_library::engine::{Engine, Sink, Traced};
use typst_library::foundations::{ use typst_library::foundations::{
Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, Arg, Args, Binding, Capturer, Closure, Content, Context, Func, NativeElement, Scope,
NativeElement, Scope, Scopes, Value, Scopes, SymbolElem, Value,
}; };
use typst_library::introspection::Introspector; use typst_library::introspection::Introspector;
use typst_library::math::LrElem; use typst_library::math::LrElem;
use typst_library::routines::Routines; use typst_library::routines::Routines;
use typst_library::text::TextElem;
use typst_library::World; use typst_library::World;
use typst_syntax::ast::{self, AstNode, Ident}; use typst_syntax::ast::{self, AstNode, Ident};
use typst_syntax::{Span, Spanned, SyntaxNode}; use typst_syntax::{Span, Spanned, SyntaxNode};
@ -197,7 +196,7 @@ pub fn eval_closure(
// Provide the closure itself for recursive calls. // Provide the closure itself for recursive calls.
if let Some(name) = name { if let Some(name) = name {
vm.define(name, Value::Func(func.clone())); vm.define(name, func.clone());
} }
let num_pos_args = args.to_pos().len(); let num_pos_args = args.to_pos().len();
@ -316,22 +315,25 @@ fn eval_field_call(
(target, args) (target, args)
}; };
if let Value::Plugin(plugin) = &target { let field_span = field.span();
// Call plugins by converting args to bytes. let sink = (&mut vm.engine, field_span);
let bytes = args.all::<Bytes>()?; if let Some(callee) = target.ty().scope().get(&field) {
args.finish()?;
let value = plugin.call(&field, bytes).at(span)?.into_value();
Ok(FieldCall::Resolved(value))
} else if let Some(callee) = target.ty().scope().get(&field) {
args.insert(0, target_expr.span(), target); args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.clone(), args)) Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args))
} else if let Value::Content(content) = &target {
if let Some(callee) = content.elem().scope().get(&field) {
args.insert(0, target_expr.span(), target);
Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args))
} else {
bail!(missing_field_call_error(target, field))
}
} else if matches!( } else if matches!(
target, target,
Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_)
) { ) {
// Certain value types may have their own ways to access method fields. // Certain value types may have their own ways to access method fields.
// e.g. `$arrow.r(v)$`, `table.cell[..]` // e.g. `$arrow.r(v)$`, `table.cell[..]`
let value = target.field(&field).at(field.span())?; let value = target.field(&field, sink).at(field_span)?;
Ok(FieldCall::Normal(value, args)) Ok(FieldCall::Normal(value, args))
} else { } else {
// Otherwise we cannot call this field. // Otherwise we cannot call this field.
@ -341,8 +343,20 @@ fn eval_field_call(
/// Produce an error when we cannot call the field. /// Produce an error when we cannot call the field.
fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
let mut error = let mut error = match &target {
error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str()); Value::Content(content) => error!(
field.span(),
"element {} has no method `{}`",
content.elem().name(),
field.as_str(),
),
_ => error!(
field.span(),
"type {} has no method `{}`",
target.ty(),
field.as_str()
),
};
match target { match target {
Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => { Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => {
@ -352,7 +366,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
field.as_str(), field.as_str(),
)); ));
} }
_ if target.field(&field).is_ok() => { _ if target.field(&field, ()).is_ok() => {
error.hint(eco_format!( error.hint(eco_format!(
"did you mean to access the field `{}`?", "did you mean to access the field `{}`?",
field.as_str(), field.as_str(),
@ -360,6 +374,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic {
} }
_ => {} _ => {}
} }
error error
} }
@ -382,16 +397,16 @@ fn wrap_args_in_math(
let mut body = Content::empty(); let mut body = Content::empty();
for (i, arg) in args.all::<Content>()?.into_iter().enumerate() { for (i, arg) in args.all::<Content>()?.into_iter().enumerate() {
if i > 0 { if i > 0 {
body += TextElem::packed(','); body += SymbolElem::packed(',');
} }
body += arg; body += arg;
} }
if trailing_comma { if trailing_comma {
body += TextElem::packed(','); body += SymbolElem::packed(',');
} }
Ok(Value::Content( Ok(Value::Content(
callee.display().spanned(callee_span) callee.display().spanned(callee_span)
+ LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')'))
.pack() .pack()
.spanned(args.span), .spanned(args.span),
)) ))
@ -445,15 +460,13 @@ impl<'a> CapturesVisitor<'a> {
// Identifiers that shouldn't count as captures because they // Identifiers that shouldn't count as captures because they
// actually bind a new name are handled below (individually through // actually bind a new name are handled below (individually through
// the expressions that contain them). // the expressions that contain them).
Some(ast::Expr::Ident(ident)) => { Some(ast::Expr::Ident(ident)) => self.capture(ident.get(), Scopes::get),
self.capture(ident.get(), ident.span(), Scopes::get)
}
Some(ast::Expr::MathIdent(ident)) => { Some(ast::Expr::MathIdent(ident)) => {
self.capture(ident.get(), ident.span(), Scopes::get_in_math) self.capture(ident.get(), Scopes::get_in_math)
} }
// Code and content blocks create a scope. // Code and content blocks create a scope.
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => {
self.internal.enter(); self.internal.enter();
for child in node.children() { for child in node.children() {
self.visit(child); self.visit(child);
@ -503,7 +516,7 @@ impl<'a> CapturesVisitor<'a> {
// A let expression contains a binding, but that binding is only // A let expression contains a binding, but that binding is only
// active after the body is evaluated. // active after the body is evaluated.
Some(ast::Expr::Let(expr)) => { Some(ast::Expr::LetBinding(expr)) => {
if let Some(init) = expr.init() { if let Some(init) = expr.init() {
self.visit(init.to_untyped()); self.visit(init.to_untyped());
} }
@ -516,7 +529,7 @@ impl<'a> CapturesVisitor<'a> {
// A for loop contains one or two bindings in its pattern. These are // A for loop contains one or two bindings in its pattern. These are
// active after the iterable is evaluated but before the body is // active after the iterable is evaluated but before the body is
// evaluated. // evaluated.
Some(ast::Expr::For(expr)) => { Some(ast::Expr::ForLoop(expr)) => {
self.visit(expr.iterable().to_untyped()); self.visit(expr.iterable().to_untyped());
self.internal.enter(); self.internal.enter();
@ -531,7 +544,7 @@ impl<'a> CapturesVisitor<'a> {
// An import contains items, but these are active only after the // An import contains items, but these are active only after the
// path is evaluated. // path is evaluated.
Some(ast::Expr::Import(expr)) => { Some(ast::Expr::ModuleImport(expr)) => {
self.visit(expr.source().to_untyped()); self.visit(expr.source().to_untyped());
if let Some(ast::Imports::Items(items)) = expr.imports() { if let Some(ast::Imports::Items(items)) = expr.imports() {
for item in items.iter() { for item in items.iter() {
@ -557,32 +570,34 @@ impl<'a> CapturesVisitor<'a> {
/// Bind a new internal variable. /// Bind a new internal variable.
fn bind(&mut self, ident: ast::Ident) { fn bind(&mut self, ident: ast::Ident) {
self.internal.top.define_ident(ident, Value::None); // The concrete value does not matter as we only use the scoping
// mechanism of `Scopes`, not the values themselves.
self.internal
.top
.bind(ident.get().clone(), Binding::detached(Value::None));
} }
/// Capture a variable if it isn't internal. /// Capture a variable if it isn't internal.
fn capture( fn capture(
&mut self, &mut self,
ident: &EcoString, ident: &EcoString,
span: Span, getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Binding>,
getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>,
) { ) {
if self.internal.get(ident).is_err() { if self.internal.get(ident).is_ok() {
let Some(value) = self return;
.external
.map(|external| getter(external, ident).ok())
.unwrap_or(Some(&Value::None))
else {
return;
};
self.captures.define_captured(
ident.clone(),
value.clone(),
self.capturer,
span,
);
} }
let binding = match self.external {
Some(external) => match getter(external, ident) {
Ok(binding) => binding.capture(self.capturer),
Err(_) => return,
},
// The external scopes are only `None` when we are doing IDE capture
// analysis, in which case the concrete value doesn't matter.
None => Binding::detached(Value::None),
};
self.captures.bind(ident.clone(), binding);
} }
} }
@ -685,8 +700,7 @@ mod tests {
// Named-params. // Named-params.
test(s, "$ foo(bar: y) $", &["foo"]); test(s, "$ foo(bar: y) $", &["foo"]);
// This should be updated when we improve named-param parsing: test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]);
test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]);
// Field access in math. // Field access in math.
test(s, "$ foo.bar $", &["foo"]); test(s, "$ foo.bar $", &["foo"]);

View File

@ -30,7 +30,7 @@ fn eval_code<'a>(
while let Some(expr) = exprs.next() { while let Some(expr) = exprs.next() {
let span = expr.span(); let span = expr.span();
let value = match expr { let value = match expr {
ast::Expr::Set(set) => { ast::Expr::SetRule(set) => {
let styles = set.eval(vm)?; let styles = set.eval(vm)?;
if vm.flow.is_some() { if vm.flow.is_some() {
break; break;
@ -39,7 +39,7 @@ fn eval_code<'a>(
let tail = eval_code(vm, exprs)?.display(); let tail = eval_code(vm, exprs)?.display();
Value::Content(tail.styled_with_map(styles)) Value::Content(tail.styled_with_map(styles))
} }
ast::Expr::Show(show) => { ast::Expr::ShowRule(show) => {
let recipe = show.eval(vm)?; let recipe = show.eval(vm)?;
if vm.flow.is_some() { if vm.flow.is_some() {
break; break;
@ -94,11 +94,12 @@ impl Eval for ast::Expr<'_> {
Self::Label(v) => v.eval(vm), Self::Label(v) => v.eval(vm),
Self::Ref(v) => v.eval(vm).map(Value::Content), Self::Ref(v) => v.eval(vm).map(Value::Content),
Self::Heading(v) => v.eval(vm).map(Value::Content), Self::Heading(v) => v.eval(vm).map(Value::Content),
Self::List(v) => v.eval(vm).map(Value::Content), Self::ListItem(v) => v.eval(vm).map(Value::Content),
Self::Enum(v) => v.eval(vm).map(Value::Content), Self::EnumItem(v) => v.eval(vm).map(Value::Content),
Self::Term(v) => v.eval(vm).map(Value::Content), Self::TermItem(v) => v.eval(vm).map(Value::Content),
Self::Equation(v) => v.eval(vm).map(Value::Content), Self::Equation(v) => v.eval(vm).map(Value::Content),
Self::Math(v) => v.eval(vm).map(Value::Content), Self::Math(v) => v.eval(vm).map(Value::Content),
Self::MathText(v) => v.eval(vm).map(Value::Content),
Self::MathIdent(v) => v.eval(vm), Self::MathIdent(v) => v.eval(vm),
Self::MathShorthand(v) => v.eval(vm), Self::MathShorthand(v) => v.eval(vm),
Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content),
@ -115,8 +116,8 @@ impl Eval for ast::Expr<'_> {
Self::Float(v) => v.eval(vm), Self::Float(v) => v.eval(vm),
Self::Numeric(v) => v.eval(vm), Self::Numeric(v) => v.eval(vm),
Self::Str(v) => v.eval(vm), Self::Str(v) => v.eval(vm),
Self::Code(v) => v.eval(vm), Self::CodeBlock(v) => v.eval(vm),
Self::Content(v) => v.eval(vm).map(Value::Content), Self::ContentBlock(v) => v.eval(vm).map(Value::Content),
Self::Array(v) => v.eval(vm).map(Value::Array), Self::Array(v) => v.eval(vm).map(Value::Array),
Self::Dict(v) => v.eval(vm).map(Value::Dict), Self::Dict(v) => v.eval(vm).map(Value::Dict),
Self::Parenthesized(v) => v.eval(vm), Self::Parenthesized(v) => v.eval(vm),
@ -125,19 +126,19 @@ impl Eval for ast::Expr<'_> {
Self::Closure(v) => v.eval(vm), Self::Closure(v) => v.eval(vm),
Self::Unary(v) => v.eval(vm), Self::Unary(v) => v.eval(vm),
Self::Binary(v) => v.eval(vm), Self::Binary(v) => v.eval(vm),
Self::Let(v) => v.eval(vm), Self::LetBinding(v) => v.eval(vm),
Self::DestructAssign(v) => v.eval(vm), Self::DestructAssignment(v) => v.eval(vm),
Self::Set(_) => bail!(forbidden("set")), Self::SetRule(_) => bail!(forbidden("set")),
Self::Show(_) => bail!(forbidden("show")), Self::ShowRule(_) => bail!(forbidden("show")),
Self::Contextual(v) => v.eval(vm).map(Value::Content), Self::Contextual(v) => v.eval(vm).map(Value::Content),
Self::Conditional(v) => v.eval(vm), Self::Conditional(v) => v.eval(vm),
Self::While(v) => v.eval(vm), Self::WhileLoop(v) => v.eval(vm),
Self::For(v) => v.eval(vm), Self::ForLoop(v) => v.eval(vm),
Self::Import(v) => v.eval(vm), Self::ModuleImport(v) => v.eval(vm),
Self::Include(v) => v.eval(vm).map(Value::Content), Self::ModuleInclude(v) => v.eval(vm).map(Value::Content),
Self::Break(v) => v.eval(vm), Self::LoopBreak(v) => v.eval(vm),
Self::Continue(v) => v.eval(vm), Self::LoopContinue(v) => v.eval(vm),
Self::Return(v) => v.eval(vm), Self::FuncReturn(v) => v.eval(vm),
}? }?
.spanned(span); .spanned(span);
@ -153,7 +154,13 @@ impl Eval for ast::Ident<'_> {
type Output = Value; type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.get(&self).cloned().at(self.span()) let span = self.span();
Ok(vm
.scopes
.get(&self)
.at(span)?
.read_checked((&mut vm.engine, span))
.clone())
} }
} }
@ -309,8 +316,9 @@ impl Eval for ast::FieldAccess<'_> {
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let value = self.target().eval(vm)?; let value = self.target().eval(vm)?;
let field = self.field(); let field = self.field();
let field_span = field.span();
let err = match value.field(&field).at(field.span()) { let err = match value.field(&field, (&mut vm.engine, field_span)).at(field_span) {
Ok(value) => return Ok(value), Ok(value) => return Ok(value),
Err(err) => err, Err(err) => err,
}; };

View File

@ -4,9 +4,9 @@ use typst_library::diag::{
bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint, bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint,
}; };
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, Module, Value}; use typst_library::foundations::{Binding, Content, Module, Value};
use typst_library::World; use typst_library::World;
use typst_syntax::ast::{self, AstNode}; use typst_syntax::ast::{self, AstNode, BareImportError};
use typst_syntax::package::{PackageManifest, PackageSpec}; use typst_syntax::package::{PackageManifest, PackageSpec};
use typst_syntax::{FileId, Span, VirtualPath}; use typst_syntax::{FileId, Span, VirtualPath};
@ -16,11 +16,11 @@ impl Eval for ast::ModuleImport<'_> {
type Output = Value; type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let source = self.source(); let source_expr = self.source();
let source_span = source.span(); let source_span = source_expr.span();
let mut source = source.eval(vm)?;
let new_name = self.new_name(); let mut source = source_expr.eval(vm)?;
let imports = self.imports(); let mut is_str = false;
match &source { match &source {
Value::Func(func) => { Value::Func(func) => {
@ -32,6 +32,7 @@ impl Eval for ast::ModuleImport<'_> {
Value::Module(_) => {} Value::Module(_) => {}
Value::Str(path) => { Value::Str(path) => {
source = Value::Module(import(&mut vm.engine, path, source_span)?); source = Value::Module(import(&mut vm.engine, path, source_span)?);
is_str = true;
} }
v => { v => {
bail!( bail!(
@ -42,6 +43,8 @@ impl Eval for ast::ModuleImport<'_> {
} }
} }
// If there is a rename, import the source itself under that name.
let new_name = self.new_name();
if let Some(new_name) = new_name { if let Some(new_name) = new_name {
if let ast::Expr::Ident(ident) = self.source() { if let ast::Expr::Ident(ident) = self.source() {
if ident.as_str() == new_name.as_str() { if ident.as_str() == new_name.as_str() {
@ -54,21 +57,42 @@ impl Eval for ast::ModuleImport<'_> {
} }
// Define renamed module on the scope. // Define renamed module on the scope.
vm.scopes.top.define_ident(new_name, source.clone()); vm.define(new_name, source.clone());
} }
let scope = source.scope().unwrap(); let scope = source.scope().unwrap();
match imports { match self.imports() {
None => { None => {
// Only import here if there is no rename.
if new_name.is_none() { if new_name.is_none() {
let name: EcoString = source.name().unwrap().into(); match self.bare_name() {
vm.scopes.top.define(name, source); // Bare dynamic string imports are not allowed.
Ok(name)
if !is_str || matches!(source_expr, ast::Expr::Str(_)) =>
{
if matches!(source_expr, ast::Expr::Ident(_)) {
vm.engine.sink.warn(warning!(
source_expr.span(),
"this import has no effect",
));
}
vm.scopes.top.bind(name, Binding::new(source, source_span));
}
Ok(_) | Err(BareImportError::Dynamic) => bail!(
source_span, "dynamic import requires an explicit name";
hint: "you can name the import with `as`"
),
Err(BareImportError::PathInvalid) => bail!(
source_span, "module name would not be a valid identifier";
hint: "you can rename the import with `as`",
),
// Bad package spec would have failed the import already.
Err(BareImportError::PackageInvalid) => unreachable!(),
}
} }
} }
Some(ast::Imports::Wildcard) => { Some(ast::Imports::Wildcard) => {
for (var, value, span) in scope.iter() { for (var, binding) in scope.iter() {
vm.scopes.top.define_spanned(var.clone(), value.clone(), span); vm.scopes.top.bind(var.clone(), binding.clone());
} }
} }
Some(ast::Imports::Items(items)) => { Some(ast::Imports::Items(items)) => {
@ -78,7 +102,7 @@ impl Eval for ast::ModuleImport<'_> {
let mut scope = scope; let mut scope = scope;
while let Some(component) = &path.next() { while let Some(component) = &path.next() {
let Some(value) = scope.get(component) else { let Some(binding) = scope.get(component) else {
errors.push(error!(component.span(), "unresolved import")); errors.push(error!(component.span(), "unresolved import"));
break; break;
}; };
@ -86,6 +110,7 @@ impl Eval for ast::ModuleImport<'_> {
if path.peek().is_some() { if path.peek().is_some() {
// Nested import, as this is not the last component. // Nested import, as this is not the last component.
// This must be a submodule. // This must be a submodule.
let value = binding.read();
let Some(submodule) = value.scope() else { let Some(submodule) = value.scope() else {
let error = if matches!(value, Value::Func(function) if function.scope().is_none()) let error = if matches!(value, Value::Func(function) if function.scope().is_none())
{ {
@ -128,7 +153,7 @@ impl Eval for ast::ModuleImport<'_> {
} }
} }
vm.define(item.bound_name(), value.clone()); vm.bind(item.bound_name(), binding.clone());
} }
} }
} }
@ -211,7 +236,7 @@ fn resolve_package(
// Evaluate the manifest. // Evaluate the manifest.
let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml"));
let bytes = engine.world.file(manifest_id).at(span)?; let bytes = engine.world.file(manifest_id).at(span)?;
let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; let string = bytes.as_str().map_err(FileError::from).at(span)?;
let manifest: PackageManifest = toml::from_str(string) let manifest: PackageManifest = toml::from_str(string)
.map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message()))
.at(span)?; .at(span)?;

View File

@ -33,7 +33,7 @@ fn eval_markup<'a>(
while let Some(expr) = exprs.next() { while let Some(expr) = exprs.next() {
match expr { match expr {
ast::Expr::Set(set) => { ast::Expr::SetRule(set) => {
let styles = set.eval(vm)?; let styles = set.eval(vm)?;
if vm.flow.is_some() { if vm.flow.is_some() {
break; break;
@ -41,7 +41,7 @@ fn eval_markup<'a>(
seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) seq.push(eval_markup(vm, exprs)?.styled_with_map(styles))
} }
ast::Expr::Show(show) => { ast::Expr::ShowRule(show) => {
let recipe = show.eval(vm)?; let recipe = show.eval(vm)?;
if vm.flow.is_some() { if vm.flow.is_some() {
break; break;

View File

@ -1,11 +1,11 @@
use ecow::eco_format; use ecow::eco_format;
use typst_library::diag::{At, SourceResult}; use typst_library::diag::{At, SourceResult};
use typst_library::foundations::{Content, NativeElement, Symbol, Value}; use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value};
use typst_library::math::{ use typst_library::math::{
AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem,
}; };
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_syntax::ast::{self, AstNode}; use typst_syntax::ast::{self, AstNode, MathTextKind};
use crate::{Eval, Vm}; use crate::{Eval, Vm};
@ -20,11 +20,28 @@ impl Eval for ast::Math<'_> {
} }
} }
impl Eval for ast::MathText<'_> {
type Output = Content;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
match self.get() {
MathTextKind::Character(c) => Ok(SymbolElem::packed(c)),
MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())),
}
}
}
impl Eval for ast::MathIdent<'_> { impl Eval for ast::MathIdent<'_> {
type Output = Value; type Output = Value;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
vm.scopes.get_in_math(&self).cloned().at(self.span()) let span = self.span();
Ok(vm
.scopes
.get_in_math(&self)
.at(span)?
.read_checked((&mut vm.engine, span))
.clone())
} }
} }
@ -102,6 +119,7 @@ impl Eval for ast::MathRoot<'_> {
type Output = Content; type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
// Use `TextElem` to match `MathTextKind::Number` above.
let index = self.index().map(|i| TextElem::packed(eco_format!("{i}"))); let index = self.index().map(|i| TextElem::packed(eco_format!("{i}")));
let radicand = self.radicand().eval_display(vm)?; let radicand = self.radicand().eval_display(vm)?;
Ok(RootElem::new(radicand).with_index(index).pack()) Ok(RootElem::new(radicand).with_index(index).pack())

View File

@ -45,7 +45,7 @@ impl Eval for ast::ShowRule<'_> {
let transform = self.transform(); let transform = self.transform();
let transform = match transform { let transform = match transform {
ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?), ast::Expr::SetRule(set) => Transformation::Style(set.eval(vm)?),
expr => expr.eval(vm)?.cast::<Transformation>().at(transform.span())?, expr => expr.eval(vm)?.cast::<Transformation>().at(transform.span())?,
}; };

View File

@ -1,7 +1,7 @@
use comemo::Tracked; use comemo::Tracked;
use typst_library::diag::warning; use typst_library::diag::warning;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Context, IntoValue, Scopes, Value}; use typst_library::foundations::{Binding, Context, IntoValue, Scopes, Value};
use typst_library::World; use typst_library::World;
use typst_syntax::ast::{self, AstNode}; use typst_syntax::ast::{self, AstNode};
use typst_syntax::Span; use typst_syntax::Span;
@ -42,13 +42,23 @@ impl<'a> Vm<'a> {
self.engine.world self.engine.world
} }
/// Define a variable in the current scope. /// Bind a value to an identifier.
///
/// This will create a [`Binding`] with the value and the identifier's span.
pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) {
let value = value.into_value(); self.bind(var, Binding::new(value, var.span()));
}
/// Insert a binding into the current scope.
///
/// This will insert the value into the top-most scope and make it available
/// for dynamic tracing, assisting IDE functionality.
pub fn bind(&mut self, var: ast::Ident, binding: Binding) {
if self.inspected == Some(var.span()) { if self.inspected == Some(var.span()) {
self.trace(value.clone()); self.trace(binding.read().clone());
} }
// This will become an error in the parser if 'is' becomes a keyword.
// This will become an error in the parser if `is` becomes a keyword.
if var.get() == "is" { if var.get() == "is" {
self.engine.sink.warn(warning!( self.engine.sink.warn(warning!(
var.span(), var.span(),
@ -58,7 +68,8 @@ impl<'a> Vm<'a> {
hint: "try `is_` instead" hint: "try `is_` instead"
)); ));
} }
self.scopes.top.define_ident(var, value);
self.scopes.top.bind(var.get().clone(), binding);
} }
/// Trace a value. /// Trace a value.

View File

@ -2,7 +2,7 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr; use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode}; use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
use typst_library::layout::Frame; use typst_library::layout::Frame;
use typst_syntax::Span; use typst_syntax::Span;
@ -12,15 +12,19 @@ pub fn html(document: &HtmlDocument) -> SourceResult<String> {
w.buf.push_str("<!DOCTYPE html>"); w.buf.push_str("<!DOCTYPE html>");
write_indent(&mut w); write_indent(&mut w);
write_element(&mut w, &document.root)?; write_element(&mut w, &document.root)?;
if w.pretty {
w.buf.push('\n');
}
Ok(w.buf) Ok(w.buf)
} }
#[derive(Default)] #[derive(Default)]
struct Writer { struct Writer {
/// The output buffer.
buf: String, buf: String,
/// current indentation level /// The current indentation level
level: usize, level: usize,
/// pretty printing enabled? /// Whether pretty printing is enabled.
pretty: bool, pretty: bool,
} }
@ -85,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
let pretty = w.pretty; let pretty = w.pretty;
if !element.children.is_empty() { if !element.children.is_empty() {
w.pretty &= is_pretty(element); let pretty_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node {
HtmlNode::Element(child) => wants_pretty_around(child.tag),
_ => false,
});
w.pretty &= pretty_inside;
let mut indent = w.pretty; let mut indent = w.pretty;
w.level += 1; w.level += 1;
for c in &element.children { for c in &element.children {
let pretty_child = match c { let pretty_around = match c {
HtmlNode::Tag(_) => continue, HtmlNode::Tag(_) => continue,
HtmlNode::Element(element) => is_pretty(element), HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false, HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
}; };
if core::mem::take(&mut indent) || pretty_child { if core::mem::take(&mut indent) || pretty_around {
write_indent(w); write_indent(w);
} }
write_node(w, c)?; write_node(w, c)?;
indent = pretty_child; indent = pretty_around;
} }
w.level -= 1; w.level -= 1;
write_indent(w) write_indent(w);
} }
w.pretty = pretty; w.pretty = pretty;
@ -115,9 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
Ok(()) Ok(())
} }
/// Whether the element should be pretty-printed. /// Whether we are allowed to add an extra newline at the start and end of the
fn is_pretty(element: &HtmlElement) -> bool { /// element's contents.
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta) ///
/// Technically, users can change CSS `display` properties such that the
/// insertion of whitespace may actually impact the visual output. For example,
/// <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how adding CSS
/// rules to `<p>` can make it sensitive to whitespace. For this reason, we
/// should also respect the `style` tag in the future.
fn allows_pretty_inside(tag: HtmlTag) -> bool {
(tag::is_block_by_default(tag) && tag != tag::pre)
|| tag::is_tabular_by_default(tag)
|| tag == tag::li
}
/// Whether newlines should be added before and after the element if the parent
/// allows it.
///
/// In contrast to `allows_pretty_inside`, which is purely spec-driven, this is
/// more subjective and depends on preference.
fn wants_pretty_around(tag: HtmlTag) -> bool {
allows_pretty_inside(tag) || tag::is_metadata(tag) || tag == tag::pre
} }
/// Escape a character. /// Escape a character.

View File

@ -14,9 +14,9 @@ use typst_library::html::{
use typst_library::introspection::{ use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem, Introspector, Locator, LocatorLink, SplitLocator, TagElem,
}; };
use typst_library::layout::{Abs, Axes, BoxElem, Region, Size}; use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem}; use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World; use typst_library::World;
use typst_syntax::Span; use typst_syntax::Span;
@ -83,8 +83,8 @@ fn html_document_impl(
)?; )?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?; let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector }) Ok(HtmlDocument { info, root, introspector })
} }
@ -139,7 +139,9 @@ fn html_fragment_impl(
let arenas = Arenas::default(); let arenas = Arenas::default();
let children = (engine.routines.realize)( let children = (engine.routines.realize)(
RealizationKind::HtmlFragment, // No need to know about the `FragmentKind` because we handle both
// uniformly.
RealizationKind::HtmlFragment(&mut FragmentKind::Block),
&mut engine, &mut engine,
&mut locator, &mut locator,
&arenas, &arenas,
@ -189,7 +191,8 @@ fn handle(
}; };
output.push(element.into()); output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() { } else if let Some(elem) = child.to_packed::<ParElem>() {
let children = handle_list(engine, locator, elem.children.iter(&styles))?; let children =
html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?;
output.push( output.push(
HtmlElement::new(tag::p) HtmlElement::new(tag::p)
.with_children(children) .with_children(children)
@ -197,13 +200,34 @@ fn handle(
.into(), .into(),
); );
} else if let Some(elem) = child.to_packed::<BoxElem>() { } else if let Some(elem) = child.to_packed::<BoxElem>() {
// FIXME: Very incomplete and hacky, but makes boxes kind fulfill their // TODO: This is rather incomplete.
// purpose for now.
if let Some(body) = elem.body(styles) { if let Some(body) = elem.body(styles) {
let children = let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?; html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.extend(children); output.push(
HtmlElement::new(tag::span)
.with_attr(attr::style, "display: inline-block;")
.with_children(children)
.spanned(elem.span())
.into(),
)
} }
} else if let Some((elem, body)) =
child
.to_packed::<BlockElem>()
.and_then(|elem| match elem.body(styles) {
Some(BlockBody::Content(body)) => Some((elem, body)),
_ => None,
})
{
// TODO: This is rather incomplete.
let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.push(
HtmlElement::new(tag::div)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if child.is::<SpaceElem>() { } else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span())); output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() { } else if let Some(elem) = child.to_packed::<TextElem>() {
@ -283,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement {
/// Determine which kind of output the user generated. /// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> { fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let len = output.len(); let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output { for node in &mut output {
let HtmlNode::Element(elem) = node else { continue }; let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag; let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, len) { match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())), (tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())), (tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!( (tag::html | tag::body, _) => bail!(
elem.span, elem.span,
"`{}` element must be the only element in the document", "`{}` element must be the only element in the document",
elem.tag elem.tag,
), ),
_ => {} _ => {}
} }

View File

@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
} }
// Behind existing atom or identifier: "$a|$" or "$abc|$". // Behind existing atom or identifier: "$a|$" or "$abc|$".
if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { if matches!(
ctx.leaf.kind(),
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent
) {
ctx.from = ctx.leaf.offset(); ctx.from = ctx.leaf.offset();
math_completions(ctx); math_completions(ctx);
return true; return true;
@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
// Behind an expression plus dot: "emoji.|". // Behind an expression plus dot: "emoji.|".
if_chain! { if_chain! {
if ctx.leaf.kind() == SyntaxKind::Dot if ctx.leaf.kind() == SyntaxKind::Dot
|| (ctx.leaf.kind() == SyntaxKind::Text || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
&& ctx.leaf.text() == "."); && ctx.leaf.text() == ".");
if ctx.leaf.range().end == ctx.cursor; if ctx.leaf.range().end == ctx.cursor;
if let Some(prev) = ctx.leaf.prev_sibling(); if let Some(prev) = ctx.leaf.prev_sibling();
@ -398,13 +401,31 @@ fn field_access_completions(
value: &Value, value: &Value,
styles: &Option<Styles>, styles: &Option<Styles>,
) { ) {
for (name, value, _) in value.ty().scope().iter() { let scopes = {
ctx.call_completion(name.clone(), value); let ty = value.ty().scope();
let elem = match value {
Value::Content(content) => Some(content.elem().scope()),
_ => None,
};
elem.into_iter().chain(Some(ty))
};
// Autocomplete methods from the element's or type's scope. We only complete
// those which have a `self` parameter.
for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
let Ok(func) = binding.read().clone().cast::<Func>() else { continue };
if func
.params()
.and_then(|params| params.first())
.is_some_and(|param| param.name == "self")
{
ctx.call_completion(name.clone(), binding.read());
}
} }
if let Some(scope) = value.scope() { if let Some(scope) = value.scope() {
for (name, value, _) in scope.iter() { for (name, binding) in scope.iter() {
ctx.call_completion(name.clone(), value); ctx.call_completion(name.clone(), binding.read());
} }
} }
@ -414,7 +435,7 @@ fn field_access_completions(
// with method syntax; // with method syntax;
// 2. We can unwrap the field's value since it's a field belonging to // 2. We can unwrap the field's value since it's a field belonging to
// this value's type, so accessing it should not fail. // this value's type, so accessing it should not fail.
ctx.value_completion(field, &value.field(field).unwrap()); ctx.value_completion(field, &value.field(field, ()).unwrap());
} }
match value { match value {
@ -452,16 +473,6 @@ fn field_access_completions(
} }
} }
} }
Value::Plugin(plugin) => {
for name in plugin.iter() {
ctx.completions.push(Completion {
kind: CompletionKind::Func,
label: name.clone(),
apply: None,
detail: None,
})
}
}
_ => {} _ => {}
} }
} }
@ -506,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
// "#import "path.typ": a, b, |". // "#import "path.typ": a, b, |".
if_chain! { if_chain! {
if let Some(prev) = ctx.leaf.prev_sibling(); if let Some(prev) = ctx.leaf.prev_sibling();
if let Some(ast::Expr::Import(import)) = prev.get().cast(); if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>()); if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
then { then {
@ -521,11 +532,13 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if_chain! { if_chain! {
if ctx.leaf.kind() == SyntaxKind::Ident; if ctx.leaf.kind() == SyntaxKind::Ident;
if let Some(parent) = ctx.leaf.parent(); if let Some(parent) = ctx.leaf.parent();
if parent.kind() == SyntaxKind::ImportItems; if parent.kind() == SyntaxKind::ImportItemPath;
if let Some(grand) = parent.parent(); if let Some(grand) = parent.parent();
if let Some(ast::Expr::Import(import)) = grand.get().cast(); if grand.kind() == SyntaxKind::ImportItems;
if let Some(great) = grand.parent();
if let Some(ast::Expr::ModuleImport(import)) = great.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = grand.children().find(|child| child.is::<ast::Expr>()); if let Some(source) = great.children().find(|child| child.is::<ast::Expr>());
then { then {
ctx.from = ctx.leaf.offset(); ctx.from = ctx.leaf.offset();
import_item_completions(ctx, items, &source); import_item_completions(ctx, items, &source);
@ -549,9 +562,9 @@ fn import_item_completions<'a>(
ctx.snippet_completion("*", "*", "Import everything."); ctx.snippet_completion("*", "*", "Import everything.");
} }
for (name, value, _) in scope.iter() { for (name, binding) in scope.iter() {
if existing.iter().all(|item| item.original_name().as_str() != name) { if existing.iter().all(|item| item.original_name().as_str() != name) {
ctx.value_completion(name.clone(), value); ctx.value_completion(name.clone(), binding.read());
} }
} }
} }
@ -664,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
if let Some(args) = parent.get().cast::<ast::Args>(); if let Some(args) = parent.get().cast::<ast::Args>();
if let Some(grand) = parent.parent(); if let Some(grand) = parent.parent();
if let Some(expr) = grand.get().cast::<ast::Expr>(); if let Some(expr) = grand.get().cast::<ast::Expr>();
let set = matches!(expr, ast::Expr::Set(_)); let set = matches!(expr, ast::Expr::SetRule(_));
if let Some(callee) = match expr { if let Some(callee) = match expr {
ast::Expr::FuncCall(call) => Some(call.callee()), ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()), ast::Expr::SetRule(set) => Some(set.target()),
_ => None, _ => None,
}; };
then { then {
@ -815,19 +828,8 @@ fn param_value_completions<'a>(
) { ) {
if param.name == "font" { if param.name == "font" {
ctx.font_completions(); ctx.font_completions();
} else if param.name == "path" { } else if let Some(extensions) = path_completion(func, param) {
ctx.file_completions_with_extensions(match func.name() { ctx.file_completions_with_extensions(extensions);
Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
Some("csv") => &["csv"],
Some("plugin") => &["wasm"],
Some("cbor") => &["cbor"],
Some("json") => &["json"],
Some("toml") => &["toml"],
Some("xml") => &["xml"],
Some("yaml") => &["yml", "yaml"],
Some("bibliography") => &["bib", "yml", "yaml"],
_ => &[],
});
} else if func.name() == Some("figure") && param.name == "body" { } else if func.name() == Some("figure") && param.name == "body" {
ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure."); ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure.");
ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure."); ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure.");
@ -836,6 +838,28 @@ fn param_value_completions<'a>(
ctx.cast_completions(&param.input); ctx.cast_completions(&param.input);
} }
/// Returns which file extensions to complete for the given parameter if any.
fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
Some(match (func.name(), param.name) {
(Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"],
(Some("csv"), "source") => &["csv"],
(Some("plugin"), "source") => &["wasm"],
(Some("cbor"), "source") => &["cbor"],
(Some("json"), "source") => &["json"],
(Some("toml"), "source") => &["toml"],
(Some("xml"), "source") => &["xml"],
(Some("yaml"), "source") => &["yml", "yaml"],
(Some("bibliography"), "sources") => &["bib", "yml", "yaml"],
(Some("bibliography"), "style") => &["csl"],
(Some("cite"), "style") => &["csl"],
(Some("raw"), "syntaxes") => &["sublime-syntax"],
(Some("raw"), "theme") => &["tmtheme"],
(Some("embed"), "path") => &[],
(None, "path") => &[],
_ => return None,
})
}
/// Resolve a callee expression to a global function. /// Resolve a callee expression to a global function.
fn resolve_global_callee<'a>( fn resolve_global_callee<'a>(
ctx: &CompletionContext<'a>, ctx: &CompletionContext<'a>,
@ -843,13 +867,11 @@ fn resolve_global_callee<'a>(
) -> Option<&'a Func> { ) -> Option<&'a Func> {
let globals = globals(ctx.world, ctx.leaf); let globals = globals(ctx.world, ctx.leaf);
let value = match callee { let value = match callee {
ast::Expr::Ident(ident) => globals.get(&ident)?, ast::Expr::Ident(ident) => globals.get(&ident)?.read(),
ast::Expr::FieldAccess(access) => match access.target() { ast::Expr::FieldAccess(access) => match access.target() {
ast::Expr::Ident(target) => match globals.get(&target)? { ast::Expr::Ident(target) => {
Value::Module(module) => module.field(&access.field()).ok()?, globals.get(&target)?.read().scope()?.get(&access.field())?.read()
Value::Func(func) => func.field(&access.field()).ok()?, }
_ => return None,
},
_ => return None, _ => return None,
}, },
_ => return None, _ => return None,
@ -1441,7 +1463,7 @@ impl<'a> CompletionContext<'a> {
let mut defined = BTreeMap::<EcoString, Option<Value>>::new(); let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
named_items(self.world, self.leaf.clone(), |item| { named_items(self.world, self.leaf.clone(), |item| {
let name = item.name(); let name = item.name();
if !name.is_empty() && item.value().as_ref().map_or(true, filter) { if !name.is_empty() && item.value().as_ref().is_none_or(filter) {
defined.insert(name.clone(), item.value()); defined.insert(name.clone(), item.value());
} }
@ -1461,7 +1483,8 @@ impl<'a> CompletionContext<'a> {
} }
} }
for (name, value, _) in globals(self.world, self.leaf).iter() { for (name, binding) in globals(self.world, self.leaf).iter() {
let value = binding.read();
if filter(value) && !defined.contains_key(name) { if filter(value) && !defined.contains_key(name) {
self.value_completion_full(Some(name.clone()), value, parens, None, None); self.value_completion_full(Some(name.clone()), value, parens, None, None);
} }
@ -1504,14 +1527,13 @@ impl BracketMode {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::borrow::Borrow;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use typst::layout::PagedDocument; use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source, VirtualPath};
use typst::World;
use super::{autocomplete, Completion}; use super::{autocomplete, Completion};
use crate::tests::{SourceExt, TestWorld}; use crate::tests::{FilePos, TestWorld, WorldLike};
/// Quote a string. /// Quote a string.
macro_rules! q { macro_rules! q {
@ -1583,60 +1605,50 @@ mod tests {
} }
#[track_caller] #[track_caller]
fn test(text: &str, cursor: isize) -> Response { fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
let world = TestWorld::new(text); let world = world.acquire();
test_with_world(&world, cursor) let world = world.borrow();
let doc = typst::compile(world).output.ok();
test_with_doc(world, pos, doc.as_ref())
} }
#[track_caller] #[track_caller]
fn test_with_world(world: &TestWorld, cursor: isize) -> Response { fn test_with_doc(
let doc = typst::compile(&world).output.ok(); world: impl WorldLike,
test_full(world, &world.main, doc.as_ref(), cursor) pos: impl FilePos,
}
#[track_caller]
fn test_with_path(world: &TestWorld, path: &str, cursor: isize) -> Response {
let doc = typst::compile(&world).output.ok();
let id = FileId::new(None, VirtualPath::new(path));
let source = world.source(id).unwrap();
test_full(world, &source, doc.as_ref(), cursor)
}
#[track_caller]
fn test_full(
world: &TestWorld,
source: &Source,
doc: Option<&PagedDocument>, doc: Option<&PagedDocument>,
cursor: isize,
) -> Response { ) -> Response {
autocomplete(world, doc, source, source.cursor(cursor), true) let world = world.acquire();
let world = world.borrow();
let (source, cursor) = pos.resolve(world);
autocomplete(world, doc, &source, cursor, true)
} }
#[test] #[test]
fn test_autocomplete_hash_expr() { fn test_autocomplete_hash_expr() {
test("#i", 2).must_include(["int", "if conditional"]); test("#i", -1).must_include(["int", "if conditional"]);
} }
#[test] #[test]
fn test_autocomplete_array_method() { fn test_autocomplete_array_method() {
test("#().", 4).must_include(["insert", "remove", "len", "all"]); test("#().", -1).must_include(["insert", "remove", "len", "all"]);
test("#{ let x = (1, 2, 3); x. }", -2).must_include(["at", "push", "pop"]); test("#{ let x = (1, 2, 3); x. }", -3).must_include(["at", "push", "pop"]);
} }
/// Test that extra space before '.' is handled correctly. /// Test that extra space before '.' is handled correctly.
#[test] #[test]
fn test_autocomplete_whitespace() { fn test_autocomplete_whitespace() {
test("#() .", 5).must_exclude(["insert", "remove", "len", "all"]); test("#() .", -1).must_exclude(["insert", "remove", "len", "all"]);
test("#{() .}", 6).must_include(["insert", "remove", "len", "all"]); test("#{() .}", -2).must_include(["insert", "remove", "len", "all"]);
test("#() .a", 6).must_exclude(["insert", "remove", "len", "all"]); test("#() .a", -1).must_exclude(["insert", "remove", "len", "all"]);
test("#{() .a}", 7).must_include(["at", "any", "all"]); test("#{() .a}", -2).must_include(["at", "any", "all"]);
} }
/// Test that the `before_window` doesn't slice into invalid byte /// Test that the `before_window` doesn't slice into invalid byte
/// boundaries. /// boundaries.
#[test] #[test]
fn test_autocomplete_before_window_char_boundary() { fn test_autocomplete_before_window_char_boundary() {
test("😀😀 #text(font: \"\")", -2); test("😀😀 #text(font: \"\")", -3);
} }
/// Ensure that autocompletion for `#cite(|)` completes bibligraphy labels, /// Ensure that autocompletion for `#cite(|)` completes bibligraphy labels,
@ -1653,7 +1665,7 @@ mod tests {
let end = world.main.len_bytes(); let end = world.main.len_bytes();
world.main.edit(end..end, " #cite()"); world.main.edit(end..end, " #cite()");
test_full(&world, &world.main, doc.as_ref(), -1) test_with_doc(&world, -2, doc.as_ref())
.must_include(["netwok", "glacier-melt", "supplement"]) .must_include(["netwok", "glacier-melt", "supplement"])
.must_exclude(["bib"]); .must_exclude(["bib"]);
} }
@ -1677,13 +1689,13 @@ mod tests {
#[test] #[test]
fn test_autocomplete_positional_param() { fn test_autocomplete_positional_param() {
// No string given yet. // No string given yet.
test("#numbering()", -1).must_include(["string", "integer"]); test("#numbering()", -2).must_include(["string", "integer"]);
// String is already given. // String is already given.
test("#numbering(\"foo\", )", -1) test("#numbering(\"foo\", )", -2)
.must_include(["integer"]) .must_include(["integer"])
.must_exclude(["string"]); .must_exclude(["string"]);
// Integer is already given, but numbering is variadic. // Integer is already given, but numbering is variadic.
test("#numbering(\"foo\", 1, )", -1) test("#numbering(\"foo\", 1, )", -2)
.must_include(["integer"]) .must_include(["integer"])
.must_exclude(["string"]); .must_exclude(["string"]);
} }
@ -1698,14 +1710,14 @@ mod tests {
"#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)", "#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)",
); );
test_with_world(&world, -1) test(&world, -2)
.must_include(["clrs", "aqua"]) .must_include(["clrs", "aqua"])
.must_exclude(["nums", "a", "b"]); .must_exclude(["nums", "a", "b"]);
} }
#[test] #[test]
fn test_autocomplete_packages() { fn test_autocomplete_packages() {
test("#import \"@\"", -1).must_include([q!("@preview/example:0.1.0")]); test("#import \"@\"", -2).must_include([q!("@preview/example:0.1.0")]);
} }
#[test] #[test]
@ -1719,28 +1731,63 @@ mod tests {
.with_asset_at("assets/rhino.png", "rhino.png") .with_asset_at("assets/rhino.png", "rhino.png")
.with_asset_at("data/example.csv", "example.csv"); .with_asset_at("data/example.csv", "example.csv");
test_with_path(&world, "main.typ", -1) test(&world, -2)
.must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")]) .must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")])
.must_exclude([q!("assets/tiger.jpg")]); .must_exclude([q!("assets/tiger.jpg")]);
test_with_path(&world, "content/c.typ", -1) test(&world, ("content/c.typ", -2))
.must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")]) .must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
.must_exclude([q!("c.typ")]); .must_exclude([q!("c.typ")]);
test_with_path(&world, "content/a.typ", -1) test(&world, ("content/a.typ", -2))
.must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")]) .must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")])
.must_exclude([q!("../data/example.csv"), q!("b.typ")]); .must_exclude([q!("../data/example.csv"), q!("b.typ")]);
test_with_path(&world, "content/b.typ", -2) test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]);
.must_include([q!("../data/example.csv")]);
} }
#[test] #[test]
fn test_autocomplete_figure_snippets() { fn test_autocomplete_figure_snippets() {
test("#figure()", -1) test("#figure()", -2)
.must_apply("image", "image(\"${}\"),") .must_apply("image", "image(\"${}\"),")
.must_apply("table", "table(\n ${}\n),"); .must_apply("table", "table(\n ${}\n),");
test("#figure(cap)", -1).must_apply("caption", "caption: [${}]"); test("#figure(cap)", -2).must_apply("caption", "caption: [${}]");
}
#[test]
fn test_autocomplete_import_items() {
let world = TestWorld::new("#import \"other.typ\": ")
.with_source("second.typ", "#import \"other.typ\": th")
.with_source("other.typ", "#let this = 1; #let that = 2");
test(&world, ("main.typ", 21))
.must_include(["*", "this", "that"])
.must_exclude(["figure"]);
test(&world, ("second.typ", 23))
.must_include(["this", "that"])
.must_exclude(["*", "figure"]);
}
#[test]
fn test_autocomplete_type_methods() {
test("#\"hello\".", -1).must_include(["len", "contains"]);
test("#table().", -1).must_exclude(["cell"]);
}
#[test]
fn test_autocomplete_content_methods() {
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
.must_include(["indented", "body", "page"]);
}
#[test]
fn test_autocomplete_symbol_variants() {
test("#sym.arrow.", -1)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
test("$ arrow. $", -3)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
} }
} }

View File

@ -55,8 +55,8 @@ pub fn definition(
} }
} }
if let Some(value) = globals(world, &leaf).get(&name) { if let Some(binding) = globals(world, &leaf).get(&name) {
return Some(Definition::Std(value.clone())); return Some(Definition::Std(binding.read().clone()));
} }
} }
@ -86,6 +86,7 @@ pub fn definition(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::borrow::Borrow;
use std::ops::Range; use std::ops::Range;
use typst::foundations::{IntoValue, NativeElement}; use typst::foundations::{IntoValue, NativeElement};
@ -93,7 +94,7 @@ mod tests {
use typst::WorldExt; use typst::WorldExt;
use super::{definition, Definition}; use super::{definition, Definition};
use crate::tests::{SourceExt, TestWorld}; use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = (TestWorld, Option<Definition>); type Response = (TestWorld, Option<Definition>);
@ -132,23 +133,19 @@ mod tests {
} }
#[track_caller] #[track_caller]
fn test(text: &str, cursor: isize, side: Side) -> Response { fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
let world = TestWorld::new(text); let world = world.acquire();
test_with_world(world, cursor, side) let world = world.borrow();
} let doc = typst::compile(world).output.ok();
let (source, cursor) = pos.resolve(world);
#[track_caller] let def = definition(world, doc.as_ref(), &source, cursor, side);
fn test_with_world(world: TestWorld, cursor: isize, side: Side) -> Response { (world.clone(), def)
let doc = typst::compile(&world).output.ok();
let source = &world.main;
let def = definition(&world, doc.as_ref(), source, source.cursor(cursor), side);
(world, def)
} }
#[test] #[test]
fn test_definition_let() { fn test_definition_let() {
test("#let x; #x", 9, Side::After).must_be_at("main.typ", 5..6); test("#let x; #x", -2, Side::After).must_be_at("main.typ", 5..6);
test("#let x() = {}; #x", 16, Side::After).must_be_at("main.typ", 5..6); test("#let x() = {}; #x", -2, Side::After).must_be_at("main.typ", 5..6);
} }
#[test] #[test]
@ -158,33 +155,33 @@ mod tests {
// The span is at the args here because that's what the function value's // The span is at the args here because that's what the function value's
// span is. Not ideal, but also not too big of a big deal. // span is. Not ideal, but also not too big of a big deal.
test_with_world(world, -1, Side::Before).must_be_at("other.typ", 8..11); test(&world, -2, Side::Before).must_be_at("other.typ", 8..11);
} }
#[test] #[test]
fn test_definition_cross_file() { fn test_definition_cross_file() {
let world = TestWorld::new("#import \"other.typ\": x; #x") let world = TestWorld::new("#import \"other.typ\": x; #x")
.with_source("other.typ", "#let x = 1"); .with_source("other.typ", "#let x = 1");
test_with_world(world, -1, Side::After).must_be_at("other.typ", 5..6); test(&world, -2, Side::After).must_be_at("other.typ", 5..6);
} }
#[test] #[test]
fn test_definition_import() { fn test_definition_import() {
let world = TestWorld::new("#import \"other.typ\" as o: x") let world = TestWorld::new("#import \"other.typ\" as o: x")
.with_source("other.typ", "#let x = 1"); .with_source("other.typ", "#let x = 1");
test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0); test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
} }
#[test] #[test]
fn test_definition_include() { fn test_definition_include() {
let world = TestWorld::new("#include \"other.typ\"") let world = TestWorld::new("#include \"other.typ\"")
.with_source("other.typ", "Hello there"); .with_source("other.typ", "Hello there");
test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0); test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
} }
#[test] #[test]
fn test_definition_ref() { fn test_definition_ref() {
test("#figure[] <hi> See @hi", 21, Side::After).must_be_at("main.typ", 1..9); test("#figure[] <hi> See @hi", -2, Side::After).must_be_at("main.typ", 1..9);
} }
#[test] #[test]

View File

@ -73,7 +73,10 @@ pub fn jump_from_click(
let Some(id) = span.id() else { continue }; let Some(id) = span.id() else { continue };
let source = world.source(id).ok()?; let source = world.source(id).ok()?;
let node = source.find(span)?; let node = source.find(span)?;
let pos = if node.kind() == SyntaxKind::Text { let pos = if matches!(
node.kind(),
SyntaxKind::Text | SyntaxKind::MathText
) {
let range = node.range(); let range = node.range();
let mut offset = range.start + usize::from(span_offset); let mut offset = range.start + usize::from(span_offset);
if (click.x - pos.x) > width / 2.0 { if (click.x - pos.x) > width / 2.0 {
@ -115,7 +118,7 @@ pub fn jump_from_cursor(
cursor: usize, cursor: usize,
) -> Vec<Position> { ) -> Vec<Position> {
fn is_text(node: &LinkedNode) -> bool { fn is_text(node: &LinkedNode) -> bool {
node.get().kind() == SyntaxKind::Text matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
} }
let root = LinkedNode::new(source.root()); let root = LinkedNode::new(source.root());
@ -182,12 +185,13 @@ mod tests {
//! )) //! ))
//! ``` //! ```
use std::borrow::Borrow;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use typst::layout::{Abs, Point, Position}; use typst::layout::{Abs, Point, Position};
use super::{jump_from_click, jump_from_cursor, Jump}; use super::{jump_from_click, jump_from_cursor, Jump};
use crate::tests::TestWorld; use crate::tests::{FilePos, TestWorld, WorldLike};
fn point(x: f64, y: f64) -> Point { fn point(x: f64, y: f64) -> Point {
Point::new(Abs::pt(x), Abs::pt(y)) Point::new(Abs::pt(x), Abs::pt(y))
@ -211,10 +215,11 @@ mod tests {
} }
#[track_caller] #[track_caller]
fn test_click(text: &str, click: Point, expected: Option<Jump>) { fn test_click(world: impl WorldLike, click: Point, expected: Option<Jump>) {
let world = TestWorld::new(text); let world = world.acquire();
let doc = typst::compile(&world).output.unwrap(); let world = world.borrow();
let jump = jump_from_click(&world, &doc, &doc.pages[0].frame, click); let doc = typst::compile(world).output.unwrap();
let jump = jump_from_click(world, &doc, &doc.pages[0].frame, click);
if let (Some(Jump::Position(pos)), Some(Jump::Position(expected))) = if let (Some(Jump::Position(pos)), Some(Jump::Position(expected))) =
(&jump, &expected) (&jump, &expected)
{ {
@ -227,10 +232,12 @@ mod tests {
} }
#[track_caller] #[track_caller]
fn test_cursor(text: &str, cursor: usize, expected: Option<Position>) { fn test_cursor(world: impl WorldLike, pos: impl FilePos, expected: Option<Position>) {
let world = TestWorld::new(text); let world = world.acquire();
let doc = typst::compile(&world).output.unwrap(); let world = world.borrow();
let pos = jump_from_cursor(&doc, &world.main, cursor); let doc = typst::compile(world).output.unwrap();
let (source, cursor) = pos.resolve(world);
let pos = jump_from_cursor(&doc, &source, cursor);
assert_eq!(!pos.is_empty(), expected.is_some()); assert_eq!(!pos.is_empty(), expected.is_some());
if let (Some(pos), Some(expected)) = (pos.first(), expected) { if let (Some(pos), Some(expected)) = (pos.first(), expected) {
assert_eq!(pos.page, expected.page); assert_eq!(pos.page, expected.page);
@ -257,6 +264,11 @@ mod tests {
test_click(s, point(21.0, 12.0), cursor(56)); test_click(s, point(21.0, 12.0), cursor(56));
} }
#[test]
fn test_jump_from_click_math() {
test_click("$a + b$", point(28.0, 14.0), cursor(5));
}
#[test] #[test]
fn test_jump_from_cursor() { fn test_jump_from_cursor() {
let s = "*Hello* #box[ABC] World"; let s = "*Hello* #box[ABC] World";
@ -264,6 +276,11 @@ mod tests {
test_cursor(s, 14, pos(1, 37.55, 16.58)); test_cursor(s, 14, pos(1, 37.55, 16.58));
} }
#[test]
fn test_jump_from_cursor_math() {
test_cursor("$a + b$", -3, pos(1, 27.51, 16.83));
}
#[test] #[test]
fn test_backlink() { fn test_backlink() {
let s = "#footnote[Hi]"; let s = "#footnote[Hi]";

View File

@ -1,7 +1,7 @@
use ecow::EcoString; use ecow::EcoString;
use typst::foundations::{Module, Value}; use typst::foundations::{Module, Value};
use typst::syntax::ast::AstNode; use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode}; use typst::syntax::{ast, LinkedNode, Span, SyntaxKind};
use crate::{analyze_import, IdeWorld}; use crate::{analyze_import, IdeWorld};
@ -30,38 +30,38 @@ pub fn named_items<T>(
if let Some(v) = node.cast::<ast::ModuleImport>() { if let Some(v) = node.cast::<ast::ModuleImport>() {
let imports = v.imports(); let imports = v.imports();
let source = node let source = v.source();
.children()
.find(|child| child.is::<ast::Expr>()) let source_value = node
.and_then(|source: LinkedNode| { .find(source.span())
Some((analyze_import(world, &source)?, source)) .and_then(|source| analyze_import(world, &source));
}); let source_value = source_value.as_ref();
let source = source.as_ref();
let module = source_value.and_then(|value| match value {
Value::Module(module) => Some(module),
_ => None,
});
let name_and_span = match (imports, v.new_name()) {
// ```plain
// import "foo" as name
// import "foo" as name: ..
// ```
(_, Some(name)) => Some((name.get().clone(), name.span())),
// ```plain
// import "foo"
// ```
(None, None) => v.bare_name().ok().map(|name| (name, source.span())),
// ```plain
// import "foo": ..
// ```
(Some(..), None) => None,
};
// Seeing the module itself. // Seeing the module itself.
if let Some((value, source)) = source { if let Some((name, span)) = name_and_span {
let site = match (imports, v.new_name()) { if let Some(res) = recv(NamedItem::Module(&name, span, module)) {
// ```plain return Some(res);
// import "foo" as name;
// import "foo" as name: ..;
// ```
(_, Some(name)) => Some(name.to_untyped()),
// ```plain
// import "foo";
// ```
(None, None) => Some(source.get()),
// ```plain
// import "foo": ..;
// ```
(Some(..), None) => None,
};
if let Some((site, value)) =
site.zip(value.clone().cast::<Module>().ok())
{
if let Some(res) = recv(NamedItem::Module(&value, site)) {
return Some(res);
}
} }
} }
@ -75,9 +75,13 @@ pub fn named_items<T>(
// import "foo": *; // import "foo": *;
// ``` // ```
Some(ast::Imports::Wildcard) => { Some(ast::Imports::Wildcard) => {
if let Some(scope) = source.and_then(|(value, _)| value.scope()) { if let Some(scope) = source_value.and_then(Value::scope) {
for (name, value, span) in scope.iter() { for (name, binding) in scope.iter() {
let item = NamedItem::Import(name, span, Some(value)); let item = NamedItem::Import(
name,
binding.span(),
Some(binding.read()),
);
if let Some(res) = recv(item) { if let Some(res) = recv(item) {
return Some(res); return Some(res);
} }
@ -89,18 +93,26 @@ pub fn named_items<T>(
// ``` // ```
Some(ast::Imports::Items(items)) => { Some(ast::Imports::Items(items)) => {
for item in items.iter() { for item in items.iter() {
let original = item.original_name(); let mut iter = item.path().iter();
let bound = item.bound_name(); let mut binding = source_value
let scope = source.and_then(|(value, _)| value.scope()); .and_then(Value::scope)
let span = scope .zip(iter.next())
.and_then(|s| s.get_span(&original)) .and_then(|(scope, first)| scope.get(&first));
.unwrap_or(Span::detached())
.or(bound.span());
let value = scope.and_then(|s| s.get(&original)); for ident in iter {
if let Some(res) = binding = binding.and_then(|binding| {
recv(NamedItem::Import(bound.get(), span, value)) binding.read().scope()?.get(&ident)
{ });
}
let bound = item.bound_name();
let (span, value) = match binding {
Some(binding) => (binding.span(), Some(binding.read())),
None => (bound.span(), None),
};
let item = NamedItem::Import(bound.get(), span, value);
if let Some(res) = recv(item) {
return Some(res); return Some(res);
} }
} }
@ -169,8 +181,8 @@ pub enum NamedItem<'a> {
Var(ast::Ident<'a>), Var(ast::Ident<'a>),
/// A function item. /// A function item.
Fn(ast::Ident<'a>), Fn(ast::Ident<'a>),
/// A (imported) module item. /// A (imported) module.
Module(&'a Module, &'a SyntaxNode), Module(&'a EcoString, Span, Option<&'a Module>),
/// An imported item. /// An imported item.
Import(&'a EcoString, Span, Option<&'a Value>), Import(&'a EcoString, Span, Option<&'a Value>),
} }
@ -180,7 +192,7 @@ impl<'a> NamedItem<'a> {
match self { match self {
NamedItem::Var(ident) => ident.get(), NamedItem::Var(ident) => ident.get(),
NamedItem::Fn(ident) => ident.get(), NamedItem::Fn(ident) => ident.get(),
NamedItem::Module(value, _) => value.name(), NamedItem::Module(name, _, _) => name,
NamedItem::Import(name, _, _) => name, NamedItem::Import(name, _, _) => name,
} }
} }
@ -188,7 +200,7 @@ impl<'a> NamedItem<'a> {
pub(crate) fn value(&self) -> Option<Value> { pub(crate) fn value(&self) -> Option<Value> {
match self { match self {
NamedItem::Var(..) | NamedItem::Fn(..) => None, NamedItem::Var(..) | NamedItem::Fn(..) => None,
NamedItem::Module(value, _) => Some(Value::Module((*value).clone())), NamedItem::Module(_, _, value) => value.cloned().map(Value::Module),
NamedItem::Import(_, _, value) => value.cloned(), NamedItem::Import(_, _, value) => value.cloned(),
} }
} }
@ -196,7 +208,7 @@ impl<'a> NamedItem<'a> {
pub(crate) fn span(&self) -> Span { pub(crate) fn span(&self) -> Span {
match *self { match *self {
NamedItem::Var(name) | NamedItem::Fn(name) => name.span(), NamedItem::Var(name) | NamedItem::Fn(name) => name.span(),
NamedItem::Module(_, site) => site.span(), NamedItem::Module(_, span, _) => span,
NamedItem::Import(_, span, _) => span, NamedItem::Import(_, span, _) => span,
} }
} }
@ -220,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
ast::Expr::FuncCall(call) => { ast::Expr::FuncCall(call) => {
DerefTarget::Callee(expr_node.find(call.callee().span())?) DerefTarget::Callee(expr_node.find(call.callee().span())?)
} }
ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?), ast::Expr::SetRule(set) => {
DerefTarget::Callee(expr_node.find(set.target().span())?)
}
ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => { ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => {
DerefTarget::VarAccess(expr_node) DerefTarget::VarAccess(expr_node)
} }
@ -266,53 +280,105 @@ pub enum DerefTarget<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::borrow::Borrow;
use ecow::EcoString;
use typst::foundations::Value;
use typst::syntax::{LinkedNode, Side}; use typst::syntax::{LinkedNode, Side};
use crate::{named_items, tests::TestWorld}; use super::named_items;
use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = Vec<(EcoString, Option<Value>)>;
trait ResponseExt {
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self;
fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self;
}
impl ResponseExt for Response {
#[track_caller]
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in includes {
assert!(
self.iter().any(|v| v.0 == item),
"{item:?} was not contained in {self:?}",
);
}
self
}
#[track_caller]
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
for item in excludes {
assert!(
!self.iter().any(|v| v.0 == item),
"{item:?} was wrongly contained in {self:?}",
);
}
self
}
#[track_caller]
fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self {
assert!(
self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value),
"{name_value:?} was not contained in {self:?}",
);
self
}
}
#[track_caller] #[track_caller]
fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool { fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
let world = TestWorld::new(text); let world = world.acquire();
let world = world.borrow();
let src = world.main.clone(); let (source, cursor) = pos.resolve(world);
let node = LinkedNode::new(src.root()); let node = LinkedNode::new(source.root());
let leaf = node.leaf_at(cursor, Side::After).unwrap(); let leaf = node.leaf_at(cursor, Side::After).unwrap();
let mut items = vec![];
let res = named_items(&world, leaf, |s| { named_items(world, leaf, |s| {
if containing == s.name() { items.push((s.name().clone(), s.value().clone()));
return Some(true); None::<()>
}
None
}); });
items
res.unwrap_or_default()
} }
#[test] #[test]
fn test_simple_named_items() { fn test_named_items_simple() {
// Has named items let s = "#let a = 1;#let b = 2;";
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a")); test(s, 8).must_include(["a"]).must_exclude(["b"]);
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a")); test(s, 15).must_include(["b"]);
// Doesn't have named items
assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b"));
} }
#[test] #[test]
fn test_param_named_items() { fn test_named_items_param() {
// Has named items let pos = "#let f(a) = 1;#let b = 2;";
assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a")); test(pos, 12).must_include(["a"]);
assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a")); test(pos, 19).must_include(["b", "f"]).must_exclude(["a"]);
// Doesn't have named items let named = "#let f(a: b) = 1;#let b = 2;";
assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a")); test(named, 15).must_include(["a", "f"]).must_exclude(["b"]);
assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b"));
} }
#[test] #[test]
fn test_import_named_items() { fn test_named_items_import() {
// Cannot test much. test("#import \"foo.typ\"", 2).must_include(["foo"]);
assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a")); test("#import \"foo.typ\" as bar", 2)
.must_include(["bar"])
.must_exclude(["foo"]);
}
#[test]
fn test_named_items_import_items() {
test("#import \"foo.typ\": a; #(a);", 2)
.must_include(["a"])
.must_exclude(["foo"]);
let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);")
.with_source("foo.typ", "#import \"a.typ\"")
.with_source("a.typ", "#let b = 1;");
test(&world, 2).must_include_value(("b", Some(&Value::Int(1))));
} }
} }

View File

@ -1,4 +1,6 @@
use std::borrow::Borrow;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use ecow::EcoString; use ecow::EcoString;
use typst::diag::{FileError, FileResult}; use typst::diag::{FileError, FileResult};
@ -13,10 +15,10 @@ use typst::{Library, World};
use crate::IdeWorld; use crate::IdeWorld;
/// A world for IDE testing. /// A world for IDE testing.
#[derive(Clone)]
pub struct TestWorld { pub struct TestWorld {
pub main: Source, pub main: Source,
assets: HashMap<FileId, Bytes>, files: Arc<TestFiles>,
sources: HashMap<FileId, Source>,
base: &'static TestBase, base: &'static TestBase,
} }
@ -29,8 +31,7 @@ impl TestWorld {
let main = Source::new(Self::main_id(), text.into()); let main = Source::new(Self::main_id(), text.into());
Self { Self {
main, main,
assets: HashMap::new(), files: Arc::new(TestFiles::default()),
sources: HashMap::new(),
base: singleton!(TestBase, TestBase::default()), base: singleton!(TestBase, TestBase::default()),
} }
} }
@ -39,7 +40,7 @@ impl TestWorld {
pub fn with_source(mut self, path: &str, text: &str) -> Self { pub fn with_source(mut self, path: &str, text: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path)); let id = FileId::new(None, VirtualPath::new(path));
let source = Source::new(id, text.into()); let source = Source::new(id, text.into());
self.sources.insert(id, source); Arc::make_mut(&mut self.files).sources.insert(id, source);
self self
} }
@ -54,8 +55,8 @@ impl TestWorld {
pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self { pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self {
let id = FileId::new(None, VirtualPath::new(path)); let id = FileId::new(None, VirtualPath::new(path));
let data = typst_dev_assets::get_by_name(filename).unwrap(); let data = typst_dev_assets::get_by_name(filename).unwrap();
let bytes = Bytes::from_static(data); let bytes = Bytes::new(data);
self.assets.insert(id, bytes); Arc::make_mut(&mut self.files).assets.insert(id, bytes);
self self
} }
@ -81,7 +82,7 @@ impl World for TestWorld {
fn source(&self, id: FileId) -> FileResult<Source> { fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.main.id() { if id == self.main.id() {
Ok(self.main.clone()) Ok(self.main.clone())
} else if let Some(source) = self.sources.get(&id) { } else if let Some(source) = self.files.sources.get(&id) {
Ok(source.clone()) Ok(source.clone())
} else { } else {
Err(FileError::NotFound(id.vpath().as_rootless_path().into())) Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
@ -89,7 +90,7 @@ impl World for TestWorld {
} }
fn file(&self, id: FileId) -> FileResult<Bytes> { fn file(&self, id: FileId) -> FileResult<Bytes> {
match self.assets.get(&id) { match self.files.assets.get(&id) {
Some(bytes) => Ok(bytes.clone()), Some(bytes) => Ok(bytes.clone()),
None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())), None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())),
} }
@ -111,8 +112,8 @@ impl IdeWorld for TestWorld {
fn files(&self) -> Vec<FileId> { fn files(&self) -> Vec<FileId> {
std::iter::once(self.main.id()) std::iter::once(self.main.id())
.chain(self.sources.keys().copied()) .chain(self.files.sources.keys().copied())
.chain(self.assets.keys().copied()) .chain(self.files.assets.keys().copied())
.collect() .collect()
} }
@ -133,20 +134,11 @@ impl IdeWorld for TestWorld {
} }
} }
/// Extra methods for [`Source`]. /// Test-specific files.
pub trait SourceExt { #[derive(Default, Clone)]
/// Negative cursors index from the back. struct TestFiles {
fn cursor(&self, cursor: isize) -> usize; assets: HashMap<FileId, Bytes>,
} sources: HashMap<FileId, Source>,
impl SourceExt for Source {
fn cursor(&self, cursor: isize) -> usize {
if cursor < 0 {
self.len_bytes().checked_add_signed(cursor).unwrap()
} else {
cursor as usize
}
}
} }
/// Shared foundation of all test worlds. /// Shared foundation of all test worlds.
@ -160,7 +152,7 @@ impl Default for TestBase {
fn default() -> Self { fn default() -> Self {
let fonts: Vec<_> = typst_assets::fonts() let fonts: Vec<_> = typst_assets::fonts()
.chain(typst_dev_assets::fonts()) .chain(typst_dev_assets::fonts())
.flat_map(|data| Font::iter(Bytes::from_static(data))) .flat_map(|data| Font::iter(Bytes::new(data)))
.collect(); .collect();
Self { Self {
@ -186,3 +178,58 @@ fn library() -> Library {
lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into()))); lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
lib lib
} }
/// The input to a test: Either just a string or a full `TestWorld`.
pub trait WorldLike {
type World: Borrow<TestWorld>;
fn acquire(self) -> Self::World;
}
impl<'a> WorldLike for &'a TestWorld {
type World = &'a TestWorld;
fn acquire(self) -> Self::World {
self
}
}
impl WorldLike for &str {
type World = TestWorld;
fn acquire(self) -> Self::World {
TestWorld::new(self)
}
}
/// Specifies a position in a file for a test.
pub trait FilePos {
fn resolve(self, world: &TestWorld) -> (Source, usize);
}
impl FilePos for isize {
#[track_caller]
fn resolve(self, world: &TestWorld) -> (Source, usize) {
(world.main.clone(), cursor(&world.main, self))
}
}
impl FilePos for (&str, isize) {
#[track_caller]
fn resolve(self, world: &TestWorld) -> (Source, usize) {
let id = FileId::new(None, VirtualPath::new(self.0));
let source = world.source(id).unwrap();
let cursor = cursor(&source, self.1);
(source, cursor)
}
}
/// Resolve a signed index (negative from the back) to a unsigned index.
#[track_caller]
fn cursor(source: &Source, cursor: isize) -> usize {
if cursor < 0 {
source.len_bytes().checked_add_signed(cursor + 1).unwrap()
} else {
cursor as usize
}
}

View File

@ -3,7 +3,7 @@ use std::fmt::Write;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use typst::engine::Sink; use typst::engine::Sink;
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; use typst::foundations::{repr, Binding, Capturer, CastInfo, Repr, Value};
use typst::layout::{Length, PagedDocument}; use typst::layout::{Length, PagedDocument};
use typst::syntax::ast::AstNode; use typst::syntax::ast::AstNode;
use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind};
@ -201,12 +201,17 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Toolti
if let Some(expr) = grand_grand.cast::<ast::Expr>(); if let Some(expr) = grand_grand.cast::<ast::Expr>();
if let Some(ast::Expr::Ident(callee)) = match expr { if let Some(ast::Expr::Ident(callee)) = match expr {
ast::Expr::FuncCall(call) => Some(call.callee()), ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()), ast::Expr::SetRule(set) => Some(set.target()),
_ => None, _ => None,
}; };
// Find metadata about the function. // Find metadata about the function.
if let Some(Value::Func(func)) = world.library().global.scope().get(&callee); if let Some(Value::Func(func)) = world
.library()
.global
.scope()
.get(&callee)
.map(Binding::read);
then { (func, named) } then { (func, named) }
else { return None; } else { return None; }
}; };
@ -274,10 +279,12 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::borrow::Borrow;
use typst::syntax::Side; use typst::syntax::Side;
use super::{tooltip, Tooltip}; use super::{tooltip, Tooltip};
use crate::tests::{SourceExt, TestWorld}; use crate::tests::{FilePos, TestWorld, WorldLike};
type Response = Option<Tooltip>; type Response = Option<Tooltip>;
@ -308,21 +315,17 @@ mod tests {
} }
#[track_caller] #[track_caller]
fn test(text: &str, cursor: isize, side: Side) -> Response { fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
let world = TestWorld::new(text); let world = world.acquire();
test_with_world(&world, cursor, side) let world = world.borrow();
} let (source, cursor) = pos.resolve(world);
let doc = typst::compile(world).output.ok();
#[track_caller] tooltip(world, doc.as_ref(), &source, cursor, side)
fn test_with_world(world: &TestWorld, cursor: isize, side: Side) -> Response {
let source = &world.main;
let doc = typst::compile(&world).output.ok();
tooltip(world, doc.as_ref(), source, source.cursor(cursor), side)
} }
#[test] #[test]
fn test_tooltip() { fn test_tooltip() {
test("#let x = 1 + 2", 14, Side::After).must_be_none(); test("#let x = 1 + 2", -1, Side::After).must_be_none();
test("#let x = 1 + 2", 5, Side::After).must_be_code("3"); test("#let x = 1 + 2", 5, Side::After).must_be_code("3");
test("#let x = 1 + 2", 6, Side::Before).must_be_code("3"); test("#let x = 1 + 2", 6, Side::Before).must_be_code("3");
test("#let x = 1 + 2", 6, Side::Before).must_be_code("3"); test("#let x = 1 + 2", 6, Side::Before).must_be_code("3");
@ -330,7 +333,7 @@ mod tests {
#[test] #[test]
fn test_tooltip_empty_contextual() { fn test_tooltip_empty_contextual() {
test("#{context}", 10, Side::Before).must_be_code("context()"); test("#{context}", -1, Side::Before).must_be_code("context()");
} }
#[test] #[test]
@ -354,12 +357,18 @@ mod tests {
.must_be_text("This closure captures `f` and `y`"); .must_be_text("This closure captures `f` and `y`");
} }
#[test]
fn test_tooltip_import() {
let world = TestWorld::new("#import \"other.typ\": a, b")
.with_source("other.typ", "#let (a, b, c) = (1, 2, 3)");
test(&world, -5, Side::After).must_be_code("1");
}
#[test] #[test]
fn test_tooltip_star_import() { fn test_tooltip_star_import() {
let world = TestWorld::new("#import \"other.typ\": *") let world = TestWorld::new("#import \"other.typ\": *")
.with_source("other.typ", "#let (a, b, c) = (1, 2, 3)"); .with_source("other.typ", "#let (a, b, c) = (1, 2, 3)");
test_with_world(&world, 21, Side::Before).must_be_none(); test(&world, -2, Side::Before).must_be_none();
test_with_world(&world, 21, Side::After) test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`");
.must_be_text("This star imports `a`, `b`, and `c`");
} }
} }

View File

@ -171,7 +171,7 @@ where
self.find_iter(content.fields().iter().map(|(_, v)| v))?; self.find_iter(content.fields().iter().map(|(_, v)| v))?;
} }
Value::Module(module) => { Value::Module(module) => {
self.find_iter(module.scope().iter().map(|(_, v, _)| v))?; self.find_iter(module.scope().iter().map(|(_, b)| b.read()))?;
} }
_ => {} _ => {}
} }

View File

@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true }
fontdb = { workspace = true, optional = true } fontdb = { workspace = true, optional = true }
native-tls = { workspace = true, optional = true } native-tls = { workspace = true, optional = true }
once_cell = { workspace = true } once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tar = { workspace = true, optional = true } tar = { workspace = true, optional = true }
ureq = { workspace = true, optional = true } ureq = { workspace = true, optional = true }

View File

@ -13,6 +13,7 @@ use std::path::{Path, PathBuf};
use std::sync::OnceLock; use std::sync::OnceLock;
use fontdb::{Database, Source}; use fontdb::{Database, Source};
use typst_library::foundations::Bytes;
use typst_library::text::{Font, FontBook, FontInfo}; use typst_library::text::{Font, FontBook, FontInfo};
use typst_timing::TimingScope; use typst_timing::TimingScope;
@ -52,9 +53,8 @@ impl FontSlot {
.as_ref() .as_ref()
.expect("`path` is not `None` if `font` is uninitialized"), .expect("`path` is not `None` if `font` is uninitialized"),
) )
.ok()? .ok()?;
.into(); Font::new(Bytes::new(data), self.index)
Font::new(data, self.index)
}) })
.clone() .clone()
} }
@ -196,7 +196,7 @@ impl FontSearcher {
#[cfg(feature = "embed-fonts")] #[cfg(feature = "embed-fonts")]
fn add_embedded(&mut self) { fn add_embedded(&mut self) {
for data in typst_assets::fonts() { for data in typst_assets::fonts() {
let buffer = typst_library::foundations::Bytes::from_static(data); let buffer = Bytes::new(data);
for (i, font) in Font::iter(buffer).enumerate() { for (i, font) in Font::iter(buffer).enumerate() {
self.book.push(font.info().clone()); self.book.push(font.info().clone());
self.fonts.push(FontSlot { self.fonts.push(FontSlot {

View File

@ -5,10 +5,9 @@ use std::path::{Path, PathBuf};
use ecow::eco_format; use ecow::eco_format;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::Deserialize;
use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
use typst_syntax::package::{ use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
};
use crate::download::{Downloader, Progress}; use crate::download::{Downloader, Progress};
@ -32,7 +31,7 @@ pub struct PackageStorage {
/// The downloader used for fetching the index and packages. /// The downloader used for fetching the index and packages.
downloader: Downloader, downloader: Downloader,
/// The cached index of the default namespace. /// The cached index of the default namespace.
index: OnceCell<Vec<PackageInfo>>, index: OnceCell<Vec<serde_json::Value>>,
} }
impl PackageStorage { impl PackageStorage {
@ -42,6 +41,18 @@ impl PackageStorage {
package_cache_path: Option<PathBuf>, package_cache_path: Option<PathBuf>,
package_path: Option<PathBuf>, package_path: Option<PathBuf>,
downloader: Downloader, downloader: Downloader,
) -> Self {
Self::with_index(package_cache_path, package_path, downloader, OnceCell::new())
}
/// Creates a new package storage with a pre-defined index.
///
/// Useful for testing.
fn with_index(
package_cache_path: Option<PathBuf>,
package_path: Option<PathBuf>,
downloader: Downloader,
index: OnceCell<Vec<serde_json::Value>>,
) -> Self { ) -> Self {
Self { Self {
package_cache_path: package_cache_path.or_else(|| { package_cache_path: package_cache_path.or_else(|| {
@ -51,7 +62,7 @@ impl PackageStorage {
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR)) dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
}), }),
downloader, downloader,
index: OnceCell::new(), index,
} }
} }
@ -109,6 +120,7 @@ impl PackageStorage {
// version. // version.
self.download_index()? self.download_index()?
.iter() .iter()
.filter_map(|value| MinimalPackageInfo::deserialize(value).ok())
.filter(|package| package.name == spec.name) .filter(|package| package.name == spec.name)
.map(|package| package.version) .map(|package| package.version)
.max() .max()
@ -131,7 +143,7 @@ impl PackageStorage {
} }
/// Download the package index. The result of this is cached for efficiency. /// Download the package index. The result of this is cached for efficiency.
pub fn download_index(&self) -> StrResult<&[PackageInfo]> { pub fn download_index(&self) -> StrResult<&[serde_json::Value]> {
self.index self.index
.get_or_try_init(|| { .get_or_try_init(|| {
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
@ -186,3 +198,54 @@ impl PackageStorage {
}) })
} }
} }
/// Minimal information required about a package to determine its latest
/// version.
#[derive(Deserialize)]
struct MinimalPackageInfo {
name: String,
version: PackageVersion,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lazy_deser_index() {
let storage = PackageStorage::with_index(
None,
None,
Downloader::new("typst/test"),
OnceCell::with_value(vec![
serde_json::json!({
"name": "charged-ieee",
"version": "0.1.0",
"entrypoint": "lib.typ",
}),
serde_json::json!({
"name": "unequivocal-ams",
// This version number is currently not valid, so this package
// can't be parsed.
"version": "0.2.0-dev",
"entrypoint": "lib.typ",
}),
]),
);
let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec {
namespace: "preview".into(),
name: "charged-ieee".into(),
});
assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 }));
let ams_version = storage.determine_latest_version(&VersionlessPackageSpec {
namespace: "preview".into(),
name: "unequivocal-ams".into(),
});
assert_eq!(
ams_version,
Err("failed to find package @preview/unequivocal-ams".into())
)
}
}

View File

@ -364,6 +364,12 @@ fn breakable_pod<'a>(
/// Distribute a fixed height spread over existing regions into a new first /// Distribute a fixed height spread over existing regions into a new first
/// height and a new backlog. /// height and a new backlog.
///
/// Note that, if the given height fits within the first region, no backlog is
/// generated and the first region's height shrinks to fit exactly the given
/// height. In particular, negative and zero heights always fit in any region,
/// so such heights are always directly returned as the new first region
/// height.
fn distribute<'a>( fn distribute<'a>(
height: Abs, height: Abs,
mut regions: Regions, mut regions: Regions,
@ -371,7 +377,19 @@ fn distribute<'a>(
) -> (Abs, &'a mut [Abs]) { ) -> (Abs, &'a mut [Abs]) {
// Build new region heights from old regions. // Build new region heights from old regions.
let mut remaining = height; let mut remaining = height;
// Negative and zero heights always fit, so just keep them.
// No backlog is generated.
if remaining <= Abs::zero() {
buf.push(remaining);
return (buf[0], &mut buf[1..]);
}
loop { loop {
// This clamp is safe (min <= max), as 'remaining' won't be negative
// due to the initial check above (on the first iteration) and due to
// stopping on 'remaining.approx_empty()' below (for the second
// iteration onwards).
let limited = regions.size.y.clamp(Abs::zero(), remaining); let limited = regions.size.y.clamp(Abs::zero(), remaining);
buf.push(limited); buf.push(limited);
remaining -= limited; remaining -= limited;

View File

@ -20,12 +20,16 @@ use typst_library::model::ParElem;
use typst_library::routines::{Pair, Routines}; use typst_library::routines::{Pair, Routines};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::World; use typst_library::World;
use typst_utils::SliceExt;
use super::{layout_multi_block, layout_single_block}; use super::{layout_multi_block, layout_single_block, FlowMode};
use crate::inline::ParSituation;
use crate::modifiers::layout_and_modify;
/// Collects all elements of the flow into prepared children. These are much /// Collects all elements of the flow into prepared children. These are much
/// simpler to handle than the raw elements. /// simpler to handle than the raw elements.
#[typst_macros::time] #[typst_macros::time]
#[allow(clippy::too_many_arguments)]
pub fn collect<'a>( pub fn collect<'a>(
engine: &mut Engine, engine: &mut Engine,
bump: &'a Bump, bump: &'a Bump,
@ -33,6 +37,7 @@ pub fn collect<'a>(
locator: Locator<'a>, locator: Locator<'a>,
base: Size, base: Size,
expand: bool, expand: bool,
mode: FlowMode,
) -> SourceResult<Vec<Child<'a>>> { ) -> SourceResult<Vec<Child<'a>>> {
Collector { Collector {
engine, engine,
@ -42,9 +47,9 @@ pub fn collect<'a>(
base, base,
expand, expand,
output: Vec::with_capacity(children.len()), output: Vec::with_capacity(children.len()),
last_was_par: false, par_situation: ParSituation::First,
} }
.run() .run(mode)
} }
/// State for collection. /// State for collection.
@ -56,12 +61,20 @@ struct Collector<'a, 'x, 'y> {
expand: bool, expand: bool,
locator: SplitLocator<'a>, locator: SplitLocator<'a>,
output: Vec<Child<'a>>, output: Vec<Child<'a>>,
last_was_par: bool, par_situation: ParSituation,
} }
impl<'a> Collector<'a, '_, '_> { impl<'a> Collector<'a, '_, '_> {
/// Perform the collection. /// Perform the collection.
fn run(mut self) -> SourceResult<Vec<Child<'a>>> { fn run(self, mode: FlowMode) -> SourceResult<Vec<Child<'a>>> {
match mode {
FlowMode::Root | FlowMode::Block => self.run_block(),
FlowMode::Inline => self.run_inline(),
}
}
/// Perform collection for block-level children.
fn run_block(mut self) -> SourceResult<Vec<Child<'a>>> {
for &(child, styles) in self.children { for &(child, styles) in self.children {
if let Some(elem) = child.to_packed::<TagElem>() { if let Some(elem) = child.to_packed::<TagElem>() {
self.output.push(Child::Tag(&elem.tag)); self.output.push(Child::Tag(&elem.tag));
@ -94,6 +107,42 @@ impl<'a> Collector<'a, '_, '_> {
Ok(self.output) Ok(self.output)
} }
/// Perform collection for inline-level children.
fn run_inline(mut self) -> SourceResult<Vec<Child<'a>>> {
// Extract leading and trailing tags.
let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::<TagElem>());
let inner = &self.children[start..end];
// Compute the shared styles, ignoring tags.
let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default();
// Layout the lines.
let lines = crate::inline::layout_inline(
self.engine,
inner,
&mut self.locator,
styles,
self.base,
self.expand,
)?
.into_frames();
for (c, _) in &self.children[..start] {
let elem = c.to_packed::<TagElem>().unwrap();
self.output.push(Child::Tag(&elem.tag));
}
let leading = ParElem::leading_in(styles);
self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] {
let elem = c.to_packed::<TagElem>().unwrap();
self.output.push(Child::Tag(&elem.tag));
}
Ok(self.output)
}
/// Collect vertical spacing into a relative or fractional child. /// Collect vertical spacing into a relative or fractional child.
fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) { fn v(&mut self, elem: &'a Packed<VElem>, styles: StyleChain<'a>) {
self.output.push(match elem.amount { self.output.push(match elem.amount {
@ -109,24 +158,35 @@ impl<'a> Collector<'a, '_, '_> {
elem: &'a Packed<ParElem>, elem: &'a Packed<ParElem>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<()> { ) -> SourceResult<()> {
let align = AlignElem::alignment_in(styles).resolve(styles); let lines = crate::inline::layout_par(
let leading = ParElem::leading_in(styles); elem,
let spacing = ParElem::spacing_in(styles);
let costs = TextElem::costs_in(styles);
let lines = crate::layout_inline(
self.engine, self.engine,
&elem.children,
self.locator.next(&elem.span()), self.locator.next(&elem.span()),
styles, styles,
self.last_was_par,
self.base, self.base,
self.expand, self.expand,
self.par_situation,
)? )?
.into_frames(); .into_frames();
let spacing = elem.spacing(styles);
let leading = elem.leading(styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
self.lines(lines, leading, styles);
self.output.push(Child::Rel(spacing.into(), 4));
self.par_situation = ParSituation::Consecutive;
Ok(())
}
/// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles);
let costs = TextElem::costs_in(styles);
// Determine whether to prevent widow and orphans. // Determine whether to prevent widow and orphans.
let len = lines.len(); let len = lines.len();
let prevent_orphans = let prevent_orphans =
@ -165,11 +225,6 @@ impl<'a> Collector<'a, '_, '_> {
self.output self.output
.push(Child::Line(self.boxed(LineChild { frame, align, need }))); .push(Child::Line(self.boxed(LineChild { frame, align, need })));
} }
self.output.push(Child::Rel(spacing.into(), 4));
self.last_was_par = true;
Ok(())
} }
/// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on
@ -218,7 +273,7 @@ impl<'a> Collector<'a, '_, '_> {
}; };
self.output.push(spacing(elem.below(styles))); self.output.push(spacing(elem.below(styles)));
self.last_was_par = false; self.par_situation = ParSituation::Other;
} }
/// Collects a placed element into a [`PlacedChild`]. /// Collects a placed element into a [`PlacedChild`].
@ -377,8 +432,9 @@ fn layout_single_impl(
route: Route::extend(route), route: Route::extend(route),
}; };
layout_single_block(elem, &mut engine, locator, styles, region) layout_and_modify(styles, |styles| {
.map(|frame| frame.post_processed(styles)) layout_single_block(elem, &mut engine, locator, styles, region)
})
} }
/// A child that encapsulates a prepared breakable block. /// A child that encapsulates a prepared breakable block.
@ -473,11 +529,8 @@ fn layout_multi_impl(
route: Route::extend(route), route: Route::extend(route),
}; };
layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { layout_and_modify(styles, |styles| {
for frame in &mut fragment { layout_multi_block(elem, &mut engine, locator, styles, regions)
frame.post_process(styles);
}
fragment
}) })
} }
@ -579,20 +632,23 @@ impl PlacedChild<'_> {
self.cell.get_or_init(base, |base| { self.cell.get_or_init(base, |base| {
let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let align = self.alignment.unwrap_or_else(|| Alignment::CENTER);
let aligned = AlignElem::set_alignment(align).wrap(); let aligned = AlignElem::set_alignment(align).wrap();
let styles = self.styles.chain(&aligned);
let mut frame = crate::layout_frame( let mut frame = layout_and_modify(styles, |styles| {
engine, crate::layout_frame(
&self.elem.body, engine,
self.locator.relayout(), &self.elem.body,
self.styles.chain(&aligned), self.locator.relayout(),
Region::new(base, Axes::splat(false)), styles,
)?; Region::new(base, Axes::splat(false)),
)
})?;
if self.float { if self.float {
frame.set_parent(self.elem.location().unwrap()); frame.set_parent(self.elem.location().unwrap());
} }
Ok(frame.post_processed(self.styles)) Ok(frame)
}) })
} }

View File

@ -15,9 +15,11 @@ use typst_library::model::{
FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker, FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker,
}; };
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::NonZeroExt; use typst_utils::{NonZeroExt, Numeric};
use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; use super::{
distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work,
};
/// Composes the contents of a single page/region. A region can have multiple /// Composes the contents of a single page/region. A region can have multiple
/// columns/subregions. /// columns/subregions.
@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
migratable: bool, migratable: bool,
) -> FlowResult<()> { ) -> FlowResult<()> {
// Footnotes are only supported at the root level. // Footnotes are only supported at the root level.
if !self.config.root { if self.config.mode != FlowMode::Root {
return Ok(()); return Ok(());
} }
@ -374,7 +376,11 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
let mut relayout = false; let mut relayout = false;
let mut regions = *regions; let mut regions = *regions;
let mut migratable = migratable && !breakable && regions.may_progress();
// The first footnote's origin frame should be migratable if the region
// may progress (already checked by the footnote function) and if the
// origin frame isn't breakable (checked here).
let mut migratable = migratable && !breakable;
for (y, elem) in notes { for (y, elem) in notes {
// The amount of space used by the in-flow content that contains the // The amount of space used by the in-flow content that contains the
@ -464,11 +470,35 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
// If the first frame is empty, then none of its content fit. If // If the first frame is empty, then none of its content fit. If
// possible, we then migrate the origin frame to the next region to // possible, we then migrate the origin frame to the next region to
// uphold the footnote invariant (that marker and entry are on the same // uphold the footnote invariant (that marker and entry are on the same
// page). If not, we just queue the footnote for the next page. // page). If not, we just queue the footnote for the next page, but
// only if that would actually make a difference (that is, if the
// footnote isn't alone in the page after not fitting in any previous
// pages, as it probably won't ever fit then).
//
// Note that a non-zero flow need also indicates that queueing would
// make a difference, because the flow need is subtracted from the
// available height in the entry's pod even if what caused that need
// wasn't considered for the input `regions`. For example, floats just
// pass the `regions` they received along to their footnotes, which
// don't take into account the space occupied by the floats themselves,
// but they do indicate their footnotes have a non-zero flow need, so
// queueing them can matter as, in the following pages, the flow need
// will be set to zero and the footnote will be alone in the page.
// Then, `may_progress()` will also be false (this time, correctly) and
// the footnote is laid out, as queueing wouldn't improve the lack of
// space anymore and would result in an infinite loop.
//
// However, it is worth noting that migration does take into account
// the original region, before inserting what prompted the flow need.
// Logically, if moving the original frame can't improve the lack of
// space, then migration should be inhibited. The space occupied by the
// original frame is not relevant for that check. Therefore,
// `regions.may_progress()` must still be checked separately for
// migration, regardless of the presence of flow need.
if first.is_empty() && exist_non_empty_frame { if first.is_empty() && exist_non_empty_frame {
if migratable { if migratable && regions.may_progress() {
return Err(Stop::Finish(false)); return Err(Stop::Finish(false));
} else { } else if regions.may_progress() || !flow_need.is_zero() {
self.footnote_queue.push(elem); self.footnote_queue.push(elem);
return Ok(()); return Ok(());
} }

View File

@ -25,7 +25,7 @@ use typst_library::layout::{
Regions, Rel, Size, Regions, Rel, Size,
}; };
use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::World; use typst_library::World;
use typst_utils::{NonZeroExt, Numeric}; use typst_utils::{NonZeroExt, Numeric};
@ -140,9 +140,10 @@ fn layout_fragment_impl(
engine.route.check_layout_depth().at(content.span())?; engine.route.check_layout_depth().at(content.span())?;
let mut kind = FragmentKind::Block;
let arenas = Arenas::default(); let arenas = Arenas::default();
let children = (engine.routines.realize)( let children = (engine.routines.realize)(
RealizationKind::LayoutFragment, RealizationKind::LayoutFragment(&mut kind),
&mut engine, &mut engine,
&mut locator, &mut locator,
&arenas, &arenas,
@ -158,56 +159,45 @@ fn layout_fragment_impl(
regions, regions,
columns, columns,
column_gutter, column_gutter,
false, kind.into(),
) )
} }
/// The mode a flow can be laid out in.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum FlowMode {
/// A root flow with block-level elements. Like `FlowMode::Block`, but can
/// additionally host footnotes and line numbers.
Root,
/// A flow whose children are block-level elements.
Block,
/// A flow whose children are inline-level elements.
Inline,
}
impl From<FragmentKind> for FlowMode {
fn from(value: FragmentKind) -> Self {
match value {
FragmentKind::Inline => Self::Inline,
FragmentKind::Block => Self::Block,
}
}
}
/// Lays out realized content into regions, potentially with columns. /// Lays out realized content into regions, potentially with columns.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn layout_flow( pub fn layout_flow<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &[Pair], children: &[Pair<'a>],
locator: &mut SplitLocator, locator: &mut SplitLocator<'a>,
shared: StyleChain, shared: StyleChain<'a>,
mut regions: Regions, mut regions: Regions,
columns: NonZeroUsize, columns: NonZeroUsize,
column_gutter: Rel<Abs>, column_gutter: Rel<Abs>,
root: bool, mode: FlowMode,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow. // Prepare configuration that is shared across the whole flow.
let config = Config { let config = configuration(shared, regions, columns, column_gutter, mode);
root,
shared,
columns: {
let mut count = columns.get();
if !regions.size.x.is_finite() {
count = 1;
}
let gutter = column_gutter.relative_to(regions.base().x);
let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
let dir = TextElem::dir_in(shared);
ColumnConfig { count, width, gutter, dir }
},
footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared),
clearance: FootnoteEntry::clearance_in(shared),
gap: FootnoteEntry::gap_in(shared),
expand: regions.expand.x,
},
line_numbers: root.then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared),
default_clearance: {
let width = if PageElem::flipped_in(shared) {
PageElem::height_in(shared)
} else {
PageElem::width_in(shared)
};
(0.026 * width.unwrap_or_default())
.clamp(Em::new(0.75).resolve(shared), Em::new(2.5).resolve(shared))
},
}),
};
// Collect the elements into pre-processed children. These are much easier // Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements. // to handle than the raw elements.
@ -219,6 +209,7 @@ pub(crate) fn layout_flow(
locator.next(&()), locator.next(&()),
Size::new(config.columns.width, regions.full), Size::new(config.columns.width, regions.full),
regions.expand.x, regions.expand.x,
mode,
)?; )?;
let mut work = Work::new(&children); let mut work = Work::new(&children);
@ -241,6 +232,55 @@ pub(crate) fn layout_flow(
Ok(Fragment::frames(finished)) Ok(Fragment::frames(finished))
} }
/// Determine the flow's configuration.
fn configuration<'x>(
shared: StyleChain<'x>,
regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
mode: FlowMode,
) -> Config<'x> {
Config {
mode,
shared,
columns: {
let mut count = columns.get();
if !regions.size.x.is_finite() {
count = 1;
}
let gutter = column_gutter.relative_to(regions.base().x);
let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64;
let dir = TextElem::dir_in(shared);
ColumnConfig { count, width, gutter, dir }
},
footnote: FootnoteConfig {
separator: FootnoteEntry::separator_in(shared),
clearance: FootnoteEntry::clearance_in(shared),
gap: FootnoteEntry::gap_in(shared),
expand: regions.expand.x,
},
line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig {
scope: ParLine::numbering_scope_in(shared),
default_clearance: {
let width = if PageElem::flipped_in(shared) {
PageElem::height_in(shared)
} else {
PageElem::width_in(shared)
};
// Clamp below is safe (min <= max): if the font size is
// negative, we set min = max = 0; otherwise,
// `0.75 * size <= 2.5 * size` for zero and positive sizes.
(0.026 * width.unwrap_or_default()).clamp(
Em::new(0.75).resolve(shared).max(Abs::zero()),
Em::new(2.5).resolve(shared).max(Abs::zero()),
)
},
}),
}
}
/// The work that is left to do by flow layout. /// The work that is left to do by flow layout.
/// ///
/// The lifetimes 'a and 'b are used across flow layout: /// The lifetimes 'a and 'b are used across flow layout:
@ -312,7 +352,7 @@ impl<'a, 'b> Work<'a, 'b> {
struct Config<'x> { struct Config<'x> {
/// Whether this is the root flow, which can host footnotes and line /// Whether this is the root flow, which can host footnotes and line
/// numbers. /// numbers.
root: bool, mode: FlowMode,
/// The styles shared by the whole flow. This is used for footnotes and line /// The styles shared by the whole flow. This is used for footnotes and line
/// numbers. /// numbers.
shared: StyleChain<'x>, shared: StyleChain<'x>,
@ -354,6 +394,16 @@ struct LineNumberConfig {
/// Where line numbers are reset. /// Where line numbers are reset.
scope: LineNumberingScope, scope: LineNumberingScope,
/// The default clearance for `auto`. /// The default clearance for `auto`.
///
/// This value should be relative to the page's width, such that the
/// clearance between line numbers and text is small when the page is,
/// itself, small. However, that could cause the clearance to be too small
/// or too large when considering the current text size; in particular, a
/// larger text size would require more clearance to be able to tell line
/// numbers apart from text, whereas a smaller text size requires less
/// clearance so they aren't way too far apart. Therefore, the default
/// value is a percentage of the page width clamped between `0.75em` and
/// `2.5em`.
default_clearance: Abs, default_clearance: Abs,
} }

View File

@ -3,6 +3,7 @@ use std::fmt::Debug;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Resolve, StyleChain}; use typst_library::foundations::{Resolve, StyleChain};
use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing, Size, Sizing,
@ -13,8 +14,8 @@ use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric}; use typst_utils::{MaybeReverseIter, Numeric};
use super::{ use super::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup, LineSegment, Rowspan, UnbreakableRowGroup,
}; };
/// Performs grid layout. /// Performs grid layout.
@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> {
let size = Size::new(available, height); let size = Size::new(available, height);
let pod = Region::new(size, Axes::splat(false)); let pod = Region::new(size, Axes::splat(false));
let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame(); let frame =
layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame();
resolved.set_max(frame.width() - already_covered_width); resolved.set_max(frame.width() - already_covered_width);
} }
@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> {
}; };
let frames = let frames =
cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames();
// Skip the first region if one cell in it is empty. Then, // Skip the first region if one cell in it is empty. Then,
// remeasure. // remeasure.
@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> {
// rows. // rows.
pod.full = self.regions.full; pod.full = self.regions.full;
} }
let frame = cell let frame =
.layout(engine, disambiguator, self.styles, pod)? layout_cell(cell, engine, disambiguator, self.styles, pod)?
.into_frame(); .into_frame();
let mut pos = pos; let mut pos = pos;
if self.is_rtl { if self.is_rtl {
// In the grid, cell colspans expand to the right, // In the grid, cell colspans expand to the right,
@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> {
// Push the layouted frames into the individual output frames. // Push the layouted frames into the individual output frames.
let fragment = let fragment =
cell.layout(engine, disambiguator, self.styles, pod)?; layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) { for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos; let mut pos = pos;
if self.is_rtl { if self.is_rtl {
@ -1375,7 +1377,7 @@ impl<'a> GridLayouter<'a> {
.footer .footer
.as_ref() .as_ref()
.and_then(Repeatable::as_repeated) .and_then(Repeatable::as_repeated)
.map_or(true, |footer| footer.start != header.end) .is_none_or(|footer| footer.start != header.end)
&& self.lrows.last().is_some_and(|row| row.index() < header.end) && self.lrows.last().is_some_and(|row| row.index() < header.end)
&& !in_last_with_offset( && !in_last_with_offset(
self.regions, self.regions,
@ -1444,7 +1446,7 @@ impl<'a> GridLayouter<'a> {
.iter_mut() .iter_mut()
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
.filter(|rowspan| { .filter(|rowspan| {
rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) rowspan.max_resolved_row.is_none_or(|max_row| y > max_row)
}) })
{ {
// If the first region wasn't defined yet, it will have the // If the first region wasn't defined yet, it will have the
@ -1492,7 +1494,7 @@ impl<'a> GridLayouter<'a> {
// laid out at the first frame of the row). // laid out at the first frame of the row).
// Any rowspans ending before this row are laid out even // Any rowspans ending before this row are laid out even
// on this row's first frame. // on this row's first frame.
if laid_out_footer_start.map_or(true, |footer_start| { if laid_out_footer_start.is_none_or(|footer_start| {
// If this is a footer row, then only lay out this rowspan // If this is a footer row, then only lay out this rowspan
// if the rowspan is contained within the footer. // if the rowspan is contained within the footer.
y < footer_start || rowspan.y >= footer_start y < footer_start || rowspan.y >= footer_start
@ -1578,5 +1580,5 @@ pub(super) fn points(
/// our case, headers). /// our case, headers).
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
regions.backlog.is_empty() regions.backlog.is_empty()
&& regions.last.map_or(true, |height| regions.size.y + offset == height) && regions.last.is_none_or(|height| regions.size.y + offset == height)
} }

View File

@ -1,41 +1,11 @@
use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use typst_library::foundations::{AlternativeFold, Fold}; use typst_library::foundations::{AlternativeFold, Fold};
use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable};
use typst_library::layout::Abs; use typst_library::layout::Abs;
use typst_library::visualize::Stroke; use typst_library::visualize::Stroke;
use super::{CellGrid, LinePosition, Repeatable, RowPiece}; use super::RowPiece;
/// Represents an explicit grid line (horizontal or vertical) specified by the
/// user.
pub struct Line {
/// The index of the track after this line. This will be the index of the
/// row a horizontal line is above of, or of the column right after a
/// vertical line.
///
/// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols`
/// or `grid.rows`, ignoring gutter tracks, as appropriate).
pub index: usize,
/// The index of the track at which this line starts being drawn.
/// This is the first column a horizontal line appears in, or the first row
/// a vertical line appears in.
///
/// Must be within `0..tracks.len()` minus gutter tracks.
pub start: usize,
/// The index after the last track through which the line is drawn.
/// Thus, the line is drawn through tracks `start..end` (note that `end` is
/// exclusive).
///
/// Must be within `1..=tracks.len()` minus gutter tracks.
/// `None` indicates the line should go all the way to the end.
pub end: Option<NonZeroUsize>,
/// The line's stroke. This is `None` when the line is explicitly used to
/// override a previously specified line.
pub stroke: Option<Arc<Stroke<Abs>>>,
/// The line's position in relation to the track with its index.
pub position: LinePosition,
}
/// Indicates which priority a particular grid line segment should have, based /// Indicates which priority a particular grid line segment should have, based
/// on the highest priority configuration that defined the segment's stroke. /// on the highest priority configuration that defined the segment's stroke.
@ -493,7 +463,7 @@ pub fn hline_stroke_at_column(
// region, we have the last index, and (as a failsafe) we don't have the // region, we have the last index, and (as a failsafe) we don't have the
// last row of cells above us. // last row of cells above us.
let use_bottom_border_stroke = !in_last_region let use_bottom_border_stroke = !in_last_region
&& local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) && local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len())
&& y == grid.rows.len(); && y == grid.rows.len();
let bottom_y = let bottom_y =
if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };
@ -588,13 +558,13 @@ pub fn hline_stroke_at_column(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use std::num::NonZeroUsize;
use typst_library::foundations::Content; use typst_library::foundations::Content;
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition};
use typst_library::layout::{Axes, Sides, Sizing}; use typst_library::layout::{Axes, Sides, Sizing};
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use super::super::cells::Entry;
use super::super::Cell;
use super::*; use super::*;
fn sample_cell() -> Cell<'static> { fn sample_cell() -> Cell<'static> {

View File

@ -1,40 +1,44 @@
mod cells;
mod layouter; mod layouter;
mod lines; mod lines;
mod repeated; mod repeated;
mod rowspans; mod rowspans;
pub use self::cells::{Cell, CellGrid};
pub use self::layouter::GridLayouter; pub use self::layouter::GridLayouter;
use std::num::NonZeroUsize; use typst_library::diag::SourceResult;
use std::sync::Arc;
use ecow::eco_format;
use typst_library::diag::{SourceResult, Trace, Tracepoint};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Fold, Packed, Smart, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::{ use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell};
Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length, use typst_library::layout::{Fragment, GridElem, Regions};
OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, use typst_library::model::TableElem;
};
use typst_library::model::{TableCell, TableChild, TableElem, TableItem};
use typst_library::text::TextElem;
use typst_library::visualize::{Paint, Stroke};
use typst_syntax::Span;
use self::cells::{
LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem,
};
use self::layouter::RowPiece; use self::layouter::RowPiece;
use self::lines::{ use self::lines::{
generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line, generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment,
LineSegment,
}; };
use self::repeated::{Footer, Header, Repeatable};
use self::rowspans::{Rowspan, UnbreakableRowGroup}; use self::rowspans::{Rowspan, UnbreakableRowGroup};
/// Layout the cell into the given regions.
///
/// The `disambiguator` indicates which instance of this cell this should be
/// layouted as. For normal cells, it is always `0`, but for headers and
/// footers, it indicates the index of the header/footer among all. See the
/// [`Locator`] docs for more details on the concepts behind this.
pub fn layout_cell(
cell: &Cell,
engine: &mut Engine,
disambiguator: usize,
styles: StyleChain,
regions: Regions,
) -> SourceResult<Fragment> {
let mut locator = cell.locator.relayout();
if disambiguator > 0 {
locator = locator.split().next_inner(disambiguator as u128);
}
crate::layout_fragment(engine, &cell.body, locator, styles, regions)
}
/// Layout the grid. /// Layout the grid.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
pub fn layout_grid( pub fn layout_grid(
@ -44,54 +48,8 @@ pub fn layout_grid(
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let inset = elem.inset(styles); let grid = grid_to_cellgrid(elem, engine, locator, styles)?;
let align = elem.align(styles); GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the grid when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("grid")));
let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles);
let children = elem.children().iter().map(|child| match child {
GridChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
GridChild::Item(item) => {
ResolvableGridChild::Item(grid_item_to_resolvable(item, styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
// Measure the columns and layout the grid row-by-row.
layouter.layout(engine)
} }
/// Layout the table. /// Layout the table.
@ -103,314 +61,6 @@ pub fn layout_table(
styles: StyleChain, styles: StyleChain,
regions: Regions, regions: Regions,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let inset = elem.inset(styles); let grid = table_to_cellgrid(elem, engine, locator, styles)?;
let align = elem.align(styles); GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine)
let columns = elem.columns(styles);
let rows = elem.rows(styles);
let column_gutter = elem.column_gutter(styles);
let row_gutter = elem.row_gutter(styles);
let fill = elem.fill(styles);
let stroke = elem.stroke(styles);
let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice());
let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice());
// Use trace to link back to the table when a specific cell errors
let tracepoint = || Tracepoint::Call(Some(eco_format!("table")));
let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles);
let children = elem.children().iter().map(|child| match child {
TableChild::Header(header) => ResolvableGridChild::Header {
repeat: header.repeat(styles),
span: header.span(),
items: header.children().iter().map(resolve_item),
},
TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles),
span: footer.span(),
items: footer.children().iter().map(resolve_item),
},
TableChild::Item(item) => {
ResolvableGridChild::Item(table_item_to_resolvable(item, styles))
}
});
let grid = CellGrid::resolve(
tracks,
gutter,
locator,
children,
fill,
align,
&inset,
&stroke,
engine,
styles,
elem.span(),
)
.trace(engine.world, tracepoint, elem.span())?;
let layouter = GridLayouter::new(&grid, regions, styles, elem.span());
layouter.layout(engine)
}
fn grid_item_to_resolvable(
item: &GridItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<GridCell>> {
match item {
GridItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
GridItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
fn table_item_to_resolvable(
item: &TableItem,
styles: StyleChain,
) -> ResolvableGridItem<Packed<TableCell>> {
match item {
TableItem::HLine(hline) => ResolvableGridItem::HLine {
y: hline.y(styles),
start: hline.start(styles),
end: hline.end(styles),
stroke: hline.stroke(styles),
span: hline.span(),
position: match hline.position(styles) {
OuterVAlignment::Top => LinePosition::Before,
OuterVAlignment::Bottom => LinePosition::After,
},
},
TableItem::VLine(vline) => ResolvableGridItem::VLine {
x: vline.x(styles),
start: vline.start(styles),
end: vline.end(styles),
stroke: vline.stroke(styles),
span: vline.span(),
position: match vline.position(styles) {
OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::After
}
OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => {
LinePosition::Before
}
OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before,
OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After,
},
},
TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()),
}
}
impl ResolvableCell for Packed<TableCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the table is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on table cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
}
impl ResolvableCell for Packed<GridCell> {
fn resolve_cell<'a>(
mut self,
x: usize,
y: usize,
fill: &Option<Paint>,
align: Smart<Alignment>,
inset: Sides<Option<Rel<Length>>>,
stroke: Sides<Option<Option<Arc<Stroke<Abs>>>>>,
breakable: bool,
locator: Locator<'a>,
styles: StyleChain,
) -> Cell<'a> {
let cell = &mut *self;
let colspan = cell.colspan(styles);
let rowspan = cell.rowspan(styles);
let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let cell_stroke = cell.stroke(styles);
let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
// Using a typical 'Sides' fold, an unspecified side loses to a
// specified side. Additionally, when both are specified, an inner
// None wins over the outer Some, and vice-versa. When both are
// specified and Some, fold occurs, which, remarkably, leads to an Arc
// clone.
//
// In the end, we flatten because, for layout purposes, an unspecified
// cell stroke is the same as specifying 'none', so we equate the two
// concepts.
let stroke = cell_stroke.fold(stroke).map(Option::flatten);
cell.push_x(Smart::Custom(x));
cell.push_y(Smart::Custom(y));
cell.push_fill(Smart::Custom(fill.clone()));
cell.push_align(match align {
Smart::Custom(align) => {
Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align)))
}
// Don't fold if the grid is using outer alignment. Use the
// cell's alignment instead (which, in the end, will fold with
// the outer alignment when it is effectively displayed).
Smart::Auto => cell.align(styles),
});
cell.push_inset(Smart::Custom(
cell.inset(styles).map_or(inset, |inner| inner.fold(inset)),
));
cell.push_stroke(
// Here we convert the resolved stroke to a regular stroke, however
// with resolved units (that is, 'em' converted to absolute units).
// We also convert any stroke unspecified by both the cell and the
// outer stroke ('None' in the folded stroke) to 'none', that is,
// all sides are present in the resulting Sides object accessible
// by show rules on grid cells.
stroke.as_ref().map(|side| {
Some(side.as_ref().map(|cell_stroke| {
Arc::new((**cell_stroke).clone().map(Length::from))
}))
}),
);
cell.push_breakable(Smart::Custom(breakable));
Cell {
body: self.pack(),
locator,
fill,
colspan,
rowspan,
stroke,
stroke_overridden,
breakable,
}
}
fn x(&self, styles: StyleChain) -> Smart<usize> {
(**self).x(styles)
}
fn y(&self, styles: StyleChain) -> Smart<usize> {
(**self).y(styles)
}
fn colspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).colspan(styles)
}
fn rowspan(&self, styles: StyleChain) -> NonZeroUsize {
(**self).rowspan(styles)
}
fn span(&self) -> Span {
Packed::span(self)
}
} }

View File

@ -1,50 +1,11 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::layout::grid::resolve::{Footer, Header, Repeatable};
use typst_library::layout::{Abs, Axes, Frame, Regions}; use typst_library::layout::{Abs, Axes, Frame, Regions};
use super::layouter::GridLayouter; use super::layouter::GridLayouter;
use super::rowspans::UnbreakableRowGroup; use super::rowspans::UnbreakableRowGroup;
/// A repeatable grid header. Starts at the first row.
pub struct Header {
/// The index after the last row included in this header.
pub end: usize,
}
/// A repeatable grid footer. Stops at the last row.
pub struct Footer {
/// The first row included in this footer.
pub start: usize,
}
/// A possibly repeatable grid object.
/// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable).
pub enum Repeatable<T> {
Repeated(T),
NotRepeated(T),
}
impl<T> Repeatable<T> {
/// Gets the value inside this repeatable, regardless of whether
/// it repeats.
pub fn unwrap(&self) -> &T {
match self {
Self::Repeated(repeated) => repeated,
Self::NotRepeated(not_repeated) => not_repeated,
}
}
/// Returns `Some` if the value is repeated, `None` otherwise.
pub fn as_repeated(&self) -> Option<&T> {
match self {
Self::Repeated(repeated) => Some(repeated),
Self::NotRepeated(_) => None,
}
}
}
impl GridLayouter<'_> { impl GridLayouter<'_> {
/// Layouts the header's rows. /// Layouts the header's rows.
/// Skips regions as necessary. /// Skips regions as necessary.

View File

@ -1,12 +1,12 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::Resolve; use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter; use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece}; use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::repeated::Repeatable; use super::{layout_cell, Cell, GridLayouter};
use super::{Cell, GridLayouter};
/// All information needed to layout a single rowspan. /// All information needed to layout a single rowspan.
pub struct Rowspan { pub struct Rowspan {
@ -141,7 +141,7 @@ impl GridLayouter<'_> {
} }
// Push the layouted frames directly into the finished frames. // Push the layouted frames directly into the finished frames.
let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?;
let (current_region, current_rrows) = current_region_data.unzip(); let (current_region, current_rrows) = current_region_data.unzip();
for ((i, finished), frame) in self for ((i, finished), frame) in self
.finished .finished
@ -588,7 +588,7 @@ impl GridLayouter<'_> {
measurement_data: &CellMeasurementData<'_>, measurement_data: &CellMeasurementData<'_>,
) -> bool { ) -> bool {
if sizes.len() <= 1 if sizes.len() <= 1
&& sizes.first().map_or(true, |&first_frame_size| { && sizes.first().is_none_or(|&first_frame_size| {
first_frame_size <= measurement_data.height_in_this_region first_frame_size <= measurement_data.height_in_this_region
}) })
{ {

View File

@ -1,16 +1,17 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use typst_library::diag::{bail, warning, At, SourceResult, StrResult}; use typst_library::diag::{warning, At, SourceResult, StrResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Packed, Smart, StyleChain}; use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size,
}; };
use typst_library::loading::Readable; use typst_library::loading::DataSource;
use typst_library::text::families; use typst_library::text::families;
use typst_library::visualize::{ use typst_library::visualize::{
Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat, Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind,
RasterImage, SvgImage, VectorFormat,
}; };
/// Layout the image. /// Layout the image.
@ -26,17 +27,17 @@ pub fn layout_image(
// Take the format that was explicitly defined, or parse the extension, // Take the format that was explicitly defined, or parse the extension,
// or try to detect the format. // or try to detect the format.
let data = elem.data(); let Derived { source, derived: data } = &elem.source;
let format = match elem.format(styles) { let format = match elem.format(styles) {
Smart::Custom(v) => v, Smart::Custom(v) => v,
Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?, Smart::Auto => determine_format(source, data).at(span)?,
}; };
// Warn the user if the image contains a foreign object. Not perfect // 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. // because the svg could also be encoded, but that's an edge case.
if format == ImageFormat::Vector(VectorFormat::Svg) { if format == ImageFormat::Vector(VectorFormat::Svg) {
let has_foreign_object = let has_foreign_object =
data.as_str().is_some_and(|s| s.contains("<foreignObject")); data.as_str().is_ok_and(|s| s.contains("<foreignObject"));
if has_foreign_object { if has_foreign_object {
engine.sink.warn(warning!( engine.sink.warn(warning!(
@ -49,15 +50,26 @@ pub fn layout_image(
} }
// Construct the image itself. // Construct the image itself.
let image = Image::with_fonts( let kind = match format {
data.clone().into(), ImageFormat::Raster(format) => ImageKind::Raster(
format, RasterImage::new(
elem.alt(styles), data.clone(),
engine.world, format,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(), elem.icc(styles).as_ref().map(|icc| icc.derived.clone()),
elem.flatten_text(styles), )
) .at(span)?,
.at(span)?; ),
ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg(
SvgImage::with_fonts(
data.clone(),
engine.world,
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
)
.at(span)?,
),
};
let image = Image::new(kind, elem.alt(styles), elem.scaling(styles));
// Determine the image's pixel aspect ratio. // Determine the image's pixel aspect ratio.
let pxw = image.width(); let pxw = image.width();
@ -83,6 +95,8 @@ pub fn layout_image(
} else { } else {
// If neither is forced, take the natural image size at the image's // If neither is forced, take the natural image size at the image's
// DPI bounded by the available space. // DPI bounded by the available space.
//
// Division by DPI is fine since it's guaranteed to be positive.
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new( Size::new(
@ -113,31 +127,29 @@ pub fn layout_image(
// Create a clipping group if only part of the image should be visible. // Create a clipping group if only part of the image should be visible.
if fit == ImageFit::Cover && !target.fits(fitted) { if fit == ImageFit::Cover && !target.fits(fitted) {
frame.clip(Path::rect(frame.size())); frame.clip(Curve::rect(frame.size()));
} }
Ok(frame) Ok(frame)
} }
/// Determine the image format based on path and data. /// Try to determine the image format based on the data.
fn determine_format(path: &str, data: &Readable) -> StrResult<ImageFormat> { fn determine_format(source: &DataSource, data: &Bytes) -> StrResult<ImageFormat> {
let ext = std::path::Path::new(path) if let DataSource::Path(path) = source {
.extension() let ext = std::path::Path::new(path.as_str())
.and_then(OsStr::to_str) .extension()
.unwrap_or_default() .and_then(OsStr::to_str)
.to_lowercase(); .unwrap_or_default()
.to_lowercase();
Ok(match ext.as_str() { match ext.as_str() {
"png" => ImageFormat::Raster(RasterFormat::Png), "png" => return Ok(ExchangeFormat::Png.into()),
"jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()),
"gif" => ImageFormat::Raster(RasterFormat::Gif), "gif" => return Ok(ExchangeFormat::Gif.into()),
"svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), "svg" | "svgz" => return Ok(VectorFormat::Svg.into()),
_ => 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"), Ok(ImageFormat::detect(data).ok_or("unknown image format")?)
},
},
})
} }

View File

@ -11,7 +11,7 @@ use typst_utils::Numeric;
use crate::flow::unbreakable_pod; use crate::flow::unbreakable_pod;
use crate::shapes::{clip_rect, fill_and_stroke}; use crate::shapes::{clip_rect, fill_and_stroke};
/// Lay out a box as part of a paragraph. /// Lay out a box as part of inline layout.
#[typst_macros::time(name = "box", span = elem.span())] #[typst_macros::time(name = "box", span = elem.span())]
pub fn layout_box( pub fn layout_box(
elem: &Packed<BoxElem>, elem: &Packed<BoxElem>,

View File

@ -1,10 +1,10 @@
use typst_library::diag::bail; use typst_library::diag::warning;
use typst_library::foundations::{Packed, Resolve}; use typst_library::foundations::{Packed, Resolve};
use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::introspection::{SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
Spacing,
}; };
use typst_library::routines::Pair;
use typst_library::text::{ use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
SpaceElem, TextElem, SpaceElem, TextElem,
@ -13,9 +13,10 @@ use typst_syntax::Span;
use typst_utils::Numeric; use typst_utils::Numeric;
use super::*; use super::*;
use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify};
// The characters by which spacing, inline content and pins are replaced in the // The characters by which spacing, inline content and pins are replaced in the
// paragraph's full text. // full text.
const SPACING_REPLACE: &str = " "; // Space const SPACING_REPLACE: &str = " "; // Space
const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character
@ -26,7 +27,7 @@ const POP_EMBEDDING: &str = "\u{202C}";
const LTR_ISOLATE: &str = "\u{2066}"; const LTR_ISOLATE: &str = "\u{2066}";
const POP_ISOLATE: &str = "\u{2069}"; const POP_ISOLATE: &str = "\u{2069}";
/// A prepared item in a paragraph layout. /// A prepared item in a inline layout.
#[derive(Debug)] #[derive(Debug)]
pub enum Item<'a> { pub enum Item<'a> {
/// A shaped text run with consistent style and direction. /// A shaped text run with consistent style and direction.
@ -36,7 +37,7 @@ pub enum Item<'a> {
/// Fractional spacing between other items. /// Fractional spacing between other items.
Fractional(Fr, Option<(&'a Packed<BoxElem>, Locator<'a>, StyleChain<'a>)>), Fractional(Fr, Option<(&'a Packed<BoxElem>, Locator<'a>, StyleChain<'a>)>),
/// Layouted inline-level content. /// Layouted inline-level content.
Frame(Frame, StyleChain<'a>), Frame(Frame),
/// A tag. /// A tag.
Tag(&'a Tag), Tag(&'a Tag),
/// An item that is invisible and needs to be skipped, e.g. a Unicode /// An item that is invisible and needs to be skipped, e.g. a Unicode
@ -67,7 +68,7 @@ impl<'a> Item<'a> {
match self { match self {
Self::Text(shaped) => shaped.text, Self::Text(shaped) => shaped.text,
Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE, Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE,
Self::Frame(_, _) => OBJ_REPLACE, Self::Frame(_) => OBJ_REPLACE,
Self::Tag(_) => "", Self::Tag(_) => "",
Self::Skip(s) => s, Self::Skip(s) => s,
} }
@ -83,7 +84,7 @@ impl<'a> Item<'a> {
match self { match self {
Self::Text(shaped) => shaped.width, Self::Text(shaped) => shaped.width,
Self::Absolute(v, _) => *v, Self::Absolute(v, _) => *v,
Self::Frame(frame, _) => frame.width(), Self::Frame(frame) => frame.width(),
Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(),
Self::Skip(_) => Abs::zero(), Self::Skip(_) => Abs::zero(),
} }
@ -112,38 +113,31 @@ impl Segment<'_> {
} }
} }
/// Collects all text of the paragraph into one string and a collection of /// Collects all text into one string and a collection of segments that
/// segments that correspond to pieces of that string. This also performs /// correspond to pieces of that string. This also performs string-level
/// string-level preprocessing like case transformations. /// preprocessing like case transformations.
#[typst_macros::time] #[typst_macros::time]
pub fn collect<'a>( pub fn collect<'a>(
children: &'a StyleVec, children: &[Pair<'a>],
engine: &mut Engine<'_>, engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: &'a StyleChain<'a>, config: &Config,
region: Size, region: Size,
consecutive: bool,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> { ) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len()); let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new(); let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(*styles); if !config.first_line_indent.is_zero() {
let first_line_indent = ParElem::first_line_indent_in(*styles); collector.push_item(Item::Absolute(config.first_line_indent, false));
if !first_line_indent.is_zero()
&& consecutive
&& AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into()
{
collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false));
collector.spans.push(1, Span::detached()); collector.spans.push(1, Span::detached());
} }
let hang = ParElem::hanging_indent_in(*styles); if !config.hanging_indent.is_zero() {
if !hang.is_zero() { collector.push_item(Item::Absolute(-config.hanging_indent, false));
collector.push_item(Item::Absolute(-hang, false));
collector.spans.push(1, Span::detached()); collector.spans.push(1, Span::detached());
} }
for (child, styles) in children.iter(styles) { for &(child, styles) in children {
let prev_len = collector.full.len(); let prev_len = collector.full.len();
if child.is::<SpaceElem>() { if child.is::<SpaceElem>() {
@ -151,7 +145,7 @@ pub fn collect<'a>(
} else if let Some(elem) = child.to_packed::<TextElem>() { } else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| { collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles); let dir = TextElem::dir_in(styles);
if dir != outer_dir { if dir != config.dir {
// Insert "Explicit Directional Embedding". // Insert "Explicit Directional Embedding".
match dir { match dir {
Dir::LTR => full.push_str(LTR_EMBEDDING), Dir::LTR => full.push_str(LTR_EMBEDDING),
@ -161,24 +155,23 @@ pub fn collect<'a>(
} }
if let Some(case) = TextElem::case_in(styles) { if let Some(case) = TextElem::case_in(styles) {
full.push_str(&case.apply(elem.text())); full.push_str(&case.apply(&elem.text));
} else { } else {
full.push_str(elem.text()); full.push_str(&elem.text);
} }
if dir != outer_dir { if dir != config.dir {
// Insert "Pop Directional Formatting". // Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING); full.push_str(POP_EMBEDDING);
} }
}); });
} else if let Some(elem) = child.to_packed::<HElem>() { } else if let Some(elem) = child.to_packed::<HElem>() {
let amount = elem.amount(); if elem.amount.is_zero() {
if amount.is_zero() {
continue; continue;
} }
collector.push_item(match amount { collector.push_item(match elem.amount {
Spacing::Fr(fr) => Item::Fractional(*fr, None), Spacing::Fr(fr) => Item::Fractional(fr, None),
Spacing::Rel(rel) => Item::Absolute( Spacing::Rel(rel) => Item::Absolute(
rel.resolve(styles).relative_to(region.x), rel.resolve(styles).relative_to(region.x),
elem.weak(styles), elem.weak(styles),
@ -211,8 +204,10 @@ pub fn collect<'a>(
InlineItem::Space(space, weak) => { InlineItem::Space(space, weak) => {
collector.push_item(Item::Absolute(space, weak)); collector.push_item(Item::Absolute(space, weak));
} }
InlineItem::Frame(frame) => { InlineItem::Frame(mut frame) => {
collector.push_item(Item::Frame(frame, styles)); frame.modify(&FrameModifiers::get_in(styles));
apply_baseline_shift(&mut frame, styles);
collector.push_item(Item::Frame(frame));
} }
} }
} }
@ -223,13 +218,22 @@ pub fn collect<'a>(
if let Sizing::Fr(v) = elem.width(styles) { if let Sizing::Fr(v) = elem.width(styles) {
collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); collector.push_item(Item::Fractional(v, Some((elem, loc, styles))));
} else { } else {
let frame = layout_box(elem, engine, loc, styles, region)?; let mut frame = layout_and_modify(styles, |styles| {
collector.push_item(Item::Frame(frame, styles)); layout_box(elem, engine, loc, styles, region)
})?;
apply_baseline_shift(&mut frame, styles);
collector.push_item(Item::Frame(frame));
} }
} else if let Some(elem) = child.to_packed::<TagElem>() { } else if let Some(elem) = child.to_packed::<TagElem>() {
collector.push_item(Item::Tag(&elem.tag)); collector.push_item(Item::Tag(&elem.tag));
} else { } else {
bail!(child.span(), "unexpected paragraph child"); // Non-paragraph inline layout should never trigger this since it
// only won't be triggered if we see any non-inline content.
engine.sink.warn(warning!(
child.span(),
"{} may not occur inside of a paragraph and was ignored",
child.func().name()
));
}; };
let len = collector.full.len() - prev_len; let len = collector.full.len() - prev_len;

View File

@ -9,19 +9,19 @@ pub fn finalize(
engine: &mut Engine, engine: &mut Engine,
p: &Preparation, p: &Preparation,
lines: &[Line], lines: &[Line],
styles: StyleChain,
region: Size, region: Size,
expand: bool, expand: bool,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Determine the paragraph's width: Full width of the region if we should // Determine the resulting width: Full width of the region if we should
// expand or there's fractional spacing, fit-to-width otherwise. // expand or there's fractional spacing, fit-to-width otherwise.
let width = if !region.x.is_finite() let width = if !region.x.is_finite()
|| (!expand && lines.iter().all(|line| line.fr().is_zero())) || (!expand && lines.iter().all(|line| line.fr().is_zero()))
{ {
region region.x.min(
.x p.config.hanging_indent
.min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + lines.iter().map(|line| line.width).max().unwrap_or_default(),
)
} else { } else {
region.x region.x
}; };
@ -29,7 +29,7 @@ pub fn finalize(
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
lines lines
.iter() .iter()
.map(|line| commit(engine, p, line, width, region.y, locator, styles)) .map(|line| commit(engine, p, line, width, region.y, locator))
.collect::<SourceResult<_>>() .collect::<SourceResult<_>>()
.map(Fragment::frames) .map(Fragment::frames)
} }

View File

@ -2,14 +2,14 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::NativeElement;
use typst_library::introspection::{SplitLocator, Tag}; use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::{ParLine, ParLineMarker}; use typst_library::model::ParLineMarker;
use typst_library::text::{Lang, TextElem}; use typst_library::text::{Lang, TextElem};
use typst_utils::Numeric; use typst_utils::Numeric;
use super::*; use super::*;
use crate::modifiers::layout_and_modify;
const SHY: char = '\u{ad}'; const SHY: char = '\u{ad}';
const HYPHEN: char = '-'; const HYPHEN: char = '-';
@ -17,12 +17,12 @@ const EN_DASH: char = '';
const EM_DASH: char = '—'; const EM_DASH: char = '—';
const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks. const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks.
/// A layouted line, consisting of a sequence of layouted paragraph items that /// A layouted line, consisting of a sequence of layouted inline items that are
/// are mostly borrowed from the preparation phase. This type enables you to /// mostly borrowed from the preparation phase. This type enables you to measure
/// measure the size of a line in a range before committing to building the /// the size of a line in a range before committing to building the line's
/// line's frame. /// frame.
/// ///
/// At most two paragraph items must be created individually for this line: The /// At most two inline items must be created individually for this line: The
/// first and last one since they may be broken apart by the start or end of the /// first and last one since they may be broken apart by the start or end of the
/// line, respectively. But even those can partially reuse previous results when /// line, respectively. But even those can partially reuse previous results when
/// the break index is safe-to-break per rustybuzz. /// the break index is safe-to-break per rustybuzz.
@ -93,7 +93,7 @@ impl Line<'_> {
pub fn has_negative_width_items(&self) -> bool { pub fn has_negative_width_items(&self) -> bool {
self.items.iter().any(|item| match item { self.items.iter().any(|item| match item {
Item::Absolute(amount, _) => *amount < Abs::zero(), Item::Absolute(amount, _) => *amount < Abs::zero(),
Item::Frame(frame, _) => frame.width() < Abs::zero(), Item::Frame(frame) => frame.width() < Abs::zero(),
_ => false, _ => false,
}) })
} }
@ -134,7 +134,7 @@ pub fn line<'a>(
// Whether the line is justified. // Whether the line is justified.
let justify = full.ends_with(LINE_SEPARATOR) let justify = full.ends_with(LINE_SEPARATOR)
|| (p.justify && breakpoint != Breakpoint::Mandatory); || (p.config.justify && breakpoint != Breakpoint::Mandatory);
// Process dashes. // Process dashes.
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
@ -154,16 +154,16 @@ pub fn line<'a>(
let mut items = collect_items(engine, p, range, trim); let mut items = collect_items(engine, p, range, trim);
// Add a hyphen at the line start, if a previous dash should be repeated. // Add a hyphen at the line start, if a previous dash should be repeated.
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { if pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) {
if let Some(shaped) = items.first_text_mut() { if let Some(shaped) = items.first_text_mut() {
shaped.prepend_hyphen(engine, p.fallback); shaped.prepend_hyphen(engine, p.config.fallback);
} }
} }
// Add a hyphen at the line end, if we ended on a soft hyphen. // Add a hyphen at the line end, if we ended on a soft hyphen.
if dash == Some(Dash::Soft) { if dash == Some(Dash::Soft) {
if let Some(shaped) = items.last_text_mut() { if let Some(shaped) = items.last_text_mut() {
shaped.push_hyphen(engine, p.fallback); shaped.push_hyphen(engine, p.config.fallback);
} }
} }
@ -233,13 +233,13 @@ where
{ {
// If there is nothing bidirectional going on, skip reordering. // If there is nothing bidirectional going on, skip reordering.
let Some(bidi) = &p.bidi else { let Some(bidi) = &p.bidi else {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
}; };
// The bidi crate panics for empty lines. // The bidi crate panics for empty lines.
if range.is_empty() { if range.is_empty() {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
} }
@ -307,13 +307,13 @@ fn collect_range<'a>(
/// punctuation marks at line start or line end. /// punctuation marks at line start or line end.
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
if text.starts_with(BEGIN_PUNCT_PAT) if text.starts_with(BEGIN_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script))
{ {
adjust_cj_at_line_start(p, items); adjust_cj_at_line_start(p, items);
} }
if text.ends_with(END_PUNCT_PAT) if text.ends_with(END_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script))
{ {
adjust_cj_at_line_end(p, items); adjust_cj_at_line_end(p, items);
} }
@ -331,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().0; let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink); glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& glyph.x_offset > Em::zero()
{
// If the first glyph is a CJK character adjusted by // If the first glyph is a CJK character adjusted by
// [`add_cjk_latin_spacing`], restore the original width. // [`add_cjk_latin_spacing`], restore the original width.
let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
@ -358,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let punct = shaped.glyphs.to_mut().last_mut().unwrap(); let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink); punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one() && (glyph.x_advance - glyph.x_offset) > Em::one()
{ {
@ -403,12 +406,17 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
// //
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española" // See § 4.1.1.1.2.e on the "Ortografía de la lengua española"
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea // https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea
Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()), Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()),
_ => false, _ => false,
} }
} }
/// Apply the current baseline shift to a frame.
pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) {
frame.translate(Point::with_y(TextElem::baseline_in(styles)));
}
/// Commit to a line and build its frame. /// Commit to a line and build its frame.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn commit( pub fn commit(
@ -418,16 +426,15 @@ pub fn commit(
width: Abs, width: Abs,
full: Abs, full: Abs,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
styles: StyleChain,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let mut remaining = width - line.width - p.hang; let mut remaining = width - line.width - p.config.hanging_indent;
let mut offset = Abs::zero(); let mut offset = Abs::zero();
// We always build the line from left to right. In an LTR paragraph, we must // We always build the line from left to right. In an LTR paragraph, we must
// thus add the hanging indent to the offset. When the paragraph is RTL, the // thus add the hanging indent to the offset. In an RTL paragraph, the
// hanging indent arises naturally due to the line width. // hanging indent arises naturally due to the line width.
if p.dir == Dir::LTR { if p.config.dir == Dir::LTR {
offset += p.hang; offset += p.config.hanging_indent;
} }
// Handle hanging punctuation to the left. // Handle hanging punctuation to the left.
@ -509,10 +516,11 @@ pub fn commit(
let amount = v.share(fr, remaining); let amount = v.share(fr, remaining);
if let Some((elem, loc, styles)) = elem { if let Some((elem, loc, styles)) = elem {
let region = Size::new(amount, full); let region = Size::new(amount, full);
let mut frame = let mut frame = layout_and_modify(*styles, |styles| {
layout_box(elem, engine, loc.relayout(), *styles, region)?; layout_box(elem, engine, loc.relayout(), styles, region)
frame.translate(Point::with_y(TextElem::baseline_in(*styles))); })?;
push(&mut offset, frame.post_processed(*styles)); apply_baseline_shift(&mut frame, *styles);
push(&mut offset, frame);
} else { } else {
offset += amount; offset += amount;
} }
@ -524,12 +532,10 @@ pub fn commit(
justification_ratio, justification_ratio,
extra_justification, extra_justification,
); );
push(&mut offset, frame.post_processed(shaped.styles)); push(&mut offset, frame);
} }
Item::Frame(frame, styles) => { Item::Frame(frame) => {
let mut frame = frame.clone(); push(&mut offset, frame.clone());
frame.translate(Point::with_y(TextElem::baseline_in(*styles)));
push(&mut offset, frame.post_processed(*styles));
} }
Item::Tag(tag) => { Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero()); let mut frame = Frame::soft(Size::zero());
@ -549,11 +555,13 @@ pub fn commit(
let mut output = Frame::soft(size); let mut output = Frame::soft(size);
output.set_baseline(top); output.set_baseline(top);
add_par_line_marker(&mut output, styles, engine, locator, top); if let Some(marker) = &p.config.numbering_marker {
add_par_line_marker(&mut output, marker, engine, locator, top);
}
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame) in frames {
let x = offset + p.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
} }
@ -570,26 +578,18 @@ pub fn commit(
/// number in the margin, is aligned to the line's baseline. /// number in the margin, is aligned to the line's baseline.
fn add_par_line_marker( fn add_par_line_marker(
output: &mut Frame, output: &mut Frame,
styles: StyleChain, marker: &Packed<ParLineMarker>,
engine: &mut Engine, engine: &mut Engine,
locator: &mut SplitLocator, locator: &mut SplitLocator,
top: Abs, top: Abs,
) { ) {
let Some(numbering) = ParLine::numbering_in(styles) else { return };
let margin = ParLine::number_margin_in(styles);
let align = ParLine::number_align_in(styles);
// Delay resolving the number clearance until line numbers are laid out to
// avoid inconsistent spacing depending on varying font size.
let clearance = ParLine::number_clearance_in(styles);
// Elements in tags must have a location for introspection to work. We do // Elements in tags must have a location for introspection to work. We do
// the work here instead of going through all of the realization process // the work here instead of going through all of the realization process
// just for this, given we don't need to actually place the marker as we // just for this, given we don't need to actually place the marker as we
// manually search for it in the frame later (when building a root flow, // manually search for it in the frame later (when building a root flow,
// where line numbers can be displayed), so we just need it to be in a tag // where line numbers can be displayed), so we just need it to be in a tag
// and to be valid (to have a location). // and to be valid (to have a location).
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); let mut marker = marker.clone();
let key = typst_utils::hash128(&marker); let key = typst_utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key); let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc); marker.set_location(loc);
@ -601,7 +601,7 @@ fn add_par_line_marker(
// line's general baseline. However, the line number will still need to // line's general baseline. However, the line number will still need to
// manually adjust its own 'y' position based on its own baseline. // manually adjust its own 'y' position based on its own baseline.
let pos = Point::with_y(top); let pos = Point::with_y(top);
output.push(pos, FrameItem::Tag(Tag::Start(marker))); output.push(pos, FrameItem::Tag(Tag::Start(marker.pack())));
output.push(pos, FrameItem::Tag(Tag::End(loc, key))); output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
} }
@ -626,7 +626,7 @@ fn overhang(c: char) -> f64 {
} }
} }
/// A collection of owned or borrowed paragraph items. /// A collection of owned or borrowed inline items.
pub struct Items<'a>(Vec<ItemEntry<'a>>); pub struct Items<'a>(Vec<ItemEntry<'a>>);
impl<'a> Items<'a> { impl<'a> Items<'a> {

View File

@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation;
use super::*; use super::*;
/// The cost of a line or paragraph layout. /// The cost of a line or inline layout.
type Cost = f64; type Cost = f64;
// Cost parameters. // Cost parameters.
@ -104,21 +104,13 @@ impl Breakpoint {
} }
} }
/// Breaks the paragraph into lines. /// Breaks the text into lines.
pub fn linebreak<'a>( pub fn linebreak<'a>(
engine: &Engine, engine: &Engine,
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
width: Abs, width: Abs,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
let linebreaks = p.linebreaks.unwrap_or_else(|| { match p.config.linebreaks {
if p.justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
match linebreaks {
Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Simple => linebreak_simple(engine, p, width),
Linebreaks::Optimized => linebreak_optimized(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width),
} }
@ -181,13 +173,12 @@ fn linebreak_simple<'a>(
/// lines with hyphens even more. /// lines with hyphens even more.
/// ///
/// To find the layout with the minimal total cost the algorithm uses dynamic /// To find the layout with the minimal total cost the algorithm uses dynamic
/// programming: For each possible breakpoint it determines the optimal /// programming: For each possible breakpoint, it determines the optimal layout
/// paragraph layout _up to that point_. It walks over all possible start points /// _up to that point_. It walks over all possible start points for a line
/// for a line ending at that point and finds the one for which the cost of the /// ending at that point and finds the one for which the cost of the line plus
/// line plus the cost of the optimal paragraph up to the start point (already /// the cost of the optimal layout up to the start point (already computed and
/// computed and stored in dynamic programming table) is minimal. The final /// stored in dynamic programming table) is minimal. The final result is simply
/// result is simply the layout determined for the last breakpoint at the end of /// the layout determined for the last breakpoint at the end of text.
/// text.
#[typst_macros::time] #[typst_macros::time]
fn linebreak_optimized<'a>( fn linebreak_optimized<'a>(
engine: &Engine, engine: &Engine,
@ -215,7 +206,7 @@ fn linebreak_optimized_bounded<'a>(
metrics: &CostMetrics, metrics: &CostMetrics,
upper_bound: Cost, upper_bound: Cost,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
/// An entry in the dynamic programming table for paragraph optimization. /// An entry in the dynamic programming table for inline layout optimization.
struct Entry<'a> { struct Entry<'a> {
pred: usize, pred: usize,
total: Cost, total: Cost,
@ -299,7 +290,7 @@ fn linebreak_optimized_bounded<'a>(
} }
// If this attempt is better than what we had before, take it! // If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) { if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry { pred: pred_index, total, line: attempt, end }); best = Some(Entry { pred: pred_index, total, line: attempt, end });
} }
} }
@ -321,7 +312,7 @@ fn linebreak_optimized_bounded<'a>(
// This should only happen if our bound was faulty. Which shouldn't happen! // This should only happen if our bound was faulty. Which shouldn't happen!
if table[idx].end != p.text.len() { if table[idx].end != p.text.len() {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
panic!("bounded paragraph layout is incomplete"); panic!("bounded inline layout is incomplete");
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY); return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY);
@ -342,7 +333,7 @@ fn linebreak_optimized_bounded<'a>(
/// (which is costly) to determine costs, it determines approximate costs using /// (which is costly) to determine costs, it determines approximate costs using
/// cumulative arrays. /// cumulative arrays.
/// ///
/// This results in a likely good paragraph layouts, for which we then compute /// This results in a likely good inline layouts, for which we then compute
/// the exact cost. This cost is an upper bound for proper optimized /// the exact cost. This cost is an upper bound for proper optimized
/// linebreaking. We can use it to heavily prune the search space. /// linebreaking. We can use it to heavily prune the search space.
#[typst_macros::time] #[typst_macros::time]
@ -355,7 +346,7 @@ fn linebreak_optimized_approximate(
// Determine the cumulative estimation metrics. // Determine the cumulative estimation metrics.
let estimates = Estimates::compute(p); let estimates = Estimates::compute(p);
/// An entry in the dynamic programming table for paragraph optimization. /// An entry in the dynamic programming table for inline layout optimization.
struct Entry { struct Entry {
pred: usize, pred: usize,
total: Cost, total: Cost,
@ -385,7 +376,7 @@ fn linebreak_optimized_approximate(
// Whether the line is justified. This is not 100% accurate w.r.t // Whether the line is justified. This is not 100% accurate w.r.t
// to line()'s behaviour, but good enough. // to line()'s behaviour, but good enough.
let justify = p.justify && breakpoint != Breakpoint::Mandatory; let justify = p.config.justify && breakpoint != Breakpoint::Mandatory;
// We don't really know whether the line naturally ends with a dash // We don't really know whether the line naturally ends with a dash
// here, so we can miss that case, but it's ok, since all of this // here, so we can miss that case, but it's ok, since all of this
@ -432,7 +423,7 @@ fn linebreak_optimized_approximate(
let total = pred.total + line_cost; let total = pred.total + line_cost;
// If this attempt is better than what we had before, take it! // If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) { if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry { best = Some(Entry {
pred: pred_index, pred: pred_index,
total, total,
@ -574,7 +565,7 @@ fn raw_ratio(
// calculate the extra amount. Also, don't divide by zero. // calculate the extra amount. Also, don't divide by zero.
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
// Normalize the amount by half the em size. // Normalize the amount by half the em size.
ratio = 1.0 + extra_stretch / (p.size / 2.0); ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0);
} }
// The min value must be < MIN_RATIO, but how much smaller doesn't matter // The min value must be < MIN_RATIO, but how much smaller doesn't matter
@ -664,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
return; return;
} }
let hyphenate = p.hyphenate != Some(false); let hyphenate = p.config.hyphenate != Some(false);
let lb = LINEBREAK_DATA.as_borrowed(); let lb = LINEBREAK_DATA.as_borrowed();
let segmenter = match p.lang { let segmenter = match p.config.lang {
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
_ => &SEGMENTER, _ => &SEGMENTER,
}; };
@ -831,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
/// Whether hyphenation is enabled at the given offset. /// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(p: &Preparation, offset: usize) -> bool { fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.hyphenate p.config.hyphenate.unwrap_or_else(|| {
.or_else(|| { let (_, item) = p.get(offset);
let (_, item) = p.get(offset); match item.text() {
let styles = item.text()?.styles; Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
Some(TextElem::hyphenate_in(styles)) None => false,
}) }
.unwrap_or(false) })
} }
/// The text language at the given offset. /// The text language at the given offset.
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> { fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.lang.or_else(|| { let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
let styles = item.text()?.styles; let styles = item.text()?.styles;
Some(TextElem::lang_in(styles)) Some(TextElem::lang_in(styles))
@ -862,17 +853,17 @@ struct CostMetrics {
} }
impl CostMetrics { impl CostMetrics {
/// Compute shared metrics for paragraph optimization. /// Compute shared metrics for inline layout optimization.
fn compute(p: &Preparation) -> Self { fn compute(p: &Preparation) -> Self {
Self { Self {
// When justifying, we may stretch spaces below their natural width. // When justifying, we may stretch spaces below their natural width.
min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
// Approximate hyphen width for estimates. // Approximate hyphen width for estimates.
approx_hyphen_width: Em::new(0.33).at(p.size), approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
// Costs. // Costs.
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
} }
} }

View File

@ -13,17 +13,22 @@ pub use self::box_::layout_box;
use comemo::{Track, Tracked, TrackedMut}; use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{StyleChain, StyleVec}; use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{Introspector, Locator, LocatorLink}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator};
use typst_library::layout::{Fragment, Size}; use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size};
use typst_library::model::ParElem; use typst_library::model::{
use typst_library::routines::Routines; EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker,
TermsElem,
};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::{Costs, Lang, TextElem};
use typst_library::World; use typst_library::World;
use typst_utils::{Numeric, SliceExt};
use self::collect::{collect, Item, Segment, SpanMapper}; use self::collect::{collect, Item, Segment, SpanMapper};
use self::deco::decorate; use self::deco::decorate;
use self::finalize::finalize; use self::finalize::finalize;
use self::line::{commit, line, Line}; use self::line::{apply_baseline_shift, commit, line, Line};
use self::linebreak::{linebreak, Breakpoint}; use self::linebreak::{linebreak, Breakpoint};
use self::prepare::{prepare, Preparation}; use self::prepare::{prepare, Preparation};
use self::shaping::{ use self::shaping::{
@ -34,18 +39,18 @@ use self::shaping::{
/// Range of a substring of text. /// Range of a substring of text.
type Range = std::ops::Range<usize>; type Range = std::ops::Range<usize>;
/// Layouts content inline. /// Layouts the paragraph.
pub fn layout_inline( pub fn layout_par(
elem: &Packed<ParElem>,
engine: &mut Engine, engine: &mut Engine,
children: &StyleVec,
locator: Locator, locator: Locator,
styles: StyleChain, styles: StyleChain,
consecutive: bool,
region: Size, region: Size,
expand: bool, expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
layout_inline_impl( layout_par_impl(
children, elem,
engine.routines, engine.routines,
engine.world, engine.world,
engine.introspector, engine.introspector,
@ -54,17 +59,17 @@ pub fn layout_inline(
engine.route.track(), engine.route.track(),
locator.track(), locator.track(),
styles, styles,
consecutive,
region, region,
expand, expand,
situation,
) )
} }
/// The internal, memoized implementation of `layout_inline`. /// The internal, memoized implementation of `layout_par`.
#[comemo::memoize] #[comemo::memoize]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn layout_inline_impl( fn layout_par_impl(
children: &StyleVec, elem: &Packed<ParElem>,
routines: &Routines, routines: &Routines,
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>, introspector: Tracked<Introspector>,
@ -73,12 +78,12 @@ fn layout_inline_impl(
route: Tracked<Route>, route: Tracked<Route>,
locator: Tracked<Locator>, locator: Tracked<Locator>,
styles: StyleChain, styles: StyleChain,
consecutive: bool,
region: Size, region: Size,
expand: bool, expand: bool,
situation: ParSituation,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let link = LocatorLink::new(locator); let link = LocatorLink::new(locator);
let locator = Locator::link(&link); let mut locator = Locator::link(&link).split();
let mut engine = Engine { let mut engine = Engine {
routines, routines,
world, world,
@ -88,18 +93,227 @@ fn layout_inline_impl(
route: Route::extend(route), route: Route::extend(route),
}; };
let mut locator = locator.split(); let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::LayoutPar,
&mut engine,
&mut locator,
&arenas,
&elem.body,
styles,
)?;
layout_inline_impl(
&mut engine,
&children,
&mut locator,
styles,
region,
expand,
Some(situation),
&ConfigBase {
justify: elem.justify(styles),
linebreaks: elem.linebreaks(styles),
first_line_indent: elem.first_line_indent(styles),
hanging_indent: elem.hanging_indent(styles),
},
)
}
/// Lays out realized content with inline layout.
pub fn layout_inline<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
locator: &mut SplitLocator<'a>,
shared: StyleChain<'a>,
region: Size,
expand: bool,
) -> SourceResult<Fragment> {
layout_inline_impl(
engine,
children,
locator,
shared,
region,
expand,
None,
&ConfigBase {
justify: ParElem::justify_in(shared),
linebreaks: ParElem::linebreaks_in(shared),
first_line_indent: ParElem::first_line_indent_in(shared),
hanging_indent: ParElem::hanging_indent_in(shared),
},
)
}
/// The internal implementation of [`layout_inline`].
#[allow(clippy::too_many_arguments)]
fn layout_inline_impl<'a>(
engine: &mut Engine,
children: &[Pair<'a>],
locator: &mut SplitLocator<'a>,
shared: StyleChain<'a>,
region: Size,
expand: bool,
par: Option<ParSituation>,
base: &ConfigBase,
) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole inline layout.
let config = configuration(base, children, shared, par);
// Collect all text into one string for BiDi analysis. // Collect all text into one string for BiDi analysis.
let (text, segments, spans) = let (text, segments, spans) = collect(children, engine, locator, &config, region)?;
collect(children, &mut engine, &mut locator, &styles, region, consecutive)?;
// Perform BiDi analysis and then prepares paragraph layout. // Perform BiDi analysis and performs some preparation steps before we
let p = prepare(&mut engine, children, &text, segments, spans, styles)?; // proceed to line breaking.
let p = prepare(engine, &config, &text, segments, spans)?;
// Break the paragraph into lines. // Break the text into lines.
let lines = linebreak(&engine, &p, region.x - p.hang); let lines = linebreak(engine, &p, region.x - config.hanging_indent);
// Turn the selected lines into frames. // Turn the selected lines into frames.
finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator) finalize(engine, &p, &lines, region, expand, locator)
}
/// Determine the inline layout's configuration.
fn configuration(
base: &ConfigBase,
children: &[Pair],
shared: StyleChain,
situation: Option<ParSituation>,
) -> Config {
let justify = base.justify;
let font_size = TextElem::size_in(shared);
let dir = TextElem::dir_in(shared);
Config {
justify,
linebreaks: base.linebreaks.unwrap_or_else(|| {
if justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
}),
first_line_indent: {
let FirstLineIndent { amount, all } = base.first_line_indent;
if !amount.is_zero()
&& match situation {
// First-line indent for the first paragraph after a list
// bullet just looks bad.
Some(ParSituation::First) => all && !in_list(shared),
Some(ParSituation::Consecutive) => true,
Some(ParSituation::Other) => all,
None => false,
}
&& AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into()
{
amount.at(font_size)
} else {
Abs::zero()
}
},
hanging_indent: if situation.is_some() {
base.hanging_indent
} else {
Abs::zero()
},
numbering_marker: ParLine::numbering_in(shared).map(|numbering| {
Packed::new(ParLineMarker::new(
numbering,
ParLine::number_align_in(shared),
ParLine::number_margin_in(shared),
// Delay resolving the number clearance until line numbers are
// laid out to avoid inconsistent spacing depending on varying
// font size.
ParLine::number_clearance_in(shared),
))
}),
align: AlignElem::alignment_in(shared).fix(dir).x,
font_size,
dir,
hyphenate: shared_get(children, shared, TextElem::hyphenate_in)
.map(|uniform| uniform.unwrap_or(justify)),
lang: shared_get(children, shared, TextElem::lang_in),
fallback: TextElem::fallback_in(shared),
cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(),
costs: TextElem::costs_in(shared),
}
}
/// Distinguishes between a few different kinds of paragraphs.
///
/// In the form `Option<ParSituation>`, `None` implies that we are creating an
/// inline layout that isn't a semantic paragraph.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum ParSituation {
/// The paragraph is the first thing in the flow.
First,
/// The paragraph follows another paragraph.
Consecutive,
/// Any other kind of paragraph.
Other,
}
/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`].
struct ConfigBase {
justify: bool,
linebreaks: Smart<Linebreaks>,
first_line_indent: FirstLineIndent,
hanging_indent: Abs,
}
/// Shared configuration for the whole inline layout.
struct Config {
/// Whether to justify text.
justify: bool,
/// How to determine line breaks.
linebreaks: Linebreaks,
/// The indent the first line of a paragraph should have.
first_line_indent: Abs,
/// The indent that all but the first line of a paragraph should have.
hanging_indent: Abs,
/// Configuration for line numbering.
numbering_marker: Option<Packed<ParLineMarker>>,
/// The resolved horizontal alignment.
align: FixedAlignment,
/// The text size.
font_size: Abs,
/// The dominant direction.
dir: Dir,
/// A uniform hyphenation setting (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
hyphenate: Option<bool>,
/// The text language (only `Some(_)` if it's the same for all
/// children, otherwise `None`).
lang: Option<Lang>,
/// Whether font fallback is enabled.
fallback: bool,
/// Whether to add spacing between CJK and Latin characters.
cjk_latin_spacing: bool,
/// Costs for various layout decisions.
costs: Costs,
}
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// Whether we have a list ancestor.
///
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
} }

View File

@ -1,23 +1,23 @@
use typst_library::foundations::{Resolve, Smart}; use typst_library::layout::{Dir, Em};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks;
use typst_library::text::{Costs, Lang, TextElem};
use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*; use super::*;
/// A paragraph representation in which children are already layouted and text /// A representation in which children are already layouted and text is already
/// is already preshaped. /// preshaped.
/// ///
/// In many cases, we can directly reuse these results when constructing a line. /// In many cases, we can directly reuse these results when constructing a line.
/// Only when a line break falls onto a text index that is not safe-to-break per /// Only when a line break falls onto a text index that is not safe-to-break per
/// rustybuzz, we have to reshape that portion. /// rustybuzz, we have to reshape that portion.
pub struct Preparation<'a> { pub struct Preparation<'a> {
/// The paragraph's full text. /// The full text.
pub text: &'a str, pub text: &'a str,
/// Bidirectional text embedding levels for the paragraph. /// Configuration for inline layout.
pub config: &'a Config,
/// Bidirectional text embedding levels.
/// ///
/// This is `None` if the paragraph is BiDi-uniform (all the base direction). /// This is `None` if all text directions are uniform (all the base
/// direction).
pub bidi: Option<BidiInfo<'a>>, pub bidi: Option<BidiInfo<'a>>,
/// Text runs, spacing and layouted elements. /// Text runs, spacing and layouted elements.
pub items: Vec<(Range, Item<'a>)>, pub items: Vec<(Range, Item<'a>)>,
@ -25,28 +25,6 @@ pub struct Preparation<'a> {
pub indices: Vec<usize>, pub indices: Vec<usize>,
/// The span mapper. /// The span mapper.
pub spans: SpanMapper, pub spans: SpanMapper,
/// Whether to hyphenate if it's the same for all children.
pub hyphenate: Option<bool>,
/// Costs for various layout decisions.
pub costs: Costs,
/// The dominant direction.
pub dir: Dir,
/// The text language if it's the same for all children.
pub lang: Option<Lang>,
/// The paragraph's resolved horizontal alignment.
pub align: FixedAlignment,
/// Whether to justify the paragraph.
pub justify: bool,
/// The paragraph's hanging indent.
pub hang: Abs,
/// Whether to add spacing between CJK and Latin characters.
pub cjk_latin_spacing: bool,
/// Whether font fallback is enabled for this paragraph.
pub fallback: bool,
/// How to determine line breaks.
pub linebreaks: Smart<Linebreaks>,
/// The text size.
pub size: Abs,
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
@ -71,20 +49,18 @@ impl<'a> Preparation<'a> {
} }
} }
/// Performs BiDi analysis and then prepares paragraph layout by building a /// Performs BiDi analysis and then prepares further layout by building a
/// representation on which we can do line breaking without layouting each and /// representation on which we can do line breaking without layouting each and
/// every line from scratch. /// every line from scratch.
#[typst_macros::time] #[typst_macros::time]
pub fn prepare<'a>( pub fn prepare<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &'a StyleVec, config: &'a Config,
text: &'a str, text: &'a str,
segments: Vec<Segment<'a>>, segments: Vec<Segment<'a>>,
spans: SpanMapper, spans: SpanMapper,
styles: StyleChain<'a>,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles); let default_level = match config.dir {
let default_level = match dir {
Dir::RTL => BidiLevel::rtl(), Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(), _ => BidiLevel::ltr(),
}; };
@ -120,28 +96,17 @@ pub fn prepare<'a>(
indices.extend(range.clone().map(|_| i)); indices.extend(range.clone().map(|_| i));
} }
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); if config.cjk_latin_spacing {
if cjk_latin_spacing {
add_cjk_latin_spacing(&mut items); add_cjk_latin_spacing(&mut items);
} }
Ok(Preparation { Ok(Preparation {
config,
text, text,
bidi: is_bidi.then_some(bidi), bidi: is_bidi.then_some(bidi),
items, items,
indices, indices,
spans, spans,
hyphenate: children.shared_get(styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles),
dir,
lang: children.shared_get(styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles),
hang: ParElem::hanging_indent_in(styles),
cjk_latin_spacing,
fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_in(styles),
size: TextElem::size_in(styles),
}) })
} }

View File

@ -20,6 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript}; use unicode_script::{Script, UnicodeScript};
use super::{decorate, Item, Range, SpanMapper}; use super::{decorate, Item, Range, SpanMapper};
use crate::modifiers::{FrameModifiers, FrameModify};
/// The result of shaping text. /// The result of shaping text.
/// ///
@ -28,7 +29,7 @@ use super::{decorate, Item, Range, SpanMapper};
/// frame. /// frame.
#[derive(Clone)] #[derive(Clone)]
pub struct ShapedText<'a> { pub struct ShapedText<'a> {
/// The start of the text in the full paragraph. /// The start of the text in the full text.
pub base: usize, pub base: usize,
/// The text that was shaped. /// The text that was shaped.
pub text: &'a str, pub text: &'a str,
@ -65,9 +66,9 @@ pub struct ShapedGlyph {
pub y_offset: Em, pub y_offset: Em,
/// The adjustability of the glyph. /// The adjustability of the glyph.
pub adjustability: Adjustability, pub adjustability: Adjustability,
/// The byte range of this glyph's cluster in the full paragraph. A cluster /// The byte range of this glyph's cluster in the full inline layout. A
/// is a sequence of one or multiple glyphs that cannot be separated and /// cluster is a sequence of one or multiple glyphs that cannot be separated
/// must always be treated as a union. /// and must always be treated as a union.
/// ///
/// The range values of the glyphs in a [`ShapedText`] should not overlap /// The range values of the glyphs in a [`ShapedText`] should not overlap
/// with each other, and they should be monotonically increasing (for /// with each other, and they should be monotonically increasing (for
@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> {
offset += width; offset += width;
} }
frame.modify(&FrameModifiers::get_in(self.styles));
frame frame
} }
@ -403,7 +405,7 @@ impl<'a> ShapedText<'a> {
/// Reshape a range of the shaped text, reusing information from this /// Reshape a range of the shaped text, reusing information from this
/// shaping process if possible. /// shaping process if possible.
/// ///
/// The text `range` is relative to the whole paragraph. /// The text `range` is relative to the whole inline layout.
pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> { pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> {
let text = &self.text[text_range.start - self.base..text_range.end - self.base]; let text = &self.text[text_range.start - self.base..text_range.end - self.base];
if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) {
@ -463,7 +465,7 @@ impl<'a> ShapedText<'a> {
None None
}; };
let mut chain = families(self.styles) let mut chain = families(self.styles)
.filter(|family| family.covers().map_or(true, |c| c.is_match("-"))) .filter(|family| family.covers().is_none_or(|c| c.is_match("-")))
.map(|family| book.select(family.as_str(), self.variant)) .map(|family| book.select(family.as_str(), self.variant))
.chain(fallback_func.iter().map(|f| f())) .chain(fallback_func.iter().map(|f| f()))
.flatten(); .flatten();
@ -568,7 +570,7 @@ impl<'a> ShapedText<'a> {
// for the next line. // for the next line.
let dec = if ltr { usize::checked_sub } else { usize::checked_add }; let dec = if ltr { usize::checked_sub } else { usize::checked_add };
while let Some(next) = dec(idx, 1) { while let Some(next) = dec(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) {
break; break;
} }
idx = next; idx = next;
@ -810,7 +812,7 @@ fn shape_segment<'a>(
.nth(1) .nth(1)
.map(|(i, _)| offset + i) .map(|(i, _)| offset + i)
.unwrap_or(text.len()); .unwrap_or(text.len());
covers.map_or(true, |cov| cov.is_match(&text[offset..end])) covers.is_none_or(|cov| cov.is_match(&text[offset..end]))
}; };
// Collect the shaped glyphs, doing fallback and shaping parts again with // Collect the shaped glyphs, doing fallback and shaping parts again with

View File

@ -6,6 +6,7 @@ mod image;
mod inline; mod inline;
mod lists; mod lists;
mod math; mod math;
mod modifiers;
mod pad; mod pad;
mod pages; mod pages;
mod repeat; mod repeat;
@ -16,15 +17,14 @@ mod transforms;
pub use self::flow::{layout_columns, layout_fragment, layout_frame}; pub use self::flow::{layout_columns, layout_fragment, layout_frame};
pub use self::grid::{layout_grid, layout_table}; pub use self::grid::{layout_grid, layout_table};
pub use self::image::layout_image; pub use self::image::layout_image;
pub use self::inline::{layout_box, layout_inline};
pub use self::lists::{layout_enum, layout_list}; pub use self::lists::{layout_enum, layout_list};
pub use self::math::{layout_equation_block, layout_equation_inline}; pub use self::math::{layout_equation_block, layout_equation_inline};
pub use self::pad::layout_pad; pub use self::pad::layout_pad;
pub use self::pages::layout_document; pub use self::pages::layout_document;
pub use self::repeat::layout_repeat; pub use self::repeat::layout_repeat;
pub use self::shapes::{ pub use self::shapes::{
layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect, layout_circle, layout_curve, layout_ellipse, layout_line, layout_path,
layout_square, layout_polygon, layout_rect, layout_square,
}; };
pub use self::stack::layout_stack; pub use self::stack::layout_stack;
pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew}; pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew};

View File

@ -4,11 +4,12 @@ use typst_library::diag::SourceResult;
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain};
use typst_library::introspection::Locator; use typst_library::introspection::Locator;
use typst_library::layout::grid::resolve::{Cell, CellGrid};
use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment};
use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
use crate::grid::{Cell, CellGrid, GridLayouter}; use crate::grid::GridLayouter;
/// Layout the list. /// Layout the list.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
@ -21,8 +22,9 @@ pub fn layout_list(
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
let indent = elem.indent(styles); let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| { let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) { if tight {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
ParElem::spacing_in(styles).into() ParElem::spacing_in(styles).into()
@ -39,12 +41,18 @@ pub fn layout_list(
let mut cells = vec![]; let mut cells = vec![];
let mut locator = locator.split(); let mut locator = locator.split();
for item in elem.children() { for item in &elem.children {
// Text in wide lists shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( cells.push(Cell::new(
item.body.clone().styled(ListElem::set_depth(Depth(1))), body.styled(ListElem::set_depth(Depth(1))),
locator.next(&item.body.span()), locator.next(&item.body.span()),
)); ));
} }
@ -77,8 +85,9 @@ pub fn layout_enum(
let reversed = elem.reversed(styles); let reversed = elem.reversed(styles);
let indent = elem.indent(styles); let indent = elem.indent(styles);
let body_indent = elem.body_indent(styles); let body_indent = elem.body_indent(styles);
let tight = elem.tight(styles);
let gutter = elem.spacing(styles).unwrap_or_else(|| { let gutter = elem.spacing(styles).unwrap_or_else(|| {
if elem.tight(styles) { if tight {
ParElem::leading_in(styles).into() ParElem::leading_in(styles).into()
} else { } else {
ParElem::spacing_in(styles).into() ParElem::spacing_in(styles).into()
@ -100,7 +109,7 @@ pub fn layout_enum(
// relation to the item it refers to. // relation to the item it refers to.
let number_align = elem.number_align(styles); let number_align = elem.number_align(styles);
for item in elem.children() { for item in &elem.children {
number = item.number(styles).unwrap_or(number); number = item.number(styles).unwrap_or(number);
let context = Context::new(None, Some(styles)); let context = Context::new(None, Some(styles));
@ -123,11 +132,17 @@ pub fn layout_enum(
let resolved = let resolved =
resolved.aligned(number_align).styled(TextElem::set_overhang(false)); resolved.aligned(number_align).styled(TextElem::set_overhang(false));
// Text in wide enums shall always turn into paragraphs.
let mut body = item.body.clone();
if !tight {
body += ParbreakElem::shared();
}
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&())));
cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&())));
cells.push(Cell::new( cells.push(Cell::new(
item.body.clone().styled(EnumElem::set_parents(smallvec![number])), body.styled(EnumElem::set_parents(smallvec![number])),
locator.next(&item.body.span()), locator.next(&item.body.span()),
)); ));
number = number =

View File

@ -1,12 +1,9 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Em, Frame, Point, Rel, Size}; use typst_library::layout::{Em, Frame, Point, Size};
use typst_library::math::{Accent, AccentElem}; use typst_library::math::{Accent, AccentElem};
use super::{ use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment};
scaled_font_size, style_cramped, FrameFragment, GlyphFragment, MathContext,
MathFragment,
};
/// How much the accent can be shorter than the base. /// How much the accent can be shorter than the base.
const ACCENT_SHORT_FALL: Em = Em::new(0.5); const ACCENT_SHORT_FALL: Em = Em::new(0.5);
@ -19,7 +16,7 @@ pub fn layout_accent(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let cramped = style_cramped(); let cramped = style_cramped();
let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
// Try to replace a glyph with its dotless variant. // Try to replace a glyph with its dotless variant.
if let MathFragment::Glyph(glyph) = &mut base { if let MathFragment::Glyph(glyph) = &mut base {
@ -30,18 +27,14 @@ pub fn layout_accent(
let base_class = base.class(); let base_class = base.class();
let base_attach = base.accent_attach(); let base_attach = base.accent_attach();
let width = elem let width = elem.size(styles).relative_to(base.width());
.size(styles)
.unwrap_or(Rel::one())
.at(scaled_font_size(ctx, styles))
.relative_to(base.width());
let Accent(c) = elem.accent(); let Accent(c) = elem.accent;
let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span());
// Try to replace accent glyph with flattened variant. // Try to replace accent glyph with flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
if base.height() > flattened_base_height { if base.ascent() > flattened_base_height {
glyph.make_flattened_accent_form(ctx); glyph.make_flattened_accent_form(ctx);
} }
@ -57,7 +50,7 @@ pub fn layout_accent(
// minus the accent base height. Only if the base is very small, we need // minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low. // a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height); let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.height().min(accent_base_height); let gap = -accent.descent() - base.ascent().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height()); let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach); let accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap); let base_pos = Point::with_y(accent.height() + gap);
@ -75,7 +68,7 @@ pub fn layout_accent(
frame.push_frame(accent_pos, accent); frame.push_frame(accent_pos, accent);
frame.push_frame(base_pos, base.into_frame()); frame.push_frame(base_pos, base.into_frame());
ctx.push( ctx.push(
FrameFragment::new(ctx, styles, frame) FrameFragment::new(styles, frame)
.with_class(base_class) .with_class(base_class)
.with_base_ascent(base_ascent) .with_base_ascent(base_ascent)
.with_italics_correction(base_italics_correction) .with_italics_correction(base_italics_correction)

View File

@ -1,10 +1,9 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, Smart, StyleChain}; use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size};
use typst_library::math::{ use typst_library::math::{
AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem,
}; };
use typst_library::text::TextElem;
use typst_utils::OptionExt; use typst_utils::OptionExt;
use super::{ use super::{
@ -29,7 +28,7 @@ pub fn layout_attach(
let elem = merged.as_ref().unwrap_or(elem); let elem = merged.as_ref().unwrap_or(elem);
let stretch = stretch_size(styles, elem); let stretch = stretch_size(styles, elem);
let mut base = ctx.layout_into_fragment(elem.base(), styles)?; let mut base = ctx.layout_into_fragment(&elem.base, styles)?;
let sup_style = style_for_superscript(styles); let sup_style = style_for_superscript(styles);
let sup_style_chain = styles.chain(&sup_style); let sup_style_chain = styles.chain(&sup_style);
let tl = elem.tl(sup_style_chain); let tl = elem.tl(sup_style_chain);
@ -95,7 +94,7 @@ pub fn layout_primes(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
match *elem.count() { match elem.count {
count @ 1..=4 => { count @ 1..=4 => {
let c = match count { let c = match count {
1 => '', 1 => '',
@ -104,13 +103,14 @@ pub fn layout_primes(
4 => '⁗', 4 => '⁗',
_ => unreachable!(), _ => unreachable!(),
}; };
let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?;
ctx.push(f); ctx.push(f);
} }
count => { count => {
// Custom amount of primes // Custom amount of primes
let prime = let prime = ctx
ctx.layout_into_fragment(&TextElem::packed(''), styles)?.into_frame(); .layout_into_fragment(&SymbolElem::packed(''), styles)?
.into_frame();
let width = prime.width() * (count + 1) as f64 / 2.0; let width = prime.width() * (count + 1) as f64 / 2.0;
let mut frame = Frame::soft(Size::new(width, prime.height())); let mut frame = Frame::soft(Size::new(width, prime.height()));
frame.set_baseline(prime.ascent()); frame.set_baseline(prime.ascent());
@ -121,7 +121,7 @@ pub fn layout_primes(
prime.clone(), prime.clone(),
) )
} }
ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); ctx.push(FrameFragment::new(styles, frame).with_text_like(true));
} }
} }
Ok(()) Ok(())
@ -134,7 +134,7 @@ pub fn layout_scripts(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(Limits::Never); fragment.set_limits(Limits::Never);
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
@ -148,21 +148,18 @@ pub fn layout_limits(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display };
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
fragment.set_limits(limits); fragment.set_limits(limits);
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
} }
/// Get the size to stretch the base to, if the attach argument is true. /// Get the size to stretch the base to.
fn stretch_size( fn stretch_size(styles: StyleChain, elem: &Packed<AttachElem>) -> Option<Rel<Abs>> {
styles: StyleChain,
elem: &Packed<AttachElem>,
) -> Option<Smart<Rel<Length>>> {
// Extract from an EquationElem. // Extract from an EquationElem.
let mut base = elem.base(); let mut base = &elem.base;
if let Some(equation) = base.to_packed::<EquationElem>() { while let Some(equation) = base.to_packed::<EquationElem>() {
base = equation.body(); base = &equation.body;
} }
base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles)) base.to_packed::<StretchElem>().map(|stretch| stretch.size(styles))
@ -277,7 +274,7 @@ fn layout_attachments(
layout!(b, b_x, b_y); // lower-limit layout!(b, b_x, b_y); // lower-limit
// Done! Note that we retain the class of the base. // Done! Note that we retain the class of the base.
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class)); ctx.push(FrameFragment::new(styles, frame).with_class(base_class));
Ok(()) Ok(())
} }

View File

@ -7,7 +7,7 @@ use typst_library::text::TextElem;
use typst_library::visualize::{FixedStroke, Geometry}; use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{scaled_font_size, FrameFragment, MathContext}; use super::{FrameFragment, MathContext};
/// Lays out a [`CancelElem`]. /// Lays out a [`CancelElem`].
#[typst_macros::time(name = "math.cancel", span = elem.span())] #[typst_macros::time(name = "math.cancel", span = elem.span())]
@ -16,7 +16,7 @@ pub fn layout_cancel(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let body = ctx.layout_into_fragment(elem.body(), styles)?; let body = ctx.layout_into_fragment(&elem.body, styles)?;
// Preserve properties of body. // Preserve properties of body.
let body_class = body.class(); let body_class = body.class();
@ -27,7 +27,7 @@ pub fn layout_cancel(
let mut body = body.into_frame(); let mut body = body.into_frame();
let body_size = body.size(); let body_size = body.size();
let span = elem.span(); let span = elem.span();
let length = elem.length(styles).at(scaled_font_size(ctx, styles)); let length = elem.length(styles);
let stroke = elem.stroke(styles).unwrap_or(FixedStroke { let stroke = elem.stroke(styles).unwrap_or(FixedStroke {
paint: TextElem::fill_in(styles).as_decoration(), paint: TextElem::fill_in(styles).as_decoration(),
@ -63,7 +63,7 @@ pub fn layout_cancel(
} }
ctx.push( ctx.push(
FrameFragment::new(ctx, styles, body) FrameFragment::new(styles, body)
.with_class(body_class) .with_class(body_class)
.with_italics_correction(body_italics) .with_italics_correction(body_italics)
.with_accent_attach(body_attach) .with_accent_attach(body_attach)

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem};
use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; use typst_library::layout::{Em, Frame, FrameItem, Point, Size};
use typst_library::math::{BinomElem, FracElem}; use typst_library::math::{BinomElem, FracElem};
use typst_library::text::TextElem; use typst_library::text::TextElem;
@ -7,8 +7,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment, style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment,
GlyphFragment, MathContext, DELIM_SHORT_FALL, MathContext, DELIM_SHORT_FALL,
}; };
const FRAC_AROUND: Em = Em::new(0.1); const FRAC_AROUND: Em = Em::new(0.1);
@ -23,8 +23,8 @@ pub fn layout_frac(
layout_frac_like( layout_frac_like(
ctx, ctx,
styles, styles,
elem.num(), &elem.num,
std::slice::from_ref(elem.denom()), std::slice::from_ref(&elem.denom),
false, false,
elem.span(), elem.span(),
) )
@ -37,7 +37,7 @@ pub fn layout_binom(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span())
} }
/// Layout a fraction or binomial. /// Layout a fraction or binomial.
@ -49,8 +49,7 @@ fn layout_frac_like(
binom: bool, binom: bool,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let font_size = scaled_font_size(ctx, styles); let short_fall = DELIM_SHORT_FALL.resolve(styles);
let short_fall = DELIM_SHORT_FALL.at(font_size);
let axis = scaled!(ctx, styles, axis_height); let axis = scaled!(ctx, styles, axis_height);
let thickness = scaled!(ctx, styles, fraction_rule_thickness); let thickness = scaled!(ctx, styles, fraction_rule_thickness);
let shift_up = scaled!( let shift_up = scaled!(
@ -81,12 +80,15 @@ fn layout_frac_like(
let denom = ctx.layout_into_frame( let denom = ctx.layout_into_frame(
&Content::sequence( &Content::sequence(
// Add a comma between each element. // Add a comma between each element.
denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1), denom
.iter()
.flat_map(|a| [SymbolElem::packed(','), a.clone()])
.skip(1),
), ),
styles.chain(&denom_style), styles.chain(&denom_style),
)?; )?;
let around = FRAC_AROUND.at(font_size); let around = FRAC_AROUND.resolve(styles);
let num_gap = (shift_up - (axis + thickness / 2.0) - num.descent()).max(num_min); let num_gap = (shift_up - (axis + thickness / 2.0) - num.descent()).max(num_min);
let denom_gap = let denom_gap =
(shift_down + (axis - thickness / 2.0) - denom.ascent()).max(denom_min); (shift_down + (axis - thickness / 2.0) - denom.ascent()).max(denom_min);
@ -111,7 +113,7 @@ fn layout_frac_like(
.stretch_vertical(ctx, height, short_fall); .stretch_vertical(ctx, height, short_fall);
left.center_on_axis(ctx); left.center_on_axis(ctx);
ctx.push(left); ctx.push(left);
ctx.push(FrameFragment::new(ctx, styles, frame)); ctx.push(FrameFragment::new(styles, frame));
let mut right = GlyphFragment::new(ctx, styles, ')', span) let mut right = GlyphFragment::new(ctx, styles, ')', span)
.stretch_vertical(ctx, height, short_fall); .stretch_vertical(ctx, height, short_fall);
right.center_on_axis(ctx); right.center_on_axis(ctx);
@ -129,7 +131,7 @@ fn layout_frac_like(
span, span,
), ),
); );
ctx.push(FrameFragment::new(ctx, styles, frame)); ctx.push(FrameFragment::new(styles, frame));
} }
Ok(()) Ok(())

View File

@ -1,23 +1,23 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use rustybuzz::Feature; use rustybuzz::Feature;
use smallvec::SmallVec;
use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable};
use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::opentype_layout::LayoutTable;
use ttf_parser::{GlyphId, Rect}; use ttf_parser::{GlyphId, Rect};
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::introspection::Tag; use typst_library::introspection::Tag;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment,
}; };
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::model::{Destination, LinkElem};
use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem};
use typst_library::visualize::Paint; use typst_library::visualize::Paint;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::default_math_class;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{scaled_font_size, stretch_glyph, MathContext, Scaled}; use super::{stretch_glyph, MathContext, Scaled};
use crate::modifiers::{FrameModifiers, FrameModify};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum MathFragment { pub enum MathFragment {
@ -245,8 +245,7 @@ pub struct GlyphFragment {
pub class: MathClass, pub class: MathClass,
pub math_size: MathSize, pub math_size: MathSize,
pub span: Span, pub span: Span,
pub dests: SmallVec<[Destination; 1]>, pub modifiers: FrameModifiers,
pub hidden: bool,
pub limits: Limits, pub limits: Limits,
pub extended_shape: bool, pub extended_shape: bool,
} }
@ -277,11 +276,7 @@ impl GlyphFragment {
span: Span, span: Span,
) -> Self { ) -> Self {
let class = EquationElem::class_in(styles) let class = EquationElem::class_in(styles)
.or_else(|| match c { .or_else(|| default_math_class(c))
':' => Some(MathClass::Relation),
'.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal),
_ => unicode_math_class::class(c),
})
.unwrap_or(MathClass::Normal); .unwrap_or(MathClass::Normal);
let mut fragment = Self { let mut fragment = Self {
@ -292,7 +287,7 @@ impl GlyphFragment {
region: TextElem::region_in(styles), region: TextElem::region_in(styles),
fill: TextElem::fill_in(styles).as_decoration(), fill: TextElem::fill_in(styles).as_decoration(),
shift: TextElem::baseline_in(styles), shift: TextElem::baseline_in(styles),
font_size: scaled_font_size(ctx, styles), font_size: TextElem::size_in(styles),
math_size: EquationElem::size_in(styles), math_size: EquationElem::size_in(styles),
width: Abs::zero(), width: Abs::zero(),
ascent: Abs::zero(), ascent: Abs::zero(),
@ -302,8 +297,7 @@ impl GlyphFragment {
accent_attach: Abs::zero(), accent_attach: Abs::zero(),
class, class,
span, span,
dests: LinkElem::dests_in(styles), modifiers: FrameModifiers::get_in(styles),
hidden: HideElem::hidden_in(styles),
extended_shape: false, extended_shape: false,
}; };
fragment.set_id(ctx, id); fragment.set_id(ctx, id);
@ -390,7 +384,7 @@ impl GlyphFragment {
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
frame.set_baseline(self.ascent); frame.set_baseline(self.ascent);
frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item));
frame.post_process_raw(self.dests, self.hidden); frame.modify(&self.modifiers);
frame frame
} }
@ -512,12 +506,12 @@ pub struct FrameFragment {
} }
impl FrameFragment { impl FrameFragment {
pub fn new(ctx: &MathContext, styles: StyleChain, frame: Frame) -> Self { pub fn new(styles: StyleChain, frame: Frame) -> Self {
let base_ascent = frame.ascent(); let base_ascent = frame.ascent();
let accent_attach = frame.width() / 2.0; let accent_attach = frame.width() / 2.0;
Self { Self {
frame: frame.post_processed(styles), frame: frame.modified(&FrameModifiers::get_in(styles)),
font_size: scaled_font_size(ctx, styles), font_size: TextElem::size_in(styles),
class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal),
math_size: EquationElem::size_in(styles), math_size: EquationElem::size_in(styles),
limits: Limits::Never, limits: Limits::Never,
@ -632,7 +626,7 @@ pub enum Limits {
impl Limits { impl Limits {
/// The default limit configuration if the given character is the base. /// The default limit configuration if the given character is the base.
pub fn for_char(c: char) -> Self { pub fn for_char(c: char) -> Self {
match unicode_math_class::class(c) { match default_math_class(c) {
Some(MathClass::Large) => { Some(MathClass::Large) => {
if is_integral_char(c) { if is_integral_char(c) {
Limits::Never Limits::Never

View File

@ -1,7 +1,8 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, Smart, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Length, Rel}; use typst_library::layout::{Abs, Axis, Rel};
use typst_library::math::{EquationElem, LrElem, MidElem}; use typst_library::math::{EquationElem, LrElem, MidElem};
use typst_utils::SliceExt;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL};
@ -13,32 +14,23 @@ pub fn layout_lr(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut body = elem.body();
// Extract from an EquationElem. // Extract from an EquationElem.
let mut body = &elem.body;
if let Some(equation) = body.to_packed::<EquationElem>() { if let Some(equation) = body.to_packed::<EquationElem>() {
body = equation.body(); body = &equation.body;
} }
// Extract implicit LrElem. // Extract implicit LrElem.
if let Some(lr) = body.to_packed::<LrElem>() { if let Some(lr) = body.to_packed::<LrElem>() {
if lr.size(styles).is_auto() { if lr.size(styles).is_one() {
body = lr.body(); body = &lr.body;
} }
} }
let mut fragments = ctx.layout_into_fragments(body, styles)?; let mut fragments = ctx.layout_into_fragments(body, styles)?;
// Ignore leading and trailing ignorant fragments. // Ignore leading and trailing ignorant fragments.
let start_idx = fragments let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant());
.iter()
.position(|f| !f.is_ignorant())
.unwrap_or(fragments.len());
let end_idx = fragments
.iter()
.skip(start_idx)
.rposition(|f| !f.is_ignorant())
.map_or(start_idx, |i| start_idx + i + 1);
let inner_fragments = &mut fragments[start_idx..end_idx]; let inner_fragments = &mut fragments[start_idx..end_idx];
let axis = scaled!(ctx, styles, axis_height); let axis = scaled!(ctx, styles, axis_height);
@ -100,7 +92,7 @@ pub fn layout_mid(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?; let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?;
for fragment in &mut fragments { for fragment in &mut fragments {
match fragment { match fragment {
@ -128,7 +120,7 @@ fn scale(
styles: StyleChain, styles: StyleChain,
fragment: &mut MathFragment, fragment: &mut MathFragment,
relative_to: Abs, relative_to: Abs,
height: Smart<Rel<Length>>, height: Rel<Abs>,
apply: Option<MathClass>, apply: Option<MathClass>,
) { ) {
if matches!( if matches!(

View File

@ -1,5 +1,5 @@
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::{Content, Packed, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
}; };
@ -9,9 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator, alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult,
AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
Scaled, DELIM_SHORT_FALL,
}; };
const VERTICAL_PADDING: Ratio = Ratio::new(0.1); const VERTICAL_PADDING: Ratio = Ratio::new(0.1);
@ -28,9 +27,9 @@ pub fn layout_vec(
let frame = layout_vec_body( let frame = layout_vec_body(
ctx, ctx,
styles, styles,
elem.children(), &elem.children,
elem.align(styles), elem.align(styles),
elem.gap(styles).at(scaled_font_size(ctx, styles)), elem.gap(styles),
LeftRightAlternator::Right, LeftRightAlternator::Right,
)?; )?;
@ -45,7 +44,7 @@ pub fn layout_mat(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let augment = elem.augment(styles); let augment = elem.augment(styles);
let rows = elem.rows(); let rows = &elem.rows;
if let Some(aug) = &augment { if let Some(aug) = &augment {
for &offset in &aug.hline.0 { for &offset in &aug.hline.0 {
@ -59,7 +58,7 @@ pub fn layout_mat(
} }
} }
let ncols = elem.rows().first().map_or(0, |row| row.len()); let ncols = rows.first().map_or(0, |row| row.len());
for &offset in &aug.vline.0 { for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols { if offset == 0 || offset.unsigned_abs() >= ncols {
@ -73,9 +72,6 @@ pub fn layout_mat(
} }
} }
let font_size = scaled_font_size(ctx, styles);
let column_gap = elem.column_gap(styles).at(font_size);
let row_gap = elem.row_gap(styles).at(font_size);
let delim = elem.delim(styles); let delim = elem.delim(styles);
let frame = layout_mat_body( let frame = layout_mat_body(
ctx, ctx,
@ -83,7 +79,7 @@ pub fn layout_mat(
rows, rows,
elem.align(styles), elem.align(styles),
augment, augment,
Axes::new(column_gap, row_gap), Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
elem.span(), elem.span(),
)?; )?;
@ -101,9 +97,9 @@ pub fn layout_cases(
let frame = layout_vec_body( let frame = layout_vec_body(
ctx, ctx,
styles, styles,
elem.children(), &elem.children,
FixedAlignment::Start, FixedAlignment::Start,
elem.gap(styles).at(scaled_font_size(ctx, styles)), elem.gap(styles),
LeftRightAlternator::None, LeftRightAlternator::None,
)?; )?;
@ -162,8 +158,7 @@ fn layout_mat_body(
// with font size to ensure that augmentation lines // with font size to ensure that augmentation lines
// look correct by default at all matrix sizes. // look correct by default at all matrix sizes.
// The line cap is also set to square because it looks more "correct". // The line cap is also set to square because it looks more "correct".
let font_size = scaled_font_size(ctx, styles); let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.resolve(styles);
let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.at(font_size);
let default_stroke = FixedStroke { let default_stroke = FixedStroke {
thickness: default_stroke_thickness, thickness: default_stroke_thickness,
paint: TextElem::fill_in(styles).as_decoration(), paint: TextElem::fill_in(styles).as_decoration(),
@ -308,9 +303,8 @@ fn layout_delimiters(
right: Option<char>, right: Option<char>,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let font_size = scaled_font_size(ctx, styles); let short_fall = DELIM_SHORT_FALL.resolve(styles);
let short_fall = DELIM_SHORT_FALL.at(font_size); let axis = scaled!(ctx, styles, axis_height);
let axis = ctx.constants.axis_height().scaled(ctx, font_size);
let height = frame.height(); let height = frame.height();
let target = height + VERTICAL_PADDING.of(height); let target = height + VERTICAL_PADDING.of(height);
frame.set_baseline(height / 2.0 + axis); frame.set_baseline(height / 2.0 + axis);
@ -322,7 +316,7 @@ fn layout_delimiters(
ctx.push(left); ctx.push(left);
} }
ctx.push(FrameFragment::new(ctx, styles, frame)); ctx.push(FrameFragment::new(styles, frame));
if let Some(right) = right { if let Some(right) = right {
let mut right = GlyphFragment::new(ctx, styles, right, span) let mut right = GlyphFragment::new(ctx, styles, right, span)

View File

@ -17,7 +17,9 @@ use rustybuzz::Feature;
use ttf_parser::Tag; use ttf_parser::Tag;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain}; use typst_library::foundations::{
Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem,
};
use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem}; use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem, Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem,
@ -28,8 +30,7 @@ use typst_library::math::*;
use typst_library::model::ParElem; use typst_library::model::ParElem;
use typst_library::routines::{Arenas, RealizationKind}; use typst_library::routines::{Arenas, RealizationKind};
use typst_library::text::{ use typst_library::text::{
families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem,
TextElem, TextSize,
}; };
use typst_library::World; use typst_library::World;
use typst_syntax::Span; use typst_syntax::Span;
@ -58,12 +59,16 @@ pub fn layout_equation_inline(
let mut locator = locator.split(); let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font); let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font);
let scale_style = style_for_script_scale(&ctx);
let styles = styles.chain(&scale_style);
let run = ctx.layout_into_run(&elem.body, styles)?; let run = ctx.layout_into_run(&elem.body, styles)?;
let mut items = if run.row_count() == 1 { let mut items = if run.row_count() == 1 {
run.into_par_items() run.into_par_items()
} else { } else {
vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())] vec![InlineItem::Frame(run.into_fragment(styles).into_frame())]
}; };
// An empty equation should have a height, so we still create a frame // An empty equation should have a height, so we still create a frame
@ -75,13 +80,12 @@ pub fn layout_equation_inline(
for item in &mut items { for item in &mut items {
let InlineItem::Frame(frame) = item else { continue }; let InlineItem::Frame(frame) = item else { continue };
let font_size = scaled_font_size(&ctx, styles);
let slack = ParElem::leading_in(styles) * 0.7; let slack = ParElem::leading_in(styles) * 0.7;
let (t, b) = font.edges( let (t, b) = font.edges(
TextElem::top_edge_in(styles), TextElem::top_edge_in(styles),
TextElem::bottom_edge_in(styles), TextElem::bottom_edge_in(styles),
font_size, TextElem::size_in(styles),
TextEdgeBounds::Frame(frame), TextEdgeBounds::Frame(frame),
); );
@ -110,9 +114,13 @@ pub fn layout_equation_block(
let mut locator = locator.split(); let mut locator = locator.split();
let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font); let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font);
let scale_style = style_for_script_scale(&ctx);
let styles = styles.chain(&scale_style);
let full_equation_builder = ctx let full_equation_builder = ctx
.layout_into_run(&elem.body, styles)? .layout_into_run(&elem.body, styles)?
.multiline_frame_builder(&ctx, styles); .multiline_frame_builder(styles);
let width = full_equation_builder.size.x; let width = full_equation_builder.size.x;
let equation_builders = if BlockElem::breakable_in(styles) { let equation_builders = if BlockElem::breakable_in(styles) {
@ -194,8 +202,7 @@ pub fn layout_equation_block(
let counter = Counter::of(EquationElem::elem()) let counter = Counter::of(EquationElem::elem())
.display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .display_at_loc(engine, elem.location().unwrap(), styles, numbering)?
.spanned(span); .spanned(span);
let number = let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?;
(engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?;
static NUMBER_GUTTER: Em = Em::new(0.5); static NUMBER_GUTTER: Em = Em::new(0.5);
let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles);
@ -469,7 +476,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
elem: &Content, elem: &Content,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<MathFragment> { ) -> SourceResult<MathFragment> {
Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles)) Ok(self.layout_into_run(elem, styles)?.into_fragment(styles))
} }
/// Layout the given element and return the result as a [`Frame`]. /// Layout the given element and return the result as a [`Frame`].
@ -502,7 +509,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> {
// Hack because the font is fixed in math. // Hack because the font is fixed in math.
if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) {
let frame = layout_external(elem, self, styles)?; let frame = layout_external(elem, self, styles)?;
self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); self.push(FrameFragment::new(styles, frame).with_spaced(true));
continue; continue;
} }
@ -522,14 +529,15 @@ fn layout_realized(
if let Some(elem) = elem.to_packed::<TagElem>() { if let Some(elem) = elem.to_packed::<TagElem>() {
ctx.push(MathFragment::Tag(elem.tag.clone())); ctx.push(MathFragment::Tag(elem.tag.clone()));
} else if elem.is::<SpaceElem>() { } else if elem.is::<SpaceElem>() {
let font_size = scaled_font_size(ctx, styles); ctx.push(MathFragment::Space(ctx.space_width.resolve(styles)));
ctx.push(MathFragment::Space(ctx.space_width.at(font_size)));
} else if elem.is::<LinebreakElem>() { } else if elem.is::<LinebreakElem>() {
ctx.push(MathFragment::Linebreak); ctx.push(MathFragment::Linebreak);
} else if let Some(elem) = elem.to_packed::<HElem>() { } else if let Some(elem) = elem.to_packed::<HElem>() {
layout_h(elem, ctx, styles)?; layout_h(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<TextElem>() { } else if let Some(elem) = elem.to_packed::<TextElem>() {
self::text::layout_text(elem, ctx, styles)?; self::text::layout_text(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<SymbolElem>() {
self::text::layout_symbol(elem, ctx, styles)?;
} else if let Some(elem) = elem.to_packed::<BoxElem>() { } else if let Some(elem) = elem.to_packed::<BoxElem>() {
layout_box(elem, ctx, styles)?; layout_box(elem, ctx, styles)?;
} else if elem.is::<AlignPointElem>() { } else if elem.is::<AlignPointElem>() {
@ -595,7 +603,7 @@ fn layout_realized(
frame.set_baseline(frame.height() / 2.0 + axis); frame.set_baseline(frame.height() / 2.0 + axis);
} }
ctx.push( ctx.push(
FrameFragment::new(ctx, styles, frame) FrameFragment::new(styles, frame)
.with_spaced(true) .with_spaced(true)
.with_ignorant(elem.is::<PlaceElem>()), .with_ignorant(elem.is::<PlaceElem>()),
); );
@ -610,15 +618,14 @@ fn layout_box(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap(); let frame = crate::inline::layout_box(
let frame = (ctx.engine.routines.layout_box)(
elem, elem,
ctx.engine, ctx.engine,
ctx.locator.next(&elem.span()), ctx.locator.next(&elem.span()),
styles.chain(&local), styles,
ctx.region.size, ctx.region.size,
)?; )?;
ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true)); ctx.push(FrameFragment::new(styles, frame).with_spaced(true));
Ok(()) Ok(())
} }
@ -628,29 +635,25 @@ fn layout_h(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
if let Spacing::Rel(rel) = elem.amount() { if let Spacing::Rel(rel) = elem.amount {
if rel.rel.is_zero() { if rel.rel.is_zero() {
ctx.push(MathFragment::Spacing( ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles)));
rel.abs.at(scaled_font_size(ctx, styles)),
elem.weak(styles),
));
} }
} }
Ok(()) Ok(())
} }
/// Lays out a [`ClassElem`]. /// Lays out a [`ClassElem`].
#[typst_macros::time(name = "math.op", span = elem.span())] #[typst_macros::time(name = "math.class", span = elem.span())]
fn layout_class( fn layout_class(
elem: &Packed<ClassElem>, elem: &Packed<ClassElem>,
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let class = *elem.class(); let style = EquationElem::set_class(Some(elem.class)).wrap();
let style = EquationElem::set_class(Some(class)).wrap(); let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?;
let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?; fragment.set_class(elem.class);
fragment.set_class(class); fragment.set_limits(Limits::for_class(elem.class));
fragment.set_limits(Limits::for_class(class));
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
} }
@ -662,13 +665,13 @@ fn layout_op(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let fragment = ctx.layout_into_fragment(elem.text(), styles)?; let fragment = ctx.layout_into_fragment(&elem.text, styles)?;
let italics = fragment.italics_correction(); let italics = fragment.italics_correction();
let accent_attach = fragment.accent_attach(); let accent_attach = fragment.accent_attach();
let text_like = fragment.is_text_like(); let text_like = fragment.is_text_like();
ctx.push( ctx.push(
FrameFragment::new(ctx, styles, fragment.into_frame()) FrameFragment::new(styles, fragment.into_frame())
.with_class(MathClass::Large) .with_class(MathClass::Large)
.with_italics_correction(italics) .with_italics_correction(italics)
.with_accent_attach(accent_attach) .with_accent_attach(accent_attach)
@ -688,12 +691,11 @@ fn layout_external(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap(); crate::layout_frame(
(ctx.engine.routines.layout_frame)(
ctx.engine, ctx.engine,
content, content,
ctx.locator.next(&content.span()), ctx.locator.next(&content.span()),
styles.chain(&local), styles,
ctx.region, ctx.region,
) )
} }

View File

@ -18,7 +18,6 @@ pub fn layout_root(
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let index = elem.index(styles); let index = elem.index(styles);
let radicand = elem.radicand();
let span = elem.span(); let span = elem.span();
let gap = scaled!( let gap = scaled!(
@ -36,9 +35,9 @@ pub fn layout_root(
let radicand = { let radicand = {
let cramped = style_cramped(); let cramped = style_cramped();
let styles = styles.chain(&cramped); let styles = styles.chain(&cramped);
let run = ctx.layout_into_run(radicand, styles)?; let run = ctx.layout_into_run(&elem.radicand, styles)?;
let multiline = run.is_multiline(); let multiline = run.is_multiline();
let mut radicand = run.into_fragment(ctx, styles).into_frame(); let mut radicand = run.into_fragment(styles).into_frame();
if multiline { if multiline {
// Align the frame center line with the math axis. // Align the frame center line with the math axis.
radicand.set_baseline( radicand.set_baseline(
@ -120,7 +119,7 @@ pub fn layout_root(
); );
frame.push_frame(radicand_pos, radicand); frame.push_frame(radicand_pos, radicand);
ctx.push(FrameFragment::new(ctx, styles, frame)); ctx.push(FrameFragment::new(styles, frame));
Ok(()) Ok(())
} }

View File

@ -6,7 +6,7 @@ use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN};
use typst_library::model::ParElem; use typst_library::model::ParElem;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment}; use super::{alignments, FrameFragment, MathFragment};
const TIGHT_LEADING: Em = Em::new(0.25); const TIGHT_LEADING: Em = Em::new(0.25);
@ -161,15 +161,15 @@ impl MathRun {
} }
} }
pub fn into_frame(self, ctx: &MathContext, styles: StyleChain) -> Frame { pub fn into_frame(self, styles: StyleChain) -> Frame {
if !self.is_multiline() { if !self.is_multiline() {
self.into_line_frame(&[], LeftRightAlternator::Right) self.into_line_frame(&[], LeftRightAlternator::Right)
} else { } else {
self.multiline_frame_builder(ctx, styles).build() self.multiline_frame_builder(styles).build()
} }
} }
pub fn into_fragment(self, ctx: &MathContext, styles: StyleChain) -> MathFragment { pub fn into_fragment(self, styles: StyleChain) -> MathFragment {
if self.0.len() == 1 { if self.0.len() == 1 {
return self.0.into_iter().next().unwrap(); return self.0.into_iter().next().unwrap();
} }
@ -181,7 +181,7 @@ impl MathRun {
.filter(|e| e.math_size().is_some()) .filter(|e| e.math_size().is_some())
.all(|e| e.is_text_like()); .all(|e| e.is_text_like());
FrameFragment::new(ctx, styles, self.into_frame(ctx, styles)) FrameFragment::new(styles, self.into_frame(styles))
.with_text_like(text_like) .with_text_like(text_like)
.into() .into()
} }
@ -189,11 +189,7 @@ impl MathRun {
/// Returns a builder that lays out the [`MathFragment`]s into a possibly /// Returns a builder that lays out the [`MathFragment`]s into a possibly
/// multi-row [`Frame`]. The rows are aligned using the same set of alignment /// multi-row [`Frame`]. The rows are aligned using the same set of alignment
/// points computed from them as a whole. /// points computed from them as a whole.
pub fn multiline_frame_builder( pub fn multiline_frame_builder(self, styles: StyleChain) -> MathRunFrameBuilder {
self,
ctx: &MathContext,
styles: StyleChain,
) -> MathRunFrameBuilder {
let rows: Vec<_> = self.rows(); let rows: Vec<_> = self.rows();
let row_count = rows.len(); let row_count = rows.len();
let alignments = alignments(&rows); let alignments = alignments(&rows);
@ -201,8 +197,7 @@ impl MathRun {
let leading = if EquationElem::size_in(styles) >= MathSize::Text { let leading = if EquationElem::size_in(styles) >= MathSize::Text {
ParElem::leading_in(styles) ParElem::leading_in(styles)
} else { } else {
let font_size = scaled_font_size(ctx, styles); TIGHT_LEADING.resolve(styles)
TIGHT_LEADING.at(font_size)
}; };
let align = AlignElem::alignment_in(styles).resolve(styles).x; let align = AlignElem::alignment_in(styles).resolve(styles).x;

View File

@ -2,7 +2,6 @@ use ttf_parser::math::MathValue;
use typst_library::foundations::{Style, StyleChain}; use typst_library::foundations::{Style, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment};
use typst_library::math::{EquationElem, MathSize}; use typst_library::math::{EquationElem, MathSize};
use typst_library::text::TextElem;
use typst_utils::LazyHash; use typst_utils::LazyHash;
use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; use super::{LeftRightAlternator, MathContext, MathFragment, MathRun};
@ -18,7 +17,7 @@ macro_rules! scaled {
$crate::math::Scaled::scaled( $crate::math::Scaled::scaled(
$ctx.constants.$name(), $ctx.constants.$name(),
$ctx, $ctx,
$crate::math::scaled_font_size($ctx, $styles), typst_library::text::TextElem::size_in($styles),
) )
}; };
} }
@ -55,16 +54,6 @@ impl Scaled for MathValue<'_> {
} }
} }
/// Get the font size scaled with the `MathSize`.
pub fn scaled_font_size(ctx: &MathContext, styles: StyleChain) -> Abs {
let factor = match EquationElem::size_in(styles) {
MathSize::Display | MathSize::Text => 1.0,
MathSize::Script => percent!(ctx, script_percent_scale_down),
MathSize::ScriptScript => percent!(ctx, script_script_percent_scale_down),
};
factor * TextElem::size_in(styles)
}
/// Styles something as cramped. /// Styles something as cramped.
pub fn style_cramped() -> LazyHash<Style> { pub fn style_cramped() -> LazyHash<Style> {
EquationElem::set_cramped(true).wrap() EquationElem::set_cramped(true).wrap()
@ -99,6 +88,15 @@ pub fn style_for_denominator(styles: StyleChain) -> [LazyHash<Style>; 2] {
[style_for_numerator(styles), EquationElem::set_cramped(true).wrap()] [style_for_numerator(styles), EquationElem::set_cramped(true).wrap()]
} }
/// Styles to add font constants to the style chain.
pub fn style_for_script_scale(ctx: &MathContext) -> LazyHash<Style> {
EquationElem::set_script_scale((
ctx.constants.script_percent_scale_down(),
ctx.constants.script_script_percent_scale_down(),
))
.wrap()
}
/// How a delimieter should be aligned when scaling. /// How a delimieter should be aligned when scaling.
pub fn delimiter_alignment(delimiter: char) -> VAlignment { pub fn delimiter_alignment(delimiter: char) -> VAlignment {
match delimiter { match delimiter {

View File

@ -1,15 +1,16 @@
use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart}; use ttf_parser::math::{GlyphAssembly, GlyphConstruction, GlyphPart};
use ttf_parser::LazyArray16; use ttf_parser::LazyArray16;
use typst_library::diag::{warning, SourceResult}; use typst_library::diag::{warning, SourceResult};
use typst_library::foundations::{Packed, Smart, StyleChain}; use typst_library::foundations::{Packed, StyleChain};
use typst_library::layout::{Abs, Axis, Frame, Length, Point, Rel, Size}; use typst_library::layout::{Abs, Axis, Frame, Point, Rel, Size};
use typst_library::math::StretchElem; use typst_library::math::StretchElem;
use typst_utils::Get; use typst_utils::Get;
use super::{ use super::{
delimiter_alignment, scaled_font_size, GlyphFragment, MathContext, MathFragment, delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled,
Scaled, VariantFragment, VariantFragment,
}; };
use crate::modifiers::FrameModify;
/// Maximum number of times extenders can be repeated. /// Maximum number of times extenders can be repeated.
const MAX_REPEATS: usize = 1024; const MAX_REPEATS: usize = 1024;
@ -21,7 +22,7 @@ pub fn layout_stretch(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?;
stretch_fragment( stretch_fragment(
ctx, ctx,
styles, styles,
@ -42,7 +43,7 @@ pub fn stretch_fragment(
fragment: &mut MathFragment, fragment: &mut MathFragment,
axis: Option<Axis>, axis: Option<Axis>,
relative_to: Option<Abs>, relative_to: Option<Abs>,
stretch: Smart<Rel<Length>>, stretch: Rel<Abs>,
short_fall: Abs, short_fall: Abs,
) { ) {
let glyph = match fragment { let glyph = match fragment {
@ -66,10 +67,7 @@ pub fn stretch_fragment(
let mut variant = stretch_glyph( let mut variant = stretch_glyph(
ctx, ctx,
glyph, glyph,
stretch stretch.relative_to(relative_to_size),
.unwrap_or(Rel::one())
.at(scaled_font_size(ctx, styles))
.relative_to(relative_to_size),
short_fall, short_fall,
axis, axis,
); );
@ -268,7 +266,7 @@ fn assemble(
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
let mut offset = Abs::zero(); let mut offset = Abs::zero();
frame.set_baseline(baseline); frame.set_baseline(baseline);
frame.post_process_raw(base.dests, base.hidden); frame.modify(&base.modifiers);
for (fragment, advance) in selected { for (fragment, advance) in selected {
let pos = match axis { let pos = match axis {

View File

@ -1,20 +1,18 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use ecow::{eco_vec, EcoString}; use ecow::EcoString;
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Packed, StyleChain, StyleVec}; use typst_library::foundations::{Packed, StyleChain, SymbolElem};
use typst_library::layout::{Abs, Size}; use typst_library::layout::{Abs, Size};
use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::math::{EquationElem, MathSize, MathVariant};
use typst_library::text::{ use typst_library::text::{
BottomEdge, BottomEdgeMetric, TextElem, TextSize, TopEdge, TopEdgeMetric, BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric,
}; };
use typst_syntax::{is_newline, Span}; use typst_syntax::{is_newline, Span};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use super::{ use super::{FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun};
scaled_font_size, FrameFragment, GlyphFragment, MathContext, MathFragment, MathRun,
};
/// Lays out a [`TextElem`]. /// Lays out a [`TextElem`].
pub fn layout_text( pub fn layout_text(
@ -22,122 +20,165 @@ pub fn layout_text(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let text = elem.text(); let text = &elem.text;
let span = elem.span(); let span = elem.span();
let mut chars = text.chars(); let fragment = if text.contains(is_newline) {
let math_size = EquationElem::size_in(styles); layout_text_lines(text.split(is_newline), span, ctx, styles)?
let mut dtls = ctx.dtls_table.is_some();
let fragment: MathFragment = if let Some(mut glyph) = chars
.next()
.filter(|_| chars.next().is_none())
.map(|c| dtls_char(c, &mut dtls))
.map(|c| styled_char(styles, c, true))
.and_then(|c| GlyphFragment::try_new(ctx, styles, c, span))
{
// A single letter that is available in the math font.
if dtls {
glyph.make_dotless_form(ctx);
}
match math_size {
MathSize::Script => {
glyph.make_script_size(ctx);
}
MathSize::ScriptScript => {
glyph.make_script_script_size(ctx);
}
_ => (),
}
if glyph.class == MathClass::Large {
let mut variant = if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.height());
glyph.stretch_vertical(ctx, height, Abs::zero())
} else {
glyph.into_variant()
};
// TeXbook p 155. Large operators are always vertically centered on the axis.
variant.center_on_axis(ctx);
variant.into()
} else {
glyph.into()
}
} else if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Numbers aren't that difficult.
let mut fragments = vec![];
for c in text.chars() {
let c = styled_char(styles, c, false);
fragments.push(GlyphFragment::new(ctx, styles, c, span).into());
}
let frame = MathRun::new(fragments).into_frame(ctx, styles);
FrameFragment::new(ctx, styles, frame).with_text_like(true).into()
} else { } else {
let local = [ layout_inline_text(text, span, ctx, styles)?
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
TextElem::set_size(TextSize(scaled_font_size(ctx, 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(layout_complex_text(piece, ctx, span, styles)?.into());
}
}
let mut frame = MathRun::new(fragments).into_frame(ctx, styles);
let axis = scaled!(ctx, styles, axis_height);
frame.set_baseline(frame.height() / 2.0 + axis);
FrameFragment::new(ctx, styles, frame).into()
} else {
layout_complex_text(&text, ctx, span, styles)?.into()
}
}; };
ctx.push(fragment); ctx.push(fragment);
Ok(()) Ok(())
} }
/// Layout the given text string into a [`FrameFragment`]. /// Layout multiple lines of text.
fn layout_complex_text( fn layout_text_lines<'a>(
text: &str, lines: impl Iterator<Item = &'a str>,
ctx: &mut MathContext,
span: Span, span: Span,
ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<FrameFragment> { ) -> SourceResult<FrameFragment> {
// There isn't a natural width for a paragraph in a math environment; let mut fragments = vec![];
// because it will be placed somewhere probably not at the left margin for (i, line) in lines.enumerate() {
// it will overflow. So emulate an `hbox` instead and allow the paragraph if i != 0 {
// to extend as far as needed. fragments.push(MathFragment::Linebreak);
let spaced = text.graphemes(true).nth(1).is_some(); }
let elem = TextElem::packed(text).spanned(span); if !line.is_empty() {
let frame = (ctx.engine.routines.layout_inline)( fragments.push(layout_inline_text(line, span, ctx, styles)?.into());
ctx.engine, }
&StyleVec::wrap(eco_vec![elem]), }
ctx.locator.next(&span), let mut frame = MathRun::new(fragments).into_frame(styles);
styles, let axis = scaled!(ctx, styles, axis_height);
false, frame.set_baseline(frame.height() / 2.0 + axis);
Size::splat(Abs::inf()), Ok(FrameFragment::new(styles, frame))
false,
)?
.into_frame();
Ok(FrameFragment::new(ctx, styles, frame)
.with_class(MathClass::Alphabetic)
.with_text_like(true)
.with_spaced(spaced))
} }
/// Select the correct styled math letter. /// Layout the given text string into a [`FrameFragment`] after styling all
/// characters for the math font (without auto-italics).
fn layout_inline_text(
text: &str,
span: Span,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<FrameFragment> {
if text.chars().all(|c| c.is_ascii_digit() || c == '.') {
// Small optimization for numbers. Note that this lays out slightly
// differently to normal text and is worth re-evaluating in the future.
let mut fragments = vec![];
let is_single = text.chars().count() == 1;
for unstyled_c in text.chars() {
let c = styled_char(styles, unstyled_c, false);
let mut glyph = GlyphFragment::new(ctx, styles, c, span);
if is_single {
// Duplicate what `layout_glyph` does exactly even if it's
// probably incorrect here.
match EquationElem::size_in(styles) {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
}
fragments.push(glyph.into());
}
let frame = MathRun::new(fragments).into_frame(styles);
Ok(FrameFragment::new(styles, frame).with_text_like(true))
} else {
let local = [
TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)),
TextElem::set_bottom_edge(BottomEdge::Metric(BottomEdgeMetric::Bounds)),
]
.map(|p| p.wrap());
let styles = styles.chain(&local);
let styled_text: EcoString =
text.chars().map(|c| styled_char(styles, c, false)).collect();
let spaced = styled_text.graphemes(true).nth(1).is_some();
let elem = TextElem::packed(styled_text).spanned(span);
// There isn't a natural width for a paragraph in a math environment;
// because it will be placed somewhere probably not at the left margin
// it will overflow. So emulate an `hbox` instead and allow the
// paragraph to extend as far as needed.
let frame = crate::inline::layout_inline(
ctx.engine,
&[(&elem, styles)],
&mut ctx.locator.next(&span).split(),
styles,
Size::splat(Abs::inf()),
false,
)?
.into_frame();
Ok(FrameFragment::new(styles, frame)
.with_class(MathClass::Alphabetic)
.with_text_like(true)
.with_spaced(spaced))
}
}
/// Layout a single character in the math font with the correct styling applied
/// (includes auto-italics).
pub fn layout_symbol(
elem: &Packed<SymbolElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
// Switch dotless char to normal when we have the dtls OpenType feature.
// This should happen before the main styling pass.
let (unstyled_c, dtls) = match try_dotless(elem.text) {
Some(c) if ctx.dtls_table.is_some() => (c, true),
_ => (elem.text, false),
};
let c = styled_char(styles, unstyled_c, true);
let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) {
Some(glyph) => layout_glyph(glyph, dtls, ctx, styles),
None => {
// Not in the math font, fallback to normal inline text layout.
layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)?
.into()
}
};
ctx.push(fragment);
Ok(())
}
/// Layout a [`GlyphFragment`].
fn layout_glyph(
mut glyph: GlyphFragment,
dtls: bool,
ctx: &mut MathContext,
styles: StyleChain,
) -> MathFragment {
if dtls {
glyph.make_dotless_form(ctx);
}
let math_size = EquationElem::size_in(styles);
match math_size {
MathSize::Script => glyph.make_script_size(ctx),
MathSize::ScriptScript => glyph.make_script_script_size(ctx),
_ => {}
}
if glyph.class == MathClass::Large {
let mut variant = if math_size == MathSize::Display {
let height = scaled!(ctx, styles, display_operator_min_height)
.max(SQRT_2 * glyph.height());
glyph.stretch_vertical(ctx, height, Abs::zero())
} else {
glyph.into_variant()
};
// TeXbook p 155. Large operators are always vertically centered on the
// axis.
variant.center_on_axis(ctx);
variant.into()
} else {
glyph.into()
}
}
/// Style the character by selecting the unicode codepoint for italic, bold,
/// caligraphic, etc.
/// ///
/// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings> /// <https://www.w3.org/TR/mathml-core/#new-text-transform-mappings>
/// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols> /// <https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols>
@ -356,15 +397,12 @@ fn greek_exception(
}) })
} }
/// Switch dotless character to non dotless character for use of the dtls /// The non-dotless version of a dotless character that can be used with the
/// OpenType feature. /// `dtls` OpenType feature.
pub fn dtls_char(c: char, dtls: &mut bool) -> char { pub fn try_dotless(c: char) -> Option<char> {
match (c, *dtls) { match c {
('ı', true) => 'i', 'ı' => Some('i'),
('ȷ', true) => 'j', 'ȷ' => Some('j'),
_ => { _ => None,
*dtls = false;
c
}
} }
} }

View File

@ -1,5 +1,5 @@
use typst_library::diag::SourceResult; use typst_library::diag::SourceResult;
use typst_library::foundations::{Content, Packed, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use typst_library::math::{ use typst_library::math::{
OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem, OverbraceElem, OverbracketElem, OverlineElem, OverparenElem, OvershellElem,
@ -10,8 +10,8 @@ use typst_library::visualize::{FixedStroke, Geometry};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
scaled_font_size, stack, style_cramped, style_for_subscript, style_for_superscript, stack, style_cramped, style_for_subscript, style_for_superscript, FrameFragment,
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, MathRun, GlyphFragment, LeftRightAlternator, MathContext, MathRun,
}; };
const BRACE_GAP: Em = Em::new(0.25); const BRACE_GAP: Em = Em::new(0.25);
@ -32,7 +32,7 @@ pub fn layout_underline(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under) layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under)
} }
/// Lays out an [`OverlineElem`]. /// Lays out an [`OverlineElem`].
@ -42,7 +42,7 @@ pub fn layout_overline(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over) layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over)
} }
/// Lays out an [`UnderbraceElem`]. /// Lays out an [`UnderbraceElem`].
@ -55,7 +55,7 @@ pub fn layout_underbrace(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏟', '⏟',
BRACE_GAP, BRACE_GAP,
@ -74,7 +74,7 @@ pub fn layout_overbrace(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏞', '⏞',
BRACE_GAP, BRACE_GAP,
@ -93,7 +93,7 @@ pub fn layout_underbracket(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⎵', '⎵',
BRACKET_GAP, BRACKET_GAP,
@ -112,7 +112,7 @@ pub fn layout_overbracket(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⎴', '⎴',
BRACKET_GAP, BRACKET_GAP,
@ -131,7 +131,7 @@ pub fn layout_underparen(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏝', '⏝',
PAREN_GAP, PAREN_GAP,
@ -150,7 +150,7 @@ pub fn layout_overparen(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏜', '⏜',
PAREN_GAP, PAREN_GAP,
@ -169,7 +169,7 @@ pub fn layout_undershell(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏡', '⏡',
SHELL_GAP, SHELL_GAP,
@ -188,7 +188,7 @@ pub fn layout_overshell(
layout_underoverspreader( layout_underoverspreader(
ctx, ctx,
styles, styles,
elem.body(), &elem.body,
&elem.annotation(styles), &elem.annotation(styles),
'⏠', '⏠',
SHELL_GAP, SHELL_GAP,
@ -260,7 +260,7 @@ fn layout_underoverline(
); );
ctx.push( ctx.push(
FrameFragment::new(ctx, styles, frame) FrameFragment::new(styles, frame)
.with_class(content_class) .with_class(content_class)
.with_text_like(content_is_text_like) .with_text_like(content_is_text_like)
.with_italics_correction(content_italics_correction), .with_italics_correction(content_italics_correction),
@ -281,11 +281,10 @@ fn layout_underoverspreader(
position: Position, position: Position,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let font_size = scaled_font_size(ctx, styles); let gap = gap.resolve(styles);
let gap = gap.at(font_size);
let body = ctx.layout_into_run(body, styles)?; let body = ctx.layout_into_run(body, styles)?;
let body_class = body.class(); let body_class = body.class();
let body = body.into_fragment(ctx, styles); let body = body.into_fragment(styles);
let glyph = GlyphFragment::new(ctx, styles, c, span); let glyph = GlyphFragment::new(ctx, styles, c, span);
let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero()); let stretched = glyph.stretch_horizontal(ctx, body.width(), Abs::zero());
@ -321,7 +320,7 @@ fn layout_underoverspreader(
LeftRightAlternator::Right, LeftRightAlternator::Right,
None, None,
); );
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class)); ctx.push(FrameFragment::new(styles, frame).with_class(body_class));
Ok(()) Ok(())
} }

View File

@ -0,0 +1,110 @@
use typst_library::foundations::StyleChain;
use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point};
use typst_library::model::{Destination, LinkElem};
/// Frame-level modifications resulting from styles that do not impose any
/// layout structure.
///
/// These are always applied at the highest level of style uniformity.
/// Consequently, they must be applied by all layouters that manually manage
/// styles of their children (because they can produce children with varying
/// styles). This currently includes flow, inline, and math layout.
///
/// Other layouters don't manually need to handle it because their parents that
/// result from realization will take care of it and the styles can only apply
/// to them as a whole, not part of it (since they don't manage styles).
///
/// Currently existing frame modifiers are:
/// - `HideElem::hidden`
/// - `LinkElem::dests`
#[derive(Debug, Clone)]
pub struct FrameModifiers {
/// A destination to link to.
dest: Option<Destination>,
/// Whether the contents of the frame should be hidden.
hidden: bool,
}
impl FrameModifiers {
/// Retrieve all modifications that should be applied per-frame.
pub fn get_in(styles: StyleChain) -> Self {
Self {
dest: LinkElem::current_in(styles),
hidden: HideElem::hidden_in(styles),
}
}
}
/// Applies [`FrameModifiers`].
pub trait FrameModify {
/// Apply the modifiers in-place.
fn modify(&mut self, modifiers: &FrameModifiers);
/// Apply the modifiers, and return the modified result.
fn modified(mut self, modifiers: &FrameModifiers) -> Self
where
Self: Sized,
{
self.modify(modifiers);
self
}
}
impl FrameModify for Frame {
fn modify(&mut self, modifiers: &FrameModifiers) {
if let Some(dest) = &modifiers.dest {
let size = self.size();
self.push(Point::zero(), FrameItem::Link(dest.clone(), size));
}
if modifiers.hidden {
self.hide();
}
}
}
impl FrameModify for Fragment {
fn modify(&mut self, modifiers: &FrameModifiers) {
for frame in self.iter_mut() {
frame.modify(modifiers);
}
}
}
impl<T, E> FrameModify for Result<T, E>
where
T: FrameModify,
{
fn modify(&mut self, props: &FrameModifiers) {
if let Ok(inner) = self {
inner.modify(props);
}
}
}
/// Performs layout and modification in one step.
///
/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,
/// but with the additional step that redundant modifiers (which are already
/// applied here) are removed from the `styles` passed to `layout`. This is used
/// for the layout of containers like `block`.
pub fn layout_and_modify<F, R>(styles: StyleChain, layout: F) -> R
where
F: FnOnce(StyleChain) -> R,
R: FrameModify,
{
let modifiers = FrameModifiers::get_in(styles);
// Disable the current link internally since it's already applied at this
// level of layout. This means we don't generate redundant nested links,
// which may bloat the output considerably.
let reset;
let outer = styles;
let mut styles = styles;
if modifiers.dest.is_some() {
reset = LinkElem::set_current(None).wrap();
styles = outer.chain(&reset);
}
layout(styles).modified(&modifiers)
}

View File

@ -23,7 +23,7 @@ pub enum Item<'a> {
/// things like tags and weak pagebreaks. /// things like tags and weak pagebreaks.
pub fn collect<'a>( pub fn collect<'a>(
mut children: &'a mut [Pair<'a>], mut children: &'a mut [Pair<'a>],
mut locator: SplitLocator<'a>, locator: &mut SplitLocator<'a>,
mut initial: StyleChain<'a>, mut initial: StyleChain<'a>,
) -> Vec<Item<'a>> { ) -> Vec<Item<'a>> {
// The collected page-level items. // The collected page-level items.

View File

@ -83,7 +83,7 @@ fn layout_document_impl(
styles, styles,
)?; )?;
let pages = layout_pages(&mut engine, &mut children, locator, styles)?; let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?;
let introspector = Introspector::paged(&pages); let introspector = Introspector::paged(&pages);
Ok(PagedDocument { pages, info, introspector }) Ok(PagedDocument { pages, info, introspector })
@ -93,7 +93,7 @@ fn layout_document_impl(
fn layout_pages<'a>( fn layout_pages<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &'a mut [Pair<'a>], children: &'a mut [Pair<'a>],
locator: SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<Vec<Page>> { ) -> SourceResult<Vec<Page>> {
// Slice up the children into logical parts. // Slice up the children into logical parts.

View File

@ -19,7 +19,7 @@ use typst_library::visualize::Paint;
use typst_library::World; use typst_library::World;
use typst_utils::Numeric; use typst_utils::Numeric;
use crate::flow::layout_flow; use crate::flow::{layout_flow, FlowMode};
/// A mostly finished layout for one page. Needs only knowledge of its exact /// A mostly finished layout for one page. Needs only knowledge of its exact
/// page number to be finalized into a `Page`. (Because the margins can depend /// page number to be finalized into a `Page`. (Because the margins can depend
@ -181,7 +181,7 @@ fn layout_page_run_impl(
Regions::repeat(area, area.map(Abs::is_finite)), Regions::repeat(area, area.map(Abs::is_finite)),
PageElem::columns_in(styles), PageElem::columns_in(styles),
ColumnsElem::gutter_in(styles), ColumnsElem::gutter_in(styles),
true, FlowMode::Root,
)?; )?;
// Layouts a single marginal. // Layouts a single marginal.

View File

@ -1,6 +1,6 @@
use std::f64::consts::SQRT_2; use std::f64::consts::SQRT_2;
use kurbo::ParamCurveExtrema; use kurbo::{CubicBez, ParamCurveExtrema};
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
@ -10,8 +10,9 @@ use typst_library::layout::{
Sides, Size, Sides, Size,
}; };
use typst_library::visualize::{ use typst_library::visualize::{
CircleElem, EllipseElem, FillRule, FixedStroke, Geometry, LineElem, Paint, Path, CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
PathElem, PathVertex, PolygonElem, RectElem, Shape, SquareElem, Stroke, FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
Shape, SquareElem, Stroke,
}; };
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{Get, Numeric}; use typst_utils::{Get, Numeric};
@ -61,7 +62,7 @@ pub fn layout_path(
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
}; };
let vertices = elem.vertices(); let vertices = &elem.vertices;
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect(); let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
let mut size = Size::zero(); let mut size = Size::zero();
@ -71,8 +72,8 @@ pub fn layout_path(
// Only create a path if there are more than zero points. // Only create a path if there are more than zero points.
// Construct a closed path given all points. // Construct a closed path given all points.
let mut path = Path::new(); let mut curve = Curve::new();
path.move_to(points[0]); curve.move_(points[0]);
let mut add_cubic = |from_point: Point, let mut add_cubic = |from_point: Point,
to_point: Point, to_point: Point,
@ -80,7 +81,7 @@ pub fn layout_path(
to: PathVertex| { to: PathVertex| {
let from_control_point = resolve(from.control_point_from()) + from_point; let from_control_point = resolve(from.control_point_from()) + from_point;
let to_control_point = resolve(to.control_point_to()) + to_point; let to_control_point = resolve(to.control_point_to()) + to_point;
path.cubic_to(from_control_point, to_control_point, to_point); curve.cubic(from_control_point, to_control_point, to_point);
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw()); let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
let p1 = kurbo::Point::new( let p1 = kurbo::Point::new(
@ -111,7 +112,7 @@ pub fn layout_path(
let to_point = points[0]; let to_point = points[0];
add_cubic(from_point, to_point, from, to); add_cubic(from_point, to_point, from, to);
path.close_path(); curve.close();
} }
if !size.is_finite() { if !size.is_finite() {
@ -129,7 +130,7 @@ pub fn layout_path(
let mut frame = Frame::soft(size); let mut frame = Frame::soft(size);
let shape = Shape { let shape = Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke, stroke,
fill, fill,
fill_rule, fill_rule,
@ -138,6 +139,257 @@ pub fn layout_path(
Ok(frame) Ok(frame)
} }
/// Layout the curve.
#[typst_macros::time(span = elem.span())]
pub fn layout_curve(
elem: &Packed<CurveElem>,
_: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let mut builder = CurveBuilder::new(region, styles);
for item in &elem.components {
match item {
CurveComponent::Move(element) => {
let relative = element.relative(styles);
let point = builder.resolve_point(element.start, relative);
builder.move_(point);
}
CurveComponent::Line(element) => {
let relative = element.relative(styles);
let point = builder.resolve_point(element.end, relative);
builder.line(point);
}
CurveComponent::Quad(element) => {
let relative = element.relative(styles);
let end = builder.resolve_point(element.end, relative);
let control = match element.control {
Smart::Auto => {
control_c2q(builder.last_point, builder.last_control_from)
}
Smart::Custom(Some(p)) => builder.resolve_point(p, relative),
Smart::Custom(None) => end,
};
builder.quad(control, end);
}
CurveComponent::Cubic(element) => {
let relative = element.relative(styles);
let end = builder.resolve_point(element.end, relative);
let c1 = match element.control_start {
Some(Smart::Custom(p)) => builder.resolve_point(p, relative),
Some(Smart::Auto) => builder.last_control_from,
None => builder.last_point,
};
let c2 = match element.control_end {
Some(p) => builder.resolve_point(p, relative),
None => end,
};
builder.cubic(c1, c2, end);
}
CurveComponent::Close(element) => {
builder.close(element.mode(styles));
}
}
}
let (curve, size) = builder.finish();
if curve.is_empty() {
return Ok(Frame::soft(size));
}
if !size.is_finite() {
bail!(elem.span(), "cannot create curve with infinite size");
}
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
};
let mut frame = Frame::soft(size);
let shape = Shape {
geometry: Geometry::Curve(curve),
stroke,
fill,
fill_rule,
};
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}
/// Builds a `Curve` from a [`CurveElem`]'s parts.
struct CurveBuilder<'a> {
/// The output curve.
curve: Curve,
/// The curve's bounds.
size: Size,
/// The region relative to which points are resolved.
region: Region,
/// The styles for the curve.
styles: StyleChain<'a>,
/// The next start point.
start_point: Point,
/// Mirror of the first cubic start control point (for closing).
start_control_into: Point,
/// The point we previously ended on.
last_point: Point,
/// Mirror of the last cubic control point (for auto control points).
last_control_from: Point,
/// Whether a component has been start. This does not mean that something
/// has been added to `self.curve` yet.
is_started: bool,
/// Whether anything was added to `self.curve` for the current component.
is_empty: bool,
}
impl<'a> CurveBuilder<'a> {
/// Create a new curve builder.
fn new(region: Region, styles: StyleChain<'a>) -> Self {
Self {
curve: Curve::new(),
size: Size::zero(),
region,
styles,
start_point: Point::zero(),
start_control_into: Point::zero(),
last_point: Point::zero(),
last_control_from: Point::zero(),
is_started: false,
is_empty: true,
}
}
/// Finish building, returning the curve and its bounding size.
fn finish(self) -> (Curve, Size) {
(self.curve, self.size)
}
/// Move to a point, starting a new segment.
fn move_(&mut self, point: Point) {
// Delay calling `curve.move` in case there is another move element
// before any actual drawing.
self.expand_bounds(point);
self.start_point = point;
self.start_control_into = point;
self.last_point = point;
self.last_control_from = point;
self.is_started = true;
self.is_empty = true;
}
/// Add a line segment.
fn line(&mut self, point: Point) {
if self.is_empty {
self.start_component();
self.start_control_into = self.start_point;
}
self.curve.line(point);
self.expand_bounds(point);
self.last_point = point;
self.last_control_from = point;
}
/// Add a quadratic curve segment.
fn quad(&mut self, control: Point, end: Point) {
let c1 = control_q2c(self.last_point, control);
let c2 = control_q2c(end, control);
self.cubic(c1, c2, end);
}
/// Add a cubic curve segment.
fn cubic(&mut self, c1: Point, c2: Point, end: Point) {
if self.is_empty {
self.start_component();
self.start_control_into = mirror_c(self.start_point, c1);
}
self.curve.cubic(c1, c2, end);
let p0 = point_to_kurbo(self.last_point);
let p1 = point_to_kurbo(c1);
let p2 = point_to_kurbo(c2);
let p3 = point_to_kurbo(end);
let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
self.size.x.set_max(Abs::raw(extrema.x1));
self.size.y.set_max(Abs::raw(extrema.y1));
self.last_point = end;
self.last_control_from = mirror_c(end, c2);
}
/// Close the curve if it was opened.
fn close(&mut self, mode: CloseMode) {
if self.is_started && !self.is_empty {
if mode == CloseMode::Smooth {
self.cubic(
self.last_control_from,
self.start_control_into,
self.start_point,
);
}
self.curve.close();
self.last_point = self.start_point;
self.last_control_from = self.start_point;
}
self.is_started = false;
self.is_empty = true;
}
/// Push the initial move component.
fn start_component(&mut self) {
self.curve.move_(self.start_point);
self.is_empty = false;
self.is_started = true;
}
/// Expand the curve's bounding box.
fn expand_bounds(&mut self, point: Point) {
self.size.x.set_max(point.x);
self.size.y.set_max(point.y);
}
/// Resolve the point relative to the region.
fn resolve_point(&self, point: Axes<Rel>, relative: bool) -> Point {
let mut p = point
.resolve(self.styles)
.zip_map(self.region.size, Rel::relative_to)
.to_point();
if relative {
p += self.last_point;
}
p
}
}
/// Convert a cubic control point into a quadratic one.
fn control_c2q(p: Point, c: Point) -> Point {
1.5 * c - 0.5 * p
}
/// Convert a quadratic control point into a cubic one.
fn control_q2c(p: Point, c: Point) -> Point {
(p + 2.0 * c) / 3.0
}
/// Mirror a control point.
fn mirror_c(p: Point, c: Point) -> Point {
2.0 * p - c
}
/// Convert a point to a `kurbo::Point`.
fn point_to_kurbo(point: Point) -> kurbo::Point {
kurbo::Point::new(point.x.to_raw(), point.y.to_raw())
}
/// Layout the polygon. /// Layout the polygon.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
pub fn layout_polygon( pub fn layout_polygon(
@ -148,7 +400,7 @@ pub fn layout_polygon(
region: Region, region: Region,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let points: Vec<Point> = elem let points: Vec<Point> = elem
.vertices() .vertices
.iter() .iter()
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
.collect(); .collect();
@ -160,7 +412,7 @@ pub fn layout_polygon(
let mut frame = Frame::hard(size); let mut frame = Frame::hard(size);
// Only create a path if there are more than zero points. // Only create a curve if there are more than zero points.
if points.is_empty() { if points.is_empty() {
return Ok(frame); return Ok(frame);
} }
@ -174,16 +426,16 @@ pub fn layout_polygon(
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default), Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
}; };
// Construct a closed path given all points. // Construct a closed curve given all points.
let mut path = Path::new(); let mut curve = Curve::new();
path.move_to(points[0]); curve.move_(points[0]);
for &point in &points[1..] { for &point in &points[1..] {
path.line_to(point); curve.line(point);
} }
path.close_path(); curve.close();
let shape = Shape { let shape = Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke, stroke,
fill, fill,
fill_rule, fill_rule,
@ -409,7 +661,7 @@ fn layout_shape(
let size = frame.size() + outset.sum_by_axis(); let size = frame.size() + outset.sum_by_axis();
let pos = Point::new(-outset.left, -outset.top); let pos = Point::new(-outset.left, -outset.top);
let shape = Shape { let shape = Shape {
geometry: Geometry::Path(Path::ellipse(size)), geometry: Geometry::Curve(Curve::ellipse(size)),
fill, fill,
stroke: stroke.left, stroke: stroke.left,
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
@ -448,13 +700,13 @@ fn quadratic_size(region: Region) -> Option<Abs> {
} }
} }
/// Creates a new rectangle as a path. /// Creates a new rectangle as a curve.
pub fn clip_rect( pub fn clip_rect(
size: Size, size: Size,
radius: &Corners<Rel<Abs>>, radius: &Corners<Rel<Abs>>,
stroke: &Sides<Option<FixedStroke>>, stroke: &Sides<Option<FixedStroke>>,
outset: &Sides<Rel<Abs>>, outset: &Sides<Rel<Abs>>,
) -> Path { ) -> Curve {
let outset = outset.relative_to(size); let outset = outset.relative_to(size);
let size = size + outset.sum_by_axis(); let size = size + outset.sum_by_axis();
@ -468,26 +720,30 @@ pub fn clip_rect(
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius)); let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
let corners = corners_control_points(size, &radius, stroke, &stroke_widths); let corners = corners_control_points(size, &radius, stroke, &stroke_widths);
let mut path = Path::new(); let mut curve = Curve::new();
if corners.top_left.arc_inner() { if corners.top_left.arc_inner() {
path.arc_move( curve.arc_move(
corners.top_left.start_inner(), corners.top_left.start_inner(),
corners.top_left.center_inner(), corners.top_left.center_inner(),
corners.top_left.end_inner(), corners.top_left.end_inner(),
); );
} else { } else {
path.move_to(corners.top_left.center_inner()); curve.move_(corners.top_left.center_inner());
} }
for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] { for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] {
if corner.arc_inner() { if corner.arc_inner() {
path.arc_line(corner.start_inner(), corner.center_inner(), corner.end_inner()) curve.arc_line(
corner.start_inner(),
corner.center_inner(),
corner.end_inner(),
)
} else { } else {
path.line_to(corner.center_inner()); curve.line(corner.center_inner());
} }
} }
path.close_path(); curve.close();
path.translate(Point::new(-outset.left, -outset.top)); curve.translate(Point::new(-outset.left, -outset.top));
path curve
} }
/// Add a fill and stroke with optional radius and outset to the frame. /// Add a fill and stroke with optional radius and outset to the frame.
@ -592,25 +848,25 @@ fn segmented_rect(
// fill shape with inner curve // fill shape with inner curve
if let Some(fill) = fill { if let Some(fill) = fill {
let mut path = Path::new(); let mut curve = Curve::new();
let c = corners.get_ref(Corner::TopLeft); let c = corners.get_ref(Corner::TopLeft);
if c.arc() { if c.arc() {
path.arc_move(c.start(), c.center(), c.end()); curve.arc_move(c.start(), c.center(), c.end());
} else { } else {
path.move_to(c.center()); curve.move_(c.center());
}; };
for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] { for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] {
let c = corners.get_ref(corner); let c = corners.get_ref(corner);
if c.arc() { if c.arc() {
path.arc_line(c.start(), c.center(), c.end()); curve.arc_line(c.start(), c.center(), c.end());
} else { } else {
path.line_to(c.center()); curve.line(c.center());
} }
} }
path.close_path(); curve.close();
res.push(Shape { res.push(Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
fill: Some(fill), fill: Some(fill),
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
stroke: None, stroke: None,
@ -649,18 +905,18 @@ fn segmented_rect(
res res
} }
fn path_segment( fn curve_segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
path: &mut Path, curve: &mut Curve,
) { ) {
// create start corner // create start corner
let c = corners.get_ref(start); let c = corners.get_ref(start);
if start == end || !c.arc() { if start == end || !c.arc() {
path.move_to(c.end()); curve.move_(c.end());
} else { } else {
path.arc_move(c.mid(), c.center(), c.end()); curve.arc_move(c.mid(), c.center(), c.end());
} }
// create corners between start and end // create corners between start and end
@ -668,9 +924,9 @@ fn path_segment(
while current != end { while current != end {
let c = corners.get_ref(current); let c = corners.get_ref(current);
if c.arc() { if c.arc() {
path.arc_line(c.start(), c.center(), c.end()); curve.arc_line(c.start(), c.center(), c.end());
} else { } else {
path.line_to(c.end()); curve.line(c.end());
} }
current = current.next_cw(); current = current.next_cw();
} }
@ -678,11 +934,11 @@ fn path_segment(
// create end corner // create end corner
let c = corners.get_ref(end); let c = corners.get_ref(end);
if !c.arc() { if !c.arc() {
path.line_to(c.start()); curve.line(c.start());
} else if start == end { } else if start == end {
path.arc_line(c.start(), c.center(), c.end()); curve.arc_line(c.start(), c.center(), c.end());
} else { } else {
path.arc_line(c.start(), c.center(), c.mid()); curve.arc_line(c.start(), c.center(), c.mid());
} }
} }
@ -739,11 +995,11 @@ fn stroke_segment(
stroke: FixedStroke, stroke: FixedStroke,
) -> Shape { ) -> Shape {
// Create start corner. // Create start corner.
let mut path = Path::new(); let mut curve = Curve::new();
path_segment(start, end, corners, &mut path); curve_segment(start, end, corners, &mut curve);
Shape { Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke: Some(stroke), stroke: Some(stroke),
fill: None, fill: None,
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
@ -757,7 +1013,7 @@ fn fill_segment(
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: &FixedStroke, stroke: &FixedStroke,
) -> Shape { ) -> Shape {
let mut path = Path::new(); let mut curve = Curve::new();
// create the start corner // create the start corner
// begin on the inside and finish on the outside // begin on the inside and finish on the outside
@ -765,33 +1021,33 @@ fn fill_segment(
// half corner if different // half corner if different
if start == end { if start == end {
let c = corners.get_ref(start); let c = corners.get_ref(start);
path.move_to(c.end_inner()); curve.move_(c.end_inner());
path.line_to(c.end_outer()); curve.line(c.end_outer());
} else { } else {
let c = corners.get_ref(start); let c = corners.get_ref(start);
if c.arc_inner() { if c.arc_inner() {
path.arc_move(c.end_inner(), c.center_inner(), c.mid_inner()); curve.arc_move(c.end_inner(), c.center_inner(), c.mid_inner());
} else { } else {
path.move_to(c.end_inner()); curve.move_(c.end_inner());
} }
if c.arc_outer() { if c.arc_outer() {
path.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
} else { } else {
path.line_to(c.outer()); curve.line(c.outer());
path.line_to(c.end_outer()); curve.line(c.end_outer());
} }
} }
// create the clockwise outside path for the corners between start and end // create the clockwise outside curve for the corners between start and end
let mut current = start.next_cw(); let mut current = start.next_cw();
while current != end { while current != end {
let c = corners.get_ref(current); let c = corners.get_ref(current);
if c.arc_outer() { if c.arc_outer() {
path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
} else { } else {
path.line_to(c.outer()); curve.line(c.outer());
} }
current = current.next_cw(); current = current.next_cw();
} }
@ -803,46 +1059,46 @@ fn fill_segment(
if start == end { if start == end {
let c = corners.get_ref(end); let c = corners.get_ref(end);
if c.arc_outer() { if c.arc_outer() {
path.arc_line(c.start_outer(), c.center_outer(), c.end_outer()); curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
} else { } else {
path.line_to(c.outer()); curve.line(c.outer());
path.line_to(c.end_outer()); curve.line(c.end_outer());
} }
if c.arc_inner() { if c.arc_inner() {
path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
} else { } else {
path.line_to(c.center_inner()); curve.line(c.center_inner());
} }
} else { } else {
let c = corners.get_ref(end); let c = corners.get_ref(end);
if c.arc_outer() { if c.arc_outer() {
path.arc_line(c.start_outer(), c.center_outer(), c.mid_outer()); curve.arc_line(c.start_outer(), c.center_outer(), c.mid_outer());
} else { } else {
path.line_to(c.outer()); curve.line(c.outer());
} }
if c.arc_inner() { if c.arc_inner() {
path.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
} else { } else {
path.line_to(c.center_inner()); curve.line(c.center_inner());
} }
} }
// create the counterclockwise inside path for the corners between start and end // create the counterclockwise inside curve for the corners between start and end
let mut current = end.next_ccw(); let mut current = end.next_ccw();
while current != start { while current != start {
let c = corners.get_ref(current); let c = corners.get_ref(current);
if c.arc_inner() { if c.arc_inner() {
path.arc_line(c.end_inner(), c.center_inner(), c.start_inner()); curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
} else { } else {
path.line_to(c.center_inner()); curve.line(c.center_inner());
} }
current = current.next_ccw(); current = current.next_ccw();
} }
path.close_path(); curve.close();
Shape { Shape {
geometry: Geometry::Path(path), geometry: Geometry::Curve(curve),
stroke: None, stroke: None,
fill: Some(stroke.paint.clone()), fill: Some(stroke.paint.clone()),
fill_rule: FillRule::default(), fill_rule: FillRule::default(),
@ -1026,31 +1282,31 @@ impl ControlPoints {
} }
} }
/// Helper to draw arcs with bezier curves. /// Helper to draw arcs with zier curves.
trait PathExt { trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point); fn arc(&mut self, start: Point, center: Point, end: Point);
fn arc_move(&mut self, start: Point, center: Point, end: Point); fn arc_move(&mut self, start: Point, center: Point, end: Point);
fn arc_line(&mut self, start: Point, center: Point, end: Point); fn arc_line(&mut self, start: Point, center: Point, end: Point);
} }
impl PathExt for Path { impl CurveExt for Curve {
fn arc(&mut self, start: Point, center: Point, end: Point) { fn arc(&mut self, start: Point, center: Point, end: Point) {
let arc = bezier_arc_control(start, center, end); let arc = bezier_arc_control(start, center, end);
self.cubic_to(arc[0], arc[1], end); self.cubic(arc[0], arc[1], end);
} }
fn arc_move(&mut self, start: Point, center: Point, end: Point) { fn arc_move(&mut self, start: Point, center: Point, end: Point) {
self.move_to(start); self.move_(start);
self.arc(start, center, end); self.arc(start, center, end);
} }
fn arc_line(&mut self, start: Point, center: Point, end: Point) { fn arc_line(&mut self, start: Point, center: Point, end: Point) {
self.line_to(start); self.line(start);
self.arc(start, center, end); self.arc(start, center, end);
} }
} }
/// Get the control points for a bezier curve that approximates a circular arc for /// Get the control points for a zier curve that approximates a circular arc for
/// a start point, an end point and a center of the circle whose arc connects /// a start point, an end point and a center of the circle whose arc connects
/// the two. /// the two.
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {

View File

@ -27,7 +27,7 @@ pub fn layout_stack(
let spacing = elem.spacing(styles); let spacing = elem.spacing(styles);
let mut deferred = None; let mut deferred = None;
for child in elem.children() { for child in &elem.children {
match child { match child {
StackChild::Spacing(kind) => { StackChild::Spacing(kind) => {
layouter.layout_spacing(*kind); layouter.layout_spacing(*kind);
@ -36,14 +36,14 @@ pub fn layout_stack(
StackChild::Block(block) => { StackChild::Block(block) => {
// Transparently handle `h`. // Transparently handle `h`.
if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) { if let (Axis::X, Some(h)) = (axis, block.to_packed::<HElem>()) {
layouter.layout_spacing(*h.amount()); layouter.layout_spacing(h.amount);
deferred = None; deferred = None;
continue; continue;
} }
// Transparently handle `v`. // Transparently handle `v`.
if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) { if let (Axis::Y, Some(v)) = (axis, block.to_packed::<VElem>()) {
layouter.layout_spacing(*v.amount()); layouter.layout_spacing(v.amount);
deferred = None; deferred = None;
continue; continue;
} }

View File

@ -52,7 +52,7 @@ pub fn layout_rotate(
region, region,
size, size,
styles, styles,
elem.body(), &elem.body,
Transform::rotate(angle), Transform::rotate(angle),
align, align,
elem.reflow(styles), elem.reflow(styles),
@ -81,7 +81,7 @@ pub fn layout_scale(
region, region,
size, size,
styles, styles,
elem.body(), &elem.body,
Transform::scale(scale.x, scale.y), Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles), elem.origin(styles).resolve(styles),
elem.reflow(styles), elem.reflow(styles),
@ -169,7 +169,7 @@ pub fn layout_skew(
region, region,
size, size,
styles, styles,
elem.body(), &elem.body,
Transform::skew(ax, ay), Transform::skew(ax, ay),
align, align,
elem.reflow(styles), elem.reflow(styles),

View File

@ -38,6 +38,7 @@ indexmap = { workspace = true }
kamadak-exif = { workspace = true } kamadak-exif = { workspace = true }
kurbo = { workspace = true } kurbo = { workspace = true }
lipsum = { workspace = true } lipsum = { workspace = true }
memchr = { workspace = true }
palette = { workspace = true } palette = { workspace = true }
phf = { workspace = true } phf = { workspace = true }
png = { workspace = true } png = { workspace = true }
@ -60,6 +61,7 @@ ttf-parser = { workspace = true }
two-face = { workspace = true } two-face = { workspace = true }
typed-arena = { workspace = true } typed-arena = { workspace = true }
unicode-math-class = { workspace = true } unicode-math-class = { workspace = true }
unicode-normalization = { workspace = true }
unicode-segmentation = { workspace = true } unicode-segmentation = { workspace = true }
unscanny = { workspace = true } unscanny = { workspace = true }
usvg = { workspace = true } usvg = { workspace = true }

View File

@ -11,6 +11,7 @@ use ecow::{eco_vec, EcoVec};
use typst_syntax::package::{PackageSpec, PackageVersion}; use typst_syntax::package::{PackageSpec, PackageVersion};
use typst_syntax::{Span, Spanned, SyntaxError}; use typst_syntax::{Span, Spanned, SyntaxError};
use crate::engine::Engine;
use crate::{World, WorldExt}; use crate::{World, WorldExt};
/// Early-return with a [`StrResult`] or [`SourceResult`]. /// Early-return with a [`StrResult`] or [`SourceResult`].
@ -228,6 +229,23 @@ impl From<SyntaxError> for SourceDiagnostic {
} }
} }
/// Destination for a deprecation message when accessing a deprecated value.
pub trait DeprecationSink {
/// Emits the given deprecation message into this sink.
fn emit(self, message: &str);
}
impl DeprecationSink for () {
fn emit(self, _: &str) {}
}
impl DeprecationSink for (&mut Engine<'_>, Span) {
/// Emits the deprecation message as a warning.
fn emit(self, message: &str) {
self.0.sink.warn(SourceDiagnostic::warning(self.1, message));
}
}
/// A part of a diagnostic's [trace](SourceDiagnostic::trace). /// A part of a diagnostic's [trace](SourceDiagnostic::trace).
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Tracepoint { pub enum Tracepoint {

View File

@ -312,7 +312,8 @@ impl Route<'_> {
if !self.within(Route::MAX_SHOW_RULE_DEPTH) { if !self.within(Route::MAX_SHOW_RULE_DEPTH) {
bail!( bail!(
"maximum show rule depth exceeded"; "maximum show rule depth exceeded";
hint: "check whether the show rule matches its own output" hint: "maybe a show rule matches its own output";
hint: "maybe there are too deeply nested elements"
); );
} }
Ok(()) Ok(())

View File

@ -1,4 +1,5 @@
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::ops::Add;
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
@ -304,8 +305,6 @@ impl Args {
/// ``` /// ```
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args, args: &mut Args,
/// The arguments to construct. /// The arguments to construct.
#[external] #[external]
@ -366,7 +365,7 @@ impl Debug for Args {
impl Repr for Args { impl Repr for Args {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
let pieces = self.items.iter().map(Arg::repr).collect::<Vec<_>>(); let pieces = self.items.iter().map(Arg::repr).collect::<Vec<_>>();
repr::pretty_array_like(&pieces, false).into() eco_format!("arguments{}", repr::pretty_array_like(&pieces, false))
} }
} }
@ -376,6 +375,21 @@ impl PartialEq for Args {
} }
} }
impl Add for Args {
type Output = Self;
fn add(mut self, rhs: Self) -> Self::Output {
self.items.retain(|item| {
!item.name.as_ref().is_some_and(|name| {
rhs.items.iter().any(|a| a.name.as_ref() == Some(name))
})
});
self.items.extend(rhs.items);
self.span = Span::detached();
self
}
}
/// An argument to a function call: `12` or `draw: false`. /// An argument to a function call: `12` or `draw: false`.
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)] #[allow(clippy::derived_hash_with_manual_eq)]

View File

@ -301,9 +301,7 @@ impl Array {
#[func] #[func]
pub fn find( pub fn find(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
searcher: Func, searcher: Func,
@ -325,9 +323,7 @@ impl Array {
#[func] #[func]
pub fn position( pub fn position(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
searcher: Func, searcher: Func,
@ -363,8 +359,6 @@ impl Array {
/// ``` /// ```
#[func] #[func]
pub fn range( pub fn range(
/// The real arguments (the other arguments are just for the docs, this
/// function is a bit involved, so we parse the arguments manually).
args: &mut Args, args: &mut Args,
/// The start of the range (inclusive). /// The start of the range (inclusive).
#[external] #[external]
@ -402,9 +396,7 @@ impl Array {
#[func] #[func]
pub fn filter( pub fn filter(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
@ -427,9 +419,7 @@ impl Array {
#[func] #[func]
pub fn map( pub fn map(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. /// The function to apply to each item.
mapper: Func, mapper: Func,
@ -481,8 +471,6 @@ impl Array {
#[func] #[func]
pub fn zip( pub fn zip(
self, self,
/// The real arguments (the `others` arguments are just for the docs, this
/// function is a bit involved, so we parse the positional arguments manually).
args: &mut Args, args: &mut Args,
/// Whether all arrays have to have the same length. /// Whether all arrays have to have the same length.
/// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an
@ -569,9 +557,7 @@ impl Array {
#[func] #[func]
pub fn fold( pub fn fold(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The initial value to start with. /// The initial value to start with.
init: Value, init: Value,
@ -631,9 +617,7 @@ impl Array {
#[func] #[func]
pub fn any( pub fn any(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
@ -651,9 +635,7 @@ impl Array {
#[func] #[func]
pub fn all( pub fn all(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The function to apply to each item. Must return a boolean. /// The function to apply to each item. Must return a boolean.
test: Func, test: Func,
@ -769,7 +751,7 @@ impl Array {
/// ///
/// ```example /// ```example
/// #let array = (1, 2, 3, 4, 5, 6, 7, 8) /// #let array = (1, 2, 3, 4, 5, 6, 7, 8)
/// #array.chunks(3) /// #array.chunks(3) \
/// #array.chunks(3, exact: true) /// #array.chunks(3, exact: true)
/// ``` /// ```
#[func] #[func]
@ -831,11 +813,8 @@ impl Array {
#[func] #[func]
pub fn sorted( pub fn sorted(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The callsite span.
span: Span, span: Span,
/// If given, applies this function to the elements in the array to /// If given, applies this function to the elements in the array to
/// determine the keys to sort by. /// determine the keys to sort by.
@ -881,9 +860,7 @@ impl Array {
#[func(title = "Deduplicate")] #[func(title = "Deduplicate")]
pub fn dedup( pub fn dedup(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// If given, applies this function to the elements in the array to /// If given, applies this function to the elements in the array to
/// determine the keys to deduplicate by. /// determine the keys to deduplicate by.
@ -967,9 +944,7 @@ impl Array {
#[func] #[func]
pub fn reduce( pub fn reduce(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The reducing function. Must have two parameters: One for the /// The reducing function. Must have two parameters: One for the
/// accumulated value and one for an item. /// accumulated value and one for an item.
@ -1124,6 +1099,53 @@ impl<T: FromValue, const N: usize> FromValue for SmallVec<[T; N]> {
} }
} }
/// One element, or multiple provided as an array.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct OneOrMultiple<T>(pub Vec<T>);
impl<T: Reflect> Reflect for OneOrMultiple<T> {
fn input() -> CastInfo {
T::input() + Array::input()
}
fn output() -> CastInfo {
T::output() + Array::output()
}
fn castable(value: &Value) -> bool {
Array::castable(value) || T::castable(value)
}
}
impl<T: IntoValue + Clone> IntoValue for OneOrMultiple<T> {
fn into_value(self) -> Value {
self.0.into_value()
}
}
impl<T: FromValue> FromValue for OneOrMultiple<T> {
fn from_value(value: Value) -> HintedStrResult<Self> {
if T::castable(&value) {
return Ok(Self(vec![T::from_value(value)?]));
}
if Array::castable(&value) {
return Ok(Self(
Array::from_value(value)?
.into_iter()
.map(|value| T::from_value(value))
.collect::<HintedStrResult<_>>()?,
));
}
Err(Self::error(&value))
}
}
impl<T> Default for OneOrMultiple<T> {
fn default() -> Self {
Self(vec![])
}
}
/// The error message when the array is empty. /// The error message when the array is empty.
#[cold] #[cold]
fn array_is_empty() -> EcoString { fn array_is_empty() -> EcoString {

View File

@ -1,6 +1,8 @@
use std::borrow::Cow; use std::any::Any;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::ops::{Add, AddAssign, Deref}; use std::ops::{Add, AddAssign, Deref};
use std::str::Utf8Error;
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
@ -39,28 +41,75 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value
/// #str(data.slice(1, 4)) /// #str(data.slice(1, 4))
/// ``` /// ```
#[ty(scope, cast)] #[ty(scope, cast)]
#[derive(Clone, Hash, Eq, PartialEq)] #[derive(Clone, Hash)]
pub struct Bytes(Arc<LazyHash<Cow<'static, [u8]>>>); #[allow(clippy::derived_hash_with_manual_eq)]
pub struct Bytes(Arc<LazyHash<dyn Bytelike>>);
impl Bytes { impl Bytes {
/// Create a buffer from a static byte slice. /// Create `Bytes` from anything byte-like.
pub fn from_static(slice: &'static [u8]) -> Self { ///
Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) /// The `data` type will directly back this bytes object. This means you can
/// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be
/// allocated.
///
/// If the type is `Vec<u8>` and the `Bytes` are unique (i.e. not cloned),
/// the vector will be reused when mutating to the `Bytes`.
///
/// If your source type is a string, prefer [`Bytes::from_string`] to
/// directly use the UTF-8 encoded string data without any copying.
pub fn new<T>(data: T) -> Self
where
T: AsRef<[u8]> + Send + Sync + 'static,
{
Self(Arc::new(LazyHash::new(data)))
}
/// Create `Bytes` from anything string-like, implicitly viewing the UTF-8
/// representation.
///
/// The `data` type will directly back this bytes object. This means you can
/// e.g. pass `String` or `EcoString` without any copying.
pub fn from_string<T>(data: T) -> Self
where
T: AsRef<str> + Send + Sync + 'static,
{
Self(Arc::new(LazyHash::new(StrWrapper(data))))
} }
/// Return `true` if the length is 0. /// Return `true` if the length is 0.
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0.is_empty() self.as_slice().is_empty()
} }
/// Return a view into the buffer. /// Return a view into the bytes.
pub fn as_slice(&self) -> &[u8] { pub fn as_slice(&self) -> &[u8] {
self self
} }
/// Return a copy of the buffer as a vector. /// Try to view the bytes as an UTF-8 string.
///
/// If these bytes were created via `Bytes::from_string`, UTF-8 validation
/// is skipped.
pub fn as_str(&self) -> Result<&str, Utf8Error> {
self.inner().as_str()
}
/// Return a copy of the bytes as a vector.
pub fn to_vec(&self) -> Vec<u8> { pub fn to_vec(&self) -> Vec<u8> {
self.0.to_vec() self.as_slice().to_vec()
}
/// Try to turn the bytes into a `Str`.
///
/// - If these bytes were created via `Bytes::from_string::<Str>`, the
/// string is cloned directly.
/// - If these bytes were created via `Bytes::from_string`, but from a
/// different type of string, UTF-8 validation is still skipped.
pub fn to_str(&self) -> Result<Str, Utf8Error> {
match self.inner().as_any().downcast_ref::<Str>() {
Some(string) => Ok(string.clone()),
None => self.as_str().map(Into::into),
}
} }
/// Resolve an index or throw an out of bounds error. /// Resolve an index or throw an out of bounds error.
@ -72,12 +121,15 @@ impl Bytes {
/// ///
/// `index == len` is considered in bounds. /// `index == len` is considered in bounds.
fn locate_opt(&self, index: i64) -> Option<usize> { fn locate_opt(&self, index: i64) -> Option<usize> {
let len = self.as_slice().len();
let wrapped = let wrapped =
if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; if index >= 0 { Some(index) } else { (len as i64).checked_add(index) };
wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len)
}
wrapped /// Access the inner `dyn Bytelike`.
.and_then(|v| usize::try_from(v).ok()) fn inner(&self) -> &dyn Bytelike {
.filter(|&v| v <= self.0.len()) &**self.0
} }
} }
@ -106,7 +158,7 @@ impl Bytes {
/// The length in bytes. /// The length in bytes.
#[func(title = "Length")] #[func(title = "Length")]
pub fn len(&self) -> usize { pub fn len(&self) -> usize {
self.0.len() self.as_slice().len()
} }
/// Returns the byte at the specified index. Returns the default value if /// Returns the byte at the specified index. Returns the default value if
@ -122,13 +174,13 @@ impl Bytes {
default: Option<Value>, default: Option<Value>,
) -> StrResult<Value> { ) -> StrResult<Value> {
self.locate_opt(index) self.locate_opt(index)
.and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) .and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into())))
.or(default) .or(default)
.ok_or_else(|| out_of_bounds_no_default(index, self.len())) .ok_or_else(|| out_of_bounds_no_default(index, self.len()))
} }
/// Extracts a subslice of the bytes. Fails with an error if the start or end /// Extracts a subslice of the bytes. Fails with an error if the start or
/// index is out of bounds. /// end index is out of bounds.
#[func] #[func]
pub fn slice( pub fn slice(
&self, &self,
@ -148,9 +200,17 @@ impl Bytes {
if end.is_none() { if end.is_none() {
end = count.map(|c: i64| start + c); end = count.map(|c: i64| start + c);
} }
let start = self.locate(start)?; let start = self.locate(start)?;
let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start);
Ok(self.0[start..end].into()) let slice = &self.as_slice()[start..end];
// We could hold a view into the original bytes here instead of
// making a copy, but it's unclear when that's worth it. Java
// originally did that for strings, but went back on it because a
// very small view into a very large buffer would be a sort of
// memory leak.
Ok(Bytes::new(slice.to_vec()))
} }
} }
@ -170,7 +230,15 @@ impl Deref for Bytes {
type Target = [u8]; type Target = [u8];
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 self.inner().as_bytes()
}
}
impl Eq for Bytes {}
impl PartialEq for Bytes {
fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0)
} }
} }
@ -180,18 +248,6 @@ impl AsRef<[u8]> for Bytes {
} }
} }
impl From<&[u8]> for Bytes {
fn from(slice: &[u8]) -> Self {
Self(Arc::new(LazyHash::new(slice.to_vec().into())))
}
}
impl From<Vec<u8>> for Bytes {
fn from(vec: Vec<u8>) -> Self {
Self(Arc::new(LazyHash::new(vec.into())))
}
}
impl Add for Bytes { impl Add for Bytes {
type Output = Self; type Output = Self;
@ -207,10 +263,12 @@ impl AddAssign for Bytes {
// Nothing to do // Nothing to do
} else if self.is_empty() { } else if self.is_empty() {
*self = rhs; *self = rhs;
} else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { } else if let Some(vec) = Arc::get_mut(&mut self.0)
Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); .and_then(|unique| unique.as_any_mut().downcast_mut::<Vec<u8>>())
{
vec.extend_from_slice(&rhs);
} else { } else {
*self = Self::from([self.as_slice(), rhs.as_slice()].concat()); *self = Self::new([self.as_slice(), rhs.as_slice()].concat());
} }
} }
} }
@ -228,20 +286,79 @@ impl Serialize for Bytes {
} }
} }
/// Any type that can back a byte buffer.
trait Bytelike: Send + Sync {
fn as_bytes(&self) -> &[u8];
fn as_str(&self) -> Result<&str, Utf8Error>;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl<T> Bytelike for T
where
T: AsRef<[u8]> + Send + Sync + 'static,
{
fn as_bytes(&self) -> &[u8] {
self.as_ref()
}
fn as_str(&self) -> Result<&str, Utf8Error> {
std::str::from_utf8(self.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Hash for dyn Bytelike {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_bytes().hash(state);
}
}
/// Makes string-like objects usable with `Bytes`.
struct StrWrapper<T>(T);
impl<T> Bytelike for StrWrapper<T>
where
T: AsRef<str> + Send + Sync + 'static,
{
fn as_bytes(&self) -> &[u8] {
self.0.as_ref().as_bytes()
}
fn as_str(&self) -> Result<&str, Utf8Error> {
Ok(self.0.as_ref())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
/// A value that can be cast to bytes. /// A value that can be cast to bytes.
pub struct ToBytes(Bytes); pub struct ToBytes(Bytes);
cast! { cast! {
ToBytes, ToBytes,
v: Str => Self(v.as_bytes().into()), v: Str => Self(Bytes::from_string(v)),
v: Array => Self(v.iter() v: Array => Self(v.iter()
.map(|item| match item { .map(|item| match item {
Value::Int(byte @ 0..=255) => Ok(*byte as u8), Value::Int(byte @ 0..=255) => Ok(*byte as u8),
Value::Int(_) => bail!("number must be between 0 and 255"), Value::Int(_) => bail!("number must be between 0 and 255"),
value => Err(<u8 as Reflect>::error(value)), value => Err(<u8 as Reflect>::error(value)),
}) })
.collect::<Result<Vec<u8>, _>>()? .collect::<Result<Vec<u8>, _>>()
.into() .map(Bytes::new)?
), ),
v: Bytes => Self(v), v: Bytes => Self(v),
} }

View File

@ -97,7 +97,6 @@ cast! {
/// ``` /// ```
#[func(title = "Power")] #[func(title = "Power")]
pub fn pow( pub fn pow(
/// The callsite span.
span: Span, span: Span,
/// The base of the power. /// The base of the power.
/// ///
@ -159,7 +158,6 @@ pub fn pow(
/// ``` /// ```
#[func(title = "Exponential")] #[func(title = "Exponential")]
pub fn exp( pub fn exp(
/// The callsite span.
span: Span, span: Span,
/// The exponent of the power. /// The exponent of the power.
exponent: Spanned<Num>, exponent: Spanned<Num>,
@ -412,7 +410,6 @@ pub fn tanh(
/// ``` /// ```
#[func(title = "Logarithm")] #[func(title = "Logarithm")]
pub fn log( pub fn log(
/// The callsite span.
span: Span, span: Span,
/// The number whose logarithm to calculate. Must be strictly positive. /// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned<Num>, value: Spanned<Num>,
@ -454,7 +451,6 @@ pub fn log(
/// ``` /// ```
#[func(title = "Natural Logarithm")] #[func(title = "Natural Logarithm")]
pub fn ln( pub fn ln(
/// The callsite span.
span: Span, span: Span,
/// The number whose logarithm to calculate. Must be strictly positive. /// The number whose logarithm to calculate. Must be strictly positive.
value: Spanned<Num>, value: Spanned<Num>,
@ -782,7 +778,6 @@ pub fn round(
/// ``` /// ```
#[func] #[func]
pub fn clamp( pub fn clamp(
/// The callsite span.
span: Span, span: Span,
/// The number to clamp. /// The number to clamp.
value: DecNum, value: DecNum,
@ -815,7 +810,6 @@ pub fn clamp(
/// ``` /// ```
#[func(title = "Minimum")] #[func(title = "Minimum")]
pub fn min( pub fn min(
/// The callsite span.
span: Span, span: Span,
/// The sequence of values from which to extract the minimum. /// The sequence of values from which to extract the minimum.
/// Must not be empty. /// Must not be empty.
@ -833,7 +827,6 @@ pub fn min(
/// ``` /// ```
#[func(title = "Maximum")] #[func(title = "Maximum")]
pub fn max( pub fn max(
/// The callsite span.
span: Span, span: Span,
/// The sequence of values from which to extract the maximum. /// The sequence of values from which to extract the maximum.
/// Must not be empty. /// Must not be empty.
@ -911,7 +904,6 @@ pub fn odd(
/// ``` /// ```
#[func(title = "Remainder")] #[func(title = "Remainder")]
pub fn rem( pub fn rem(
/// The span of the function call.
span: Span, span: Span,
/// The dividend of the remainder. /// The dividend of the remainder.
dividend: DecNum, dividend: DecNum,
@ -950,7 +942,6 @@ pub fn rem(
/// ``` /// ```
#[func(title = "Euclidean Division")] #[func(title = "Euclidean Division")]
pub fn div_euclid( pub fn div_euclid(
/// The callsite span.
span: Span, span: Span,
/// The dividend of the division. /// The dividend of the division.
dividend: DecNum, dividend: DecNum,
@ -992,9 +983,8 @@ pub fn div_euclid(
/// #calc.rem-euclid(1.75, 0.5) \ /// #calc.rem-euclid(1.75, 0.5) \
/// #calc.rem-euclid(decimal("1.75"), decimal("0.5")) /// #calc.rem-euclid(decimal("1.75"), decimal("0.5"))
/// ``` /// ```
#[func(title = "Euclidean Remainder")] #[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])]
pub fn rem_euclid( pub fn rem_euclid(
/// The callsite span.
span: Span, span: Span,
/// The dividend of the remainder. /// The dividend of the remainder.
dividend: DecNum, dividend: DecNum,
@ -1031,7 +1021,6 @@ pub fn rem_euclid(
/// ``` /// ```
#[func(title = "Quotient")] #[func(title = "Quotient")]
pub fn quo( pub fn quo(
/// The span of the function call.
span: Span, span: Span,
/// The dividend of the quotient. /// The dividend of the quotient.
dividend: DecNum, dividend: DecNum,

View File

@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; use crate::foundations::{
array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value,
};
/// Determine details of a type. /// Determine details of a type.
/// ///
@ -497,3 +499,58 @@ cast! {
/// An operator that can be both unary or binary like `+`. /// An operator that can be both unary or binary like `+`.
"vary" => MathClass::Vary, "vary" => MathClass::Vary,
} }
/// A type that contains a user-visible source portion and something that is
/// derived from it, but not user-visible.
///
/// An example usage would be `source` being a `DataSource` and `derived` a
/// TextMate theme parsed from it. With `Derived`, we can store both parts in
/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue`
/// impls.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Derived<S, D> {
/// The source portion.
pub source: S,
/// The derived portion.
pub derived: D,
}
impl<S, D> Derived<S, D> {
/// Create a new instance from the `source` and the `derived` data.
pub fn new(source: S, derived: D) -> Self {
Self { source, derived }
}
}
impl<S: Reflect, D> Reflect for Derived<S, D> {
fn input() -> CastInfo {
S::input()
}
fn output() -> CastInfo {
S::output()
}
fn castable(value: &Value) -> bool {
S::castable(value)
}
fn error(found: &Value) -> HintedString {
S::error(found)
}
}
impl<S: IntoValue, D> IntoValue for Derived<S, D> {
fn into_value(self) -> Value {
self.source.into_value()
}
}
impl<S: Fold, D: Fold> Fold for Derived<S, D> {
fn fold(self, outer: Self) -> Self {
Self {
source: self.source.fold(outer.source),
derived: self.derived.fold(outer.derived),
}
}
}

View File

@ -9,7 +9,6 @@ use std::sync::Arc;
use comemo::Tracked; use comemo::Tracked;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use smallvec::smallvec;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; use typst_utils::{fat, singleton, LazyHash, SmallBitSet};
@ -211,9 +210,10 @@ impl Content {
/// instead. /// instead.
pub fn get_by_name(&self, name: &str) -> Result<Value, FieldAccessError> { pub fn get_by_name(&self, name: &str) -> Result<Value, FieldAccessError> {
if name == "label" { if name == "label" {
if let Some(label) = self.label() { return self
return Ok(label.into_value()); .label()
} .map(|label| label.into_value())
.ok_or(FieldAccessError::Unknown);
} }
let id = self.elem().field_id(name).ok_or(FieldAccessError::Unknown)?; let id = self.elem().field_id(name).ok_or(FieldAccessError::Unknown)?;
self.get(id, None) self.get(id, None)
@ -499,7 +499,7 @@ impl Content {
/// Link the content somewhere. /// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self { pub fn linked(self, dest: Destination) -> Self {
self.styled(LinkElem::set_dests(smallvec![dest])) self.styled(LinkElem::set_current(Some(dest)))
} }
/// Set alignments for this content. /// Set alignments for this content.

View File

@ -318,7 +318,6 @@ impl Datetime {
/// ``` /// ```
#[func] #[func]
pub fn today( pub fn today(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// An offset to apply to the current UTC date. If set to `{auto}`, the /// An offset to apply to the current UTC date. If set to `{auto}`, the
/// offset will be the local offset. /// offset will be the local offset.

View File

@ -261,7 +261,12 @@ pub struct ToDict(Dict);
cast! { cast! {
ToDict, ToDict,
v: Module => Self(v.scope().iter().map(|(k, v, _)| (Str::from(k.clone()), v.clone())).collect()), v: Module => Self(v
.scope()
.iter()
.map(|(k, b)| (Str::from(k.clone()), b.read().clone()))
.collect()
),
} }
impl Debug for Dict { impl Debug for Dict {

View File

@ -110,7 +110,7 @@ impl f64 {
f64::signum(self) f64::signum(self)
} }
/// Converts bytes to a float. /// Interprets bytes as a float.
/// ///
/// ```example /// ```example
/// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \
@ -120,8 +120,10 @@ impl f64 {
pub fn from_bytes( pub fn from_bytes(
/// The bytes that should be converted to a float. /// The bytes that should be converted to a float.
/// ///
/// Must be of length exactly 8 so that the result fits into a 64-bit /// Must have a length of either 4 or 8. The bytes are then
/// float. /// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s
/// binary32 (single-precision) or binary64 (double-precision) format
/// depending on the length of the bytes.
bytes: Bytes, bytes: Bytes,
/// The endianness of the conversion. /// The endianness of the conversion.
#[named] #[named]
@ -158,23 +160,26 @@ impl f64 {
#[named] #[named]
#[default(Endianness::Little)] #[default(Endianness::Little)]
endian: Endianness, endian: Endianness,
/// The size of the resulting bytes.
///
/// This must be either 4 or 8. The call will return the
/// representation of this float in either
/// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32
/// (single-precision) or binary64 (double-precision) format
/// depending on the provided size.
#[named] #[named]
#[default(8)] #[default(8)]
size: u32, size: u32,
) -> StrResult<Bytes> { ) -> StrResult<Bytes> {
Ok(match size { Ok(match size {
8 => match endian { 8 => Bytes::new(match endian {
Endianness::Little => self.to_le_bytes(), Endianness::Little => self.to_le_bytes(),
Endianness::Big => self.to_be_bytes(), Endianness::Big => self.to_be_bytes(),
} }),
.as_slice() 4 => Bytes::new(match endian {
.into(),
4 => match endian {
Endianness::Little => (self as f32).to_le_bytes(), Endianness::Little => (self as f32).to_le_bytes(),
Endianness::Big => (self as f32).to_be_bytes(), Endianness::Big => (self as f32).to_be_bytes(),
} }),
.as_slice()
.into(),
_ => bail!("size must be either 4 or 8"), _ => bail!("size must be either 4 or 8"),
}) })
} }

View File

@ -9,11 +9,11 @@ use ecow::{eco_format, EcoString};
use typst_syntax::{ast, Span, SyntaxNode}; use typst_syntax::{ast, Span, SyntaxNode};
use typst_utils::{singleton, LazyHash, Static}; use typst_utils::{singleton, LazyHash, Static};
use crate::diag::{bail, SourceResult, StrResult}; use crate::diag::{bail, At, DeprecationSink, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs,
Selector, Type, Value, PluginFunc, Scope, Selector, Type, Value,
}; };
/// A mapping from argument values to a return value. /// A mapping from argument values to a return value.
@ -151,6 +151,8 @@ enum Repr {
Element(Element), Element(Element),
/// A user-defined closure. /// A user-defined closure.
Closure(Arc<LazyHash<Closure>>), Closure(Arc<LazyHash<Closure>>),
/// A plugin WebAssembly function.
Plugin(Arc<PluginFunc>),
/// A nested function with pre-applied arguments. /// A nested function with pre-applied arguments.
With(Arc<(Func, Args)>), With(Arc<(Func, Args)>),
} }
@ -164,6 +166,7 @@ impl Func {
Repr::Native(native) => Some(native.name), Repr::Native(native) => Some(native.name),
Repr::Element(elem) => Some(elem.name()), Repr::Element(elem) => Some(elem.name()),
Repr::Closure(closure) => closure.name(), Repr::Closure(closure) => closure.name(),
Repr::Plugin(func) => Some(func.name()),
Repr::With(with) => with.0.name(), Repr::With(with) => with.0.name(),
} }
} }
@ -176,6 +179,7 @@ impl Func {
Repr::Native(native) => Some(native.title), Repr::Native(native) => Some(native.title),
Repr::Element(elem) => Some(elem.title()), Repr::Element(elem) => Some(elem.title()),
Repr::Closure(_) => None, Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.title(), Repr::With(with) => with.0.title(),
} }
} }
@ -186,6 +190,7 @@ impl Func {
Repr::Native(native) => Some(native.docs), Repr::Native(native) => Some(native.docs),
Repr::Element(elem) => Some(elem.docs()), Repr::Element(elem) => Some(elem.docs()),
Repr::Closure(_) => None, Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.docs(), Repr::With(with) => with.0.docs(),
} }
} }
@ -204,6 +209,7 @@ impl Func {
Repr::Native(native) => Some(&native.0.params), Repr::Native(native) => Some(&native.0.params),
Repr::Element(elem) => Some(elem.params()), Repr::Element(elem) => Some(elem.params()),
Repr::Closure(_) => None, Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.params(), Repr::With(with) => with.0.params(),
} }
} }
@ -221,6 +227,7 @@ impl Func {
Some(singleton!(CastInfo, CastInfo::Type(Type::of::<Content>()))) Some(singleton!(CastInfo, CastInfo::Type(Type::of::<Content>())))
} }
Repr::Closure(_) => None, Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.returns(), Repr::With(with) => with.0.returns(),
} }
} }
@ -231,6 +238,7 @@ impl Func {
Repr::Native(native) => native.keywords, Repr::Native(native) => native.keywords,
Repr::Element(elem) => elem.keywords(), Repr::Element(elem) => elem.keywords(),
Repr::Closure(_) => &[], Repr::Closure(_) => &[],
Repr::Plugin(_) => &[],
Repr::With(with) => with.0.keywords(), Repr::With(with) => with.0.keywords(),
} }
} }
@ -241,16 +249,21 @@ impl Func {
Repr::Native(native) => Some(&native.0.scope), Repr::Native(native) => Some(&native.0.scope),
Repr::Element(elem) => Some(elem.scope()), Repr::Element(elem) => Some(elem.scope()),
Repr::Closure(_) => None, Repr::Closure(_) => None,
Repr::Plugin(_) => None,
Repr::With(with) => with.0.scope(), Repr::With(with) => with.0.scope(),
} }
} }
/// Get a field from this function's scope, if possible. /// Get a field from this function's scope, if possible.
pub fn field(&self, field: &str) -> StrResult<&'static Value> { pub fn field(
&self,
field: &str,
sink: impl DeprecationSink,
) -> StrResult<&'static Value> {
let scope = let scope =
self.scope().ok_or("cannot access fields on user-defined functions")?; self.scope().ok_or("cannot access fields on user-defined functions")?;
match scope.get(field) { match scope.get(field) {
Some(field) => Ok(field), Some(binding) => Ok(binding.read_checked(sink)),
None => match self.name() { None => match self.name() {
Some(name) => bail!("function `{name}` does not contain field `{field}`"), Some(name) => bail!("function `{name}` does not contain field `{field}`"),
None => bail!("function does not contain field `{field}`"), None => bail!("function does not contain field `{field}`"),
@ -266,6 +279,14 @@ impl Func {
} }
} }
/// Extract the plugin function, if it is one.
pub fn to_plugin(&self) -> Option<&PluginFunc> {
match &self.repr {
Repr::Plugin(func) => Some(func),
_ => None,
}
}
/// Call the function with the given context and arguments. /// Call the function with the given context and arguments.
pub fn call<A: IntoArgs>( pub fn call<A: IntoArgs>(
&self, &self,
@ -307,6 +328,12 @@ impl Func {
context, context,
args, args,
), ),
Repr::Plugin(func) => {
let inputs = args.all::<Bytes>()?;
let output = func.call(inputs).at(args.span)?;
args.finish()?;
Ok(Value::Bytes(output))
}
Repr::With(with) => { Repr::With(with) => {
args.items = with.1.items.iter().cloned().chain(args.items).collect(); args.items = with.1.items.iter().cloned().chain(args.items).collect();
with.0.call(engine, context, args) with.0.call(engine, context, args)
@ -334,8 +361,6 @@ impl Func {
#[func] #[func]
pub fn with( pub fn with(
self, self,
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args, args: &mut Args,
/// The arguments to apply to the function. /// The arguments to apply to the function.
#[external] #[external]
@ -361,8 +386,6 @@ impl Func {
#[func] #[func]
pub fn where_( pub fn where_(
self, self,
/// The real arguments (the other argument is just for the docs).
/// The docs argument cannot be called `args`.
args: &mut Args, args: &mut Args,
/// The fields to filter for. /// The fields to filter for.
#[variadic] #[variadic]
@ -414,10 +437,10 @@ impl PartialEq for Func {
} }
} }
impl PartialEq<&NativeFuncData> for Func { impl PartialEq<&'static NativeFuncData> for Func {
fn eq(&self, other: &&NativeFuncData) -> bool { fn eq(&self, other: &&'static NativeFuncData) -> bool {
match &self.repr { match &self.repr {
Repr::Native(native) => native.function == other.function, Repr::Native(native) => *native == Static(*other),
_ => false, _ => false,
} }
} }
@ -429,12 +452,30 @@ impl From<Repr> for Func {
} }
} }
impl From<&'static NativeFuncData> for Func {
fn from(data: &'static NativeFuncData) -> Self {
Repr::Native(Static(data)).into()
}
}
impl From<Element> for Func { impl From<Element> for Func {
fn from(func: Element) -> Self { fn from(func: Element) -> Self {
Repr::Element(func).into() Repr::Element(func).into()
} }
} }
impl From<Closure> for Func {
fn from(closure: Closure) -> Self {
Repr::Closure(Arc::new(LazyHash::new(closure))).into()
}
}
impl From<PluginFunc> for Func {
fn from(func: PluginFunc) -> Self {
Repr::Plugin(Arc::new(func)).into()
}
}
/// A Typst function that is defined by a native Rust type that shadows a /// A Typst function that is defined by a native Rust type that shadows a
/// native Rust function. /// native Rust function.
pub trait NativeFunc { pub trait NativeFunc {
@ -470,12 +511,6 @@ pub struct NativeFuncData {
pub returns: LazyLock<CastInfo>, pub returns: LazyLock<CastInfo>,
} }
impl From<&'static NativeFuncData> for Func {
fn from(data: &'static NativeFuncData) -> Self {
Repr::Native(Static(data)).into()
}
}
cast! { cast! {
&'static NativeFuncData, &'static NativeFuncData,
self => Func::from(self).into_value(), self => Func::from(self).into_value(),
@ -529,12 +564,6 @@ impl Closure {
} }
} }
impl From<Closure> for Func {
fn from(closure: Closure) -> Self {
Repr::Closure(Arc::new(LazyHash::new(closure))).into()
}
}
cast! { cast! {
Closure, Closure,
self => Value::Func(self.into()), self => Value::Func(self.into()),

View File

@ -1,6 +1,7 @@
use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use smallvec::SmallVec;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{ use crate::foundations::{
@ -322,7 +323,7 @@ impl i64 {
Endianness::Little => self.to_le_bytes(), Endianness::Little => self.to_le_bytes(),
}; };
let mut buf = vec![0u8; size]; let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size);
match endian { match endian {
Endianness::Big => { Endianness::Big => {
// Copy the bytes from the array to the buffer, starting from // Copy the bytes from the array to the buffer, starting from
@ -339,7 +340,7 @@ impl i64 {
} }
} }
Bytes::from(buf) Bytes::new(buf)
} }
} }

View File

@ -25,7 +25,8 @@ mod int;
mod label; mod label;
mod module; mod module;
mod none; mod none;
mod plugin; #[path = "plugin.rs"]
mod plugin_;
mod scope; mod scope;
mod selector; mod selector;
mod str; mod str;
@ -56,7 +57,7 @@ pub use self::int::*;
pub use self::label::*; pub use self::label::*;
pub use self::module::*; pub use self::module::*;
pub use self::none::*; pub use self::none::*;
pub use self::plugin::*; pub use self::plugin_::*;
pub use self::repr::Repr; pub use self::repr::Repr;
pub use self::scope::*; pub use self::scope::*;
pub use self::selector::*; pub use self::selector::*;
@ -84,16 +85,9 @@ use crate::engine::Engine;
use crate::routines::EvalMode; use crate::routines::EvalMode;
use crate::{Feature, Features}; use crate::{Feature, Features};
/// Foundational types and functions.
///
/// Here, you'll find documentation for basic data types like [integers]($int)
/// and [strings]($str) as well as details about core computational functions.
#[category]
pub static FOUNDATIONS: Category;
/// Hook up all `foundations` definitions. /// Hook up all `foundations` definitions.
pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
global.category(FOUNDATIONS); global.start_category(crate::Category::Foundations);
global.define_type::<bool>(); global.define_type::<bool>();
global.define_type::<i64>(); global.define_type::<i64>();
global.define_type::<f64>(); global.define_type::<f64>();
@ -114,16 +108,17 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
global.define_type::<Symbol>(); global.define_type::<Symbol>();
global.define_type::<Duration>(); global.define_type::<Duration>();
global.define_type::<Version>(); global.define_type::<Version>();
global.define_type::<Plugin>();
global.define_func::<repr::repr>(); global.define_func::<repr::repr>();
global.define_func::<panic>(); global.define_func::<panic>();
global.define_func::<assert>(); global.define_func::<assert>();
global.define_func::<eval>(); global.define_func::<eval>();
global.define_func::<plugin>();
if features.is_enabled(Feature::Html) { if features.is_enabled(Feature::Html) {
global.define_func::<target>(); global.define_func::<target>();
} }
global.define_module(calc::module()); global.define("calc", calc::module());
global.define_module(sys::module(inputs)); global.define("sys", sys::module(inputs));
global.reset_category();
} }
/// Fails with an error. /// Fails with an error.
@ -266,7 +261,6 @@ impl assert {
/// ``` /// ```
#[func(title = "Evaluate")] #[func(title = "Evaluate")]
pub fn eval( pub fn eval(
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// A string of Typst code to evaluate. /// A string of Typst code to evaluate.
source: Spanned<String>, source: Spanned<String>,
@ -301,7 +295,7 @@ pub fn eval(
let dict = scope; let dict = scope;
let mut scope = Scope::new(); let mut scope = Scope::new();
for (key, value) in dict { for (key, value) in dict {
scope.define_spanned(key, value, span); scope.bind(key.into(), Binding::new(value, span));
} }
(engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope)
} }

View File

@ -4,17 +4,23 @@ use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use typst_syntax::FileId; use typst_syntax::FileId;
use crate::diag::StrResult; use crate::diag::{bail, DeprecationSink, StrResult};
use crate::foundations::{repr, ty, Content, Scope, Value}; use crate::foundations::{repr, ty, Content, Scope, Value};
/// An evaluated module, either built-in or resulting from a file. /// An module of definitions.
/// ///
/// You can access definitions from the module using /// A module
/// [field access notation]($scripting/#fields) and interact with it using the /// - be built-in
/// [import and include syntaxes]($scripting/#modules). Alternatively, it is /// - stem from a [file import]($scripting/#modules)
/// possible to convert a module to a dictionary, and therefore access its /// - stem from a [package import]($scripting/#packages) (and thus indirectly
/// contents dynamically, using the /// its entrypoint file)
/// [dictionary constructor]($dictionary/#constructor). /// - result from a call to the [plugin]($plugin) function
///
/// You can access definitions from the module using [field access
/// notation]($scripting/#fields) and interact with it using the [import and
/// include syntaxes]($scripting/#modules). Alternatively, it is possible to
/// convert a module to a dictionary, and therefore access its contents
/// dynamically, using the [dictionary constructor]($dictionary/#constructor).
/// ///
/// # Example /// # Example
/// ```example /// ```example
@ -32,7 +38,7 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
#[allow(clippy::derived_hash_with_manual_eq)] #[allow(clippy::derived_hash_with_manual_eq)]
pub struct Module { pub struct Module {
/// The module's name. /// The module's name.
name: EcoString, name: Option<EcoString>,
/// The reference-counted inner fields. /// The reference-counted inner fields.
inner: Arc<Repr>, inner: Arc<Repr>,
} }
@ -52,14 +58,22 @@ impl Module {
/// Create a new module. /// Create a new module.
pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self { pub fn new(name: impl Into<EcoString>, scope: Scope) -> Self {
Self { Self {
name: name.into(), name: Some(name.into()),
inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
}
}
/// Create a new anonymous module without a name.
pub fn anonymous(scope: Scope) -> Self {
Self {
name: None,
inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }),
} }
} }
/// Update the module's name. /// Update the module's name.
pub fn with_name(mut self, name: impl Into<EcoString>) -> Self { pub fn with_name(mut self, name: impl Into<EcoString>) -> Self {
self.name = name.into(); self.name = Some(name.into());
self self
} }
@ -82,8 +96,8 @@ impl Module {
} }
/// Get the module's name. /// Get the module's name.
pub fn name(&self) -> &EcoString { pub fn name(&self) -> Option<&EcoString> {
&self.name self.name.as_ref()
} }
/// Access the module's scope. /// Access the module's scope.
@ -104,10 +118,14 @@ impl Module {
} }
/// Try to access a definition in the module. /// Try to access a definition in the module.
pub fn field(&self, name: &str) -> StrResult<&Value> { pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<&Value> {
self.scope().get(name).ok_or_else(|| { match self.scope().get(field) {
eco_format!("module `{}` does not contain `{name}`", self.name()) Some(binding) => Ok(binding.read_checked(sink)),
}) None => match &self.name {
Some(name) => bail!("module `{name}` does not contain `{field}`"),
None => bail!("module does not contain `{field}`"),
},
}
} }
/// Extract the module's content. /// Extract the module's content.
@ -131,7 +149,10 @@ impl Debug for Module {
impl repr::Repr for Module { impl repr::Repr for Module {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
eco_format!("<module {}>", self.name()) match &self.name {
Some(module) => eco_format!("<module {module}>"),
None => "<module>".into(),
}
} }
} }

View File

@ -6,7 +6,9 @@ use ecow::eco_format;
use typst_utils::Numeric; use typst_utils::Numeric;
use crate::diag::{bail, HintedStrResult, StrResult}; use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value}; use crate::foundations::{
format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value,
};
use crate::layout::{Alignment, Length, Rel}; use crate::layout::{Alignment, Length, Rel};
use crate::text::TextElem; use crate::text::TextElem;
use crate::visualize::Stroke; use crate::visualize::Stroke;
@ -30,12 +32,13 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b), (Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b), (Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b), (Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b),
(a, b) => mismatch!("cannot join {} with {}", a, b), (a, b) => mismatch!("cannot join {} with {}", a, b),
}) })
} }
@ -129,13 +132,14 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
(Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")),
(Bytes(a), Bytes(b)) => Bytes(a + b), (Bytes(a), Bytes(b)) => Bytes(a + b),
(Content(a), Content(b)) => Content(a + b), (Content(a), Content(b)) => Content(a + b),
(Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())),
(Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Content(a), Str(b)) => Content(a + TextElem::packed(b)),
(Str(a), Content(b)) => Content(TextElem::packed(a) + b), (Str(a), Content(b)) => Content(TextElem::packed(a) + b),
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b),
(Array(a), Array(b)) => Array(a + b), (Array(a), Array(b)) => Array(a + b),
(Dict(a), Dict(b)) => Dict(a + b), (Dict(a), Dict(b)) => Dict(a + b),
(Args(a), Args(b)) => Args(a + b),
(Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => {
Stroke::from_pair(color, thickness).into_value() Stroke::from_pair(color, thickness).into_value()
@ -443,7 +447,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
(Args(a), Args(b)) => a == b, (Args(a), Args(b)) => a == b,
(Type(a), Type(b)) => a == b, (Type(a), Type(b)) => a == b,
(Module(a), Module(b)) => a == b, (Module(a), Module(b)) => a == b,
(Plugin(a), Plugin(b)) => a == b,
(Datetime(a), Datetime(b)) => a == b, (Datetime(a), Datetime(b)) => a == b,
(Duration(a), Duration(b)) => a == b, (Duration(a), Duration(b)) => a == b,
(Dyn(a), Dyn(b)) => a == b, (Dyn(a), Dyn(b)) => a == b,

View File

@ -4,43 +4,27 @@ use std::sync::{Arc, Mutex};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use typst_syntax::Spanned; use typst_syntax::Spanned;
use wasmi::{AsContext, AsContextMut}; use wasmi::Memory;
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{func, repr, scope, ty, Bytes}; use crate::foundations::{cast, func, scope, Binding, Bytes, Func, Module, Scope, Value};
use crate::World; use crate::loading::{DataSource, Load};
/// A WebAssembly plugin. /// Loads a WebAssembly module.
/// ///
/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin /// The resulting [module] will contain one Typst [function] for each function
/// functions may accept multiple [byte buffers]($bytes) as arguments and return /// export of the loaded WebAssembly module.
/// a single byte buffer. They should typically be wrapped in idiomatic Typst
/// functions that perform the necessary conversions between native Typst types
/// and bytes.
/// ///
/// Plugins run in isolation from your system, which means that printing, /// Typst WebAssembly plugins need to follow a specific
/// reading files, or anything like that will not be supported for security /// [protocol]($plugin/#protocol). To run as a plugin, a program needs to be
/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit /// compiled to a 32-bit shared WebAssembly library. Plugin functions may accept
/// shared WebAssembly library. Many compilers will use the /// multiple [byte buffers]($bytes) as arguments and return a single byte
/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. /// buffer. They should typically be wrapped in idiomatic Typst functions that
/// emscripten), which allows printing, reading files, etc. This ABI will not /// perform the necessary conversions between native Typst types and bytes.
/// directly work with Typst. You will either need to compile to a different
/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub).
/// ///
/// # Plugins and Packages /// For security reasons, plugins run in isolation from your system. This means
/// Plugins are distributed as packages. A package can make use of a plugin /// that printing, reading files, or similar things are not supported.
/// simply by including a WebAssembly file and loading it. Because the
/// byte-based plugin interface is quite low-level, plugins are typically
/// exposed through wrapper functions, that also live in the same package.
///
/// # Purity
/// Plugin functions must be pure: Given the same arguments, they must always
/// return the same value. The reason for this is that Typst functions must be
/// pure (which is quite fundamental to the language design) and, since Typst
/// function can call plugin functions, this requirement is inherited. In
/// particular, if a plugin function is called twice with the same arguments,
/// Typst might cache the results and call your function only once.
/// ///
/// # Example /// # Example
/// ```example /// ```example
@ -55,6 +39,50 @@ use crate::World;
/// #concat("hello", "world") /// #concat("hello", "world")
/// ``` /// ```
/// ///
/// Since the plugin function returns a module, it can be used with import
/// syntax:
/// ```typ
/// #import plugin("hello.wasm"): concatenate
/// ```
///
/// # Purity
/// Plugin functions **must be pure:** A plugin function call most not have any
/// observable side effects on future plugin calls and given the same arguments,
/// it must always return the same value.
///
/// The reason for this is that Typst functions must be pure (which is quite
/// fundamental to the language design) and, since Typst function can call
/// plugin functions, this requirement is inherited. In particular, if a plugin
/// function is called twice with the same arguments, Typst might cache the
/// results and call your function only once. Moreover, Typst may run multiple
/// instances of your plugin in multiple threads, with no state shared between
/// them.
///
/// Typst does not enforce plugin function purity (for efficiency reasons), but
/// calling an impure function will lead to unpredictable and irreproducible
/// results and must be avoided.
///
/// That said, mutable operations _can be_ useful for plugins that require
/// costly runtime initialization. Due to the purity requirement, such
/// initialization cannot be performed through a normal function call. Instead,
/// Typst exposes a [plugin transition API]($plugin.transition), which executes
/// a function call and then creates a derived module with new functions which
/// will observe the side effects produced by the transition call. The original
/// plugin remains unaffected.
///
/// # Plugins and Packages
/// Any Typst code can make use of a plugin simply by including a WebAssembly
/// file and loading it. However, because the byte-based plugin interface is
/// quite low-level, plugins are typically exposed through a package containing
/// the plugin and idiomatic wrapper functions.
///
/// # WASI
/// Many compilers will use the [WASI ABI](https://wasi.dev/) by default or as
/// their only option (e.g. emscripten), which allows printing, reading files,
/// etc. This ABI will not directly work with Typst. You will either need to
/// compile to a different target or [stub all
/// functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub).
///
/// # Protocol /// # Protocol
/// To be used as a plugin, a WebAssembly module must conform to the following /// To be used as a plugin, a WebAssembly module must conform to the following
/// protocol: /// protocol:
@ -67,8 +95,8 @@ use crate::World;
/// lengths, so `usize/size_t` may be preferable), and return one 32-bit /// lengths, so `usize/size_t` may be preferable), and return one 32-bit
/// integer. /// integer.
/// ///
/// - The function should first allocate a buffer `buf` of length /// - The function should first allocate a buffer `buf` of length `a_1 + a_2 +
/// `a_1 + a_2 + ... + a_n`, and then call /// ... + a_n`, and then call
/// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. /// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`.
/// ///
/// - The `a_1` first bytes of the buffer now constitute the first argument, the /// - The `a_1` first bytes of the buffer now constitute the first argument, the
@ -85,19 +113,21 @@ use crate::World;
/// then interpreted as an UTF-8 encoded error message. /// then interpreted as an UTF-8 encoded error message.
/// ///
/// ## Imports /// ## Imports
/// Plugin modules need to import two functions that are provided by the runtime. /// Plugin modules need to import two functions that are provided by the
/// (Types and functions are described using WAT syntax.) /// runtime. (Types and functions are described using WAT syntax.)
/// ///
/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` /// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func
/// (param i32)))`
/// ///
/// Writes the arguments for the current function into a plugin-allocated /// Writes the arguments for the current function into a plugin-allocated
/// buffer. When a plugin function is called, it /// buffer. When a plugin function is called, it [receives the
/// [receives the lengths](#exports) of its input buffers as arguments. It /// lengths](#exports) of its input buffers as arguments. It should then
/// should then allocate a buffer whose capacity is at least the sum of these /// allocate a buffer whose capacity is at least the sum of these lengths. It
/// lengths. It should then call this function with a `ptr` to the buffer to /// should then call this function with a `ptr` to the buffer to fill it with
/// fill it with the arguments, one after another. /// the arguments, one after another.
/// ///
/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` /// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func
/// (param i32 i32)))`
/// ///
/// Sends the output of the current function to the host (Typst). The first /// Sends the output of the current function to the host (Typst). The first
/// parameter shall be a pointer to a buffer (`ptr`), while the second is the /// parameter shall be a pointer to a buffer (`ptr`), while the second is the
@ -106,75 +136,145 @@ use crate::World;
/// interpreted as an error message, it should be encoded as UTF-8. /// interpreted as an error message, it should be encoded as UTF-8.
/// ///
/// # Resources /// # Resources
/// For more resources, check out the /// For more resources, check out the [wasm-minimal-protocol
/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). /// repository](https://github.com/astrale-sharp/wasm-minimal-protocol). It
/// It contains: /// contains:
/// ///
/// - A list of example plugin implementations and a test runner for these /// - A list of example plugin implementations and a test runner for these
/// examples /// examples
/// - Wrappers to help you write your plugin in Rust (Zig wrapper in /// - Wrappers to help you write your plugin in Rust (Zig wrapper in
/// development) /// development)
/// - A stubber for WASI /// - A stubber for WASI
#[ty(scope, cast)] #[func(scope)]
#[derive(Clone)] pub fn plugin(
pub struct Plugin(Arc<Repr>); engine: &mut Engine,
/// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
/// The internal representation of a plugin. source: Spanned<DataSource>,
struct Repr { ) -> SourceResult<Module> {
/// The raw WebAssembly bytes. let data = source.load(engine.world)?;
bytes: Bytes, Plugin::module(data).at(source.span)
/// The function defined by the WebAssembly module.
functions: Vec<(EcoString, wasmi::Func)>,
/// Owns all data associated with the WebAssembly module.
store: Mutex<Store>,
}
/// Owns all data associated with the WebAssembly module.
type Store = wasmi::Store<StoreData>;
/// If there was an error reading/writing memory, keep the offset + length to
/// display an error message.
struct MemoryError {
offset: u32,
length: u32,
write: bool,
}
/// The persistent store data used for communication between store and host.
#[derive(Default)]
struct StoreData {
args: Vec<Bytes>,
output: Vec<u8>,
memory_error: Option<MemoryError>,
} }
#[scope] #[scope]
impl Plugin { impl plugin {
/// Creates a new plugin from a WebAssembly file. /// Calls a plugin function that has side effects and returns a new module
#[func(constructor)] /// with plugin functions that are guaranteed to have observed the results
pub fn construct( /// of the mutable call.
/// The engine. ///
engine: &mut Engine, /// Note that calling an impure function through a normal function call
/// Path to a WebAssembly file. /// (without use of the transition API) is forbidden and leads to
/// /// unpredictable behaviour. Read the [section on purity]($plugin/#purity)
/// For more details, see the [Paths section]($syntax/#paths). /// for more details.
path: Spanned<EcoString>, ///
) -> SourceResult<Plugin> { /// In the example below, we load the plugin `hello-mut.wasm` which exports
let Spanned { v: path, span } = path; /// two functions: The `get()` function retrieves a global array as a
let id = span.resolve_path(&path).at(span)?; /// string. The `add(value)` function adds a value to the global array.
let data = engine.world.file(id).at(span)?; ///
Plugin::new(data).at(span) /// We call `add` via the transition API. The call `mutated.get()` on the
/// derived module will observe the addition. Meanwhile the original module
/// remains untouched as demonstrated by the `base.get()` call.
///
/// _Note:_ Due to limitations in the internal WebAssembly implementation,
/// the transition API can only guarantee to reflect changes in the plugin's
/// memory, not in WebAssembly globals. If your plugin relies on changes to
/// globals being visible after transition, you might want to avoid use of
/// the transition API for now. We hope to lift this limitation in the
/// future.
///
/// ```typ
/// #let base = plugin("hello-mut.wasm")
/// #assert.eq(base.get(), "[]")
///
/// #let mutated = plugin.transition(base.add, "hello")
/// #assert.eq(base.get(), "[]")
/// #assert.eq(mutated.get(), "[hello]")
/// ```
#[func]
pub fn transition(
/// The plugin function to call.
func: PluginFunc,
/// The byte buffers to call the function with.
#[variadic]
arguments: Vec<Bytes>,
) -> StrResult<Module> {
func.transition(arguments)
} }
} }
/// A function loaded from a WebAssembly plugin.
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct PluginFunc {
/// The underlying plugin, shared by this and the other functions.
plugin: Arc<Plugin>,
/// The name of the plugin function.
name: EcoString,
}
impl PluginFunc {
/// The name of the plugin function.
pub fn name(&self) -> &str {
&self.name
}
/// Call the WebAssembly function with the given arguments.
#[comemo::memoize]
#[typst_macros::time(name = "call plugin")]
pub fn call(&self, args: Vec<Bytes>) -> StrResult<Bytes> {
self.plugin.call(&self.name, args)
}
/// Transition a plugin and turn the result into a module.
#[comemo::memoize]
#[typst_macros::time(name = "transition plugin")]
pub fn transition(&self, args: Vec<Bytes>) -> StrResult<Module> {
self.plugin.transition(&self.name, args).map(Plugin::into_module)
}
}
cast! {
PluginFunc,
self => Value::Func(self.into()),
v: Func => v.to_plugin().ok_or("expected plugin function")?.clone(),
}
/// A plugin with potentially multiple instances for multi-threaded
/// execution.
struct Plugin {
/// Shared by all variants of the plugin.
base: Arc<PluginBase>,
/// A pool of plugin instances.
///
/// When multiple plugin calls run concurrently due to multi-threading, we
/// create new instances whenever we run out of ones.
pool: Mutex<Vec<PluginInstance>>,
/// A snapshot that new instances should be restored to.
snapshot: Option<Snapshot>,
/// A combined hash that incorporates all function names and arguments used
/// in transitions of this plugin, such that this plugin has a deterministic
/// hash and equality check that can differentiate it from "siblings" (same
/// base, different transitions).
fingerprint: u128,
}
impl Plugin { impl Plugin {
/// Create a new plugin from raw WebAssembly bytes. /// Create a plugin and turn it into a module.
#[comemo::memoize] #[comemo::memoize]
#[typst_macros::time(name = "load plugin")] #[typst_macros::time(name = "load plugin")]
pub fn new(bytes: Bytes) -> StrResult<Plugin> { fn module(bytes: Bytes) -> StrResult<Module> {
Self::new(bytes).map(Self::into_module)
}
/// Create a new plugin from raw WebAssembly bytes.
fn new(bytes: Bytes) -> StrResult<Self> {
let engine = wasmi::Engine::default(); let engine = wasmi::Engine::default();
let module = wasmi::Module::new(&engine, bytes.as_slice()) let module = wasmi::Module::new(&engine, bytes.as_slice())
.map_err(|err| format!("failed to load WebAssembly module ({err})"))?; .map_err(|err| format!("failed to load WebAssembly module ({err})"))?;
// Ensure that the plugin exports its memory.
if !matches!(module.get_export("memory"), Some(wasmi::ExternType::Memory(_))) {
bail!("plugin does not export its memory");
}
let mut linker = wasmi::Linker::new(&engine); let mut linker = wasmi::Linker::new(&engine);
linker linker
.func_wrap( .func_wrap(
@ -191,58 +291,174 @@ impl Plugin {
) )
.unwrap(); .unwrap();
let mut store = Store::new(&engine, StoreData::default()); let base = Arc::new(PluginBase { bytes, linker, module });
let instance = linker let instance = PluginInstance::new(&base, None)?;
.instantiate(&mut store, &module)
Ok(Self {
base,
snapshot: None,
fingerprint: 0,
pool: Mutex::new(vec![instance]),
})
}
/// Execute a function with access to an instsance.
fn call(&self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
// Acquire an instance from the pool (potentially creating a new one).
let mut instance = self.acquire()?;
// Execute the call on an instance from the pool. If the call fails, we
// return early and _don't_ return the instance to the pool as it might
// be irrecoverably damaged.
let output = instance.call(func, args)?;
// Return the instance to the pool.
self.pool.lock().unwrap().push(instance);
Ok(output)
}
/// Call a mutable plugin function, producing a new mutable whose functions
/// are guaranteed to be able to observe the mutation.
fn transition(&self, func: &str, args: Vec<Bytes>) -> StrResult<Plugin> {
// Derive a new transition hash from the old one and the function and arguments.
let fingerprint = typst_utils::hash128(&(self.fingerprint, func, &args));
// Execute the mutable call on an instance.
let mut instance = self.acquire()?;
// Call the function. If the call fails, we return early and _don't_
// return the instance to the pool as it might be irrecoverably damaged.
instance.call(func, args)?;
// Snapshot the instance after the mutable call.
let snapshot = instance.snapshot();
// Create a new plugin and move (this is important!) the used instance
// into it, so that the old plugin won't observe the mutation. Also
// save the snapshot so that instances that are initialized for the
// transitioned plugin's pool observe the mutation.
Ok(Self {
base: self.base.clone(),
snapshot: Some(snapshot),
fingerprint,
pool: Mutex::new(vec![instance]),
})
}
/// Acquire an instance from the pool (or create a new one).
fn acquire(&self) -> StrResult<PluginInstance> {
// Don't use match to ensure that the lock is released before we create
// a new instance.
if let Some(instance) = self.pool.lock().unwrap().pop() {
return Ok(instance);
}
PluginInstance::new(&self.base, self.snapshot.as_ref())
}
/// Turn a plugin into a Typst module containing plugin functions.
fn into_module(self) -> Module {
let shared = Arc::new(self);
// Build a scope from the collected functions.
let mut scope = Scope::new();
for export in shared.base.module.exports() {
if matches!(export.ty(), wasmi::ExternType::Func(_)) {
let name = EcoString::from(export.name());
let func = PluginFunc { plugin: shared.clone(), name: name.clone() };
scope.bind(name, Binding::detached(Func::from(func)));
}
}
Module::anonymous(scope)
}
}
impl Debug for Plugin {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.pad("Plugin(..)")
}
}
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.base.bytes == other.base.bytes && self.fingerprint == other.fingerprint
}
}
impl Hash for Plugin {
fn hash<H: Hasher>(&self, state: &mut H) {
self.base.bytes.hash(state);
self.fingerprint.hash(state);
}
}
/// Shared by all pooled & transitioned variants of the plugin.
struct PluginBase {
/// The raw WebAssembly bytes.
bytes: Bytes,
/// The compiled WebAssembly module.
module: wasmi::Module,
/// A linker used to create a `Store` for execution.
linker: wasmi::Linker<CallData>,
}
/// An single plugin instance for single-threaded execution.
struct PluginInstance {
/// The underlying wasmi instance.
instance: wasmi::Instance,
/// The execution store of this concrete plugin instance.
store: wasmi::Store<CallData>,
}
/// A snapshot of a plugin instance.
struct Snapshot {
/// The number of pages in the main memory.
mem_pages: u32,
/// The data in the main memory.
mem_data: Vec<u8>,
}
impl PluginInstance {
/// Create a new execution instance of a plugin, potentially restoring
/// a snapshot.
#[typst_macros::time(name = "create plugin instance")]
fn new(base: &PluginBase, snapshot: Option<&Snapshot>) -> StrResult<PluginInstance> {
let mut store = wasmi::Store::new(base.linker.engine(), CallData::default());
let instance = base
.linker
.instantiate(&mut store, &base.module)
.and_then(|pre_instance| pre_instance.start(&mut store)) .and_then(|pre_instance| pre_instance.start(&mut store))
.map_err(|e| eco_format!("{e}"))?; .map_err(|e| eco_format!("{e}"))?;
// Ensure that the plugin exports its memory. let mut instance = PluginInstance { instance, store };
if !matches!( if let Some(snapshot) = snapshot {
instance.get_export(&store, "memory"), instance.restore(snapshot);
Some(wasmi::Extern::Memory(_))
) {
bail!("plugin does not export its memory");
} }
Ok(instance)
// Collect exported functions.
let functions = instance
.exports(&store)
.filter_map(|export| {
let name = export.name().into();
export.into_func().map(|func| (name, func))
})
.collect();
Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) })))
} }
/// Call the plugin function with the given `name`. /// Call a plugin function with byte arguments.
#[comemo::memoize] fn call(&mut self, func: &str, args: Vec<Bytes>) -> StrResult<Bytes> {
#[typst_macros::time(name = "call plugin")] let handle = self
pub fn call(&self, name: &str, args: Vec<Bytes>) -> StrResult<Bytes> { .instance
// Find the function with the given name. .get_export(&self.store, func)
let func = self .unwrap()
.0 .into_func()
.functions .unwrap();
.iter() let ty = handle.ty(&self.store);
.find(|(v, _)| v == name)
.map(|&(_, func)| func)
.ok_or_else(|| {
eco_format!("plugin does not contain a function called {name}")
})?;
let mut store = self.0.store.lock().unwrap(); // Check function signature. Do this lazily only when a function is called
let ty = func.ty(store.as_context()); // because there might be exported functions like `_initialize` that don't
// match the schema.
// Check function signature.
if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) { if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) {
bail!( bail!(
"plugin function `{name}` has a parameter that is not a 32-bit integer" "plugin function `{func}` has a parameter that is not a 32-bit integer"
); );
} }
if ty.results() != [wasmi::core::ValType::I32] { if ty.results() != [wasmi::core::ValType::I32] {
bail!("plugin function `{name}` does not return exactly one 32-bit integer"); bail!("plugin function `{func}` does not return exactly one 32-bit integer");
} }
// Check inputs. // Check inputs.
@ -263,23 +479,26 @@ impl Plugin {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Store the input data. // Store the input data.
store.data_mut().args = args; self.store.data_mut().args = args;
// Call the function. // Call the function.
let mut code = wasmi::Val::I32(-1); let mut code = wasmi::Val::I32(-1);
func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) handle
.call(&mut self.store, &lengths, std::slice::from_mut(&mut code))
.map_err(|err| eco_format!("plugin panicked: {err}"))?; .map_err(|err| eco_format!("plugin panicked: {err}"))?;
if let Some(MemoryError { offset, length, write }) = if let Some(MemoryError { offset, length, write }) =
store.data_mut().memory_error.take() self.store.data_mut().memory_error.take()
{ {
return Err(eco_format!( return Err(eco_format!(
"plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}", "plugin tried to {kind} out of bounds: \
pointer {offset:#x} is out of bounds for {kind} of length {length}",
kind = if write { "write" } else { "read" } kind = if write { "write" } else { "read" }
)); ));
} }
// Extract the returned data. // Extract the returned data.
let output = std::mem::take(&mut store.data_mut().output); let output = std::mem::take(&mut self.store.data_mut().output);
// Parse the functions return value. // Parse the functions return value.
match code { match code {
@ -293,42 +512,66 @@ impl Plugin {
_ => bail!("plugin did not respect the protocol"), _ => bail!("plugin did not respect the protocol"),
}; };
Ok(output.into()) Ok(Bytes::new(output))
} }
/// An iterator over all the function names defined by the plugin. /// Creates a snapshot of this instance from which another one can be
pub fn iter(&self) -> impl Iterator<Item = &EcoString> { /// initialized.
self.0.functions.as_slice().iter().map(|(func_name, _)| func_name) #[typst_macros::time(name = "save snapshot")]
fn snapshot(&self) -> Snapshot {
let memory = self.memory();
let mem_pages = memory.size(&self.store);
let mem_data = memory.data(&self.store).to_vec();
Snapshot { mem_pages, mem_data }
}
/// Restores the instance to a snapshot.
#[typst_macros::time(name = "restore snapshot")]
fn restore(&mut self, snapshot: &Snapshot) {
let memory = self.memory();
let current_size = memory.size(&self.store);
if current_size < snapshot.mem_pages {
memory
.grow(&mut self.store, snapshot.mem_pages - current_size)
.unwrap();
}
memory.data_mut(&mut self.store)[..snapshot.mem_data.len()]
.copy_from_slice(&snapshot.mem_data);
}
/// Retrieves a handle to the plugin's main memory.
fn memory(&self) -> Memory {
self.instance
.get_export(&self.store, "memory")
.unwrap()
.into_memory()
.unwrap()
} }
} }
impl Debug for Plugin { /// The persistent store data used for communication between store and host.
fn fmt(&self, f: &mut Formatter) -> fmt::Result { #[derive(Default)]
f.pad("Plugin(..)") struct CallData {
} /// Arguments for a current call.
args: Vec<Bytes>,
/// The results of the current call.
output: Vec<u8>,
/// A memory error that occured during execution of the current call.
memory_error: Option<MemoryError>,
} }
impl repr::Repr for Plugin { /// If there was an error reading/writing memory, keep the offset + length to
fn repr(&self) -> EcoString { /// display an error message.
"plugin(..)".into() struct MemoryError {
} offset: u32,
} length: u32,
write: bool,
impl PartialEq for Plugin {
fn eq(&self, other: &Self) -> bool {
self.0.bytes == other.0.bytes
}
}
impl Hash for Plugin {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.bytes.hash(state);
}
} }
/// Write the arguments to the plugin function into the plugin's memory. /// Write the arguments to the plugin function into the plugin's memory.
fn wasm_minimal_protocol_write_args_to_buffer( fn wasm_minimal_protocol_write_args_to_buffer(
mut caller: wasmi::Caller<StoreData>, mut caller: wasmi::Caller<CallData>,
ptr: u32, ptr: u32,
) { ) {
let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
@ -349,7 +592,7 @@ fn wasm_minimal_protocol_write_args_to_buffer(
/// Extracts the output of the plugin function from the plugin's memory. /// Extracts the output of the plugin function from the plugin's memory.
fn wasm_minimal_protocol_send_result_to_host( fn wasm_minimal_protocol_send_result_to_host(
mut caller: wasmi::Caller<StoreData>, mut caller: wasmi::Caller<CallData>,
ptr: u32, ptr: u32,
len: u32, len: u32,
) { ) {

View File

@ -1,21 +1,17 @@
#[doc(inline)]
pub use typst_macros::category;
use std::fmt::{self, Debug, Formatter}; use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use indexmap::map::Entry;
use indexmap::IndexMap; use indexmap::IndexMap;
use typst_syntax::ast::{self, AstNode};
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::Static;
use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult};
use crate::foundations::{ use crate::foundations::{
Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType,
NativeType, Type, Value, Type, Value,
}; };
use crate::Library; use crate::{Category, Library};
/// A stack of scopes. /// A stack of scopes.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -46,14 +42,14 @@ impl<'a> Scopes<'a> {
self.top = self.scopes.pop().expect("no pushed scope"); self.top = self.scopes.pop().expect("no pushed scope");
} }
/// Try to access a variable immutably. /// Try to access a binding immutably.
pub fn get(&self, var: &str) -> HintedStrResult<&Value> { pub fn get(&self, var: &str) -> HintedStrResult<&Binding> {
std::iter::once(&self.top) std::iter::once(&self.top)
.chain(self.scopes.iter().rev()) .chain(self.scopes.iter().rev())
.find_map(|scope| scope.get(var)) .find_map(|scope| scope.get(var))
.or_else(|| { .or_else(|| {
self.base.and_then(|base| match base.global.scope().get(var) { self.base.and_then(|base| match base.global.scope().get(var) {
Some(value) => Some(value), Some(binding) => Some(binding),
None if var == "std" => Some(&base.std), None if var == "std" => Some(&base.std),
None => None, None => None,
}) })
@ -61,14 +57,28 @@ impl<'a> Scopes<'a> {
.ok_or_else(|| unknown_variable(var)) .ok_or_else(|| unknown_variable(var))
} }
/// Try to access a variable immutably in math. /// Try to access a binding mutably.
pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> { pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Binding> {
std::iter::once(&mut self.top)
.chain(&mut self.scopes.iter_mut().rev())
.find_map(|scope| scope.get_mut(var))
.ok_or_else(|| {
match self.base.and_then(|base| base.global.scope().get(var)) {
Some(_) => cannot_mutate_constant(var),
_ if var == "std" => cannot_mutate_constant(var),
_ => unknown_variable(var),
}
})
}
/// Try to access a binding immutably in math.
pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Binding> {
std::iter::once(&self.top) std::iter::once(&self.top)
.chain(self.scopes.iter().rev()) .chain(self.scopes.iter().rev())
.find_map(|scope| scope.get(var)) .find_map(|scope| scope.get(var))
.or_else(|| { .or_else(|| {
self.base.and_then(|base| match base.math.scope().get(var) { self.base.and_then(|base| match base.math.scope().get(var) {
Some(value) => Some(value), Some(binding) => Some(binding),
None if var == "std" => Some(&base.std), None if var == "std" => Some(&base.std),
None => None, None => None,
}) })
@ -81,20 +91,6 @@ impl<'a> Scopes<'a> {
}) })
} }
/// Try to access a variable mutably.
pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Value> {
std::iter::once(&mut self.top)
.chain(&mut self.scopes.iter_mut().rev())
.find_map(|scope| scope.get_mut(var))
.ok_or_else(|| {
match self.base.and_then(|base| base.global.scope().get(var)) {
Some(_) => cannot_mutate_constant(var),
_ if var == "std" => cannot_mutate_constant(var),
_ => unknown_variable(var),
}
})?
}
/// Check if an std variable is shadowed. /// Check if an std variable is shadowed.
pub fn check_std_shadowed(&self, var: &str) -> bool { pub fn check_std_shadowed(&self, var: &str) -> bool {
self.base.is_some_and(|base| base.global.scope().get(var).is_some()) self.base.is_some_and(|base| base.global.scope().get(var).is_some())
@ -104,63 +100,15 @@ impl<'a> Scopes<'a> {
} }
} }
#[cold]
fn cannot_mutate_constant(var: &str) -> HintedString {
eco_format!("cannot mutate a constant: {}", var).into()
}
/// The error message when a variable is not found.
#[cold]
fn unknown_variable(var: &str) -> HintedString {
let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
if var.contains('-') {
res.hint(eco_format!(
"if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`",
if var.matches('-').count() > 1 { "s" } else { "" },
var.replace('-', " - ")
));
}
res
}
#[cold]
fn unknown_variable_math(var: &str, in_global: bool) -> HintedString {
let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
if matches!(var, "none" | "auto" | "false" | "true") {
res.hint(eco_format!(
"if you meant to use a literal, try adding a hash before it: `#{var}`",
));
} else if in_global {
res.hint(eco_format!(
"`{var}` is not available directly in math, try adding a hash before it: `#{var}`",
));
} else {
res.hint(eco_format!(
"if you meant to display multiple letters as is, try adding spaces between each letter: `{}`",
var.chars()
.flat_map(|c| [' ', c])
.skip(1)
.collect::<EcoString>()
));
res.hint(eco_format!(
"or if you meant to display this as text, try placing it in quotes: `\"{var}\"`"
));
}
res
}
/// A map from binding names to values. /// A map from binding names to values.
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct Scope { pub struct Scope {
map: IndexMap<EcoString, Slot>, map: IndexMap<EcoString, Binding>,
deduplicate: bool, deduplicate: bool,
category: Option<Category>, category: Option<Category>,
} }
/// Scope construction.
impl Scope { impl Scope {
/// Create a new empty scope. /// Create a new empty scope.
pub fn new() -> Self { pub fn new() -> Self {
@ -173,7 +121,7 @@ impl Scope {
} }
/// Enter a new category. /// Enter a new category.
pub fn category(&mut self, category: Category) { pub fn start_category(&mut self, category: Category) {
self.category = Some(category); self.category = Some(category);
} }
@ -182,107 +130,87 @@ impl Scope {
self.category = None; self.category = None;
} }
/// Bind a value to a name.
#[track_caller]
pub fn define(&mut self, name: impl Into<EcoString>, value: impl IntoValue) {
self.define_spanned(name, value, Span::detached())
}
/// Bind a value to a name defined by an identifier.
#[track_caller]
pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) {
self.define_spanned(ident.get().clone(), value, ident.span())
}
/// Bind a value to a name.
#[track_caller]
pub fn define_spanned(
&mut self,
name: impl Into<EcoString>,
value: impl IntoValue,
span: Span,
) {
let name = name.into();
#[cfg(debug_assertions)]
if self.deduplicate && self.map.contains_key(&name) {
panic!("duplicate definition: {name}");
}
self.map.insert(
name,
Slot::new(value.into_value(), span, Kind::Normal, self.category),
);
}
/// Define a captured, immutable binding.
pub fn define_captured(
&mut self,
name: EcoString,
value: Value,
capturer: Capturer,
span: Span,
) {
self.map.insert(
name,
Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category),
);
}
/// Define a native function through a Rust type that shadows the function. /// Define a native function through a Rust type that shadows the function.
pub fn define_func<T: NativeFunc>(&mut self) { #[track_caller]
pub fn define_func<T: NativeFunc>(&mut self) -> &mut Binding {
let data = T::data(); let data = T::data();
self.define(data.name, Func::from(data)); self.define(data.name, Func::from(data))
} }
/// Define a native function with raw function data. /// Define a native function with raw function data.
pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { #[track_caller]
self.define(data.name, Func::from(data)); pub fn define_func_with_data(
&mut self,
data: &'static NativeFuncData,
) -> &mut Binding {
self.define(data.name, Func::from(data))
} }
/// Define a native type. /// Define a native type.
pub fn define_type<T: NativeType>(&mut self) { #[track_caller]
pub fn define_type<T: NativeType>(&mut self) -> &mut Binding {
let data = T::data(); let data = T::data();
self.define(data.name, Type::from(data)); self.define(data.name, Type::from(data))
} }
/// Define a native element. /// Define a native element.
pub fn define_elem<T: NativeElement>(&mut self) { #[track_caller]
pub fn define_elem<T: NativeElement>(&mut self) -> &mut Binding {
let data = T::data(); let data = T::data();
self.define(data.name, Element::from(data)); self.define(data.name, Element::from(data))
} }
/// Define a module. /// Define a built-in with compile-time known name and returns a mutable
pub fn define_module(&mut self, module: Module) { /// reference to it.
self.define(module.name().clone(), module); ///
/// When the name isn't compile-time known, you should instead use:
/// - `Vm::bind` if you already have [`Binding`]
/// - `Vm::define` if you only have a [`Value`]
/// - [`Scope::bind`](Self::bind) if you are not operating in the context of
/// a `Vm` or if you are binding to something that is not an AST
/// identifier (e.g. when constructing a dynamic
/// [`Module`](super::Module))
#[track_caller]
pub fn define(&mut self, name: &'static str, value: impl IntoValue) -> &mut Binding {
#[cfg(debug_assertions)]
if self.deduplicate && self.map.contains_key(name) {
panic!("duplicate definition: {name}");
}
let mut binding = Binding::detached(value);
binding.category = self.category;
self.bind(name.into(), binding)
}
}
/// Scope manipulation and access.
impl Scope {
/// Inserts a binding into this scope and returns a mutable reference to it.
///
/// Prefer `Vm::bind` if you are operating in the context of a `Vm`.
pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding {
match self.map.entry(name) {
Entry::Occupied(mut entry) => {
entry.insert(binding);
entry.into_mut()
}
Entry::Vacant(entry) => entry.insert(binding),
}
} }
/// Try to access a variable immutably. /// Try to access a binding immutably.
pub fn get(&self, var: &str) -> Option<&Value> { pub fn get(&self, var: &str) -> Option<&Binding> {
self.map.get(var).map(Slot::read) self.map.get(var)
} }
/// Try to access a variable mutably. /// Try to access a binding mutably.
pub fn get_mut(&mut self, var: &str) -> Option<HintedStrResult<&mut Value>> { pub fn get_mut(&mut self, var: &str) -> Option<&mut Binding> {
self.map self.map.get_mut(var)
.get_mut(var)
.map(Slot::write)
.map(|res| res.map_err(HintedString::from))
}
/// Get the span of a definition.
pub fn get_span(&self, var: &str) -> Option<Span> {
Some(self.map.get(var)?.span)
}
/// Get the category of a definition.
pub fn get_category(&self, var: &str) -> Option<Category> {
self.map.get(var)?.category
} }
/// Iterate over all definitions. /// Iterate over all definitions.
pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Value, Span)> { pub fn iter(&self) -> impl Iterator<Item = (&EcoString, &Binding)> {
self.map.iter().map(|(k, v)| (k, v.read(), v.span)) self.map.iter()
} }
} }
@ -315,28 +243,111 @@ pub trait NativeScope {
fn scope() -> Scope; fn scope() -> Scope;
} }
/// A slot where a value is stored. /// A bound value with metadata.
#[derive(Clone, Hash)] #[derive(Debug, Clone, Hash)]
struct Slot { pub struct Binding {
/// The stored value. /// The bound value.
value: Value, value: Value,
/// The kind of slot, determines how the value can be accessed. /// The kind of binding, determines how the value can be accessed.
kind: Kind, kind: BindingKind,
/// A span associated with the stored value. /// A span associated with the binding.
span: Span, span: Span,
/// The category of the slot. /// The category of the binding.
category: Option<Category>, category: Option<Category>,
/// A deprecation message for the definition.
deprecation: Option<&'static str>,
} }
/// The different kinds of slots. /// The different kinds of slots.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum Kind { enum BindingKind {
/// A normal, mutable binding. /// A normal, mutable binding.
Normal, Normal,
/// A captured copy of another variable. /// A captured copy of another variable.
Captured(Capturer), Captured(Capturer),
} }
impl Binding {
/// Create a new binding with a span marking its definition site.
pub fn new(value: impl IntoValue, span: Span) -> Self {
Self {
value: value.into_value(),
span,
kind: BindingKind::Normal,
category: None,
deprecation: None,
}
}
/// Create a binding without a span.
pub fn detached(value: impl IntoValue) -> Self {
Self::new(value, Span::detached())
}
/// Marks this binding as deprecated, with the given `message`.
pub fn deprecated(&mut self, message: &'static str) -> &mut Self {
self.deprecation = Some(message);
self
}
/// Read the value.
pub fn read(&self) -> &Value {
&self.value
}
/// Read the value, checking for deprecation.
///
/// As the `sink`
/// - pass `()` to ignore the message.
/// - pass `(&mut engine, span)` to emit a warning into the engine.
pub fn read_checked(&self, sink: impl DeprecationSink) -> &Value {
if let Some(message) = self.deprecation {
sink.emit(message);
}
&self.value
}
/// Try to write to the value.
///
/// This fails if the value is a read-only closure capture.
pub fn write(&mut self) -> StrResult<&mut Value> {
match self.kind {
BindingKind::Normal => Ok(&mut self.value),
BindingKind::Captured(capturer) => bail!(
"variables from outside the {} are \
read-only and cannot be modified",
match capturer {
Capturer::Function => "function",
Capturer::Context => "context expression",
}
),
}
}
/// Create a copy of the binding for closure capturing.
pub fn capture(&self, capturer: Capturer) -> Self {
Self {
kind: BindingKind::Captured(capturer),
..self.clone()
}
}
/// A span associated with the stored value.
pub fn span(&self) -> Span {
self.span
}
/// A deprecation message for the value, if any.
pub fn deprecation(&self) -> Option<&'static str> {
self.deprecation
}
/// The category of the value, if any.
pub fn category(&self) -> Option<Category> {
self.category
}
}
/// What the variable was captured by. /// What the variable was captured by.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Capturer { pub enum Capturer {
@ -346,71 +357,56 @@ pub enum Capturer {
Context, Context,
} }
impl Slot { /// The error message when trying to mutate a variable from the standard
/// Create a new slot. /// library.
fn new(value: Value, span: Span, kind: Kind, category: Option<Category>) -> Self { #[cold]
Self { value, span, kind, category } fn cannot_mutate_constant(var: &str) -> HintedString {
} eco_format!("cannot mutate a constant: {}", var).into()
/// Read the value.
fn read(&self) -> &Value {
&self.value
}
/// Try to write to the value.
fn write(&mut self) -> StrResult<&mut Value> {
match self.kind {
Kind::Normal => Ok(&mut self.value),
Kind::Captured(capturer) => {
bail!(
"variables from outside the {} are \
read-only and cannot be modified",
match capturer {
Capturer::Function => "function",
Capturer::Context => "context expression",
}
)
}
}
}
} }
/// A group of related definitions. /// The error message when a variable wasn't found.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[cold]
pub struct Category(Static<CategoryData>); fn unknown_variable(var: &str) -> HintedString {
let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
impl Category { if var.contains('-') {
/// Create a new category from raw data. res.hint(eco_format!(
pub const fn from_data(data: &'static CategoryData) -> Self { "if you meant to use subtraction, \
Self(Static(data)) try adding spaces around the minus sign{}: `{}`",
if var.matches('-').count() > 1 { "s" } else { "" },
var.replace('-', " - ")
));
} }
/// The category's name. res
pub fn name(&self) -> &'static str {
self.0.name
}
/// The type's title case name, for use in documentation (e.g. `String`).
pub fn title(&self) -> &'static str {
self.0.title
}
/// Documentation for the category.
pub fn docs(&self) -> &'static str {
self.0.docs
}
} }
impl Debug for Category { /// The error message when a variable wasn't found it math.
fn fmt(&self, f: &mut Formatter) -> fmt::Result { #[cold]
write!(f, "Category({})", self.name()) fn unknown_variable_math(var: &str, in_global: bool) -> HintedString {
} let mut res = HintedString::new(eco_format!("unknown variable: {}", var));
}
/// Defines a category. if matches!(var, "none" | "auto" | "false" | "true") {
#[derive(Debug)] res.hint(eco_format!(
pub struct CategoryData { "if you meant to use a literal, \
pub name: &'static str, try adding a hash before it: `#{var}`",
pub title: &'static str, ));
pub docs: &'static str, } else if in_global {
res.hint(eco_format!(
"`{var}` is not available directly in math, \
try adding a hash before it: `#{var}`",
));
} else {
res.hint(eco_format!(
"if you meant to display multiple letters as is, \
try adding spaces between each letter: `{}`",
var.chars().flat_map(|c| [' ', c]).skip(1).collect::<EcoString>()
));
res.hint(eco_format!(
"or if you meant to display this as text, \
try placing it in quotes: `\"{var}\"`"
));
}
res
} }

View File

@ -7,12 +7,13 @@ use comemo::Tracked;
use ecow::EcoString; use ecow::EcoString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func, cast, dict, func, repr, scope, ty, Array, Bytes, Cast, Context, Decimal, Dict, Func,
IntoValue, Label, Repr, Type, Value, Version, IntoValue, Label, Repr, Type, Value, Version,
}; };
use crate::layout::Alignment; use crate::layout::Alignment;
@ -286,6 +287,30 @@ impl Str {
Ok(c.into()) Ok(c.into())
} }
/// Normalizes the string to the given Unicode normal form.
///
/// This is useful when manipulating strings containing Unicode combining
/// characters.
///
/// ```typ
/// #assert.eq("é".normalize(form: "nfd"), "e\u{0301}")
/// #assert.eq("ſ́".normalize(form: "nfkc"), "ś")
/// ```
#[func]
pub fn normalize(
&self,
#[named]
#[default(UnicodeNormalForm::Nfc)]
form: UnicodeNormalForm,
) -> Str {
match form {
UnicodeNormalForm::Nfc => self.nfc().collect(),
UnicodeNormalForm::Nfd => self.nfd().collect(),
UnicodeNormalForm::Nfkc => self.nfkc().collect(),
UnicodeNormalForm::Nfkd => self.nfkd().collect(),
}
}
/// Whether the string contains the specified pattern. /// Whether the string contains the specified pattern.
/// ///
/// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}`
@ -425,9 +450,7 @@ impl Str {
#[func] #[func]
pub fn replace( pub fn replace(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The pattern to search for. /// The pattern to search for.
pattern: StrPattern, pattern: StrPattern,
@ -575,6 +598,12 @@ impl Str {
/// Splits a string at matches of a specified pattern and returns an array /// Splits a string at matches of a specified pattern and returns an array
/// of the resulting parts. /// of the resulting parts.
///
/// When the empty string is used as a separator, it separates every
/// character (i.e., Unicode code point) in the string, along with the
/// beginning and end of the string. In practice, this means that the
/// resulting list of parts will contain the empty string at the start
/// and end of the list.
#[func] #[func]
pub fn split( pub fn split(
&self, &self,
@ -778,16 +807,31 @@ cast! {
v: f64 => Self::Str(repr::display_float(v).into()), v: f64 => Self::Str(repr::display_float(v).into()),
v: Decimal => Self::Str(format_str!("{}", v)), v: Decimal => Self::Str(format_str!("{}", v)),
v: Version => Self::Str(format_str!("{}", v)), v: Version => Self::Str(format_str!("{}", v)),
v: Bytes => Self::Str( v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?),
std::str::from_utf8(&v)
.map_err(|_| "bytes are not valid utf-8")?
.into()
),
v: Label => Self::Str(v.resolve().as_str().into()), v: Label => Self::Str(v.resolve().as_str().into()),
v: Type => Self::Str(v.long_name().into()), v: Type => Self::Str(v.long_name().into()),
v: Str => Self::Str(v), v: Str => Self::Str(v),
} }
/// A Unicode normalization form.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum UnicodeNormalForm {
/// Canonical composition where e.g. accented letters are turned into a
/// single Unicode codepoint.
#[string("nfc")]
Nfc,
/// Canonical decomposition where e.g. accented letters are split into a
/// separate base and diacritic.
#[string("nfd")]
Nfd,
/// Like NFC, but using the Unicode compatibility decompositions.
#[string("nfkc")]
Nfkc,
/// Like NFD, but using the Unicode compatibility decompositions.
#[string("nfkd")]
Nfkd,
}
/// Convert an item of std's `match_indices` to a dictionary. /// Convert an item of std's `match_indices` to a dictionary.
fn match_to_dict((start, text): (usize, &str)) -> Dict { fn match_to_dict((start, text): (usize, &str)) -> Dict {
dict! { dict! {

View File

@ -12,7 +12,8 @@ use typst_utils::LazyHash;
use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector, cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr,
Selector,
}; };
use crate::text::{FontFamily, FontList, TextElem}; use crate::text::{FontFamily, FontList, TextElem};
@ -470,7 +471,8 @@ impl Debug for Recipe {
selector.fmt(f)?; selector.fmt(f)?;
f.write_str(", ")?; f.write_str(", ")?;
} }
self.transform.fmt(f) self.transform.fmt(f)?;
f.write_str(")")
} }
} }
@ -775,107 +777,6 @@ impl<'a> Iterator for Links<'a> {
} }
} }
/// A sequence of elements with associated styles.
#[derive(Clone, PartialEq, Hash)]
pub struct StyleVec {
/// The elements themselves.
elements: EcoVec<Content>,
/// A run-length encoded list of style lists.
///
/// Each element is a (styles, count) pair. Any elements whose
/// style falls after the end of this list is considered to
/// have an empty style list.
styles: EcoVec<(Styles, usize)>,
}
impl StyleVec {
/// Create a style vector from an unstyled vector content.
pub fn wrap(elements: EcoVec<Content>) -> Self {
Self { elements, styles: EcoVec::new() }
}
/// Create a `StyleVec` from a list of content with style chains.
pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) {
let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default();
let depth = trunk.links().count();
let mut elements = EcoVec::with_capacity(buf.len());
let mut styles = EcoVec::<(Styles, usize)>::new();
let mut last: Option<(StyleChain<'a>, usize)> = None;
for &(element, chain) in buf {
elements.push(element.clone());
if let Some((prev, run)) = &mut last {
if chain == *prev {
*run += 1;
} else {
styles.push((prev.suffix(depth), *run));
last = Some((chain, 1));
}
} else {
last = Some((chain, 1));
}
}
if let Some((last, run)) = last {
let skippable = styles.is_empty() && last == trunk;
if !skippable {
styles.push((last.suffix(depth), run));
}
}
(StyleVec { elements, styles }, trunk)
}
/// Whether there are no elements.
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
/// The number of elements.
pub fn len(&self) -> usize {
self.elements.len()
}
/// Iterate over the contained content and style chains.
pub fn iter<'a>(
&'a self,
outer: &'a StyleChain<'_>,
) -> impl Iterator<Item = (&'a Content, StyleChain<'a>)> {
static EMPTY: Styles = Styles::new();
self.elements
.iter()
.zip(
self.styles
.iter()
.flat_map(|(local, count)| std::iter::repeat(local).take(*count))
.chain(std::iter::repeat(&EMPTY)),
)
.map(|(element, local)| (element, outer.chain(local)))
}
/// Get a style property, but only if it is the same for all children of the
/// style vector.
pub fn shared_get<T: PartialEq>(
&self,
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
self.styles
.iter()
.all(|(local, _)| getter(styles.chain(local)) == value)
.then_some(value)
}
}
impl Debug for StyleVec {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_list().entries(&self.elements).finish()
}
}
/// A property that is resolved with other properties from the style chain. /// A property that is resolved with other properties from the style chain.
pub trait Resolve { pub trait Resolve {
/// The type of the resolved output. /// The type of the resolved output.
@ -939,6 +840,13 @@ impl<T, const N: usize> Fold for SmallVec<[T; N]> {
} }
} }
impl<T> Fold for OneOrMultiple<T> {
fn fold(self, mut outer: Self) -> Self {
outer.0.extend(self.0);
outer
}
}
/// A variant of fold for foldable optional (`Option<T>`) values where an inner /// A variant of fold for foldable optional (`Option<T>`) values where an inner
/// `None` value isn't respected (contrary to `Option`'s usual `Fold` /// `None` value isn't respected (contrary to `Option`'s usual `Fold`
/// implementation, with which folding with an inner `None` always returns /// implementation, with which folding with an inner `None` always returns

View File

@ -1,14 +1,18 @@
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::BTreeSet; use std::collections::{BTreeSet, HashMap};
use std::fmt::{self, Debug, Display, Formatter, Write}; use std::fmt::{self, Debug, Display, Formatter, Write};
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use typst_syntax::{is_ident, Span, Spanned}; use typst_syntax::{is_ident, Span, Spanned};
use typst_utils::hash128;
use crate::diag::{bail, SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _}; use crate::foundations::{
cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
PlainText, Repr as _,
};
/// A Unicode symbol. /// A Unicode symbol.
/// ///
@ -17,6 +21,7 @@ use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as
/// be accessed using [field access notation]($scripting/#fields): /// be accessed using [field access notation]($scripting/#fields):
/// ///
/// - General symbols are defined in the [`sym` module]($category/symbols/sym) /// - General symbols are defined in the [`sym` module]($category/symbols/sym)
/// and are accessible without the `sym.` prefix in math mode.
/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) /// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
/// ///
/// Moreover, you can define custom symbols with this type's constructor /// Moreover, you can define custom symbols with this type's constructor
@ -186,7 +191,6 @@ impl Symbol {
/// ``` /// ```
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The callsite span.
span: Span, span: Span,
/// The variants of the symbol. /// The variants of the symbol.
/// ///
@ -198,24 +202,62 @@ impl Symbol {
#[variadic] #[variadic]
variants: Vec<Spanned<SymbolVariant>>, variants: Vec<Spanned<SymbolVariant>>,
) -> SourceResult<Symbol> { ) -> SourceResult<Symbol> {
let mut list = Vec::new();
if variants.is_empty() { if variants.is_empty() {
bail!(span, "expected at least one variant"); bail!(span, "expected at least one variant");
} }
for Spanned { v, span } in variants {
if list.iter().any(|(prev, _)| &v.0 == prev) { // Maps from canonicalized 128-bit hashes to indices of variants we've
bail!(span, "duplicate variant"); // seen before.
} let mut seen = HashMap::<u128, usize>::new();
// A list of modifiers, cleared & reused in each iteration.
let mut modifiers = Vec::new();
// Validate the variants.
for (i, &Spanned { ref v, span }) in variants.iter().enumerate() {
modifiers.clear();
if !v.0.is_empty() { if !v.0.is_empty() {
// Collect all modifiers.
for modifier in v.0.split('.') { for modifier in v.0.split('.') {
if !is_ident(modifier) { if !is_ident(modifier) {
bail!(span, "invalid symbol modifier: {}", modifier.repr()); bail!(span, "invalid symbol modifier: {}", modifier.repr());
} }
modifiers.push(modifier);
} }
} }
list.push((v.0, v.1));
// Canonicalize the modifier order.
modifiers.sort();
// Ensure that there are no duplicate modifiers.
if let Some(ms) = modifiers.windows(2).find(|ms| ms[0] == ms[1]) {
bail!(
span, "duplicate modifier within variant: {}", ms[0].repr();
hint: "modifiers are not ordered, so each one may appear only once"
)
}
// Check whether we had this set of modifiers before.
let hash = hash128(&modifiers);
if let Some(&i) = seen.get(&hash) {
if v.0.is_empty() {
bail!(span, "duplicate default variant");
} else if v.0 == variants[i].v.0 {
bail!(span, "duplicate variant: {}", v.0.repr());
} else {
bail!(
span, "duplicate variant: {}", v.0.repr();
hint: "variants with the same modifiers are identical, regardless of their order"
)
}
}
seen.insert(hash, i);
} }
Ok(Symbol::runtime(list.into_boxed_slice()))
let list = variants.into_iter().map(|s| (s.v.0, s.v.1)).collect();
Ok(Symbol::runtime(list))
} }
} }
@ -369,7 +411,7 @@ fn find<'a>(
} }
let score = (matching, Reverse(total)); let score = (matching, Reverse(total));
if best_score.map_or(true, |b| score > b) { if best_score.is_none_or(|b| score > b) {
best = Some(candidate.1); best = Some(candidate.1);
best_score = Some(score); best_score = Some(score);
} }
@ -387,3 +429,31 @@ fn parts(modifiers: &str) -> impl Iterator<Item = &str> {
fn contained(modifiers: &str, m: &str) -> bool { fn contained(modifiers: &str, m: &str) -> bool {
parts(modifiers).any(|part| part == m) parts(modifiers).any(|part| part == m)
} }
/// A single character.
#[elem(Repr, PlainText)]
pub struct SymbolElem {
/// The symbol's character.
#[required]
pub text: char, // This is called `text` for consistency with `TextElem`.
}
impl SymbolElem {
/// Create a new packed symbol element.
pub fn packed(text: impl Into<char>) -> Content {
Self::new(text.into()).pack()
}
}
impl PlainText for Packed<SymbolElem> {
fn plain_text(&self, text: &mut EcoString) {
text.push(self.text);
}
}
impl crate::foundations::Repr for SymbolElem {
/// Use a custom repr that matches normal content.
fn repr(&self) -> EcoString {
eco_format!("[{}]", self.text)
}
}

View File

@ -3,7 +3,7 @@ use comemo::Tracked;
use crate::diag::HintedStrResult; use crate::diag::HintedStrResult;
use crate::foundations::{elem, func, Cast, Context}; use crate::foundations::{elem, func, Cast, Context};
/// The compilation target. /// The export target.
#[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)] #[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)]
pub enum Target { pub enum Target {
/// The target that is used for paged, fully laid-out content. /// The target that is used for paged, fully laid-out content.
@ -28,11 +28,50 @@ pub struct TargetElem {
pub target: Target, pub target: Target,
} }
/// Returns the current compilation target. /// Returns the current export target.
///
/// This function returns either
/// - `{"paged"}` (for PDF, PNG, and SVG export), or
/// - `{"html"}` (for HTML export).
///
/// The design of this function is not yet finalized and for this reason it is
/// guarded behind the `html` feature. Visit the [HTML documentation
/// page]($html) for more details.
///
/// # When to use it
/// This function allows you to format your document properly across both HTML
/// and paged export targets. It should primarily be used in templates and show
/// rules, rather than directly in content. This way, the document's contents
/// can be fully agnostic to the export target and content can be shared between
/// PDF and HTML export.
///
/// # Varying targets
/// This function is [contextual]($context) as the target can vary within a
/// single compilation: When exporting to HTML, the target will be `{"paged"}`
/// while within an [`html.frame`].
///
/// # Example
/// ```example
/// #let kbd(it) = context {
/// if target() == "html" {
/// html.elem("kbd", it)
/// } else {
/// set text(fill: rgb("#1f2328"))
/// let r = 3pt
/// box(
/// fill: rgb("#f6f8fa"),
/// stroke: rgb("#d1d9e0b3"),
/// outset: (y: r),
/// inset: (x: r),
/// radius: r,
/// raw(it)
/// )
/// }
/// }
///
/// Press #kbd("F1") for help.
/// ```
#[func(contextual)] #[func(contextual)]
pub fn target( pub fn target(context: Tracked<Context>) -> HintedStrResult<Target> {
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Target> {
Ok(TargetElem::target_in(context.styles()?)) Ok(TargetElem::target_in(context.styles()?))
} }

View File

@ -8,7 +8,7 @@ use std::sync::LazyLock;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use typst_utils::Static; use typst_utils::Static;
use crate::diag::StrResult; use crate::diag::{bail, DeprecationSink, StrResult};
use crate::foundations::{ use crate::foundations::{
cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value,
}; };
@ -94,10 +94,15 @@ impl Type {
} }
/// Get a field from this type's scope, if possible. /// Get a field from this type's scope, if possible.
pub fn field(&self, field: &str) -> StrResult<&'static Value> { pub fn field(
self.scope() &self,
.get(field) field: &str,
.ok_or_else(|| eco_format!("type {self} does not contain field `{field}`")) sink: impl DeprecationSink,
) -> StrResult<&'static Value> {
match self.scope().get(field) {
Some(binding) => Ok(binding.read_checked(sink)),
None => bail!("type {self} does not contain field `{field}`"),
}
} }
} }
@ -136,7 +141,7 @@ impl Repr for Type {
} else if *self == Type::of::<NoneValue>() { } else if *self == Type::of::<NoneValue>() {
"type(none)" "type(none)"
} else { } else {
self.long_name() self.short_name()
} }
.into() .into()
} }

View File

@ -11,12 +11,12 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
use typst_syntax::{ast, Span}; use typst_syntax::{ast, Span};
use typst_utils::ArcExt; use typst_utils::ArcExt;
use crate::diag::{HintedStrResult, HintedString, StrResult}; use crate::diag::{DeprecationSink, HintedStrResult, HintedString, StrResult};
use crate::foundations::{ use crate::foundations::{
fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime,
Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module,
NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, NativeElement, NativeType, NoneValue, Reflect, Repr, Resolve, Scope, Str, Styles,
Styles, Symbol, Type, Version, Symbol, SymbolElem, Type, Version,
}; };
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
use crate::text::{RawContent, RawElem, TextElem}; use crate::text::{RawContent, RawElem, TextElem};
@ -84,8 +84,6 @@ pub enum Value {
Type(Type), Type(Type),
/// A module. /// A module.
Module(Module), Module(Module),
/// A WebAssembly plugin.
Plugin(Plugin),
/// A dynamic value. /// A dynamic value.
Dyn(Dynamic), Dyn(Dynamic),
} }
@ -147,7 +145,6 @@ impl Value {
Self::Args(_) => Type::of::<Args>(), Self::Args(_) => Type::of::<Args>(),
Self::Type(_) => Type::of::<Type>(), Self::Type(_) => Type::of::<Type>(),
Self::Module(_) => Type::of::<Module>(), Self::Module(_) => Type::of::<Module>(),
Self::Plugin(_) => Type::of::<Plugin>(),
Self::Dyn(v) => v.ty(), Self::Dyn(v) => v.ty(),
} }
} }
@ -158,15 +155,15 @@ impl Value {
} }
/// Try to access a field on the value. /// Try to access a field on the value.
pub fn field(&self, field: &str) -> StrResult<Value> { pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<Value> {
match self { match self {
Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol),
Self::Version(version) => version.component(field).map(Self::Int), Self::Version(version) => version.component(field).map(Self::Int),
Self::Dict(dict) => dict.get(field).cloned(), Self::Dict(dict) => dict.get(field).cloned(),
Self::Content(content) => content.field_by_name(field), Self::Content(content) => content.field_by_name(field),
Self::Type(ty) => ty.field(field).cloned(), Self::Type(ty) => ty.field(field, sink).cloned(),
Self::Func(func) => func.field(field).cloned(), Self::Func(func) => func.field(field, sink).cloned(),
Self::Module(module) => module.field(field).cloned(), Self::Module(module) => module.field(field, sink).cloned(),
_ => fields::field(self, field), _ => fields::field(self, field),
} }
} }
@ -181,16 +178,6 @@ impl Value {
} }
} }
/// The name, if this is a function, type, or module.
pub fn name(&self) -> Option<&str> {
match self {
Self::Func(func) => func.name(),
Self::Type(ty) => Some(ty.short_name()),
Self::Module(module) => Some(module.name()),
_ => None,
}
}
/// Try to extract documentation for the value. /// Try to extract documentation for the value.
pub fn docs(&self) -> Option<&'static str> { pub fn docs(&self) -> Option<&'static str> {
match self { match self {
@ -209,7 +196,7 @@ impl Value {
Self::Decimal(v) => TextElem::packed(eco_format!("{v}")), Self::Decimal(v) => TextElem::packed(eco_format!("{v}")),
Self::Str(v) => TextElem::packed(v), Self::Str(v) => TextElem::packed(v),
Self::Version(v) => TextElem::packed(eco_format!("{v}")), Self::Version(v) => TextElem::packed(eco_format!("{v}")),
Self::Symbol(v) => TextElem::packed(v.get()), Self::Symbol(v) => SymbolElem::packed(v.get()),
Self::Content(v) => v, Self::Content(v) => v,
Self::Module(module) => module.content(), Self::Module(module) => module.content(),
_ => RawElem::new(RawContent::Text(self.repr())) _ => RawElem::new(RawContent::Text(self.repr()))
@ -261,7 +248,6 @@ impl Debug for Value {
Self::Args(v) => Debug::fmt(v, f), Self::Args(v) => Debug::fmt(v, f),
Self::Type(v) => Debug::fmt(v, f), Self::Type(v) => Debug::fmt(v, f),
Self::Module(v) => Debug::fmt(v, f), Self::Module(v) => Debug::fmt(v, f),
Self::Plugin(v) => Debug::fmt(v, f),
Self::Dyn(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f),
} }
} }
@ -299,7 +285,6 @@ impl Repr for Value {
Self::Args(v) => v.repr(), Self::Args(v) => v.repr(),
Self::Type(v) => v.repr(), Self::Type(v) => v.repr(),
Self::Module(v) => v.repr(), Self::Module(v) => v.repr(),
Self::Plugin(v) => v.repr(),
Self::Dyn(v) => v.repr(), Self::Dyn(v) => v.repr(),
} }
} }
@ -350,7 +335,6 @@ impl Hash for Value {
Self::Args(v) => v.hash(state), Self::Args(v) => v.hash(state),
Self::Type(v) => v.hash(state), Self::Type(v) => v.hash(state),
Self::Module(v) => v.hash(state), Self::Module(v) => v.hash(state),
Self::Plugin(v) => v.hash(state),
Self::Dyn(v) => v.hash(state), Self::Dyn(v) => v.hash(state),
} }
} }
@ -459,15 +443,15 @@ impl<'de> Visitor<'de> for ValueVisitor {
} }
fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> { fn visit_bytes<E: Error>(self, v: &[u8]) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value()) Ok(Bytes::new(v.to_vec()).into_value())
} }
fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> { fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value()) Ok(Bytes::new(v.to_vec()).into_value())
} }
fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> { fn visit_byte_buf<E: Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
Ok(Bytes::from(v).into_value()) Ok(Bytes::new(v).into_value())
} }
fn visit_none<E: Error>(self) -> Result<Self::Value, E> { fn visit_none<E: Error>(self) -> Result<Self::Value, E> {
@ -656,7 +640,7 @@ primitive! { Duration: "duration", Duration }
primitive! { Content: "content", primitive! { Content: "content",
Content, Content,
None => Content::empty(), None => Content::empty(),
Symbol(v) => TextElem::packed(v.get()), Symbol(v) => SymbolElem::packed(v.get()),
Str(v) => TextElem::packed(v) Str(v) => TextElem::packed(v)
} }
primitive! { Styles: "styles", Styles } primitive! { Styles: "styles", Styles }
@ -671,7 +655,6 @@ primitive! {
primitive! { Args: "arguments", Args } primitive! { Args: "arguments", Args }
primitive! { Type: "type", Type } primitive! { Type: "type", Type }
primitive! { Module: "module", Module } primitive! { Module: "module", Module }
primitive! { Plugin: "plugin", Plugin }
impl<T: Reflect> Reflect for Arc<T> { impl<T: Reflect> Reflect for Arc<T> {
fn input() -> CastInfo { fn input() -> CastInfo {
@ -730,6 +713,11 @@ mod tests {
assert_eq!(value.into_value().repr(), exp); assert_eq!(value.into_value().repr(), exp);
} }
#[test]
fn test_value_size() {
assert!(std::mem::size_of::<Value>() <= 32);
}
#[test] #[test]
fn test_value_debug() { fn test_value_debug() {
// Primitives. // Primitives.

View File

@ -210,7 +210,10 @@ impl HtmlAttr {
/// Creates a compile-time constant `HtmlAttr`. /// Creates a compile-time constant `HtmlAttr`.
/// ///
/// Should only be used in const contexts because it can panic. /// Must only be used in const contexts (in a constant definition or
/// explicit `const { .. }` block) because otherwise a panic for a malformed
/// attribute or not auto-internible constant will only be caught at
/// runtime.
#[track_caller] #[track_caller]
pub const fn constant(string: &'static str) -> Self { pub const fn constant(string: &'static str) -> Self {
if string.is_empty() { if string.is_empty() {
@ -472,17 +475,55 @@ pub mod tag {
wbr wbr
} }
/// Whether this is a void tag whose associated element may not have a
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
tag,
self::area
| self::base
| self::br
| self::col
| self::embed
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
)
}
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
/// Whether an element is considered metadata.
pub fn is_metadata(tag: HtmlTag) -> bool {
matches!(
tag,
self::base
| self::link
| self::meta
| self::noscript
| self::script
| self::style
| self::template
| self::title
)
}
/// Whether nodes with the tag have the CSS property `display: block` by /// Whether nodes with the tag have the CSS property `display: block` by
/// default. /// default.
///
/// If this is true, then pretty-printing can insert spaces around such
/// nodes and around the contents of such nodes.
///
/// However, when users change the properties of such tags via CSS, the
/// insertion of whitespace may actually impact the visual output; for
/// example, <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how
/// adding CSS rules to `<p>` can make it sensitive to whitespace. In such
/// cases, users should disable pretty-printing.
pub fn is_block_by_default(tag: HtmlTag) -> bool { pub fn is_block_by_default(tag: HtmlTag) -> bool {
matches!( matches!(
tag, tag,
@ -569,42 +610,29 @@ pub mod tag {
) )
} }
/// Whether this is a void tag whose associated element may not have a /// Whether nodes with the tag have the CSS property `display: table(-.*)?`
/// children. /// by default.
pub fn is_void(tag: HtmlTag) -> bool { pub fn is_tabular_by_default(tag: HtmlTag) -> bool {
matches!( matches!(
tag, tag,
self::area self::table
| self::base | self::thead
| self::br | self::tbody
| self::tfoot
| self::tr
| self::th
| self::td
| self::caption
| self::col | self::col
| self::embed | self::colgroup
| self::hr
| self::img
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
) )
} }
/// Whether this is a tag containing raw text.
pub fn is_raw(tag: HtmlTag) -> bool {
matches!(tag, self::script | self::style)
}
/// Whether this is a tag containing escapable raw text.
pub fn is_escapable_raw(tag: HtmlTag) -> bool {
matches!(tag, self::textarea | self::title)
}
} }
/// Predefined constants for HTML attributes. /// Predefined constants for HTML attributes.
/// ///
/// Note: These are very incomplete. /// Note: These are very incomplete.
#[allow(non_upper_case_globals)]
pub mod attr { pub mod attr {
use super::HtmlAttr; use super::HtmlAttr;
@ -619,13 +647,18 @@ pub mod attr {
attrs! { attrs! {
charset charset
cite
colspan
content content
href href
name name
value reversed
role role
rowspan
start
style
value
} }
#[allow(non_upper_case_globals)]
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
} }

View File

@ -6,53 +6,77 @@ pub use self::dom::*;
use ecow::EcoString; use ecow::EcoString;
use crate::foundations::{category, elem, Category, Content, Module, Scope}; use crate::foundations::{elem, Content, Module, Scope};
/// HTML output.
#[category]
pub static HTML: Category;
/// Create a module with all HTML definitions. /// Create a module with all HTML definitions.
pub fn module() -> Module { pub fn module() -> Module {
let mut html = Scope::deduplicating(); let mut html = Scope::deduplicating();
html.category(HTML); html.start_category(crate::Category::Html);
html.define_elem::<HtmlElem>(); html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>(); html.define_elem::<FrameElem>();
Module::new("html", html) Module::new("html", html)
} }
/// A HTML element that can contain Typst content. /// An HTML element that can contain Typst content.
///
/// Typst's HTML export automatically generates the appropriate tags for most
/// elements. However, sometimes, it is desirable to retain more control. For
/// example, when using Typst to generate your blog, you could use this function
/// to wrap each article in an `<article>` tag.
///
/// Typst is aware of what is valid HTML. A tag and its attributes must form
/// syntactically valid HTML. Some tags, like `meta` do not accept content.
/// Hence, you must not provide a body for them. We may add more checks in the
/// future, so be sure that you are generating valid HTML when using this
/// function.
///
/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If
/// you instead create them with this function, Typst will omit its own tags.
///
/// ```typ
/// #html.elem("div", attrs: (style: "background: aqua"))[
/// A div with _Typst content_ inside!
/// ]
/// ```
#[elem(name = "elem")] #[elem(name = "elem")]
pub struct HtmlElem { pub struct HtmlElem {
/// The element's tag. /// The element's tag.
#[required] #[required]
pub tag: HtmlTag, pub tag: HtmlTag,
/// The element's attributes. /// The element's HTML attributes.
#[borrowed] #[borrowed]
pub attrs: HtmlAttrs, pub attrs: HtmlAttrs,
/// The contents of the HTML element. /// The contents of the HTML element.
///
/// The body can be arbitrary Typst content.
#[positional] #[positional]
#[borrowed] #[borrowed]
pub body: Option<Content>, pub body: Option<Content>,
} }
impl HtmlElem { impl HtmlElem {
/// Add an atribute to the element. /// Add an attribute to the element.
pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self { pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into<EcoString>) -> Self {
self.attrs.get_or_insert_with(Default::default).push(attr, value); self.attrs.get_or_insert_with(Default::default).push(attr, value);
self self
} }
} }
/// An element that forces its contents to be laid out. /// An element that lays out its content as an inline SVG.
/// ///
/// Integrates content that requires layout (e.g. a plot) into HTML output /// Sometimes, converting Typst content to HTML is not desirable. This can be
/// by turning it into an inline SVG. /// the case for plots and other content that relies on positioning and styling
/// to convey its message.
///
/// This function allows you to use the Typst layout engine that would also be
/// used for PDF, SVG, and PNG export to render a part of your document exactly
/// how it would appear when exported in one of these formats. It embeds the
/// content as an inline SVG.
#[elem] #[elem]
pub struct FrameElem { pub struct FrameElem {
/// The contents that shall be laid out. /// The content that shall be laid out.
#[positional] #[positional]
#[required] #[required]
pub body: Content, pub body: Content,

View File

@ -428,11 +428,8 @@ impl Counter {
#[func(contextual)] #[func(contextual)]
pub fn get( pub fn get(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The callsite span.
span: Span, span: Span,
) -> SourceResult<CounterState> { ) -> SourceResult<CounterState> {
let loc = context.location().at(span)?; let loc = context.location().at(span)?;
@ -444,11 +441,8 @@ impl Counter {
#[func(contextual)] #[func(contextual)]
pub fn display( pub fn display(
self, self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The call span of the display.
span: Span, span: Span,
/// A [numbering pattern or a function]($numbering), which specifies how /// A [numbering pattern or a function]($numbering), which specifies how
/// to display the counter. If given a function, that function receives /// to display the counter. If given a function, that function receives
@ -482,11 +476,8 @@ impl Counter {
#[func(contextual)] #[func(contextual)]
pub fn at( pub fn at(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The callsite span.
span: Span, span: Span,
/// The place at which the counter's value should be retrieved. /// The place at which the counter's value should be retrieved.
selector: LocatableSelector, selector: LocatableSelector,
@ -500,11 +491,8 @@ impl Counter {
#[func(contextual)] #[func(contextual)]
pub fn final_( pub fn final_(
&self, &self,
/// The engine.
engine: &mut Engine, engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>, context: Tracked<Context>,
/// The callsite span.
span: Span, span: Span,
) -> SourceResult<CounterState> { ) -> SourceResult<CounterState> {
context.introspect().at(span)?; context.introspect().at(span)?;
@ -528,7 +516,6 @@ impl Counter {
#[func] #[func]
pub fn step( pub fn step(
self, self,
/// The call span of the update.
span: Span, span: Span,
/// The depth at which to step the counter. Defaults to `{1}`. /// The depth at which to step the counter. Defaults to `{1}`.
#[named] #[named]
@ -545,7 +532,6 @@ impl Counter {
#[func] #[func]
pub fn update( pub fn update(
self, self,
/// The call span of the update.
span: Span, span: Span,
/// If given an integer or array of integers, sets the counter to that /// If given an integer or array of integers, sets the counter to that
/// value. If given a function, that function receives the previous /// value. If given a function, that function receives the previous
@ -800,7 +786,7 @@ impl ManualPageCounter {
let Some(elem) = elem.to_packed::<CounterUpdateElem>() else { let Some(elem) = elem.to_packed::<CounterUpdateElem>() else {
continue; continue;
}; };
if *elem.key() == CounterKey::Page { if elem.key == CounterKey::Page {
let mut state = CounterState(smallvec![self.logical]); let mut state = CounterState(smallvec![self.logical]);
state.update(engine, elem.update.clone())?; state.update(engine, elem.update.clone())?;
self.logical = state.first(); self.logical = state.first();

View File

@ -44,9 +44,6 @@ use crate::introspection::Location;
/// ``` /// ```
/// Refer to the [`selector`] type for more details on before/after selectors. /// Refer to the [`selector`] type for more details on before/after selectors.
#[func(contextual)] #[func(contextual)]
pub fn here( pub fn here(context: Tracked<Context>) -> HintedStrResult<Location> {
/// The callsite context.
context: Tracked<Context>,
) -> HintedStrResult<Location> {
context.location() context.location()
} }

Some files were not shown because too many files have changed in this diff Show More