diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 730bf75f83..1b4f768718 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,6 +4,7 @@ on: [push, pull_request, merge_group]
env:
RUSTFLAGS: "-Dwarnings"
RUSTDOCFLAGS: "-Dwarnings"
+ TYPST_TESTS_EXTENDED: true
jobs:
# This allows us to have one branch protection rule for the full test matrix.
@@ -29,8 +30,9 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@stable
+ - uses: dtolnay/rust-toolchain@1.77.0
- uses: Swatinem/rust-cache@v2
+ - run: cargo test --workspace --no-run
- run: cargo test --workspace --no-fail-fast
checks:
@@ -38,7 +40,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@stable
+ - uses: dtolnay/rust-toolchain@1.77.0
+ with:
+ components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- run: cargo clippy --workspace --all-targets --all-features
- run: cargo fmt --check --all
@@ -62,5 +66,5 @@ jobs:
with:
toolchain: nightly-2023-09-13
- uses: Swatinem/rust-cache@v2
- - run: cargo install cargo-fuzz
+ - run: cargo install --locked cargo-fuzz@0.12.0
- run: cd tests/fuzz && cargo fuzz build --dev
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index c62ad56dbd..4e855064e5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -37,7 +37,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@stable
+ - uses: dtolnay/rust-toolchain@1.77.0
with:
target: ${{ matrix.target }}
diff --git a/.gitignore b/.gitignore
index 9a5deded35..f9daa3d7ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,11 +6,8 @@ desktop.ini
.DS_Store
# Tests and benchmarks
-tests/png
-tests/pdf
-tests/svg
-tests/target
-tests/typ/**/*.pdf
+tests/store
+tests/suite/**/*.pdf
tests/fuzz/target
tests/fuzz/corpus
tests/fuzz/artifacts
@@ -23,6 +20,7 @@ tarpaulin-report.html
# Node
node_modules
+tools/test-helper/dist
package-lock.json
# Nix
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0faf6436bd..006daff274 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,7 +22,7 @@ Typst日本語ドキュメント翻訳プロジェクトにご興味をお持ち
2. `./docs`内のMarkdownファイル群は、Typstのチュートリアルや入門ガイドなど、言語リファレンス以外のページの本体です。**既存のMarkdownファイルを直接書き換えて翻訳してください**。
それに加えて、`./docs/src/lib.rs`ファイルの[`urlify`関数](https://github.com/search?q=repo%3Atypst-jp/typst-jp.github.io%20urlify&type=code)を編集して、中国語版の記事タイトルを日本語版のものに書き換えてください。このプロセスを抜かすと、WebページのURLが正しく生成されません。
3. 「サードパーティパッケージ」のページの翻訳を追加する場合は、`./static/assets/index2ja.json`も編集する必要があります。
-3. 翻訳の際は、[後述のガイドライン](#スタイルマニュアル)を参照し、[v0.11.0時点での公式ドキュメント](https://github.com/typst/typst/tree/v0.11.0/docs)から翻訳してください。
+3. 翻訳の際は、[後述のガイドライン](#スタイルマニュアル)を参照し、[v0.11.1時点での公式ドキュメント](https://github.com/typst/typst/tree/v0.11.1/docs)から翻訳してください。
4. 翻訳作業の途中でも、Draft Pull Requestを作成して、翻訳の進捗状況を共有することができます。
5. 翻訳作業が終わったら、Pull Requestを作成し、送信してください。
diff --git a/Cargo.lock b/Cargo.lock
index 5e8e6a6d54..6bc69aa103 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -288,9 +288,9 @@ dependencies = [
[[package]]
name = "citationberg"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82108f2b676c954076d2e5044f19a6a03887b24bd42804f322e0650d13035899"
+checksum = "d259fe9fd78ffa05a119581d20fddb50bfba428311057b12741ffb9015123d0b"
dependencies = [
"quick-xml",
"serde",
@@ -827,9 +827,9 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hayagriva"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc2e670de5191df083ddd112cd253049f8213277ccf0c15e18a8bf10e6c666cc"
+checksum = "1d0d20c98b77b86ce737876b2a1653e2e6abbeee84afbb39d72111091191c97a"
dependencies = [
"biblatex",
"ciborium",
@@ -2177,6 +2177,12 @@ dependencies = [
"unsafe-libyaml",
]
+[[package]]
+name = "shell-escape"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f"
+
[[package]]
name = "simd-adler32"
version = "0.3.7"
@@ -2587,7 +2593,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typst"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"az",
"bitflags 2.4.2",
@@ -2648,13 +2654,13 @@ dependencies = [
[[package]]
name = "typst-assets"
-version = "0.11.0"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f13f85360328da54847dd7fefaf272dfa5b6d1fdeb53f32938924c39bf5b2c6c"
+checksum = "2b3061f8d268e8eec7481c9ab24540455cb4912983c49aae38fa6e8bf8ef4d9c"
[[package]]
name = "typst-cli"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"chrono",
"clap",
@@ -2682,6 +2688,7 @@ dependencies = [
"serde",
"serde_json",
"serde_yaml 0.9.32",
+ "shell-escape",
"tar",
"tempfile",
"toml",
@@ -2699,12 +2706,12 @@ dependencies = [
[[package]]
name = "typst-dev-assets"
-version = "0.11.0"
-source = "git+https://github.com/typst/typst-dev-assets?tag=v0.11.0#e0ef7ad46f28a440c41bc8e78563ace86cc02678"
+version = "0.11.1"
+source = "git+https://github.com/typst/typst-dev-assets?tag=v0.11.1#35caed3a870d46e827cffaa9dc450e38bede2a37"
[[package]]
name = "typst-docs"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"clap",
"comemo",
@@ -2730,7 +2737,7 @@ dependencies = [
[[package]]
name = "typst-fuzz"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"comemo",
"libfuzzer-sys",
@@ -2742,20 +2749,23 @@ dependencies = [
[[package]]
name = "typst-ide"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"comemo",
"ecow",
"if_chain",
"log",
+ "once_cell",
"serde",
"typst",
+ "typst-assets",
+ "typst-dev-assets",
"unscanny",
]
[[package]]
name = "typst-macros"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"heck",
"proc-macro2",
@@ -2765,7 +2775,7 @@ dependencies = [
[[package]]
name = "typst-pdf"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"base64 0.22.0",
"bytemuck",
@@ -2789,7 +2799,7 @@ dependencies = [
[[package]]
name = "typst-render"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"bytemuck",
"comemo",
@@ -2808,7 +2818,7 @@ dependencies = [
[[package]]
name = "typst-svg"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"base64 0.22.0",
"comemo",
@@ -2824,7 +2834,7 @@ dependencies = [
[[package]]
name = "typst-syntax"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"comemo",
"ecow",
@@ -2839,20 +2849,20 @@ dependencies = [
[[package]]
name = "typst-tests"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"clap",
"comemo",
"ecow",
"once_cell",
"oxipng",
+ "parking_lot",
"rayon",
"tiny-skia",
"ttf-parser",
"typst",
"typst-assets",
"typst-dev-assets",
- "typst-ide",
"typst-pdf",
"typst-render",
"typst-svg",
@@ -2862,7 +2872,7 @@ dependencies = [
[[package]]
name = "typst-timing"
-version = "0.11.0"
+version = "0.11.1"
dependencies = [
"parking_lot",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 6510678136..dee38c0712 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ default-members = ["crates/typst-cli"]
resolver = "2"
[workspace.package]
-version = "0.11.0"
+version = "0.11.1"
rust-version = "1.74" # also change in ci.yml
authors = ["The Typst Project Developers"]
edition = "2021"
@@ -16,17 +16,17 @@ keywords = ["typst"]
readme = "README.md"
[workspace.dependencies]
-typst = { path = "crates/typst", version = "0.11.0" }
-typst-cli = { path = "crates/typst-cli", version = "0.11.0" }
-typst-ide = { path = "crates/typst-ide", version = "0.11.0" }
-typst-macros = { path = "crates/typst-macros", version = "0.11.0" }
-typst-pdf = { path = "crates/typst-pdf", version = "0.11.0" }
-typst-render = { path = "crates/typst-render", version = "0.11.0" }
-typst-svg = { path = "crates/typst-svg", version = "0.11.0" }
-typst-syntax = { path = "crates/typst-syntax", version = "0.11.0" }
-typst-timing = { path = "crates/typst-timing", version = "0.11.0" }
-typst-assets = "0.11.0"
-typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.11.0" }
+typst = { path = "crates/typst", version = "0.11.1" }
+typst-cli = { path = "crates/typst-cli", version = "0.11.1" }
+typst-ide = { path = "crates/typst-ide", version = "0.11.1" }
+typst-macros = { path = "crates/typst-macros", version = "0.11.1" }
+typst-pdf = { path = "crates/typst-pdf", version = "0.11.1" }
+typst-render = { path = "crates/typst-render", version = "0.11.1" }
+typst-svg = { path = "crates/typst-svg", version = "0.11.1" }
+typst-syntax = { path = "crates/typst-syntax", version = "0.11.1" }
+typst-timing = { path = "crates/typst-timing", version = "0.11.1" }
+typst-assets = "0.11.1"
+typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", tag = "v0.11.1" }
az = "1.2"
base64 = "0.22"
bitflags = { version = "2", features = ["serde"] }
@@ -47,7 +47,7 @@ env_proxy = "0.4"
flate2 = "1"
fontdb = { version = "0.16", default-features = false }
fs_extra = "1.3"
-hayagriva = "0.5.2"
+hayagriva = "0.5.3"
heck = "0.4"
hypher = "0.1.4"
icu_properties = { version = "1.4", features = ["serde"] }
@@ -94,6 +94,7 @@ semver = "1"
serde = { version = "1.0.184", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
+shell-escape = "0.1.5"
siphasher = "1"
smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] }
stacker = "0.1.15"
diff --git a/README.en.md b/README.en.md
index 9f62c97e14..0528fc99bd 100644
--- a/README.en.md
+++ b/README.en.md
@@ -5,7 +5,7 @@
This is an unofficial Japanese translation of the documentation for the typesetting system [Typst](https://typst.app/docs). It has been created with the permission of [Typst GmbH](https://typst.app/legal/).
-The repository was forked from the [Chinese version](https://github.com/typst-doc-cn/typst-doc-cn.github.io) and is translated into Japanese based on the official documentation of [Typst v0.11.0](https://typst.app/docs/changelog/#v0.11.0) as of June 2024.
+The repository was forked from the [Chinese version](https://github.com/typst-doc-cn/typst-doc-cn.github.io) and is translated into Japanese based on the official documentation of [Typst v0.11.1](https://typst.app/docs/changelog/#v0.11.1) as of June 2024.
The actual working web version can be viewed at the following URL.
> https://typst-jp.github.io/docs/
diff --git a/README.md b/README.md
index 1c3213e4f4..a4a79909f3 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
組版システム [Typst](https://typst.app/docs) の非公式な日本語ドキュメントです。[Typst GmbH](https://typst.app/legal/) の許諾を得て作成されています。
-このリポジトリは[中国語版](https://github.com/typst-doc-cn/typst-doc-cn.github.io)からフォークして作成され、2024年6月時点での最新版である [Typst v0.11.0](https://typst.app/docs/changelog/#v0.11.0) の公式ドキュメントを元に日本語訳を行います。
+このリポジトリは[中国語版](https://github.com/typst-doc-cn/typst-doc-cn.github.io)からフォークして作成され、2024年6月時点での最新版である [Typst v0.11.1](https://typst.app/docs/changelog/#v0.11.1) の公式ドキュメントを元に日本語訳を行います。
実際に作動している Web 版は、以下の URL から閲覧できます。
> https://typst-jp.github.io/docs/
diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml
index 57251a64a0..c2b23df069 100644
--- a/crates/typst-cli/Cargo.toml
+++ b/crates/typst-cli/Cargo.toml
@@ -15,9 +15,6 @@ readme = { workspace = true }
[[bin]]
name = "typst"
path = "src/main.rs"
-test = false
-doctest = false
-bench = false
doc = false
[dependencies]
@@ -51,6 +48,7 @@ semver = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
+shell-escape = { workspace = true }
tar = { workspace = true }
tempfile = { workspace = true }
toml = { workspace = true }
diff --git a/crates/typst-cli/src/init.rs b/crates/typst-cli/src/init.rs
index 01fdb02f0c..b0446bd1ea 100644
--- a/crates/typst-cli/src/init.rs
+++ b/crates/typst-cli/src/init.rs
@@ -104,11 +104,19 @@ fn print_summary(
out.set_color(&gray)?;
write!(out, "> ")?;
out.reset()?;
- writeln!(out, "cd {}", project_dir.display())?;
+ writeln!(
+ out,
+ "cd {}",
+ shell_escape::escape(project_dir.display().to_string().into()),
+ )?;
out.set_color(&gray)?;
write!(out, "> ")?;
out.reset()?;
- writeln!(out, "typst watch {}", template.entrypoint)?;
+ writeln!(
+ out,
+ "typst watch {}",
+ shell_escape::escape(template.entrypoint.to_string().into()),
+ )?;
writeln!(out)?;
Ok(())
}
diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs
index da0a57fd55..c8bd391442 100644
--- a/crates/typst-cli/src/main.rs
+++ b/crates/typst-cli/src/main.rs
@@ -26,7 +26,7 @@ use crate::timings::Timer;
thread_local! {
/// The CLI's exit code.
- static EXIT: Cell = Cell::new(ExitCode::SUCCESS);
+ static EXIT: Cell = const { Cell::new(ExitCode::SUCCESS) };
}
/// The parsed commandline arguments.
diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs
index 4e0bcd54ab..711fd02582 100644
--- a/crates/typst-cli/src/world.rs
+++ b/crates/typst-cli/src/world.rs
@@ -226,7 +226,7 @@ struct FileSlot {
}
impl FileSlot {
- /// Create a new path slot.
+ /// Create a new file slot.
fn new(id: FileId) -> Self {
Self { id, file: SlotCell::new(), source: SlotCell::new() }
}
diff --git a/crates/typst-ide/Cargo.toml b/crates/typst-ide/Cargo.toml
index b292756222..01f7a10620 100644
--- a/crates/typst-ide/Cargo.toml
+++ b/crates/typst-ide/Cargo.toml
@@ -12,11 +12,6 @@ categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
-[lib]
-test = false
-doctest = false
-bench = false
-
[dependencies]
typst = { workspace = true }
comemo = { workspace = true }
@@ -26,5 +21,10 @@ log = { workspace = true }
serde = { workspace = true }
unscanny = { workspace = true }
+[dev-dependencies]
+typst-assets = { workspace = true }
+typst-dev-assets = { workspace = true }
+once_cell = { workspace = true }
+
[lints]
workspace = true
diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs
index 9d67b322d7..47214481db 100644
--- a/crates/typst-ide/src/analyze.rs
+++ b/crates/typst-ide/src/analyze.rs
@@ -50,6 +50,9 @@ pub fn analyze_expr(
/// Try to load a module from the current source file.
pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option {
+ // Use span in the node for resolving imports with relative paths.
+ let source_span = source.span();
+
let (source, _) = analyze_expr(world, source).into_iter().next()?;
if source.scope().is_some() {
return Some(source);
@@ -73,7 +76,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option {
Scopes::new(Some(world.library())),
Span::detached(),
);
- typst::eval::import(&mut vm, source, Span::detached(), true)
+ typst::eval::import(&mut vm, source, source_span, true)
.ok()
.map(Value::Module)
}
diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs
index 4e4b89182c..24cd747515 100644
--- a/crates/typst-ide/src/complete.rs
+++ b/crates/typst-ide/src/complete.rs
@@ -1053,7 +1053,7 @@ impl<'a> CompletionContext<'a> {
/// A small window of context before the cursor.
fn before_window(&self, size: usize) -> &str {
- &self.before[self.cursor.saturating_sub(size)..]
+ Scanner::new(self.before).from(self.cursor.saturating_sub(size))
}
/// Add a prefix and suffix to all applications.
@@ -1403,3 +1403,42 @@ impl<'a> CompletionContext<'a> {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use typst::eval::Tracer;
+
+ use super::autocomplete;
+ use crate::tests::TestWorld;
+
+ #[track_caller]
+ fn test(text: &str, cursor: usize, contains: &[&str], excludes: &[&str]) {
+ let world = TestWorld::new(text);
+ let doc = typst::compile(&world, &mut Tracer::new()).ok();
+ let (_, completions) =
+ autocomplete(&world, doc.as_ref(), &world.main, cursor, true)
+ .unwrap_or_default();
+
+ let labels: Vec<_> = completions.iter().map(|c| c.label.as_str()).collect();
+ for item in contains {
+ assert!(labels.contains(item), "{item:?} was not contained in {labels:?}");
+ }
+ for item in excludes {
+ assert!(!labels.contains(item), "{item:?} was not excluded in {labels:?}");
+ }
+ }
+
+ #[test]
+ fn test_autocomplete() {
+ test("#i", 2, &["int", "if conditional"], &["foo"]);
+ test("#().", 4, &["insert", "remove", "len", "all"], &["foo"]);
+ }
+
+ #[test]
+ fn test_before_window_char_boundary() {
+ // Check that the `before_window` doesn't slice into invalid byte
+ // boundaries.
+ let s = "😀😀 #text(font: \"\")";
+ test(s, s.len() - 2, &[], &[]);
+ }
+}
diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs
index bbfd56d950..3967aaad47 100644
--- a/crates/typst-ide/src/lib.rs
+++ b/crates/typst-ide/src/lib.rs
@@ -90,3 +90,88 @@ fn summarize_font_family<'a>(variants: impl Iterator- ) -> Ec
detail
}
+
+#[cfg(test)]
+mod tests {
+ use comemo::Prehashed;
+ use once_cell::sync::Lazy;
+ use typst::diag::{FileError, FileResult};
+ use typst::foundations::{Bytes, Datetime};
+ use typst::syntax::{FileId, Source};
+ use typst::text::{Font, FontBook};
+ use typst::{Library, World};
+
+ /// A world for IDE testing.
+ pub struct TestWorld {
+ pub main: Source,
+ base: &'static TestBase,
+ }
+
+ impl TestWorld {
+ /// Create a new world for a single test.
+ ///
+ /// This is cheap because the shared base for all test runs is lazily
+ /// initialized just once.
+ pub fn new(text: &str) -> Self {
+ static BASE: Lazy
= Lazy::new(TestBase::default);
+ let main = Source::detached(text);
+ Self { main, base: &*BASE }
+ }
+ }
+
+ impl World for TestWorld {
+ fn library(&self) -> &Prehashed {
+ &self.base.library
+ }
+
+ fn book(&self) -> &Prehashed {
+ &self.base.book
+ }
+
+ fn main(&self) -> Source {
+ self.main.clone()
+ }
+
+ fn source(&self, id: FileId) -> FileResult {
+ if id == self.main.id() {
+ Ok(self.main.clone())
+ } else {
+ Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
+ }
+ }
+
+ fn file(&self, id: FileId) -> FileResult {
+ Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
+ }
+
+ fn font(&self, index: usize) -> Option {
+ Some(self.base.fonts[index].clone())
+ }
+
+ fn today(&self, _: Option) -> Option {
+ None
+ }
+ }
+
+ /// Shared foundation of all test worlds.
+ struct TestBase {
+ library: Prehashed,
+ book: Prehashed,
+ fonts: Vec,
+ }
+
+ impl Default for TestBase {
+ fn default() -> Self {
+ let fonts: Vec<_> = typst_assets::fonts()
+ .chain(typst_dev_assets::fonts())
+ .flat_map(|data| Font::iter(Bytes::from_static(data)))
+ .collect();
+
+ Self {
+ library: Prehashed::new(Library::default()),
+ book: Prehashed::new(FontBook::from_fonts(&fonts)),
+ fonts,
+ }
+ }
+ }
+}
diff --git a/crates/typst-macros/Cargo.toml b/crates/typst-macros/Cargo.toml
index caef7eb4b3..b6b496cb37 100644
--- a/crates/typst-macros/Cargo.toml
+++ b/crates/typst-macros/Cargo.toml
@@ -14,9 +14,6 @@ readme = { workspace = true }
[lib]
proc-macro = true
-test = false
-doctest = false
-bench = false
[dependencies]
heck = { workspace = true }
diff --git a/crates/typst-macros/src/category.rs b/crates/typst-macros/src/category.rs
index ac8c813df6..26ec879ccb 100644
--- a/crates/typst-macros/src/category.rs
+++ b/crates/typst-macros/src/category.rs
@@ -33,6 +33,7 @@ pub fn category(_: TokenStream, item: syn::Item) -> Result {
}
/// Parse a bare `pub static CATEGORY: Category;` item.
+#[allow(dead_code)]
pub struct BareStatic {
pub attrs: Vec,
pub vis: Visibility,
diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs
index 35c48b4a3d..58d963863e 100644
--- a/crates/typst-macros/src/lib.rs
+++ b/crates/typst-macros/src/lib.rs
@@ -136,7 +136,7 @@ pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
///
/// This implements `NativeElement` for the given type.
///
-/// ```
+/// ```ignore
/// /// A section heading.
/// #[elem(Show, Count)]
/// struct HeadingElem {
diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs
index bfe222855d..b6acc7d81b 100644
--- a/crates/typst-macros/src/util.rs
+++ b/crates/typst-macros/src/util.rs
@@ -232,6 +232,7 @@ impl Parse for BlockWithReturn {
}
/// Parse a bare `type Name;` item.
+#[allow(dead_code)]
pub struct BareType {
pub attrs: Vec,
pub type_token: Token![type],
diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml
index 709ed10880..99c52dc6ac 100644
--- a/crates/typst-pdf/Cargo.toml
+++ b/crates/typst-pdf/Cargo.toml
@@ -12,10 +12,6 @@ categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
-[lib]
-doctest = false
-bench = false
-
[dependencies]
typst = { workspace = true }
typst-assets = { workspace = true }
diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs
index 49ec0d860e..e8b1c30a16 100644
--- a/crates/typst-pdf/src/lib.rs
+++ b/crates/typst-pdf/src/lib.rs
@@ -9,7 +9,6 @@ mod outline;
mod page;
mod pattern;
-use std::cmp::Eq;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::hash::Hash;
use std::sync::Arc;
diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs
index 0342e9fe68..42358db516 100644
--- a/crates/typst-pdf/src/page.rs
+++ b/crates/typst-pdf/src/page.rs
@@ -229,17 +229,24 @@ fn write_page(ctx: &mut PdfContext, i: usize, resources_ref: Ref) {
/// Write the page labels.
pub(crate) fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)> {
+ // If there is no page labeled, we skip the writing
+ if !ctx.pages.iter().any(|p| {
+ p.label
+ .as_ref()
+ .is_some_and(|l| l.prefix.is_some() || l.style.is_some())
+ }) {
+ return Vec::new();
+ }
+
let mut result = vec![];
+ let empty_label = PdfPageLabel::default();
let mut prev: Option<&PdfPageLabel> = None;
for (i, page) in ctx.pages.iter().enumerate() {
let nr = NonZeroUsize::new(1 + i).unwrap();
- let Some(label) = &page.label else { continue };
-
- // Don't create a label if neither style nor prefix are specified.
- if label.prefix.is_none() && label.style.is_none() {
- continue;
- }
+ // If there are pages with empty labels between labeled pages, we must
+ // write empty PageLabel entries.
+ let label = page.label.as_ref().unwrap_or(&empty_label);
if let Some(pre) = prev {
if label.prefix == pre.prefix
@@ -316,9 +323,7 @@ impl PdfPageLabel {
return None;
};
- let Some((prefix, kind, case)) = pat.pieces.first() else {
- return None;
- };
+ let (prefix, kind, case) = pat.pieces.first()?;
// If there is a suffix, we cannot use the common style optimisation,
// since PDF does not provide a suffix field.
@@ -372,7 +377,7 @@ pub struct EncodedPage {
}
/// Represents a resource being used in a PDF page by its name.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct PageResource {
kind: ResourceKind,
name: EcoString,
@@ -385,7 +390,7 @@ impl PageResource {
}
/// A kind of resource being used in a PDF page.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum ResourceKind {
XObject,
Font,
@@ -829,7 +834,7 @@ fn write_shape(ctx: &mut PageContext, pos: Point, shape: &Shape) {
Geometry::Rect(size) => {
let w = size.x.to_f32();
let h = size.y.to_f32();
- if w > 0.0 && h > 0.0 {
+ if w.abs() > f32::EPSILON && h.abs() > f32::EPSILON {
ctx.content.rect(x, y, w, h);
}
}
diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs
index 0829ef32db..5d5942bc96 100644
--- a/crates/typst-pdf/src/pattern.rs
+++ b/crates/typst-pdf/src/pattern.rs
@@ -73,7 +73,9 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) {
resources_map.finish();
tiling_pattern
.matrix(transform_to_array(
- transform.pre_concat(Transform::scale(Ratio::one(), -Ratio::one())),
+ transform
+ .pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
+ .post_concat(Transform::translate(Abs::zero(), pattern.spacing().y)),
))
.filter(Filter::FlateDecode);
}
@@ -82,7 +84,7 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) {
/// A pattern and its transform.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct PdfPattern {
- /// The transform to apply to the gradient.
+ /// The transform to apply to the pattern.
pub transform: Transform,
/// The pattern to paint.
pub pattern: Pattern,
@@ -116,13 +118,15 @@ fn register_pattern(
// Render the body.
let (_, content) = construct_page(ctx.parent, pattern.frame());
- let pdf_pattern = PdfPattern {
+ let mut pdf_pattern = PdfPattern {
transform,
pattern: pattern.clone(),
content: content.content.wait().clone(),
resources: content.resources.into_iter().collect(),
};
+ pdf_pattern.resources.sort();
+
ctx.parent.pattern_map.insert(pdf_pattern)
}
diff --git a/crates/typst-render/Cargo.toml b/crates/typst-render/Cargo.toml
index 2db9b5edae..cc58f785f9 100644
--- a/crates/typst-render/Cargo.toml
+++ b/crates/typst-render/Cargo.toml
@@ -12,10 +12,6 @@ categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
-[lib]
-doctest = false
-bench = false
-
[dependencies]
typst = { workspace = true }
typst-macros = { workspace = true }
diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs
index 5d116e4958..28302180ad 100644
--- a/crates/typst-render/src/lib.rs
+++ b/crates/typst-render/src/lib.rs
@@ -42,13 +42,13 @@ pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap {
/// Export a document with potentially multiple pages into a single raster image.
///
-/// The padding will be added around and between the individual frames.
+/// The gap will be added between the individual frames.
pub fn render_merged(
document: &Document,
pixel_per_pt: f32,
frame_fill: Color,
- padding: Abs,
- padding_fill: Color,
+ gap: Abs,
+ gap_fill: Color,
) -> sk::Pixmap {
let pixmaps: Vec<_> = document
.pages
@@ -56,19 +56,18 @@ pub fn render_merged(
.map(|page| render(&page.frame, pixel_per_pt, frame_fill))
.collect();
- let padding = (pixel_per_pt * padding.to_f32()).round() as u32;
- let pxw =
- 2 * padding + pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default();
- let pxh =
- padding + pixmaps.iter().map(|pixmap| pixmap.height() + padding).sum::();
+ let gap = (pixel_per_pt * gap.to_f32()).round() as u32;
+ let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default();
+ let pxh = pixmaps.iter().map(|pixmap| pixmap.height()).sum::()
+ + gap * pixmaps.len().saturating_sub(1) as u32;
let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
- canvas.fill(to_sk_color(padding_fill));
+ canvas.fill(to_sk_color(gap_fill));
- let [x, mut y] = [padding; 2];
+ let mut y = 0;
for pixmap in pixmaps {
canvas.draw_pixmap(
- x as i32,
+ 0,
y as i32,
pixmap.as_ref(),
&sk::PixmapPaint::default(),
@@ -76,7 +75,7 @@ pub fn render_merged(
None,
);
- y += pixmap.height() + padding;
+ y += pixmap.height() + gap;
}
canvas
@@ -570,7 +569,18 @@ fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Option<
Geometry::Rect(size) => {
let w = size.x.to_f32();
let h = size.y.to_f32();
- let rect = sk::Rect::from_xywh(0.0, 0.0, w, h)?;
+ let rect = if w < 0.0 || h < 0.0 {
+ // Skia doesn't normally allow for negative dimensions, but
+ // Typst supports them, so we apply a transform if needed
+ // Because this operation is expensive according to tiny-skia's
+ // docs, we prefer to not apply it if not needed
+ let transform = sk::Transform::from_scale(w.signum(), h.signum());
+ let rect = sk::Rect::from_xywh(0.0, 0.0, w.abs(), h.abs())?;
+ rect.transform(transform)?
+ } else {
+ sk::Rect::from_xywh(0.0, 0.0, w, h)?
+ };
+
sk::PathBuilder::from_rect(rect)
}
Geometry::Path(ref path) => convert_path(path)?,
@@ -941,8 +951,10 @@ fn to_sk_paint<'a>(
.container_transform
.post_concat(state.transform.invert().unwrap()),
};
- let width = (container_size.x.to_f32() * state.pixel_per_pt).ceil() as u32;
- let height = (container_size.y.to_f32() * state.pixel_per_pt).ceil() as u32;
+ let width =
+ (container_size.x.to_f32().abs() * state.pixel_per_pt).ceil() as u32;
+ let height =
+ (container_size.y.to_f32().abs() * state.pixel_per_pt).ceil() as u32;
*pixmap = Some(cached(
gradient,
@@ -958,8 +970,10 @@ fn to_sk_paint<'a>(
sk::SpreadMode::Pad,
sk::FilterQuality::Nearest,
1.0,
- fill_transform
- .pre_scale(1.0 / state.pixel_per_pt, 1.0 / state.pixel_per_pt),
+ fill_transform.pre_scale(
+ container_size.x.signum() as f32 / state.pixel_per_pt,
+ container_size.y.signum() as f32 / state.pixel_per_pt,
+ ),
);
sk_paint.anti_alias = gradient.anti_alias();
@@ -1080,7 +1094,7 @@ impl OutlineBuilder for WrappedPathBuilder {
}
}
-/// Additional methods for [`Length`].
+/// Additional methods for [`Abs`].
trait AbsExt {
/// Convert to a number of points as f32.
fn to_f32(self) -> f32;
diff --git a/crates/typst-svg/Cargo.toml b/crates/typst-svg/Cargo.toml
index 143e88ed77..df49a2b171 100644
--- a/crates/typst-svg/Cargo.toml
+++ b/crates/typst-svg/Cargo.toml
@@ -12,10 +12,6 @@ categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
-[lib]
-doctest = false
-bench = false
-
[dependencies]
typst = { workspace = true }
typst-macros = { workspace = true }
diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml
index f92b6d9b9e..001d405c48 100644
--- a/crates/typst-syntax/Cargo.toml
+++ b/crates/typst-syntax/Cargo.toml
@@ -12,10 +12,6 @@ categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
-[lib]
-doctest = false
-bench = false
-
[dependencies]
comemo = { workspace = true }
ecow = { workspace = true }
diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs
index c34f600292..c84e535838 100644
--- a/crates/typst-syntax/src/kind.rs
+++ b/crates/typst-syntax/src/kind.rs
@@ -32,7 +32,7 @@ pub enum SyntaxKind {
RawLang,
/// A raw delimiter consisting of 1 or 3+ backticks: `` ` ``.
RawDelim,
- /// A sequence of whitespace to ignore in a raw block: ` `.
+ /// A sequence of whitespace to ignore in a raw text: ` `.
RawTrimmed,
/// A hyperlink: `https://typst.org`.
Link,
diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs
index aacbee62ec..6e64beff30 100644
--- a/crates/typst-syntax/src/lexer.rs
+++ b/crates/typst-syntax/src/lexer.rs
@@ -88,8 +88,10 @@ impl Lexer<'_> {
}
}
-/// Shared.
+/// Shared methods with all [`LexMode`].
impl Lexer<'_> {
+ /// Proceed to the next token and return its [`SyntaxKind`]. Note the
+ /// token could be a [trivia](SyntaxKind::is_trivia).
pub fn next(&mut self) -> SyntaxKind {
if self.mode == LexMode::Raw {
let Some((kind, end)) = self.raw.pop() else {
@@ -121,6 +123,7 @@ impl Lexer<'_> {
}
}
+ /// Eat whitespace characters greedily.
fn whitespace(&mut self, start: usize, c: char) -> SyntaxKind {
let more = self.s.eat_while(|c| is_space(c, self.mode));
let newlines = match c {
@@ -272,10 +275,7 @@ impl Lexer<'_> {
if backticks >= 3 {
self.blocky_raw(start, end, backticks);
} else {
- // Single backtick needs no trimming or extra fancyness.
- self.s.jump(end - backticks);
- self.push_raw(SyntaxKind::Text);
- self.s.jump(end);
+ self.inline_raw(start, end, backticks);
}
// Closing delimiter.
@@ -297,16 +297,12 @@ impl Lexer<'_> {
self.push_raw(SyntaxKind::RawLang);
}
- // Determine inner content between backticks and with trimmed
- // single spaces (line trimming comes later).
+ // Determine inner content between backticks.
self.s.eat_if(' ');
- let mut inner = self.s.to(end - backticks);
- if inner.trim_end().ends_with('`') {
- inner = inner.strip_suffix(' ').unwrap_or(inner);
- }
+ let inner = self.s.to(end - backticks);
// Determine dedent level.
- let lines = split_newlines(inner);
+ let mut lines = split_newlines(inner);
let dedent = lines
.iter()
.skip(1)
@@ -317,6 +313,15 @@ impl Lexer<'_> {
.min()
.unwrap_or(0);
+ // Trim single space in last line if text ends with a backtick. The last
+ // line is the one directly before the closing backticks and if it is
+ // just whitespace, it will be completely trimmed below.
+ if inner.trim_end().ends_with('`') {
+ if let Some(last) = lines.last_mut() {
+ *last = last.strip_suffix(' ').unwrap_or(last);
+ }
+ }
+
let is_whitespace = |line: &&str| line.chars().all(char::is_whitespace);
let starts_whitespace = lines.first().is_some_and(is_whitespace);
let ends_whitespace = lines.last().is_some_and(is_whitespace);
@@ -353,6 +358,25 @@ impl Lexer<'_> {
self.s.jump(end);
}
+ fn inline_raw(&mut self, start: usize, end: usize, backticks: usize) {
+ self.s.jump(start + backticks);
+
+ while self.s.cursor() < end - backticks {
+ if self.s.at(is_newline) {
+ self.push_raw(SyntaxKind::Text);
+ self.s.eat_newline();
+ self.push_raw(SyntaxKind::RawTrimmed);
+ continue;
+ }
+ self.s.eat();
+ }
+ self.push_raw(SyntaxKind::Text);
+
+ self.s.jump(end);
+ }
+
+ /// Push the current cursor that marks the end of a raw segment of
+ /// the given `kind`.
fn push_raw(&mut self, kind: SyntaxKind) {
let end = self.s.cursor();
self.raw.push((kind, end));
@@ -760,7 +784,7 @@ impl ScannerExt for Scanner<'_> {
}
}
-/// Whether a character will become a Space token in Typst
+/// Whether a character will become a [`SyntaxKind::Space`] token.
#[inline]
fn is_space(character: char, mode: LexMode) -> bool {
match mode {
@@ -818,7 +842,7 @@ pub fn link_prefix(text: &str) -> (&str, bool) {
(s.before(), brackets.is_empty())
}
-/// Split text at newlines.
+/// Split text at newlines. These newline characters are not kept.
pub fn split_newlines(text: &str) -> Vec<&str> {
let mut s = Scanner::new(text);
let mut lines = Vec::new();
diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs
index 50032898e8..82a4c729f3 100644
--- a/crates/typst-syntax/src/parser.rs
+++ b/crates/typst-syntax/src/parser.rs
@@ -592,10 +592,23 @@ fn math_args(p: &mut Parser) {
p.wrap(m, SyntaxKind::Args);
}
+/// Wrap math function arguments in a "Math" SyntaxKind to combine adjacent expressions
+/// or create blank content.
+///
+/// We don't wrap when `exprs == 1`, as there is only one expression, so the grouping
+/// isn't needed, and this would change the type of the expression from potentially
+/// non-content to content.
+///
+/// Note that `exprs` might be 0 if we have whitespace or trivia before a comma i.e.
+/// `mat(; ,)` or `sin(x, , , ,)`. This would create an empty Math element before that
+/// trivia if we called `p.wrap()` -- breaking the expected AST for 2-d arguments -- so
+/// we instead manually wrap to our current marker using `p.wrap_within()`.
fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, named: Option) {
let exprs = p.post_process(arg).filter(|node| node.is::()).count();
if exprs != 1 {
- p.wrap(arg, SyntaxKind::Math);
+ // Convert 0 exprs into a blank math element (so empty arguments are allowed).
+ // Convert 2+ exprs into a math element (so they become a joined sequence).
+ p.wrap_within(arg, p.marker(), SyntaxKind::Math);
}
if let Some(m) = named {
@@ -1748,15 +1761,27 @@ impl<'s> Parser<'s> {
}
}
+ fn next_non_trivia(lexer: &mut Lexer<'s>) -> SyntaxKind {
+ loop {
+ let next = lexer.next();
+ // Loop is terminatable, because SyntaxKind::Eof is not a trivia.
+ if !next.is_trivia() {
+ break next;
+ }
+ }
+ }
+
fn lex(&mut self) {
self.current_start = self.lexer.cursor();
self.current = self.lexer.next();
+
+ // Special cases to handle newlines in code mode.
if self.lexer.mode() == LexMode::Code
&& self.lexer.newline()
&& match self.newline_modes.last() {
Some(NewlineMode::Continue) => false,
Some(NewlineMode::Contextual) => !matches!(
- self.lexer.clone().next(),
+ Self::next_non_trivia(&mut self.lexer.clone()),
SyntaxKind::Else | SyntaxKind::Dot
),
Some(NewlineMode::Stop) => true,
diff --git a/crates/typst-syntax/src/source.rs b/crates/typst-syntax/src/source.rs
index b4a80d3188..a68a53da34 100644
--- a/crates/typst-syntax/src/source.rs
+++ b/crates/typst-syntax/src/source.rs
@@ -2,6 +2,7 @@
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
+use std::iter::zip;
use std::ops::Range;
use std::sync::Arc;
@@ -76,12 +77,8 @@ impl Source {
pub fn replace(&mut self, new: &str) -> Range {
let old = self.text();
- let mut prefix = old
- .as_bytes()
- .iter()
- .zip(new.as_bytes())
- .take_while(|(x, y)| x == y)
- .count();
+ let mut prefix =
+ zip(old.bytes(), new.bytes()).take_while(|(x, y)| x == y).count();
if prefix == old.len() && prefix == new.len() {
return 0..0;
@@ -91,11 +88,7 @@ impl Source {
prefix -= 1;
}
- let mut suffix = old[prefix..]
- .as_bytes()
- .iter()
- .zip(new[prefix..].as_bytes())
- .rev()
+ let mut suffix = zip(old[prefix..].bytes().rev(), new[prefix..].bytes().rev())
.take_while(|(x, y)| x == y)
.count();
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index 53a28ffff3..a2828fa2f4 100644
--- a/crates/typst/Cargo.toml
+++ b/crates/typst/Cargo.toml
@@ -11,10 +11,6 @@ homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
-[lib]
-doctest = false
-bench = false
-
[dependencies]
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
diff --git a/crates/typst/src/diag.rs b/crates/typst/src/diag.rs
index c6cd6acb46..6e119591a5 100644
--- a/crates/typst/src/diag.rs
+++ b/crates/typst/src/diag.rs
@@ -21,7 +21,7 @@ use crate::{World, WorldExt};
///
/// You can also emit hints with the `; hint: "..."` syntax.
///
-/// ```
+/// ```ignore
/// bail!("bailing with a {}", "string result");
/// bail!(span, "bailing with a {}", "source result");
/// bail!(
@@ -81,7 +81,7 @@ macro_rules! __error {
///
/// You can also emit hints with the `; hint: "..."` syntax.
///
-/// ```
+/// ```ignore
/// warning!(span, "warning with a {}", "source result");
/// warning!(
/// span, "warning with a {}", "source result";
diff --git a/crates/typst/src/foundations/cast.rs b/crates/typst/src/foundations/cast.rs
index 3b74f1fb17..b29e447322 100644
--- a/crates/typst/src/foundations/cast.rs
+++ b/crates/typst/src/foundations/cast.rs
@@ -44,9 +44,9 @@ pub trait Reflect {
/// Produce an error message for an inacceptable value type.
///
- /// ```
+ /// ```ignore
/// assert_eq!(
- /// ::error(&Value::None),
+ /// ::error(&Value::None),
/// "expected integer, found none",
/// );
/// ```
diff --git a/crates/typst/src/foundations/int.rs b/crates/typst/src/foundations/int.rs
index ef21f7853a..7b6c02638f 100644
--- a/crates/typst/src/foundations/int.rs
+++ b/crates/typst/src/foundations/int.rs
@@ -261,7 +261,14 @@ macro_rules! unsigned_int {
($($ty:ty)*) => {
$(cast! {
$ty,
- self => Value::Int(self as _),
+ self => if let Ok(int) = i64::try_from(self) {
+ Value::Int(int)
+ } else {
+ // Some u64 are too large to be cast as i64
+ // In that case, we accept that there may be a
+ // precision loss, and use a floating point number
+ Value::Float(self as _)
+ },
v: i64 => v.try_into().map_err(|_| {
if v < 0 {
"number must be at least zero"
diff --git a/crates/typst/src/foundations/selector.rs b/crates/typst/src/foundations/selector.rs
index 4dc8ab3b9d..90663500e9 100644
--- a/crates/typst/src/foundations/selector.rs
+++ b/crates/typst/src/foundations/selector.rs
@@ -15,10 +15,6 @@ use crate::symbols::Symbol;
use crate::text::TextElem;
/// A helper macro to create a field selector used in [`Selector::Elem`]
-///
-/// ```ignore
-/// select_where!(SequenceElem, Children => vec![]);
-/// ```
#[macro_export]
#[doc(hidden)]
macro_rules! __select_where {
diff --git a/crates/typst/src/foundations/str.rs b/crates/typst/src/foundations/str.rs
index 897ca45b39..515a4e2102 100644
--- a/crates/typst/src/foundations/str.rs
+++ b/crates/typst/src/foundations/str.rs
@@ -491,11 +491,11 @@ impl Str {
#[func]
pub fn trim(
&self,
- /// The pattern to search for.
+ /// The pattern to search for. If `{none}`, trims white spaces.
#[default]
pattern: Option,
- /// Can be `start` or `end` to only trim the start or end of the string.
- /// If omitted, both sides are trimmed.
+ /// Can be `{start}` or `{end}` to only trim the start or end of the
+ /// string. If omitted, both sides are trimmed.
#[named]
at: Option,
/// Whether to repeatedly removes matches of the pattern or just once.
@@ -535,16 +535,16 @@ impl Str {
}
Some(StrPattern::Regex(re)) => {
let s = self.as_str();
- let mut last = 0;
+ let mut last = None;
let mut range = 0..s.len();
for m in re.find_iter(s) {
// Does this match follow directly after the last one?
- let consecutive = last == m.start();
+ let consecutive = last == Some(m.start());
- // As long as we're consecutive and still trimming at the
- // start, trim.
- start &= consecutive;
+ // As long as we're at the beginning or in a consecutive run
+ // of matches, and we're still trimming at the start, trim.
+ start &= m.start() == 0 || consecutive;
if start {
range.start = m.end();
start &= repeat;
@@ -556,11 +556,11 @@ impl Str {
range.end = m.start();
}
- last = m.end();
+ last = Some(m.end());
}
// Is the last match directly at the end?
- if last < s.len() {
+ if last.is_some_and(|last| last < s.len()) {
range.end = s.len();
}
diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs
index dd09b60562..35460088c9 100644
--- a/crates/typst/src/foundations/styles.rs
+++ b/crates/typst/src/foundations/styles.rs
@@ -321,9 +321,6 @@ trait Blockable: Debug + Send + Sync + 'static {
/// Equivalent to `downcast_ref` for the block.
fn as_any(&self) -> &dyn Any;
- /// Equivalent to `downcast_mut` for the block.
- fn as_any_mut(&mut self) -> &mut dyn Any;
-
/// Equivalent to [`Hash`] for the block.
fn dyn_hash(&self, state: &mut dyn Hasher);
@@ -336,10 +333,6 @@ impl Blockable for T {
self
}
- fn as_any_mut(&mut self) -> &mut dyn Any {
- self
- }
-
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
// Also hash the TypeId since values with different types but
// equal data should be different.
diff --git a/crates/typst/src/introspection/here.rs b/crates/typst/src/introspection/here.rs
index e921079b94..9d6133816e 100644
--- a/crates/typst/src/introspection/here.rs
+++ b/crates/typst/src/introspection/here.rs
@@ -20,8 +20,8 @@ use crate::introspection::Location;
/// instance, have been reset after a preface.
///
/// # Examples
-/// Determining the current position in the document in combination with
-/// [`locate`]:
+/// Determining the current position in the document in combination with the
+/// [`position`]($location.position) method:
/// ```example
/// #context [
/// I am located at
diff --git a/crates/typst/src/introspection/metadata.rs b/crates/typst/src/introspection/metadata.rs
index 2d6d8953fa..610c238e93 100644
--- a/crates/typst/src/introspection/metadata.rs
+++ b/crates/typst/src/introspection/metadata.rs
@@ -7,7 +7,8 @@ use crate::realize::{Behave, Behaviour};
/// Exposes a value to the query system without producing visible content.
///
/// This element can be retrieved with the [`query`] function and from the
-/// command line with [`typst query`]($reference/meta/query/#cli-queries). Its
+/// command line with
+/// [`typst query`]($reference/introspection/query/#command-line-queries). Its
/// purpose is to expose an arbitrary value to the introspection system. To
/// identify a metadata value among others, you can attach a [`label`] to it and
/// query for that label.
diff --git a/crates/typst/src/layout/abs.rs b/crates/typst/src/layout/abs.rs
index b191679540..5c07c5a060 100644
--- a/crates/typst/src/layout/abs.rs
+++ b/crates/typst/src/layout/abs.rs
@@ -117,6 +117,11 @@ impl Abs {
pub fn approx_eq(self, other: Self) -> bool {
self == other || (self - other).to_raw().abs() < 1e-6
}
+
+ /// Returns a number that represent the sign of this length
+ pub fn signum(self) -> f64 {
+ self.0.get().signum()
+ }
}
impl Numeric for Abs {
diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs
index 8f0def1791..3cc53c6a4d 100644
--- a/crates/typst/src/layout/flow.rs
+++ b/crates/typst/src/layout/flow.rs
@@ -141,15 +141,6 @@ enum FlowItem {
}
impl FlowItem {
- /// The inherent height of the item.
- fn height(&self) -> Abs {
- match self {
- Self::Absolute(v, _) => *v,
- Self::Fractional(_) | Self::Placed { .. } => Abs::zero(),
- Self::Frame { frame, .. } | Self::Footnote(frame) => frame.height(),
- }
- }
-
/// Whether this item is out-of-flow.
///
/// Out-of-flow items are guaranteed to have a [`Size::zero()`].
@@ -411,12 +402,16 @@ impl<'a> FlowLayouter<'a> {
self.finish_region(engine, false)?;
}
+ let in_last = self.regions.in_last();
self.regions.size.y -= height;
if self.root && movable {
let mut notes = Vec::new();
find_footnotes(&mut notes, frame);
self.items.push(item);
- if !self.handle_footnotes(engine, &mut notes, true, false)? {
+
+ // When we are already in_last, we can directly force the
+ // footnotes.
+ if !self.handle_footnotes(engine, &mut notes, true, in_last)? {
let item = self.items.pop();
self.finish_region(engine, false)?;
self.items.extend(item);
@@ -651,7 +646,16 @@ impl FlowLayouter<'_> {
engine: &mut Engine,
mut notes: Vec>,
) -> SourceResult<()> {
- if self.root && !self.handle_footnotes(engine, &mut notes, false, false)? {
+ // When we are already in_last, we can directly force the
+ // footnotes.
+ if self.root
+ && !self.handle_footnotes(
+ engine,
+ &mut notes,
+ false,
+ self.regions.in_last(),
+ )?
+ {
self.finish_region(engine, false)?;
self.handle_footnotes(engine, &mut notes, false, true)?;
}
@@ -666,8 +670,11 @@ impl FlowLayouter<'_> {
movable: bool,
force: bool,
) -> SourceResult {
- let items_len = self.items.len();
- let notes_len = notes.len();
+ let prev_notes_len = notes.len();
+ let prev_items_len = self.items.len();
+ let prev_size = self.regions.size;
+ let prev_has_footnotes = self.has_footnotes;
+ let prev_locator = engine.locator.clone();
// Process footnotes one at a time.
let mut k = 0;
@@ -682,7 +689,6 @@ impl FlowLayouter<'_> {
}
self.regions.size.y -= self.footnote_config.gap;
- let checkpoint = engine.locator.clone();
let frames = FootnoteEntry::new(notes[k].clone())
.pack()
.layout(engine, self.styles, self.regions.with_root(false))?
@@ -694,18 +700,12 @@ impl FlowLayouter<'_> {
&& (k == 0 || movable)
&& frames.first().is_some_and(Frame::is_empty)
{
- // Remove existing footnotes attempts because we need to
- // move the item to the next page.
- notes.truncate(notes_len);
-
- // Undo region modifications.
- for item in self.items.drain(items_len..) {
- self.regions.size.y -= item.height();
- }
-
- // Undo locator modifications.
- *engine.locator = checkpoint;
-
+ // Undo everything.
+ notes.truncate(prev_notes_len);
+ self.items.truncate(prev_items_len);
+ self.regions.size = prev_size;
+ self.has_footnotes = prev_has_footnotes;
+ *engine.locator = prev_locator;
return Ok(false);
}
diff --git a/crates/typst/src/layout/grid/layout.rs b/crates/typst/src/layout/grid/layout.rs
index 6dbe151e6f..398ad1bdc1 100644
--- a/crates/typst/src/layout/grid/layout.rs
+++ b/crates/typst/src/layout/grid/layout.rs
@@ -2692,6 +2692,10 @@ impl<'a> GridLayouter<'a> {
height: Abs,
y: usize,
) -> SourceResult {
+ if !self.width.is_finite() {
+ bail!(self.span, "cannot create grid with infinite width");
+ }
+
if !height.is_finite() {
bail!(self.span, "cannot create grid with infinite height");
}
diff --git a/crates/typst/src/layout/grid/lines.rs b/crates/typst/src/layout/grid/lines.rs
index c976da2d75..7ea021d794 100644
--- a/crates/typst/src/layout/grid/lines.rs
+++ b/crates/typst/src/layout/grid/lines.rs
@@ -597,7 +597,7 @@ pub(super) fn hline_stroke_at_column(
#[cfg(test)]
mod test {
- use super::super::layout::{Entry, RowPiece};
+ use super::super::layout::Entry;
use super::*;
use crate::foundations::Content;
use crate::layout::{Axes, Cell, Sides, Sizing};
diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs
index 16da1539fd..4fe831e288 100644
--- a/crates/typst/src/layout/inline/mod.rs
+++ b/crates/typst/src/layout/inline/mod.rs
@@ -73,7 +73,8 @@ pub(crate) fn layout_inline(
let lines = linebreak(&engine, &p, region.x - p.hang);
// Stack the lines into one frame per region.
- finalize(&mut engine, &p, &lines, region, expand)
+ let shrink = ParElem::shrink_in(styles);
+ finalize(&mut engine, &p, &lines, region, expand, shrink)
}
let fragment = cached(
@@ -101,6 +102,13 @@ type Range = std::ops::Range;
const SPACING_REPLACE: char = ' '; // Space
const OBJ_REPLACE: char = '\u{FFFC}'; // Object Replacement Character
+// Unicode BiDi control characters.
+const LTR_EMBEDDING: char = '\u{202A}';
+const RTL_EMBEDDING: char = '\u{202B}';
+const POP_EMBEDDING: char = '\u{202C}';
+const LTR_ISOLATE: char = '\u{2066}';
+const POP_ISOLATE: char = '\u{2069}';
+
/// A paragraph representation in which children are already layouted and text
/// is already preshaped.
///
@@ -189,7 +197,7 @@ enum Segment<'a> {
/// Horizontal spacing between other segments.
Spacing(Spacing),
/// A mathematical equation.
- Equation(&'a Packed, Vec),
+ Equation(Vec),
/// A box with arbitrary content.
Box(&'a Packed, bool),
/// Metadata.
@@ -205,9 +213,12 @@ impl Segment<'_> {
Self::Box(_, frac) => {
(if frac { SPACING_REPLACE } else { OBJ_REPLACE }).len_utf8()
}
- Self::Equation(_, ref par_items) => {
- par_items.iter().map(MathParItem::text).map(char::len_utf8).sum()
- }
+ Self::Equation(ref par_items) => par_items
+ .iter()
+ .map(MathParItem::text)
+ .chain([LTR_ISOLATE, POP_ISOLATE])
+ .map(char::len_utf8)
+ .sum(),
Self::Meta => 0,
}
}
@@ -226,6 +237,9 @@ enum Item<'a> {
Frame(Frame),
/// Metadata.
Meta(Frame),
+ /// An item that is invisible and needs to be skipped, e.g. a Unicode
+ /// isolate.
+ Skip(char),
}
impl<'a> Item<'a> {
@@ -252,6 +266,7 @@ impl<'a> Item<'a> {
Self::Absolute(_) | Self::Fractional(_, _) => SPACING_REPLACE.len_utf8(),
Self::Frame(_) => OBJ_REPLACE.len_utf8(),
Self::Meta(_) => 0,
+ Self::Skip(c) => c.len_utf8(),
}
}
@@ -262,6 +277,7 @@ impl<'a> Item<'a> {
Self::Absolute(v) => *v,
Self::Frame(frame) => frame.width(),
Self::Fractional(_, _) | Self::Meta(_) => Abs::zero(),
+ Self::Skip(_) => Abs::zero(),
}
}
}
@@ -449,10 +465,10 @@ fn collect<'a>(
let prev = full.len();
let dir = TextElem::dir_in(styles);
if dir != outer_dir {
- // Insert "Explicit Directional Isolate".
+ // Insert "Explicit Directional Embedding".
match dir {
- Dir::LTR => full.push('\u{2066}'),
- Dir::RTL => full.push('\u{2067}'),
+ Dir::LTR => full.push(LTR_EMBEDDING),
+ Dir::RTL => full.push(RTL_EMBEDDING),
_ => {}
}
}
@@ -464,8 +480,8 @@ fn collect<'a>(
}
if dir != outer_dir {
- // Insert "Pop Directional Isolate".
- full.push('\u{2069}');
+ // Insert "Pop Directional Formatting".
+ full.push(POP_EMBEDDING);
}
Segment::Text(full.len() - prev)
} else if let Some(elem) = child.to_packed::() {
@@ -520,8 +536,10 @@ fn collect<'a>(
let MathParItem::Frame(frame) = item else { continue };
frame.meta(styles, false);
}
+ full.push(LTR_ISOLATE);
full.extend(items.iter().map(MathParItem::text));
- Segment::Equation(elem, items)
+ full.push(POP_ISOLATE);
+ Segment::Equation(items)
} else if let Some(elem) = child.to_packed::() {
let frac = elem.width(styles).is_fractional();
full.push(if frac { SPACING_REPLACE } else { OBJ_REPLACE });
@@ -592,7 +610,8 @@ fn prepare<'a>(
items.push(Item::Fractional(v, None));
}
},
- Segment::Equation(_, par_items) => {
+ Segment::Equation(par_items) => {
+ items.push(Item::Skip(LTR_ISOLATE));
for item in par_items {
match item {
MathParItem::Space(s) => items.push(Item::Absolute(s)),
@@ -602,6 +621,7 @@ fn prepare<'a>(
}
}
}
+ items.push(Item::Skip(POP_ISOLATE));
}
Segment::Box(elem, _) => {
if let Sizing::Fr(v) = elem.width(styles) {
@@ -1191,6 +1211,7 @@ fn finalize(
lines: &[Line],
region: Size,
expand: bool,
+ shrink: bool,
) -> SourceResult {
// Determine the paragraph's width: Full width of the region if we
// should expand or there's fractional spacing, fit-to-width otherwise.
@@ -1207,7 +1228,7 @@ fn finalize(
// Stack the lines into one frame per region.
let mut frames: Vec = lines
.iter()
- .map(|line| commit(engine, p, line, width, region.y))
+ .map(|line| commit(engine, p, line, width, region.y, shrink))
.collect::>()?;
// Prevent orphans.
@@ -1243,6 +1264,7 @@ fn commit(
line: &Line,
width: Abs,
full: Abs,
+ shrink: bool,
) -> SourceResult {
let mut remaining = width - line.width - p.hang;
let mut offset = Abs::zero();
@@ -1289,12 +1311,12 @@ fn commit(
let mut justification_ratio = 0.0;
let mut extra_justification = Abs::zero();
- let shrink = line.shrinkability();
+ let shrinkability = line.shrinkability();
let stretch = line.stretchability();
- if remaining < Abs::zero() && shrink > Abs::zero() {
+ if remaining < Abs::zero() && shrinkability > Abs::zero() && shrink {
// Attempt to reduce the length of the line, using shrinkability.
- justification_ratio = (remaining / shrink).max(-1.0);
- remaining = (remaining + shrink).min(Abs::zero());
+ justification_ratio = (remaining / shrinkability).max(-1.0);
+ remaining = (remaining + shrinkability).min(Abs::zero());
} else if line.justify && fr.is_zero() {
// Attempt to increase the length of the line, using stretchability.
if stretch > Abs::zero() {
@@ -1350,6 +1372,7 @@ fn commit(
Item::Frame(frame) | Item::Meta(frame) => {
push(&mut offset, frame.clone());
}
+ Item::Skip(_) => {}
}
}
@@ -1415,7 +1438,7 @@ fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) {
/// How much a character should hang into the end margin.
///
/// For more discussion, see:
-/// https://recoveringphysicist.com/21/
+///
fn overhang(c: char) -> f64 {
match c {
// Dashes.
diff --git a/crates/typst/src/layout/inline/shaping.rs b/crates/typst/src/layout/inline/shaping.rs
index c346233e30..82a33f9b09 100644
--- a/crates/typst/src/layout/inline/shaping.rs
+++ b/crates/typst/src/layout/inline/shaping.rs
@@ -189,14 +189,12 @@ impl ShapedGlyph {
self.x_offset -= amount;
self.x_advance -= amount;
self.adjustability.shrinkability.0 -= amount;
- self.adjustability.stretchability.0 += amount;
}
/// Shrink the width of glyph on the right side.
pub fn shrink_right(&mut self, amount: Em) {
self.x_advance -= amount;
self.adjustability.shrinkability.1 -= amount;
- self.adjustability.stretchability.1 += amount;
}
}
diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs
index 176eaf472c..7d41b03760 100644
--- a/crates/typst/src/layout/place.rs
+++ b/crates/typst/src/layout/place.rs
@@ -30,8 +30,8 @@ use crate::realize::{Behave, Behaviour};
pub struct PlaceElem {
/// Relative to which position in the parent container to place the content.
///
- /// Cannot be `{auto}` if `float` is `{false}` and must be either
- /// `{auto}`, `{top}`, or `{bottom}` if `float` is `{true}`.
+ /// - If `float` is `{false}`, then this can be any alignment other than `{auto}`.
+ /// - If `float` is `{true}`, then this must be `{auto}`, `{top}`, or `{bottom}`.
///
/// When an axis of the page is `{auto}` sized, all alignments relative to
/// that axis will be ignored, instead, the item will be placed in the
@@ -77,9 +77,17 @@ pub struct PlaceElem {
/// place(center, dx: amount - 32pt, dy: amount)[A]
/// }
/// ```
+ ///
+ /// This does not affect the layout of in-flow content.
+ /// In other words, the placed content is treated as if it
+ /// were wrapped in a [`move`] element.
pub dx: Rel,
/// The vertical displacement of the placed content.
+ ///
+ /// This does not affect the layout of in-flow content.
+ /// In other words, the placed content is treated as if it
+ /// were wrapped in a [`move`] element.
pub dy: Rel,
/// The content to place.
diff --git a/crates/typst/src/layout/stack.rs b/crates/typst/src/layout/stack.rs
index caa78264ab..05b415faf8 100644
--- a/crates/typst/src/layout/stack.rs
+++ b/crates/typst/src/layout/stack.rs
@@ -1,6 +1,7 @@
use std::fmt::{self, Debug, Formatter};
+use typst_syntax::Span;
-use crate::diag::SourceResult;
+use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{cast, elem, Content, Packed, Resolve, StyleChain, StyledElem};
use crate::layout::{
@@ -59,7 +60,8 @@ impl LayoutMultiple for Packed {
styles: StyleChain,
regions: Regions,
) -> SourceResult {
- let mut layouter = StackLayouter::new(self.dir(styles), regions, styles);
+ let mut layouter =
+ StackLayouter::new(self.span(), self.dir(styles), regions, styles);
let axis = layouter.dir.axis();
// Spacing to insert before the next block.
@@ -97,7 +99,7 @@ impl LayoutMultiple for Packed {
}
}
- Ok(layouter.finish())
+ layouter.finish()
}
}
@@ -131,6 +133,8 @@ cast! {
/// Performs stack layout.
struct StackLayouter<'a> {
+ /// The span to raise errors at during layout.
+ span: Span,
/// The stacking direction.
dir: Dir,
/// The axis of the stacking direction.
@@ -166,7 +170,12 @@ enum StackItem {
impl<'a> StackLayouter<'a> {
/// Create a new stack layouter.
- fn new(dir: Dir, mut regions: Regions<'a>, styles: StyleChain<'a>) -> Self {
+ fn new(
+ span: Span,
+ dir: Dir,
+ mut regions: Regions<'a>,
+ styles: StyleChain<'a>,
+ ) -> Self {
let axis = dir.axis();
let expand = regions.expand;
@@ -174,6 +183,7 @@ impl<'a> StackLayouter<'a> {
regions.expand.set(axis, false);
Self {
+ span,
dir,
axis,
regions,
@@ -218,7 +228,7 @@ impl<'a> StackLayouter<'a> {
styles: StyleChain,
) -> SourceResult<()> {
if self.regions.is_full() {
- self.finish_region();
+ self.finish_region()?;
}
// Block-axis alignment of the `AlignElement` is respected by stacks.
@@ -251,7 +261,7 @@ impl<'a> StackLayouter<'a> {
self.items.push(StackItem::Frame(frame, align));
if i + 1 < len {
- self.finish_region();
+ self.finish_region()?;
}
}
@@ -259,7 +269,7 @@ impl<'a> StackLayouter<'a> {
}
/// Advance to the next region.
- fn finish_region(&mut self) {
+ fn finish_region(&mut self) -> SourceResult<()> {
// Determine the size of the stack in this region depending on whether
// the region expands.
let mut size = self
@@ -275,6 +285,10 @@ impl<'a> StackLayouter<'a> {
size.set(self.axis, full);
}
+ if !size.is_finite() {
+ bail!(self.span, "stack spacing is infinite");
+ }
+
let mut output = Frame::hard(size);
let mut cursor = Abs::zero();
let mut ruler: FixedAlignment = self.dir.start().into();
@@ -320,12 +334,14 @@ impl<'a> StackLayouter<'a> {
self.used = Gen::zero();
self.fr = Fr::zero();
self.finished.push(output);
+
+ Ok(())
}
/// Finish layouting and return the resulting frames.
- fn finish(mut self) -> Fragment {
- self.finish_region();
- Fragment::frames(self.finished)
+ fn finish(mut self) -> SourceResult {
+ self.finish_region()?;
+ Ok(Fragment::frames(self.finished))
}
}
diff --git a/crates/typst/src/loading/cbor.rs b/crates/typst/src/loading/cbor.rs
index ddf559f33f..bce0927122 100644
--- a/crates/typst/src/loading/cbor.rs
+++ b/crates/typst/src/loading/cbor.rs
@@ -14,6 +14,9 @@ use crate::World;
/// equivalents, null-values (`null`, `~` or empty ``) will be converted into
/// `{none}`, and numbers will be converted to floats or integers depending on
/// whether they are whole numbers.
+///
+/// Be aware that integers larger than 263 -1 will be converted to
+/// floating point numbers, which may result in an approximative value.
#[func(scope, title = "CBOR")]
pub fn cbor(
/// The engine.
diff --git a/crates/typst/src/loading/json.rs b/crates/typst/src/loading/json.rs
index aae1f4ad5b..8e829594b0 100644
--- a/crates/typst/src/loading/json.rs
+++ b/crates/typst/src/loading/json.rs
@@ -9,13 +9,18 @@ use crate::World;
/// Reads structured data from a JSON file.
///
-/// The file must contain a valid JSON object or array. JSON objects will be
-/// converted into Typst dictionaries, and JSON arrays will be converted into
-/// Typst arrays. Strings and booleans will be converted into the Typst
-/// equivalents, `null` will be converted into `{none}`, and numbers will be
-/// converted to floats or integers depending on whether they are whole numbers.
+/// The file must contain a valid JSON value, such as object or array. JSON
+/// objects will be converted into Typst dictionaries, and JSON arrays will be
+/// converted into Typst arrays. Strings and booleans will be converted into the
+/// Typst equivalents, `null` will be converted into `{none}`, and numbers will
+/// be converted to floats or integers depending on whether they are whole
+/// numbers.
///
-/// The function returns a dictionary or an array, depending on the JSON file.
+/// Be aware that integers larger than 263 -1 will be converted to
+/// floating point numbers, which may result in an approximative value.
+///
+/// The function returns a dictionary, an array or, depending on the JSON file,
+/// another JSON data type.
///
/// The JSON files in the example contain objects with the keys `temperature`,
/// `unit`, and `weather`.
diff --git a/crates/typst/src/loading/yaml.rs b/crates/typst/src/loading/yaml.rs
index 501e306665..d578eda4cd 100644
--- a/crates/typst/src/loading/yaml.rs
+++ b/crates/typst/src/loading/yaml.rs
@@ -17,6 +17,9 @@ use crate::World;
/// whether they are whole numbers. Custom YAML tags are ignored, though the
/// loaded value will still be present.
///
+/// Be aware that integers larger than 263 -1 will be converted to
+/// floating point numbers, which may give an approximative value.
+///
/// The YAML files in the example contain objects with authors as keys,
/// each with a sequence of their own submapping with the keys
/// "title" and "published"
diff --git a/crates/typst/src/math/equation.rs b/crates/typst/src/math/equation.rs
index c5cb91322e..4a5d88e21b 100644
--- a/crates/typst/src/math/equation.rs
+++ b/crates/typst/src/math/equation.rs
@@ -224,6 +224,12 @@ impl Packed {
vec![MathParItem::Frame(run.into_fragment(&ctx, styles).into_frame())]
};
+ // An empty equation should have a height, so we still create a frame
+ // (which is then resized in the loop).
+ if items.is_empty() {
+ items.push(MathParItem::Frame(Frame::soft(Size::zero())));
+ }
+
for item in &mut items {
let MathParItem::Frame(frame) = item else { continue };
diff --git a/crates/typst/src/math/lr.rs b/crates/typst/src/math/lr.rs
index f92afbd570..671aa7df98 100644
--- a/crates/typst/src/math/lr.rs
+++ b/crates/typst/src/math/lr.rs
@@ -122,10 +122,12 @@ impl LayoutMath for Packed {
MathFragment::Glyph(glyph) => {
let mut new = glyph.clone().into_variant();
new.mid_stretched = Some(false);
+ new.class = MathClass::Fence;
*fragment = MathFragment::Variant(new);
}
MathFragment::Variant(variant) => {
variant.mid_stretched = Some(false);
+ variant.class = MathClass::Fence;
}
_ => {}
}
diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs
index 83d50dd6cd..9d2f65b391 100644
--- a/crates/typst/src/math/matrix.rs
+++ b/crates/typst/src/math/matrix.rs
@@ -10,7 +10,8 @@ use crate::layout::{
};
use crate::math::{
alignments, scaled_font_size, stack, style_for_denominator, AlignmentResult,
- FrameFragment, GlyphFragment, LayoutMath, MathContext, Scaled, DELIM_SHORT_FALL,
+ FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, Scaled,
+ DELIM_SHORT_FALL,
};
use crate::syntax::{Span, Spanned};
use crate::text::TextElem;
@@ -67,6 +68,7 @@ impl LayoutMath for Packed {
self.children(),
FixedAlignment::Center,
self.gap(styles),
+ LeftRightAlternator::Right,
)?;
layout_delimiters(
@@ -324,6 +326,7 @@ impl LayoutMath for Packed {
self.children(),
FixedAlignment::Start,
self.gap(styles),
+ LeftRightAlternator::None,
)?;
let (open, close) = if self.reverse(styles) {
@@ -387,6 +390,7 @@ fn layout_vec_body(
column: &[Content],
align: FixedAlignment,
row_gap: Rel,
+ alternator: LeftRightAlternator,
) -> SourceResult {
let gap = row_gap.relative_to(ctx.regions.base().y);
@@ -396,7 +400,7 @@ fn layout_vec_body(
flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?);
}
- Ok(stack(flat, align, gap, 0))
+ Ok(stack(flat, align, gap, 0, alternator))
}
/// Layout the inner contents of a matrix.
@@ -480,7 +484,7 @@ fn layout_mat_body(
let mut y = Abs::zero();
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
- let cell = cell.into_line_frame(&points, FixedAlignment::Center);
+ let cell = cell.into_line_frame(&points, LeftRightAlternator::Right);
let pos = Point::new(
if points.is_empty() { x + (rcol - cell.width()) / 2.0 } else { x },
y + ascent - cell.ascent(),
diff --git a/crates/typst/src/math/root.rs b/crates/typst/src/math/root.rs
index 90d3111b22..6a76044529 100644
--- a/crates/typst/src/math/root.rs
+++ b/crates/typst/src/math/root.rs
@@ -50,7 +50,7 @@ impl LayoutMath for Packed {
/// Layout a root.
///
/// TeXbook page 443, page 360
-/// See also: https://www.w3.org/TR/mathml-core/#radicals-msqrt-mroot
+/// See also:
fn layout(
ctx: &mut MathContext,
styles: StyleChain,
diff --git a/crates/typst/src/math/row.rs b/crates/typst/src/math/row.rs
index afed82316d..652cae12c7 100644
--- a/crates/typst/src/math/row.rs
+++ b/crates/typst/src/math/row.rs
@@ -3,7 +3,7 @@ use std::iter::once;
use unicode_math_class::MathClass;
use crate::foundations::{Resolve, StyleChain};
-use crate::layout::{Abs, AlignElem, Em, FixedAlignment, Frame, FrameKind, Point, Size};
+use crate::layout::{Abs, AlignElem, Em, Frame, Point, Size};
use crate::math::{
alignments, scaled_font_size, spacing, EquationElem, FrameFragment, MathContext,
MathFragment, MathParItem, MathSize,
@@ -140,7 +140,7 @@ impl MathRun {
pub fn into_frame(self, ctx: &MathContext, styles: StyleChain) -> Frame {
if !self.is_multiline() {
- self.into_line_frame(&[], AlignElem::alignment_in(styles).resolve(styles).x)
+ self.into_line_frame(&[], LeftRightAlternator::Right)
} else {
self.multiline_frame_builder(ctx, styles).build()
}
@@ -181,7 +181,7 @@ impl MathRun {
continue;
}
- let sub = row.into_line_frame(&alignments.points, align);
+ let sub = row.into_line_frame(&alignments.points, LeftRightAlternator::Right);
if i > 0 {
size.y += leading;
}
@@ -200,43 +200,37 @@ impl MathRun {
/// Lay out [`MathFragment`]s into a one-row [`Frame`], using the
/// caller-provided alignment points.
- pub fn into_line_frame(self, points: &[Abs], align: FixedAlignment) -> Frame {
+ pub fn into_line_frame(
+ self,
+ points: &[Abs],
+ mut alternator: LeftRightAlternator,
+ ) -> Frame {
let ascent = self.ascent();
let mut frame = Frame::soft(Size::new(Abs::zero(), ascent + self.descent()));
frame.set_baseline(ascent);
let mut next_x = {
- let mut widths = Vec::new();
- if !points.is_empty() && align != FixedAlignment::Start {
- let mut width = Abs::zero();
- for fragment in self.iter() {
- if matches!(fragment, MathFragment::Align) {
- widths.push(width);
- width = Abs::zero();
- } else {
- width += fragment.width();
- }
- }
- widths.push(width);
- }
- let widths = widths;
+ let widths: Vec = if points.is_empty() {
+ vec![]
+ } else {
+ self.iter()
+ .as_slice()
+ .split(|e| matches!(e, MathFragment::Align))
+ .map(|chunk| chunk.iter().map(|e| e.width()).sum())
+ .collect()
+ };
let mut prev_points = once(Abs::zero()).chain(points.iter().copied());
let mut point_widths = points.iter().copied().zip(widths);
- let mut alternator = LeftRightAlternator::Right;
- move || match align {
- FixedAlignment::Start => prev_points.next(),
- FixedAlignment::End => {
- point_widths.next().map(|(point, width)| point - width)
- }
- _ => point_widths
+ move || {
+ point_widths
.next()
.zip(prev_points.next())
.zip(alternator.next())
.map(|(((point, width), prev_point), alternator)| match alternator {
- LeftRightAlternator::Left => prev_point,
LeftRightAlternator::Right => point - width,
- }),
+ _ => prev_point,
+ })
}
};
let mut x = next_x().unwrap_or_default();
@@ -263,7 +257,7 @@ impl MathRun {
let mut x = Abs::zero();
let mut ascent = Abs::zero();
let mut descent = Abs::zero();
- let mut frame = Frame::new(Size::zero(), FrameKind::Soft);
+ let mut frame = Frame::soft(Size::zero());
let mut empty = true;
let finalize_frame = |frame: &mut Frame, x, ascent, descent| {
@@ -307,10 +301,8 @@ impl MathRun {
|| (class == MathClass::Relation
&& !iter.peek().map(is_relation).unwrap_or_default())
{
- let mut frame_prev = std::mem::replace(
- &mut frame,
- Frame::new(Size::zero(), FrameKind::Soft),
- );
+ let mut frame_prev =
+ std::mem::replace(&mut frame, Frame::soft(Size::zero()));
finalize_frame(&mut frame_prev, x, ascent, descent);
items.push(MathParItem::Frame(frame_prev));
@@ -352,8 +344,11 @@ impl> From for MathRun {
}
}
+/// An iterator that alternates between the `Left` and `Right` values, if the
+/// initial value is not `None`.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-enum LeftRightAlternator {
+pub enum LeftRightAlternator {
+ None,
Left,
Right,
}
@@ -364,6 +359,7 @@ impl Iterator for LeftRightAlternator {
fn next(&mut self) -> Option {
let r = Some(*self);
match self {
+ Self::None => {}
Self::Left => *self = Self::Right,
Self::Right => *self = Self::Left,
}
diff --git a/crates/typst/src/math/style.rs b/crates/typst/src/math/style.rs
index bd910a7987..332ce58665 100644
--- a/crates/typst/src/math/style.rs
+++ b/crates/typst/src/math/style.rs
@@ -300,7 +300,8 @@ pub fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char {
c,
'a'..='z' | 'ı' | 'ȷ' | 'A'..='Z' | 'α'..='ω' |
'∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ'
- ),
+ )
+ && matches!(variant, Sans | Serif),
);
if let Some(c) = basic_exception(c) {
@@ -444,6 +445,11 @@ fn latin_exception(
('Q', Bb, ..) => 'ℚ',
('R', Bb, ..) => 'ℝ',
('Z', Bb, ..) => 'ℤ',
+ ('D', Bb, _, true) => 'ⅅ',
+ ('d', Bb, _, true) => 'ⅆ',
+ ('e', Bb, _, true) => 'ⅇ',
+ ('i', Bb, _, true) => 'ⅈ',
+ ('j', Bb, _, true) => 'ⅉ',
('h', Serif, false, true) => 'ℎ',
('e', Cal, false, _) => 'ℯ',
('g', Cal, false, _) => 'ℊ',
@@ -462,15 +468,20 @@ fn greek_exception(
) -> Option {
use MathVariant::*;
let list = match c {
- 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡'],
- '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩'],
- '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃'],
- 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄'],
- 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅'],
- 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆'],
- 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇'],
- 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈'],
- 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉'],
+ 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'],
+ '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'],
+ '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'],
+ 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'],
+ 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'],
+ 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'],
+ 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'],
+ 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'],
+ 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'],
+ 'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'],
+ 'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'],
+ 'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'],
+ 'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'],
+ '∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'],
_ => return None,
};
@@ -480,6 +491,7 @@ fn greek_exception(
(Serif, true, true) => list[2],
(Sans, _, false) => list[3],
(Sans, _, true) => list[4],
+ (Bb, ..) => list[5],
_ => return None,
})
}
diff --git a/crates/typst/src/math/underover.rs b/crates/typst/src/math/underover.rs
index 4117342bd9..6be86d9f5f 100644
--- a/crates/typst/src/math/underover.rs
+++ b/crates/typst/src/math/underover.rs
@@ -3,7 +3,8 @@ use crate::foundations::{elem, Content, Packed, StyleChain};
use crate::layout::{Abs, Em, FixedAlignment, Frame, FrameItem, Point, Size};
use crate::math::{
alignments, scaled_font_size, style_cramped, style_for_subscript, AlignmentResult,
- FrameFragment, GlyphFragment, LayoutMath, MathContext, MathRun, Scaled,
+ FrameFragment, GlyphFragment, LayoutMath, LeftRightAlternator, MathContext, MathRun,
+ Scaled,
};
use crate::syntax::Span;
use crate::text::TextElem;
@@ -290,7 +291,8 @@ fn layout_underoverspreader(
baseline = rows.len() - 1;
}
- let frame = stack(rows, FixedAlignment::Center, gap, baseline);
+ let frame =
+ stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right);
ctx.push(FrameFragment::new(ctx, styles, frame).with_class(body_class));
Ok(())
@@ -298,28 +300,30 @@ fn layout_underoverspreader(
/// Stack rows on top of each other.
///
-/// Add a `gap` between each row and uses the baseline of the `baseline`th
-/// row for the whole frame.
+/// Add a `gap` between each row and uses the baseline of the `baseline`-th
+/// row for the whole frame. `alternator` controls the left/right alternating
+/// alignment behavior of `AlignPointElem` in the rows.
pub(super) fn stack(
rows: Vec,
align: FixedAlignment,
gap: Abs,
baseline: usize,
+ alternator: LeftRightAlternator,
) -> Frame {
let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
let AlignmentResult { points, width } = alignments(&rows);
let rows: Vec<_> = rows
.into_iter()
- .map(|row| row.into_line_frame(&points, align))
+ .map(|row| row.into_line_frame(&points, alternator))
.collect();
- let mut y = Abs::zero();
let mut frame = Frame::soft(Size::new(
width,
rows.iter().map(|row| row.height()).sum::()
+ rows.len().saturating_sub(1) as f64 * gap,
));
+ let mut y = Abs::zero();
for (i, row) in rows.into_iter().enumerate() {
let x = align.position(width - row.width());
let pos = Point::new(x, y);
diff --git a/crates/typst/src/model/footnote.rs b/crates/typst/src/model/footnote.rs
index 44942341b0..4945ebb18f 100644
--- a/crates/typst/src/model/footnote.rs
+++ b/crates/typst/src/model/footnote.rs
@@ -167,11 +167,6 @@ cast! {
/// This function is not intended to be called directly. Instead, it is used
/// in set and show rules to customize footnote listings.
///
-/// _Note:_ Set and show rules for `footnote.entry` must be defined at the
-/// beginning of the document in order to work correctly.
-/// See [here](https://github.com/typst/typst/issues/1348#issuecomment-1566316463)
-/// for more information.
-///
/// ```example
/// #show footnote.entry: set text(red)
///
@@ -179,6 +174,12 @@ cast! {
/// #footnote[It's down here]
/// has red text!
/// ```
+///
+/// _Note:_ Set and show rules for `footnote.entry` must be defined at the
+/// beginning of the document in order to work correctly. See [here][issue] for
+/// more information.
+///
+/// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
pub struct FootnoteEntry {
/// The footnote for this entry. It's location can be used to determine
diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs
index 6fb1f601f1..a85d1cdf5b 100644
--- a/crates/typst/src/model/outline.rs
+++ b/crates/typst/src/model/outline.rs
@@ -10,8 +10,10 @@ use crate::foundations::{
NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles,
};
use crate::introspection::{Counter, CounterKey, Locatable};
-use crate::layout::{BoxElem, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing};
-use crate::model::{Destination, HeadingElem, NumberingPattern, ParbreakElem, Refable};
+use crate::layout::{BoxElem, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing};
+use crate::model::{
+ Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable,
+};
use crate::syntax::Span;
use crate::text::{Lang, LinebreakElem, LocalName, Region, SpaceElem, TextElem};
use crate::util::{option_eq, NonZeroExt};
@@ -71,7 +73,6 @@ pub struct OutlineElem {
/// The outline's heading will not be numbered by default, but you can
/// force it to be with a show-set rule:
/// `{show outline: set heading(numbering: "1.")}`
- /// ```
#[default(Some(Smart::Auto))]
pub title: Option>,
@@ -265,6 +266,7 @@ impl ShowSet for Packed {
let mut out = Styles::new();
out.set(HeadingElem::set_outlined(false));
out.set(HeadingElem::set_numbering(None));
+ out.set(ParElem::set_first_line_indent(Em::new(0.0).into()));
out
}
}
diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs
index cc4a1fcb7b..f8a833aca3 100644
--- a/crates/typst/src/model/par.rs
+++ b/crates/typst/src/model/par.rs
@@ -96,6 +96,15 @@ pub struct ParElem {
#[resolve]
pub hanging_indent: Length,
+ /// Indicates wheter an overflowing line should be shrunk.
+ ///
+ /// This property is set to `false` on raw blocks, because shrinking a line
+ /// could visually break the indentation.
+ #[ghost]
+ #[internal]
+ #[default(true)]
+ pub shrink: bool,
+
/// The contents of the paragraph.
#[external]
#[required]
diff --git a/crates/typst/src/realize/process.rs b/crates/typst/src/realize/process.rs
index 8d3d01a614..a7bb7d1dd5 100644
--- a/crates/typst/src/realize/process.rs
+++ b/crates/typst/src/realize/process.rs
@@ -198,9 +198,14 @@ fn prepare(
// Generate a location for the element, which uniquely identifies it in
// the document. This has some overhead, so we only do it for elements
// that are explicitly marked as locatable and labelled elements.
- if target.can::() || target.label().is_some() {
+ //
+ // The element could already have a location even if it is not prepared
+ // when it stems from a query.
+ let mut located = target.location().is_some();
+ if !located && (target.can::() || target.label().is_some()) {
let location = engine.locator.locate(hash128(&target));
target.set_location(location);
+ located = true;
}
// Apply built-in show-set rules. User-defined show-set rules are already
@@ -220,24 +225,24 @@ fn prepare(
// available in rules.
target.materialize(styles.chain(map));
+ if located {
+ // Apply metadata to be able to find the element in the frames. Do this
+ // after synthesis and materialization, so that it includes the
+ // synthesized fields. Do it before marking as prepared so that show-set
+ // rules will apply to this element when queried. This adds a style to
+ // the whole element's subtree identifying it as belonging to the
+ // element.
+ map.set(MetaElem::set_data(smallvec![Meta::Elem(target.clone())]));
+ }
+
// Ensure that this preparation only runs once by marking the element as
// prepared.
target.mark_prepared();
- // Apply metadata be able to find the element in the frames.
- // Do this after synthesis, so that it includes the synthesized fields.
- if target.location().is_some() {
- // Add a style to the whole element's subtree identifying it as
- // belonging to the element.
- map.set(MetaElem::set_data(smallvec![Meta::Elem(target.clone())]));
-
- // Return an extra meta elem that will be attached so that the metadata
- // styles are not lost in case the element's show rule results in
- // nothing.
- return Ok(Some(Packed::new(MetaElem::new()).spanned(target.span())));
- }
-
- Ok(None)
+ // If the element is located, return an extra meta elem that will be
+ // attached so that the metadata styles are not lost in case the element's
+ // show rule results in nothing.
+ Ok(located.then(|| Packed::new(MetaElem::new()).spanned(target.span())))
}
/// Apply a step.
diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs
index bcfca08d1a..b06ec373cf 100644
--- a/crates/typst/src/text/mod.rs
+++ b/crates/typst/src/text/mod.rs
@@ -102,11 +102,16 @@ pub struct TextElem {
/// - In the web app, you can see the list of available fonts by clicking on
/// the "Ag" button. You can provide additional fonts by uploading `.ttf`
/// or `.otf` files into your project. They will be discovered
- /// automatically.
- ///
- /// - Locally, Typst uses your installed system fonts. In addition, you can
- /// use the `--font-path` argument or `TYPST_FONT_PATHS` environment
- /// variable to add directories that should be scanned for fonts.
+ /// automatically. The priority is: project fonts > server fonts.
+ ///
+ /// - Locally, Typst uses your installed system fonts or embedded fonts in
+ /// the CLI, which are `Linux Libertine`, `New Computer Modern`,
+ /// `New Computer Modern Math`, and `DejaVu Sans Mono`. In addition, you
+ /// can use the `--font-path` argument or `TYPST_FONT_PATHS` environment
+ /// variable to add directories that should be scanned for fonts. The
+ /// priority is: `--font-paths` > system fonts > embedded fonts. Run
+ /// `typst fonts` to see the fonts that Typst has discovered on your
+ /// system.
///
/// ```example
/// #set text(font: "PT Sans")
diff --git a/crates/typst/src/text/raw.rs b/crates/typst/src/text/raw.rs
index 424adb7220..58e64ce516 100644
--- a/crates/typst/src/text/raw.rs
+++ b/crates/typst/src/text/raw.rs
@@ -16,7 +16,7 @@ use crate::foundations::{
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, Value,
};
use crate::layout::{BlockElem, Em, HAlignment};
-use crate::model::Figurable;
+use crate::model::{Figurable, ParElem};
use crate::syntax::{split_newlines, LinkedNode, Span, Spanned};
use crate::text::{
FontFamily, FontList, Hyphenate, Lang, LinebreakElem, LocalName, Region,
@@ -59,6 +59,8 @@ type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec);
/// # Syntax
/// This function also has dedicated syntax. You can enclose text in 1 or 3+
/// backticks (`` ` ``) to make it raw. Two backticks produce empty raw text.
+/// This works both in markup and code.
+///
/// When you use three or more backticks, you can additionally specify a
/// language tag for syntax highlighting directly after the opening backticks.
/// Within raw blocks, everything (except for the language tag, if applicable)
@@ -300,22 +302,7 @@ impl Packed {
#[comemo::memoize]
fn highlight(&self, styles: StyleChain) -> Vec> {
let elem = self.as_ref();
-
- let text = elem.text();
- let lines = match text {
- RawContent::Lines(lines) if !lines.iter().any(|(s, _)| s.contains('\t')) => {
- lines.clone()
- }
- _ => {
- let mut text = text.get();
- if text.contains('\t') {
- let tab_size = RawElem::tab_size_in(styles);
- text = align_tabs(&text, tab_size);
- }
- let lines = split_newlines(&text);
- lines.into_iter().map(|line| (line.into(), self.span())).collect()
- }
- };
+ let lines = preprocess(elem.text(), styles, self.span());
let count = lines.len() as i64;
let lang = elem
@@ -339,7 +326,8 @@ impl Packed {
let mut seq = vec![];
if matches!(lang.as_deref(), Some("typ" | "typst" | "typc")) {
- let text = text.get();
+ let text =
+ lines.iter().map(|(s, _)| s.clone()).collect::>().join("\n");
let root = match lang.as_deref() {
Some("typc") => syntax::parse_code(&text),
_ => syntax::parse(&text),
@@ -454,13 +442,17 @@ impl Show for Packed {
}
impl ShowSet for Packed {
- fn show_set(&self, _: StyleChain) -> Styles {
+ fn show_set(&self, styles: StyleChain) -> Styles {
let mut out = Styles::new();
out.set(TextElem::set_overhang(false));
+ out.set(TextElem::set_lang(Lang::ENGLISH));
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false))));
out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
out.set(SmartQuoteElem::set_enabled(false));
+ if self.block(styles) {
+ out.set(ParElem::set_shrink(false));
+ }
out
}
}
@@ -683,6 +675,28 @@ impl<'a> ThemedHighlighter<'a> {
}
}
+fn preprocess(
+ text: &RawContent,
+ styles: StyleChain,
+ span: Span,
+) -> EcoVec<(EcoString, Span)> {
+ if let RawContent::Lines(lines) = text {
+ if lines.iter().all(|(s, _)| !s.contains('\t')) {
+ return lines.clone();
+ }
+ }
+
+ let mut text = text.get();
+ if text.contains('\t') {
+ let tab_size = RawElem::tab_size_in(styles);
+ text = align_tabs(&text, tab_size);
+ }
+ split_newlines(&text)
+ .into_iter()
+ .map(|line| (line.into(), span))
+ .collect()
+}
+
/// Style a piece of text with a syntect style.
fn styled(
piece: &str,
diff --git a/crates/typst/src/text/smartquote.rs b/crates/typst/src/text/smartquote.rs
index dbcca6dc34..4bb5ca019f 100644
--- a/crates/typst/src/text/smartquote.rs
+++ b/crates/typst/src/text/smartquote.rs
@@ -2,7 +2,9 @@ use ecow::EcoString;
use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, StrResult};
-use crate::foundations::{array, cast, dict, elem, Array, Dict, FromValue, Smart, Str};
+use crate::foundations::{
+ array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str,
+};
use crate::layout::Dir;
use crate::syntax::is_newline;
use crate::text::{Lang, Region};
@@ -26,7 +28,7 @@ use crate::text::{Lang, Region};
/// # Syntax
/// This function also has dedicated syntax: The normal quote characters
/// (`'` and `"`). Typst automatically makes your quotes smart.
-#[elem(name = "smartquote")]
+#[elem(name = "smartquote", PlainText)]
pub struct SmartQuoteElem {
/// Whether this should be a double quote.
#[default(true)]
@@ -85,6 +87,16 @@ pub struct SmartQuoteElem {
pub quotes: Smart,
}
+impl PlainText for Packed {
+ fn plain_text(&self, text: &mut EcoString) {
+ if self.double.unwrap_or(true) {
+ text.push_str("\"");
+ } else {
+ text.push_str("'");
+ }
+ }
+}
+
/// State machine for smart quote substitution.
#[derive(Debug, Clone)]
pub struct SmartQuoter {
diff --git a/crates/typst/src/visualize/color.rs b/crates/typst/src/visualize/color.rs
index 365a069554..185e990b7a 100644
--- a/crates/typst/src/visualize/color.rs
+++ b/crates/typst/src/visualize/color.rs
@@ -32,7 +32,7 @@ pub type Luma = palette::luma::Lumaa;
/// This is a minimal CMYK profile that only contains the necessary information
/// to convert from CMYK to RGB. It is based on the CGATS TR 001-1995
/// specification. See
-/// https://github.com/saucecontrol/Compact-ICC-Profiles#cmyk.
+/// .
static CMYK_TO_XYZ: Lazy> =
Lazy::new(|| Profile::new_from_slice(typst_assets::icc::CMYK_TO_XYZ, false).unwrap());
@@ -117,13 +117,12 @@ static TO_SRGB: Lazy = Lazy::new(|| {
/// columns: 9,
/// gutter: 10pt,
/// ..colors.map(name => {
-/// let c = eval(name)
-/// let cp = c.components()
-/// let x = cp.sum() / cp.len()
-/// set text(fill: white) if x < 50%
-/// set square(stroke: black) if c == white
+/// let col = eval(name)
+/// let luminance = luma(col).components().first()
+/// set text(fill: white) if luminance < 50%
+/// set square(stroke: black) if col == white
/// set align(center + horizon)
-/// square(size: 50pt, fill: c, name)
+/// square(size: 50pt, fill: col, name)
/// })
/// )
/// ```
@@ -736,7 +735,7 @@ impl Color {
///
/// ```example
/// // note that the alpha component is included by default
- /// #(rgb(40%, 60%, 80%).components() == (40%, 60%, 80%, 100%))
+ /// #rgb(40%, 60%, 80%).components()
/// ```
#[func]
pub fn components(
diff --git a/crates/typst/src/visualize/image/svg.rs b/crates/typst/src/visualize/image/svg.rs
index 9685e45478..50c5cb3ad7 100644
--- a/crates/typst/src/visualize/image/svg.rs
+++ b/crates/typst/src/visualize/image/svg.rs
@@ -4,8 +4,9 @@ use std::sync::Arc;
use comemo::Tracked;
use ecow::EcoString;
+use once_cell::sync::Lazy;
use siphasher::sip128::Hasher128;
-use usvg::{Node, PostProcessingSteps, TreeParsing, TreePostProc};
+use usvg::{ImageHrefResolver, Node, PostProcessingSteps, TreeParsing, TreePostProc};
use crate::diag::{format_xml_like_error, StrResult};
use crate::foundations::Bytes;
@@ -30,7 +31,7 @@ impl SvgImage {
/// Decode an SVG image without fonts.
#[comemo::memoize]
pub fn new(data: Bytes) -> StrResult {
- let tree = usvg::Tree::from_data(&data, &options()).map_err(format_usvg_error)?;
+ let tree = usvg::Tree::from_data(&data, &OPTIONS).map_err(format_usvg_error)?;
Ok(Self(Arc::new(Repr {
data,
size: tree_size(&tree),
@@ -48,7 +49,7 @@ impl SvgImage {
families: &[String],
) -> StrResult {
let mut tree =
- usvg::Tree::from_data(&data, &options()).map_err(format_usvg_error)?;
+ usvg::Tree::from_data(&data, &OPTIONS).map_err(format_usvg_error)?;
let mut font_hash = 0;
if tree.has_text_nodes() {
let (fontdb, hash) = load_svg_fonts(world, &mut tree, families);
@@ -122,20 +123,27 @@ impl Hash for Repr {
}
/// The conversion options.
-fn options() -> usvg::Options {
+static OPTIONS: Lazy = Lazy::new(|| usvg::Options {
// Disable usvg's default to "Times New Roman". Instead, we default to
// the empty family and later, when we traverse the SVG, we check for
// empty and non-existing family names and replace them with the true
- // fallback family. This way, we can memoize SVG decoding with and without
- // fonts if the SVG does not contain text.
- usvg::Options {
- font_family: String::new(),
- // We override the DPI here so that we get the correct the size when
- // scaling the image to its natural size.
- dpi: Image::DEFAULT_DPI as f32,
- ..Default::default()
- }
-}
+ // fallback family. This way, we can memoize SVG decoding with and
+ // without fonts if the SVG does not contain text.
+ font_family: String::new(),
+
+ // We override the DPI here so that we get the correct the size when
+ // scaling the image to its natural size.
+ dpi: Image::DEFAULT_DPI as f32,
+
+ // Override usvg's resource loading defaults.
+ resources_dir: None,
+ image_href_resolver: ImageHrefResolver {
+ resolve_data: ImageHrefResolver::default_data_resolver(),
+ resolve_string: Box::new(|_, _| None),
+ },
+
+ ..Default::default()
+});
/// Discover and load the fonts referenced by an SVG.
fn load_svg_fonts(
diff --git a/crates/typst/src/visualize/path.rs b/crates/typst/src/visualize/path.rs
index 5ee9922f23..170a1386ab 100644
--- a/crates/typst/src/visualize/path.rs
+++ b/crates/typst/src/visualize/path.rs
@@ -6,8 +6,7 @@ use crate::foundations::{
array, cast, elem, Array, Packed, Reflect, Resolve, Smart, StyleChain,
};
use crate::layout::{
- Abs, Axes, Fragment, Frame, FrameItem, LayoutMultiple, Length, Point, Regions, Rel,
- Size,
+ Abs, Axes, Frame, FrameItem, LayoutSingle, Length, Point, Regions, Rel, Size,
};
use crate::visualize::{FixedStroke, Geometry, Paint, Shape, Stroke};
@@ -26,7 +25,7 @@ use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
/// ((50%, 0pt), (40pt, 0pt)),
/// )
/// ```
-#[elem(LayoutMultiple)]
+#[elem(LayoutSingle)]
pub struct PathElem {
/// How to fill the path.
///
@@ -70,14 +69,14 @@ pub struct PathElem {
pub vertices: Vec,
}
-impl LayoutMultiple for Packed {
+impl LayoutSingle for Packed {
#[typst_macros::time(name = "path", span = self.span())]
fn layout(
&self,
_: &mut Engine,
styles: StyleChain,
regions: Regions,
- ) -> SourceResult {
+ ) -> SourceResult {
let resolve = |axes: Axes>| {
axes.resolve(styles)
.zip_map(regions.base(), Rel::relative_to)
@@ -89,7 +88,7 @@ impl LayoutMultiple for Packed {
let mut size = Size::zero();
if points.is_empty() {
- return Ok(Fragment::frame(Frame::soft(size)));
+ return Ok(Frame::soft(size));
}
// Only create a path if there are more than zero points.
@@ -148,8 +147,7 @@ impl LayoutMultiple for Packed {
let mut frame = Frame::soft(size);
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
frame.push(Point::zero(), FrameItem::Shape(shape, self.span()));
-
- Ok(Fragment::frame(frame))
+ Ok(frame)
}
}
diff --git a/crates/typst/src/visualize/shape.rs b/crates/typst/src/visualize/shape.rs
index 4567739e9b..e37e5f90cd 100644
--- a/crates/typst/src/visualize/shape.rs
+++ b/crates/typst/src/visualize/shape.rs
@@ -1108,7 +1108,7 @@ impl ControlPoints {
+ 2.0 * (o.y - c_i.y).to_raw() * (c_i.y - c_o.y).to_raw();
let c = (c_i.x - c_o.x).to_raw().powi(2) + (c_i.y - c_o.y).to_raw().powi(2)
- r.to_raw().powi(2);
- let t = (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a);
+ let t = (-b + (b * b - 4.0 * a * c).max(0.0).sqrt()) / (2.0 * a);
c_i + t * (o - c_i)
}
diff --git a/docs/Cargo.toml b/docs/Cargo.toml
index 27fe427c06..2e14a5ee2f 100644
--- a/docs/Cargo.toml
+++ b/docs/Cargo.toml
@@ -6,13 +6,10 @@ authors = { workspace = true }
edition = { workspace = true }
publish = false
-[lib]
-doctest = false
-bench = false
-
[[bin]]
name = "typst-docs"
required-features = ["cli"]
+doc = false
[features]
default = ["cli"]
diff --git a/docs/changelog.md b/docs/changelog.md
index 34652f4243..7821360533 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -5,6 +5,62 @@ description: |
---
# Changelog
+## Version 0.11.1 (May 17, 2024) { #v0.11.1 }
+- Security
+ - Fixed a vulnerability where image files at known paths could be embedded
+ into the PDF even if they were outside of the project directory
+
+- Bibliography
+ - Fixed et-al handling in subsequent citations
+ - Fixed suppression of title for citations and bibliography references with no
+ author
+ - Fixed handling of initials in citation styles without a delimiter
+ - Fixed bug with citations in footnotes
+
+- Text and Layout
+ - Fixed interaction of [`first-line-indent`]($par.first-line-indent) and
+ [`outline`]
+ - Fixed compression of CJK punctuation marks at line start and end
+ - Fixed handling of [rectangles]($rect) with negative dimensions
+ - Fixed layout of [`path`] in explicitly sized container
+ - Fixed broken [`raw`] text in right-to-left paragraphs
+ - Fixed tab rendering in `raw` text with language `typ` or `typc`
+ - Fixed highlighting of multi-line `raw` text enclosed by single backticks
+ - Fixed indentation of overflowing lines in `raw` blocks
+ - Fixed extra space when `raw` text ends with a backtick
+
+- Math
+ - Fixed broken [equations]($math.equation) in right-to-left paragraphs
+ - Fixed missing [blackboard bold]($math.bb) letters
+ - Fixed error on empty arguments in 2D math argument list
+ - Fixed stretching via [`mid`]($math.mid) for various characters
+ - Fixed that alignment points in equations were affected by `{set align(..)}`
+
+- Export
+ - Fixed [smart quotes]($smartquote) in PDF outline
+ - Fixed [patterns]($pattern) with spacing in PDF
+ - Fixed wrong PDF page labels when [page numbering]($page.numbering) was
+ disabled after being previously enabled
+
+- Scripting
+ - Fixed overflow for large numbers in external data files (by converting to
+ floats instead)
+ - Fixed [`{str.trim(regex, at: end)}`]($str.trim) when the whole string is
+ matched
+
+- Miscellaneous
+ - Fixed deformed strokes for specific shapes and thicknesses
+ - Fixed newline handling in code mode: There can now be comments within
+ chained method calls and between an `if` branch and the `else` keyword
+ - Fixed inefficiency with incremental reparsing
+ - Fixed autocompletions for relative file imports
+ - Fixed crash in autocompletion handler
+ - Fixed a bug where the path and entrypoint printed by `typst init` were not
+ properly escaped
+ - Fixed various documentation errors
+
+
+
## Version 0.11.0 (March 15, 2024) { #v0.11.0 }
- Tables (thanks to [@PgBiel](https://github.com/PgBiel))
- Tables are now _much_ more flexible, read the new
diff --git a/docs/guides/tables.md b/docs/guides/tables.md
index df6f0ea794..dbeb3955a4 100644
--- a/docs/guides/tables.md
+++ b/docs/guides/tables.md
@@ -115,7 +115,7 @@ limitation of Typst that will be fixed in a future release.
Congratulations, you have created your first table! Now you can proceed to
[change column sizes](#column-sizes), [adjust the strokes](#strokes), [add
-striped rows](#striped-rows-and-columns), and more!
+striped rows](#fills), and more!
## How to change the column sizes? { #column-sizes }
If you create a table and specify the number of columns, Typst will make each
@@ -997,7 +997,7 @@ by the `table.cell` set rule). Finally, we align all the content of all table
cells in the body in the center. If you want to know more about the functions
passed to `align`, `stroke`, and `fill`, you can check out the sections on
[alignment], [strokes](#stroke-functions), and [striped
-tables](#striped-rows-and-columns).
+tables](#fills).
This table would be a great candidate for fully automated generation from an
external data source! Check out the [section about importing
@@ -1311,7 +1311,7 @@ row. In our table, we have added our custom header that tells the reader that
we've applied a logarithm to the values. Then, we spread the flattened data as
above.
-We also styled the table with [stripes](#striped-rows-and-columns), a
+We also styled the table with [stripes](#fills), a
[horizontal line](#individual-lines) below the first row, [aligned](#alignment)
everything to the right, and emboldened the first column. Click on the links to
go to the relevant guide sections and see how it's done!
diff --git a/docs/i18n/category/data-loading-en.yaml b/docs/i18n/category/data-loading-en.yaml
index 3d1ab09a31..fcf8d9b15f 100644
--- a/docs/i18n/category/data-loading-en.yaml
+++ b/docs/i18n/category/data-loading-en.yaml
@@ -96,6 +96,8 @@ children:
equivalents, null-values (null, ~ or empty ``) will be converted into
none , and numbers will be converted to floats or integers depending on
whether they are whole numbers.
+ Be aware that integers larger than 263 -1 will be converted to
+ floating point numbers, which may result in an approximative value.
example: null
self: false
params:
@@ -394,12 +396,16 @@ children:
contextual: false
details: |-
Reads structured data from a JSON file.
- The file must contain a valid JSON object or array. JSON objects will be
- converted into Typst dictionaries, and JSON arrays will be converted into
- Typst arrays. Strings and booleans will be converted into the Typst
- equivalents, null will be converted into none , and numbers will be
- converted to floats or integers depending on whether they are whole numbers.
- The function returns a dictionary or an array, depending on the JSON file.
+ The file must contain a valid JSON value, such as object or array. JSON
+ objects will be converted into Typst dictionaries, and JSON arrays will be
+ converted into Typst arrays. Strings and booleans will be converted into the
+ Typst equivalents, null will be converted into none , and numbers will
+ be converted to floats or integers depending on whether they are whole
+ numbers.
+ Be aware that integers larger than 263 -1 will be converted to
+ floating point numbers, which may result in an approximative value.
+ The function returns a dictionary, an array or, depending on the JSON file,
+ another JSON data type.
The JSON files in the example contain objects with the keys temperature,
unit, and weather.
Example
@@ -918,6 +924,8 @@ children:
none , and numbers will be converted to floats or integers depending on
whether they are whole numbers. Custom YAML tags are ignored, though the
loaded value will still be present.
+ Be aware that integers larger than 263 -1 will be converted to
+ floating point numbers, which may give an approximative value.
The YAML files in the example contain objects with authors as keys,
each with a sequence of their own submapping with the keys
"title" and "published"
diff --git a/docs/i18n/category/foundations-en.yaml b/docs/i18n/category/foundations-en.yaml
index da827581d0..e3c91c72af 100644
--- a/docs/i18n/category/foundations-en.yaml
+++ b/docs/i18n/category/foundations-en.yaml
@@ -31,6 +31,10 @@ body:
route: /docs/reference/foundations/assert/
oneliner: Ensures that a condition is fulfilled.
code: true
+ - name: auto
+ route: /docs/reference/foundations/auto/
+ oneliner: A value that indicates a smart default.
+ code: true
- name: bool
route: /docs/reference/foundations/bool/
oneliner: A type with two states.
@@ -83,6 +87,10 @@ body:
route: /docs/reference/foundations/module/
oneliner: An evaluated module, either built-in or resulting from a file.
code: true
+ - name: none
+ route: /docs/reference/foundations/none/
+ oneliner: A value that indicates the absence of any other value.
+ code: true
- name: panic
route: /docs/reference/foundations/panic/
oneliner: Fails with an error.
@@ -1709,6 +1717,31 @@ children:
returns: []
scope: []
children: []
+- route: /docs/reference/foundations/auto/
+ title: Auto
+ description: Documentation for the Auto type.
+ part: null
+ outline:
+ - id: summary
+ name: Summary
+ children: []
+ body:
+ kind: type
+ content:
+ name: auto
+ title: Auto
+ keywords: []
+ oneliner: A value that indicates a smart default.
+ details: |-
+ A value that indicates a smart default.
+ The auto type has exactly one value: auto .
+ Parameters that support the auto value have some smart default or
+ contextual behaviour. A good example is the text direction
+ parameter. Setting it to auto lets Typst automatically determine the
+ direction from the text language .
+ constructor: null
+ scope: []
+ children: []
- route: /docs/reference/foundations/bool/
title: Boolean
description: Documentation for the Boolean type.
@@ -5851,6 +5884,36 @@ children:
constructor: null
scope: []
children: []
+- route: /docs/reference/foundations/none/
+ title: None
+ description: Documentation for the None type.
+ part: null
+ outline:
+ - id: summary
+ name: Summary
+ children: []
+ - id: example
+ name: Example
+ children: []
+ body:
+ kind: type
+ content:
+ name: none
+ title: None
+ keywords: []
+ oneliner: A value that indicates the absence of any other value.
+ details: |-
+ A value that indicates the absence of any other value.
+ The none type has exactly one value: none .
+ When inserted into the document, it is not visible. This is also the value
+ that is produced by empty code blocks. It can be
+ joined with any value, yielding the other value.
+ Example
+
+ constructor: null
+ scope: []
+ children: []
- route: /docs/reference/foundations/panic/
title: Panic
description: Documentation for the `panic` function.
@@ -7300,7 +7363,8 @@ children:
self: true
params:
- name: pattern
- details: The pattern to search for.
+ details: The pattern to search for. If none ,
+ trims white spaces.
example: null
types:
- none
@@ -7315,8 +7379,8 @@ children:
settable: false
- name: at
details: |-
- Can be start or end to only trim the start or end of the string.
- If omitted, both sides are trimmed.
+ Can be start or end to only trim the start or end of the
+ string. If omitted, both sides are trimmed.
example: null
types:
- alignment
diff --git a/docs/i18n/category/introspection-en.yaml b/docs/i18n/category/introspection-en.yaml
index dbe647d66c..33aca181da 100644
--- a/docs/i18n/category/introspection-en.yaml
+++ b/docs/i18n/category/introspection-en.yaml
@@ -594,8 +594,8 @@ children:
page number, typically you want the logical page number that may, for
instance, have been reset after a preface.
Examples
- Determining the current position in the document in combination with
- locate :
+ Determining the current position in the document in combination with the
+ position method:
# context [
I am located at
# here ( ) . position ( )
@@ -848,7 +848,8 @@ children:
details: |-
Exposes a value to the query system without producing visible content.
This element can be retrieved with the query function and from the
- command line with typst query . Its
+ command line with
+ typst query . Its
purpose is to expose an arbitrary value to the introspection system. To
identify a metadata value among others, you can attach a label to it and
query for that label.
diff --git a/docs/i18n/category/layout-en.yaml b/docs/i18n/category/layout-en.yaml
index 145ebc9597..1b9d632b76 100644
--- a/docs/i18n/category/layout-en.yaml
+++ b/docs/i18n/category/layout-en.yaml
@@ -3918,8 +3918,10 @@ children:
- name: alignment
details: |-
Relative to which position in the parent container to place the content.
- Cannot be auto if float is false and must be either
- auto , top, or bottom if float is true .
+
+ If float is false , then this can be any alignment other than auto .
+ If float is true , then this must be auto , top, or bottom.
+
When an axis of the page is auto sized, all alignments relative to
that axis will be ignored, instead, the item will be placed in the
origin of the axis.
@@ -3985,6 +3987,9 @@ children:
place ( center, dx: amount - 32pt , dy: amount) [ A]
}
+ This does not affect the layout of in-flow content.
+ In other words, the placed content is treated as if it
+ were wrapped in a move element.
types:
- relative
strings: []
@@ -3995,7 +4000,11 @@ children:
variadic: false
settable: true
- name: dy
- details: The vertical displacement of the placed content.
+ details: |-
+ The vertical displacement of the placed content.
+ This does not affect the layout of in-flow content.
+ In other words, the placed content is treated as if it
+ were wrapped in a move element.
example: null
types:
- relative
diff --git a/docs/i18n/category/model-en.yaml b/docs/i18n/category/model-en.yaml
index 2dd68ba825..566adc4cbc 100644
--- a/docs/i18n/category/model-en.yaml
+++ b/docs/i18n/category/model-en.yaml
@@ -1725,10 +1725,6 @@ children:
An entry in a footnote list.
This function is not intended to be called directly. Instead, it is used
in set and show rules to customize footnote listings.
- Note: Set and show rules for footnote.entry must be defined at the
- beginning of the document in order to work correctly.
- See here
- for more information.
example: |-
# show footnote. entry : set text ( red)
@@ -1736,6 +1732,9 @@ children:
# footnote [ It's down here]
has red text!
+ Note: Set and show rules for footnote.entry must be defined at the
+ beginning of the document in order to work correctly. See here for
+ more information.
self: false
params:
- name: note
@@ -2810,7 +2809,7 @@ children:
The outline's heading will not be numbered by default, but you can
force it to be with a show-set rule:
show outline : set heading ( numbering: "1." )
- example:
+ example: null
types:
- none
- auto
diff --git a/docs/i18n/category/text-en.yaml b/docs/i18n/category/text-en.yaml
index c9963cf17c..4c335ffc72 100644
--- a/docs/i18n/category/text-en.yaml
+++ b/docs/i18n/category/text-en.yaml
@@ -719,7 +719,8 @@ children:
Syntax
This function also has dedicated syntax. You can enclose text in 1 or 3+
backticks (`) to make it raw. Two backticks produce empty raw text.
- When you use three or more backticks, you can additionally specify a
+ This works both in markup and code.
+ When you use three or more backticks, you can additionally specify a
language tag for syntax highlighting directly after the opening backticks.
Within raw blocks, everything (except for the language tag, if applicable)
is rendered as is, in particular, there are no escape sequences.
@@ -1738,12 +1739,17 @@ children:
In the web app, you can see the list of available fonts by clicking on
the "Ag" button. You can provide additional fonts by uploading .ttf
or .otf files into your project. They will be discovered
- automatically.
+ automatically. The priority is: project fonts > server fonts.
- Locally, Typst uses your installed system fonts. In addition, you can
- use the --font-path argument or TYPST_FONT_PATHS environment
- variable to add directories that should be scanned for fonts.
+ Locally, Typst uses your installed system fonts or embedded fonts in
+ the CLI, which are Linux Libertine, New Computer Modern,
+ New Computer Modern Math, and DejaVu Sans Mono. In addition, you
+ can use the --font-path argument or TYPST_FONT_PATHS environment
+ variable to add directories that should be scanned for fonts. The
+ priority is: --font-paths > system fonts > embedded fonts. Run
+ typst fonts to see the fonts that Typst has discovered on your
+ system.
example: |-
diff --git a/docs/i18n/category/visualize-en.yaml b/docs/i18n/category/visualize-en.yaml
index 9ac2243279..0046404d19 100644
--- a/docs/i18n/category/visualize-en.yaml
+++ b/docs/i18n/category/visualize-en.yaml
@@ -1378,8 +1378,8 @@ children:
component is included.
example: |-
- # ( rgb ( 40% , 60% , 80% ) . components ( ) == ( 40% , 60% , 80% , 100% ) )
-
+ # rgb ( 40% , 60% , 80% ) . components ( )
+
self: true
params:
- name: alpha
diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md
index 251514ada2..1604e1031e 100644
--- a/docs/reference/syntax.md
+++ b/docs/reference/syntax.md
@@ -48,10 +48,10 @@ Typstは、最も一般的な文書要素に対する組み込みのマークア
| 数式 | `[$x^2$]` | [Math]($category/math) |
| 改行 | `[\]` | [`linebreak`]($linebreak) |
| スマートクオート | `['single' or "double"]` | [`smartquote`]($smartquote) |
-| 短縮記号 | `[~, ---]` | [Symbols]($category/symbols/sym) |
+| 短縮記号 | `[~]`, `[---]` | [Symbols]($category/symbols/sym) |
| コード構文 | `[#rect(width: 1cm)]` | [Scripting]($scripting/#expressions) |
-| 文字エスケープ | `[Tweet at us \#ad]` | [Below]($category/syntax/#escapes) |
-| コメント | `[/* block */, // line]` | [Below]($category/syntax/#comments) |
+| 文字エスケープ | `[Tweet at us \#ad]` | [Below]($category/syntax/#escapes) |
+| コメント | `[/* block */]`, `[// line]` | [Below]($category/syntax/#comments) |
## 数式モード { #math }
@@ -74,12 +74,12 @@ Typstは、最も一般的な文書要素に対する組み込みのマークア
| 変数アクセス | `[$#x$, $pi$]` | [Math]($category/math) |
| フィールドアクセス | `[$arrow.r.long$]` | [Scripting]($scripting/#fields) |
| 暗黙の乗算 | `[$x y$]` | [Math]($category/math) |
-| 短縮記号 | `[$->, !=$]` | [Symbols]($category/symbols/sym) |
+| 短縮記号 | `[$->$]`, `[$!=$]` | [Symbols]($category/symbols/sym) |
| 数式内のテキスト/文字列 | `[$a "is natural"$]` | [Math]($category/math) |
| 数式関数呼び出し | `[$floor(x)$]` | [Math]($category/math) |
| コード構文 | `[$#rect(width: 1cm)$]` | [Scripting]($scripting/#expressions) |
-| 文字エスケープ | `[$x\^2$]` | [Below]($category/syntax/#escapes) |
-| コメント | `[$/* comment */$]` | [Below]($category/syntax/#comments) |
+| 文字エスケープ | `[$x\^2$]` | [Below]($category/syntax/#escapes) |
+| コメント | `[$/* comment */$]` | [Below]($category/syntax/#comments) |
## コードモード { #code }
@@ -87,21 +87,21 @@ Typstは、最も一般的な文書要素に対する組み込みのマークア
多くの構文要素は式に特有のものです。
以下に、コードモードで利用可能なすべての構文の一覧表を示します。
-| 名称 | 例 | 参照 |
-| -------------------------- | ----------------------------- | ---------------------------------------- |
-| none | `{none}` | [`none`]($reference/foundations/none) |
-| 自動 | `{auto}` | [`auto`]($reference/foundations/auto) |
-| ブール値 | `{false}`, `{true}` | [`bool`]($reference/foundations/bool) |
-| 整数 | `{10}`, `{0xff}` | [`int`]($reference/foundations/int) |
-| 浮動小数点数 | `{3.14}`, `{1e5}` | [`float`]($reference/foundations/float) |
-| 長さ | `{2pt}`, `{3mm}`, `{1em}`, .. | [`length`]($reference/layout/length) |
-| 角度 | `{90deg}`, `{1rad}` | [`angle`]($reference/layout/angle) |
+| 名称 | 例 | 参照 |
+| -------------------------- | ----------------------------- | ------------------------------------- |
+| none | `{none}` | [`none`]($reference/foundations/none) |
+| 自動 | `{auto}` | [`auto`]($reference/foundations/auto) |
+| ブール値 | `{false}`, `{true}` | [`bool`]($reference/foundations/bool) |
+| 整数 | `{10}`, `{0xff}` | [`int`]($reference/foundations/int) |
+| 浮動小数点数 | `{3.14}`, `{1e5}` | [`float`]($reference/foundations/float) |
+| 長さ | `{2pt}`, `{3mm}`, `{1em}`, .. | [`length`]($reference/layout/length) |
+| 角度 | `{90deg}`, `{1rad}` | [`angle`]($reference/layout/angle) |
| 比率 | `{2fr}` | [`fraction`]($reference/layout/fraction) |
-| 割合 | `{50%}` | [`ratio`]($reference/layout/ratio) |
-| 文字列 | `{"hello"}` | [`str`]($reference/foundations/str) |
-| ラベル | `{}` | [`label`]($reference/foundations/label) |
-| 数式 | `[$x^2$]` | [Math]($category/math) |
-| rawテキスト | ``[`print(1)`]`` | [`raw`]($reference/text/raw) |
+| 割合 | `{50%}` | [`ratio`]($reference/layout/ratio) |
+| 文字列 | `{"hello"}` | [`str`]($reference/foundations/str) |
+| ラベル | `{}` | [`label`]($reference/foundations/label) |
+| 数式 | `[$x^2$]` | [Math]($category/math) |
+| rawテキスト | ``[`print(1)`]`` | [`raw`]($reference/text/raw) |
| 変数アクセス | `{x}` | [Scripting]($scripting/#blocks) |
| コードブロック | `{{ let x = 1; x + 2 }}` | [Scripting]($scripting/#blocks) |
| コンテンツブロック | `{[*Hello*]}` | [Scripting]($scripting/#blocks) |
@@ -120,7 +120,7 @@ Typstは、最も一般的な文書要素に対する組み込みのマークア
| 名前付き関数 | `{let f(x) = 2 * x}` | [Function]($type/$function) |
| setルール | `{set text(14pt)}` | [Styling]($styling/#set-rules) |
| set-ifルール | `{set text(..) if .. }` | [Styling]($styling/#set-rules) |
-| show-setルール | `{show heading: set block(..)}` | [Styling]($styling/#show-rules) |
+| show-setルール | `{show par: set block(..)}` | [Styling]($styling/#show-rules) |
| 関数付きshowルール | `{show raw: it => {..}}` | [Styling]($styling/#show-rules) |
| show-everythingルール | `{show: columns.with(2)}` | [Styling]($styling/#show-rules) |
| コンテキスト式 | `{context text.lang}` | [Context]($context) |
diff --git a/docs/src/contribs.rs b/docs/src/contribs.rs
index 58a730e222..d9824c39ae 100644
--- a/docs/src/contribs.rs
+++ b/docs/src/contribs.rs
@@ -9,6 +9,7 @@ use crate::{Html, Resolver};
/// Build HTML detailing the contributors between two tags.
pub fn contributors(resolver: &dyn Resolver, from: &str, to: &str) -> Option {
let staff = ["laurmaedje", "reknih"];
+ let bots = ["dependabot[bot]"];
// Determine number of contributions per person.
let mut contributors = HashMap::::new();
@@ -26,7 +27,10 @@ pub fn contributors(resolver: &dyn Resolver, from: &str, to: &str) -> Option = contributors
.into_values()
- .filter(|c| !staff.contains(&c.login.as_str()))
+ .filter(|c| {
+ let login = c.login.as_str();
+ !staff.contains(&login) && !bots.contains(&login)
+ })
.collect();
// Sort by highest number of commits.
diff --git a/docs/src/lib.rs b/docs/src/lib.rs
index c6c9731ea2..cf582fed31 100644
--- a/docs/src/lib.rs
+++ b/docs/src/lib.rs
@@ -15,7 +15,9 @@ use once_cell::sync::Lazy;
use serde::Deserialize;
use serde_yaml as yaml;
use typst::diag::{bail, StrResult};
+use typst::foundations::AutoValue;
use typst::foundations::Bytes;
+use typst::foundations::NoneValue;
use typst::foundations::{
CastInfo, Category, Func, Module, ParamInfo, Repr, Scope, Smart, Type, Value,
FOUNDATIONS,
@@ -56,12 +58,21 @@ static GROUPS: Lazy> = Lazy::new(|| {
static LIBRARY: Lazy> = Lazy::new(|| {
let mut lib = Library::default();
+ let scope = lib.global.scope_mut();
+
+ // Add those types, so that they show up in the docs.
+ scope.category(FOUNDATIONS);
+ scope.define_type::();
+ scope.define_type::();
+
+ // Adjust the default look.
lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(240.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto));
lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
Abs::pt(15.0).into(),
)))));
+
Prehashed::new(lib)
});
@@ -242,7 +253,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel {
shorthands = Some(ShorthandsModel { markup, math });
}
- // Add functions.
+ // Add values and types.
let scope = module.scope();
for (name, value) in scope.iter() {
if scope.get_category(name) != Some(category) {
diff --git a/templates/base_template.html.j2 b/templates/base_template.html.j2
index d064df45ab..6d642fc458 100644
--- a/templates/base_template.html.j2
+++ b/templates/base_template.html.j2
@@ -42,9 +42,9 @@
diff --git a/tests/Cargo.toml b/tests/Cargo.toml
index 1f650c9761..62e7a493ad 100644
--- a/tests/Cargo.toml
+++ b/tests/Cargo.toml
@@ -6,29 +6,29 @@ authors = { workspace = true }
edition = { workspace = true }
publish = false
-[dev-dependencies]
+[[test]]
+name = "tests"
+path = "src/tests.rs"
+harness = false
+
+[dependencies]
typst = { workspace = true }
typst-assets = { workspace = true, features = ["fonts"] }
typst-dev-assets = { workspace = true }
typst-pdf = { workspace = true }
typst-render = { workspace = true }
typst-svg = { workspace = true }
-typst-ide = { workspace = true }
clap = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
once_cell = { workspace = true }
oxipng = { workspace = true }
+parking_lot = { workspace = true }
rayon = { workspace = true }
tiny-skia = { workspace = true }
ttf-parser = { workspace = true }
unscanny = { workspace = true }
walkdir = { workspace = true }
-[[test]]
-name = "tests"
-path = "src/tests.rs"
-harness = false
-
[lints]
workspace = true
diff --git a/tests/README.md b/tests/README.md
index 68b72f2dff..6cc79217a3 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -3,13 +3,10 @@
## Directory structure
Top level directory structure:
- `src`: Testing code.
-- `typ`: Input files. The tests in `compiler` specifically test the compiler
- while the others test the standard library (but also the compiler
- indirectly).
+- `suite`: Input files. Mostly organize in parallel to the code.
- `ref`: Reference images which the output is compared with to determine whether
a test passed or failed.
-- `png`: PNG files produced by tests.
-- `pdf`: PDF files produced by tests.
+- `store`: Store for PNG, PDF, and SVG output files produced by the tests.
## Running the tests
Running all tests (including unit tests):
@@ -37,11 +34,6 @@ Running a test with the exact filename `page.typ`.
testit --exact page.typ
```
-Debug-printing the layout trees for all executed tests.
-```bash
-testit --debug empty.typ
-```
-
To make the integration tests go faster they don't generate PDFs by default.
Pass the `--pdf` flag to generate those. Mind that PDFs are not tested
automatically at the moment, so you should always check the output manually when
@@ -50,18 +42,48 @@ making changes.
testit --pdf
```
-## Update expected images
+## Writing tests
+The syntax for an individual test is `--- {name} ---` followed by some Typst
+code that should be tested. The name must be globally unique in the test suite,
+so that tests can be easily migrated across files.
+
+There are, broadly speaking, three kinds of tests:
+
+- Tests that just ensure that the code runs successfully: Those typically make
+ use of `test` or `assert.eq` (both are very similar, `test` is just shorter)
+ to ensure certain properties hold when executing the Typst code.
+
+- Tests that ensure the code fails with a particular error: Those have inline
+ annotations like `// Error: 2-7 thing was wrong`. An annotation can be
+ either an "Error", a "Warning", or a "Hint". The range designates where
+ in the next non-comment line the error is and after it follows the message.
+ If you the error is in a line further below, you can also write ranges like
+ `3:2-3:7` to indicate the 2-7 column in the 3rd non-comment line.
+
+- Tests that ensure certain visual output is produced: Those render the result
+ of the test with the `typst-render` crate and compare against a reference
+ image stored in the repository. The test runner automatically detects whether
+ a test has visual output and requires a reference image in this case.
+
+ To prevent bloat, it is important that the test images are kept as small as
+ possible. To that effect, the test runner enforces a maximum size of 20 KiB.
+ If truly necessary, this limit can however be lifted by adding `// LARGE` as
+ the first line of a test.
+
+If you have the choice between writing a test using assertions or using
+reference images, prefer assertions. This makes the test easier to understand
+in isolation and prevents bloat due to images.
+
+## Updating reference images
If you created a new test or fixed a bug in an existing test, you need to update
-the reference image used for comparison. For this, you can use the
-`UPDATE_EXPECT` environment variable or the `--update` flag:
+the reference image used for comparison. For this, you can use the `--update`
+flag:
```bash
testit mytest --update
```
If you use the VS Code test helper extension (see the `tools` folder), you can
-alternatively use the checkmark button to update the reference image. In that
-case you should also install `oxipng` on your system so that the test helper
-can optimize the reference images.
+alternatively use the save button to update the reference image.
## Making an alias
If you want to have a quicker way to run the tests, consider adding a shortcut
diff --git a/tests/ref/align-center-in-flow.png b/tests/ref/align-center-in-flow.png
new file mode 100644
index 0000000000..ecfe49dc53
Binary files /dev/null and b/tests/ref/align-center-in-flow.png differ
diff --git a/tests/ref/align-in-stack.png b/tests/ref/align-in-stack.png
new file mode 100644
index 0000000000..556721ab09
Binary files /dev/null and b/tests/ref/align-in-stack.png differ
diff --git a/tests/ref/align-right.png b/tests/ref/align-right.png
new file mode 100644
index 0000000000..edab885100
Binary files /dev/null and b/tests/ref/align-right.png differ
diff --git a/tests/ref/align-start-and-end.png b/tests/ref/align-start-and-end.png
new file mode 100644
index 0000000000..cf3faeae6f
Binary files /dev/null and b/tests/ref/align-start-and-end.png differ
diff --git a/tests/ref/array-basic-syntax.png b/tests/ref/array-basic-syntax.png
new file mode 100644
index 0000000000..6eb95305c4
Binary files /dev/null and b/tests/ref/array-basic-syntax.png differ
diff --git a/tests/ref/array-insert-and-remove.png b/tests/ref/array-insert-and-remove.png
new file mode 100644
index 0000000000..ea4b8cf2a3
Binary files /dev/null and b/tests/ref/array-insert-and-remove.png differ
diff --git a/tests/ref/array-join-content.png b/tests/ref/array-join-content.png
new file mode 100644
index 0000000000..4d08142ebb
Binary files /dev/null and b/tests/ref/array-join-content.png differ
diff --git a/tests/ref/baseline-box.png b/tests/ref/baseline-box.png
new file mode 100644
index 0000000000..8d7627c609
Binary files /dev/null and b/tests/ref/baseline-box.png differ
diff --git a/tests/ref/baseline-text.png b/tests/ref/baseline-text.png
new file mode 100644
index 0000000000..72beac7959
Binary files /dev/null and b/tests/ref/baseline-text.png differ
diff --git a/tests/ref/bibliography-basic.png b/tests/ref/bibliography-basic.png
new file mode 100644
index 0000000000..eeb773bfe8
Binary files /dev/null and b/tests/ref/bibliography-basic.png differ
diff --git a/tests/ref/bibliography-before-content.png b/tests/ref/bibliography-before-content.png
new file mode 100644
index 0000000000..806daa089c
Binary files /dev/null and b/tests/ref/bibliography-before-content.png differ
diff --git a/tests/ref/bibliography-full.png b/tests/ref/bibliography-full.png
new file mode 100644
index 0000000000..1da15d1668
Binary files /dev/null and b/tests/ref/bibliography-full.png differ
diff --git a/tests/ref/bibliography-math.png b/tests/ref/bibliography-math.png
new file mode 100644
index 0000000000..3fc36efca6
Binary files /dev/null and b/tests/ref/bibliography-math.png differ
diff --git a/tests/ref/bibliography-multiple-files.png b/tests/ref/bibliography-multiple-files.png
new file mode 100644
index 0000000000..1293ba22b4
Binary files /dev/null and b/tests/ref/bibliography-multiple-files.png differ
diff --git a/tests/ref/bibliography-ordering.png b/tests/ref/bibliography-ordering.png
new file mode 100644
index 0000000000..b1e14c9adf
Binary files /dev/null and b/tests/ref/bibliography-ordering.png differ
diff --git a/tests/ref/bidi-consecutive-embedded-ltr-runs.png b/tests/ref/bidi-consecutive-embedded-ltr-runs.png
new file mode 100644
index 0000000000..dbaaff07db
Binary files /dev/null and b/tests/ref/bidi-consecutive-embedded-ltr-runs.png differ
diff --git a/tests/ref/bidi-consecutive-embedded-rtl-runs.png b/tests/ref/bidi-consecutive-embedded-rtl-runs.png
new file mode 100644
index 0000000000..4cf62d3e0f
Binary files /dev/null and b/tests/ref/bidi-consecutive-embedded-rtl-runs.png differ
diff --git a/tests/ref/bidi-en-he-top-level.png b/tests/ref/bidi-en-he-top-level.png
new file mode 100644
index 0000000000..abab54f4b4
Binary files /dev/null and b/tests/ref/bidi-en-he-top-level.png differ
diff --git a/tests/ref/bidi-explicit-dir.png b/tests/ref/bidi-explicit-dir.png
new file mode 100644
index 0000000000..8b813be0cc
Binary files /dev/null and b/tests/ref/bidi-explicit-dir.png differ
diff --git a/tests/ref/bidi-manual-linebreak.png b/tests/ref/bidi-manual-linebreak.png
new file mode 100644
index 0000000000..4d0eb6f890
Binary files /dev/null and b/tests/ref/bidi-manual-linebreak.png differ
diff --git a/tests/ref/bidi-nesting.png b/tests/ref/bidi-nesting.png
new file mode 100644
index 0000000000..e18d6c0a00
Binary files /dev/null and b/tests/ref/bidi-nesting.png differ
diff --git a/tests/ref/bidi-obj.png b/tests/ref/bidi-obj.png
new file mode 100644
index 0000000000..8cc41528ab
Binary files /dev/null and b/tests/ref/bidi-obj.png differ
diff --git a/tests/ref/bidi-raw.png b/tests/ref/bidi-raw.png
new file mode 100644
index 0000000000..24503ee932
Binary files /dev/null and b/tests/ref/bidi-raw.png differ
diff --git a/tests/ref/bidi-spacing.png b/tests/ref/bidi-spacing.png
new file mode 100644
index 0000000000..44ede76f10
Binary files /dev/null and b/tests/ref/bidi-spacing.png differ
diff --git a/tests/ref/bidi-whitespace-reset.png b/tests/ref/bidi-whitespace-reset.png
new file mode 100644
index 0000000000..7d64012f9e
Binary files /dev/null and b/tests/ref/bidi-whitespace-reset.png differ
diff --git a/tests/ref/block-box-fill.png b/tests/ref/block-box-fill.png
new file mode 100644
index 0000000000..fe4f725802
Binary files /dev/null and b/tests/ref/block-box-fill.png differ
diff --git a/tests/ref/block-clip-svg-glyphs.png b/tests/ref/block-clip-svg-glyphs.png
new file mode 100644
index 0000000000..d8db5b61ef
Binary files /dev/null and b/tests/ref/block-clip-svg-glyphs.png differ
diff --git a/tests/ref/block-clip-text.png b/tests/ref/block-clip-text.png
new file mode 100644
index 0000000000..7cd86ddbe7
Binary files /dev/null and b/tests/ref/block-clip-text.png differ
diff --git a/tests/ref/block-clipping-multiple-pages.png b/tests/ref/block-clipping-multiple-pages.png
new file mode 100644
index 0000000000..9c9aa89b43
Binary files /dev/null and b/tests/ref/block-clipping-multiple-pages.png differ
diff --git a/tests/ref/block-fixed-height.png b/tests/ref/block-fixed-height.png
new file mode 100644
index 0000000000..95c3be1e59
Binary files /dev/null and b/tests/ref/block-fixed-height.png differ
diff --git a/tests/ref/block-multiple-pages.png b/tests/ref/block-multiple-pages.png
new file mode 100644
index 0000000000..c2f192bd61
Binary files /dev/null and b/tests/ref/block-multiple-pages.png differ
diff --git a/tests/ref/block-sizing.png b/tests/ref/block-sizing.png
new file mode 100644
index 0000000000..76cb04dfa0
Binary files /dev/null and b/tests/ref/block-sizing.png differ
diff --git a/tests/ref/block-spacing-basic.png b/tests/ref/block-spacing-basic.png
new file mode 100644
index 0000000000..875410acb1
Binary files /dev/null and b/tests/ref/block-spacing-basic.png differ
diff --git a/tests/ref/block-spacing-collapse-text-style.png b/tests/ref/block-spacing-collapse-text-style.png
new file mode 100644
index 0000000000..6c631457bc
Binary files /dev/null and b/tests/ref/block-spacing-collapse-text-style.png differ
diff --git a/tests/ref/block-spacing-maximum.png b/tests/ref/block-spacing-maximum.png
new file mode 100644
index 0000000000..755b1cc306
Binary files /dev/null and b/tests/ref/block-spacing-maximum.png differ
diff --git a/tests/ref/block-spacing-table.png b/tests/ref/block-spacing-table.png
new file mode 100644
index 0000000000..1591acb7c0
Binary files /dev/null and b/tests/ref/block-spacing-table.png differ
diff --git a/tests/ref/box-clip-radius-without-stroke.png b/tests/ref/box-clip-radius-without-stroke.png
new file mode 100644
index 0000000000..121373582e
Binary files /dev/null and b/tests/ref/box-clip-radius-without-stroke.png differ
diff --git a/tests/ref/box-clip-radius.png b/tests/ref/box-clip-radius.png
new file mode 100644
index 0000000000..da20fa5bfc
Binary files /dev/null and b/tests/ref/box-clip-radius.png differ
diff --git a/tests/ref/box-clip-rect.png b/tests/ref/box-clip-rect.png
new file mode 100644
index 0000000000..49a4e4abc6
Binary files /dev/null and b/tests/ref/box-clip-rect.png differ
diff --git a/tests/ref/box-width-fr.png b/tests/ref/box-width-fr.png
new file mode 100644
index 0000000000..30d4816316
Binary files /dev/null and b/tests/ref/box-width-fr.png differ
diff --git a/tests/ref/box.png b/tests/ref/box.png
new file mode 100644
index 0000000000..fde288a80d
Binary files /dev/null and b/tests/ref/box.png differ
diff --git a/tests/ref/bugs/1050-terms-indent.png b/tests/ref/bugs/1050-terms-indent.png
deleted file mode 100644
index 58a7feae88..0000000000
Binary files a/tests/ref/bugs/1050-terms-indent.png and /dev/null differ
diff --git a/tests/ref/bugs/1240-stack-fr.png b/tests/ref/bugs/1240-stack-fr.png
deleted file mode 100644
index 29df5d44a5..0000000000
Binary files a/tests/ref/bugs/1240-stack-fr.png and /dev/null differ
diff --git a/tests/ref/bugs/1597-cite-footnote.png b/tests/ref/bugs/1597-cite-footnote.png
deleted file mode 100644
index c2e219f275..0000000000
Binary files a/tests/ref/bugs/1597-cite-footnote.png and /dev/null differ
diff --git a/tests/ref/bugs/2044-invalid-parsed-ident.png b/tests/ref/bugs/2044-invalid-parsed-ident.png
deleted file mode 100644
index 327150e788..0000000000
Binary files a/tests/ref/bugs/2044-invalid-parsed-ident.png and /dev/null differ
diff --git a/tests/ref/bugs/2105-linebreak-tofu.png b/tests/ref/bugs/2105-linebreak-tofu.png
deleted file mode 100644
index 78f937eb2c..0000000000
Binary files a/tests/ref/bugs/2105-linebreak-tofu.png and /dev/null differ
diff --git a/tests/ref/bugs/2595-float-overlap.png b/tests/ref/bugs/2595-float-overlap.png
deleted file mode 100644
index 6d8eaf9415..0000000000
Binary files a/tests/ref/bugs/2595-float-overlap.png and /dev/null differ
diff --git a/tests/ref/bugs/2650-cjk-latin-spacing-meta.png b/tests/ref/bugs/2650-cjk-latin-spacing-meta.png
deleted file mode 100644
index 35ff0e62d8..0000000000
Binary files a/tests/ref/bugs/2650-cjk-latin-spacing-meta.png and /dev/null differ
diff --git a/tests/ref/bugs/2715-float-order.png b/tests/ref/bugs/2715-float-order.png
deleted file mode 100644
index 0a4f8812eb..0000000000
Binary files a/tests/ref/bugs/2715-float-order.png and /dev/null differ
diff --git a/tests/ref/bugs/3082-chinese-punctuation.png b/tests/ref/bugs/3082-chinese-punctuation.png
deleted file mode 100644
index c187d49558..0000000000
Binary files a/tests/ref/bugs/3082-chinese-punctuation.png and /dev/null differ
diff --git a/tests/ref/bugs/3641-float-loop.png b/tests/ref/bugs/3641-float-loop.png
deleted file mode 100644
index 092b2ff510..0000000000
Binary files a/tests/ref/bugs/3641-float-loop.png and /dev/null differ
diff --git a/tests/ref/bugs/3650-italic-equation.png b/tests/ref/bugs/3650-italic-equation.png
deleted file mode 100644
index 41f071ab80..0000000000
Binary files a/tests/ref/bugs/3650-italic-equation.png and /dev/null differ
diff --git a/tests/ref/bugs/3658-math-size.png b/tests/ref/bugs/3658-math-size.png
deleted file mode 100644
index 94c8d38816..0000000000
Binary files a/tests/ref/bugs/3658-math-size.png and /dev/null differ
diff --git a/tests/ref/bugs/870-image-rotation.png b/tests/ref/bugs/870-image-rotation.png
deleted file mode 100644
index 83d9267d1c..0000000000
Binary files a/tests/ref/bugs/870-image-rotation.png and /dev/null differ
diff --git a/tests/ref/bugs/args-sink.png b/tests/ref/bugs/args-sink.png
deleted file mode 100644
index 564c59a2b3..0000000000
Binary files a/tests/ref/bugs/args-sink.png and /dev/null differ
diff --git a/tests/ref/bugs/bibliography-math.png b/tests/ref/bugs/bibliography-math.png
deleted file mode 100644
index 0ab308dc98..0000000000
Binary files a/tests/ref/bugs/bibliography-math.png and /dev/null differ
diff --git a/tests/ref/bugs/bidi-tofus.png b/tests/ref/bugs/bidi-tofus.png
deleted file mode 100644
index 1b7a7d8b4b..0000000000
Binary files a/tests/ref/bugs/bidi-tofus.png and /dev/null differ
diff --git a/tests/ref/bugs/block-width-box.png b/tests/ref/bugs/block-width-box.png
deleted file mode 100644
index 9cb27a5def..0000000000
Binary files a/tests/ref/bugs/block-width-box.png and /dev/null differ
diff --git a/tests/ref/bugs/cite-locate.png b/tests/ref/bugs/cite-locate.png
deleted file mode 100644
index bd31df7db6..0000000000
Binary files a/tests/ref/bugs/cite-locate.png and /dev/null differ
diff --git a/tests/ref/bugs/cite-show-set.png b/tests/ref/bugs/cite-show-set.png
deleted file mode 100644
index 566186a4cf..0000000000
Binary files a/tests/ref/bugs/cite-show-set.png and /dev/null differ
diff --git a/tests/ref/bugs/clamp-panic.png b/tests/ref/bugs/clamp-panic.png
deleted file mode 100644
index c0c4912e36..0000000000
Binary files a/tests/ref/bugs/clamp-panic.png and /dev/null differ
diff --git a/tests/ref/bugs/columns-1.png b/tests/ref/bugs/columns-1.png
deleted file mode 100644
index 4b462b60a8..0000000000
Binary files a/tests/ref/bugs/columns-1.png and /dev/null differ
diff --git a/tests/ref/bugs/emoji-linebreak.png b/tests/ref/bugs/emoji-linebreak.png
deleted file mode 100644
index 6944233d8f..0000000000
Binary files a/tests/ref/bugs/emoji-linebreak.png and /dev/null differ
diff --git a/tests/ref/bugs/flow-1.png b/tests/ref/bugs/flow-1.png
deleted file mode 100644
index 662a7b1472..0000000000
Binary files a/tests/ref/bugs/flow-1.png and /dev/null differ
diff --git a/tests/ref/bugs/flow-2.png b/tests/ref/bugs/flow-2.png
deleted file mode 100644
index c7ece30839..0000000000
Binary files a/tests/ref/bugs/flow-2.png and /dev/null differ
diff --git a/tests/ref/bugs/flow-3.png b/tests/ref/bugs/flow-3.png
deleted file mode 100644
index 25acc06dfe..0000000000
Binary files a/tests/ref/bugs/flow-3.png and /dev/null differ
diff --git a/tests/ref/bugs/flow-4.png b/tests/ref/bugs/flow-4.png
deleted file mode 100644
index 2adcbe1573..0000000000
Binary files a/tests/ref/bugs/flow-4.png and /dev/null differ
diff --git a/tests/ref/bugs/flow-5.png b/tests/ref/bugs/flow-5.png
deleted file mode 100644
index 648c8c44fa..0000000000
Binary files a/tests/ref/bugs/flow-5.png and /dev/null differ
diff --git a/tests/ref/bugs/fold-vector.png b/tests/ref/bugs/fold-vector.png
deleted file mode 100644
index d8503a8eb9..0000000000
Binary files a/tests/ref/bugs/fold-vector.png and /dev/null differ
diff --git a/tests/ref/bugs/footnote-keep-multiple.png b/tests/ref/bugs/footnote-keep-multiple.png
deleted file mode 100644
index f3b67a7456..0000000000
Binary files a/tests/ref/bugs/footnote-keep-multiple.png and /dev/null differ
diff --git a/tests/ref/bugs/footnote-list.png b/tests/ref/bugs/footnote-list.png
deleted file mode 100644
index 1b56f2274f..0000000000
Binary files a/tests/ref/bugs/footnote-list.png and /dev/null differ
diff --git a/tests/ref/bugs/gradient-cmyk-encode.png b/tests/ref/bugs/gradient-cmyk-encode.png
deleted file mode 100644
index 5002442f7b..0000000000
Binary files a/tests/ref/bugs/gradient-cmyk-encode.png and /dev/null differ
diff --git a/tests/ref/bugs/grid-1.png b/tests/ref/bugs/grid-1.png
deleted file mode 100644
index f60ad7f475..0000000000
Binary files a/tests/ref/bugs/grid-1.png and /dev/null differ
diff --git a/tests/ref/bugs/grid-2.png b/tests/ref/bugs/grid-2.png
deleted file mode 100644
index 882e0d6ad1..0000000000
Binary files a/tests/ref/bugs/grid-2.png and /dev/null differ
diff --git a/tests/ref/bugs/grid-3.png b/tests/ref/bugs/grid-3.png
deleted file mode 100644
index 6b5ae649ef..0000000000
Binary files a/tests/ref/bugs/grid-3.png and /dev/null differ
diff --git a/tests/ref/bugs/grid-4.png b/tests/ref/bugs/grid-4.png
deleted file mode 100644
index 475f561ea7..0000000000
Binary files a/tests/ref/bugs/grid-4.png and /dev/null differ
diff --git a/tests/ref/bugs/hide-meta.png b/tests/ref/bugs/hide-meta.png
deleted file mode 100644
index 76b4671a06..0000000000
Binary files a/tests/ref/bugs/hide-meta.png and /dev/null differ
diff --git a/tests/ref/bugs/justify-hanging-indent.png b/tests/ref/bugs/justify-hanging-indent.png
deleted file mode 100644
index 015cc44e61..0000000000
Binary files a/tests/ref/bugs/justify-hanging-indent.png and /dev/null differ
diff --git a/tests/ref/bugs/line-align.png b/tests/ref/bugs/line-align.png
deleted file mode 100644
index 1117ed6bf4..0000000000
Binary files a/tests/ref/bugs/line-align.png and /dev/null differ
diff --git a/tests/ref/bugs/linebreak-no-justifiables.png b/tests/ref/bugs/linebreak-no-justifiables.png
deleted file mode 100644
index 3f934592ab..0000000000
Binary files a/tests/ref/bugs/linebreak-no-justifiables.png and /dev/null differ
diff --git a/tests/ref/bugs/mat-aug-color.png b/tests/ref/bugs/mat-aug-color.png
deleted file mode 100644
index 472c196864..0000000000
Binary files a/tests/ref/bugs/mat-aug-color.png and /dev/null differ
diff --git a/tests/ref/bugs/math-eval.png b/tests/ref/bugs/math-eval.png
deleted file mode 100644
index b673e50394..0000000000
Binary files a/tests/ref/bugs/math-eval.png and /dev/null differ
diff --git a/tests/ref/bugs/math-hide.png b/tests/ref/bugs/math-hide.png
deleted file mode 100644
index 7ac5d2f10c..0000000000
Binary files a/tests/ref/bugs/math-hide.png and /dev/null differ
diff --git a/tests/ref/bugs/math-number-spacing.png b/tests/ref/bugs/math-number-spacing.png
deleted file mode 100644
index 5ec65df306..0000000000
Binary files a/tests/ref/bugs/math-number-spacing.png and /dev/null differ
diff --git a/tests/ref/bugs/math-realize.png b/tests/ref/bugs/math-realize.png
deleted file mode 100644
index e972e099c1..0000000000
Binary files a/tests/ref/bugs/math-realize.png and /dev/null differ
diff --git a/tests/ref/bugs/math-shift.png b/tests/ref/bugs/math-shift.png
deleted file mode 100644
index d6a2ef3b17..0000000000
Binary files a/tests/ref/bugs/math-shift.png and /dev/null differ
diff --git a/tests/ref/bugs/math-text-break.png b/tests/ref/bugs/math-text-break.png
deleted file mode 100644
index 768ca65f6b..0000000000
Binary files a/tests/ref/bugs/math-text-break.png and /dev/null differ
diff --git a/tests/ref/bugs/new-cm-svg.png b/tests/ref/bugs/new-cm-svg.png
deleted file mode 100644
index d75a6dbb0e..0000000000
Binary files a/tests/ref/bugs/new-cm-svg.png and /dev/null differ
diff --git a/tests/ref/bugs/newline-mode.png b/tests/ref/bugs/newline-mode.png
deleted file mode 100644
index d4b6c6d85e..0000000000
Binary files a/tests/ref/bugs/newline-mode.png and /dev/null differ
diff --git a/tests/ref/bugs/pagebreak-bibliography.png b/tests/ref/bugs/pagebreak-bibliography.png
deleted file mode 100644
index 43de157446..0000000000
Binary files a/tests/ref/bugs/pagebreak-bibliography.png and /dev/null differ
diff --git a/tests/ref/bugs/pagebreak-numbering.png b/tests/ref/bugs/pagebreak-numbering.png
deleted file mode 100644
index 96f047a889..0000000000
Binary files a/tests/ref/bugs/pagebreak-numbering.png and /dev/null differ
diff --git a/tests/ref/bugs/pagebreak-set-style.png b/tests/ref/bugs/pagebreak-set-style.png
deleted file mode 100644
index f81b8c2f20..0000000000
Binary files a/tests/ref/bugs/pagebreak-set-style.png and /dev/null differ
diff --git a/tests/ref/bugs/place-base.png b/tests/ref/bugs/place-base.png
deleted file mode 100644
index 4442b173cf..0000000000
Binary files a/tests/ref/bugs/place-base.png and /dev/null differ
diff --git a/tests/ref/bugs/place-nested.png b/tests/ref/bugs/place-nested.png
deleted file mode 100644
index b59dc5d3d8..0000000000
Binary files a/tests/ref/bugs/place-nested.png and /dev/null differ
diff --git a/tests/ref/bugs/place-pagebreak.png b/tests/ref/bugs/place-pagebreak.png
deleted file mode 100644
index 2aa3d6b00a..0000000000
Binary files a/tests/ref/bugs/place-pagebreak.png and /dev/null differ
diff --git a/tests/ref/bugs/place-spacing.png b/tests/ref/bugs/place-spacing.png
deleted file mode 100644
index d14ce6ec13..0000000000
Binary files a/tests/ref/bugs/place-spacing.png and /dev/null differ
diff --git a/tests/ref/bugs/raw-color-overwrite.png b/tests/ref/bugs/raw-color-overwrite.png
deleted file mode 100644
index b01d86a49c..0000000000
Binary files a/tests/ref/bugs/raw-color-overwrite.png and /dev/null differ
diff --git a/tests/ref/bugs/smartquotes-in-outline.png b/tests/ref/bugs/smartquotes-in-outline.png
deleted file mode 100644
index 8a2cbc6aea..0000000000
Binary files a/tests/ref/bugs/smartquotes-in-outline.png and /dev/null differ
diff --git a/tests/ref/bugs/smartquotes-on-newline.png b/tests/ref/bugs/smartquotes-on-newline.png
deleted file mode 100644
index fdf4623ad0..0000000000
Binary files a/tests/ref/bugs/smartquotes-on-newline.png and /dev/null differ
diff --git a/tests/ref/bugs/spacing-behaviour.png b/tests/ref/bugs/spacing-behaviour.png
deleted file mode 100644
index 08fcfa7394..0000000000
Binary files a/tests/ref/bugs/spacing-behaviour.png and /dev/null differ
diff --git a/tests/ref/bugs/square-base.png b/tests/ref/bugs/square-base.png
deleted file mode 100644
index 290ee54e5f..0000000000
Binary files a/tests/ref/bugs/square-base.png and /dev/null differ
diff --git a/tests/ref/bugs/table-lines.png b/tests/ref/bugs/table-lines.png
deleted file mode 100644
index 600391cb54..0000000000
Binary files a/tests/ref/bugs/table-lines.png and /dev/null differ
diff --git a/tests/ref/bugs/table-row-missing.png b/tests/ref/bugs/table-row-missing.png
deleted file mode 100644
index 90c46d3235..0000000000
Binary files a/tests/ref/bugs/table-row-missing.png and /dev/null differ
diff --git a/tests/ref/call-basic.png b/tests/ref/call-basic.png
new file mode 100644
index 0000000000..9016e9e8d1
Binary files /dev/null and b/tests/ref/call-basic.png differ
diff --git a/tests/ref/circle-auto-sizing.png b/tests/ref/circle-auto-sizing.png
new file mode 100644
index 0000000000..377dbe1ddb
Binary files /dev/null and b/tests/ref/circle-auto-sizing.png differ
diff --git a/tests/ref/circle-directly-in-rect.png b/tests/ref/circle-directly-in-rect.png
new file mode 100644
index 0000000000..cb74496d78
Binary files /dev/null and b/tests/ref/circle-directly-in-rect.png differ
diff --git a/tests/ref/circle-relative-sizing.png b/tests/ref/circle-relative-sizing.png
new file mode 100644
index 0000000000..efff34cf95
Binary files /dev/null and b/tests/ref/circle-relative-sizing.png differ
diff --git a/tests/ref/circle-sizing-options.png b/tests/ref/circle-sizing-options.png
new file mode 100644
index 0000000000..778a824957
Binary files /dev/null and b/tests/ref/circle-sizing-options.png differ
diff --git a/tests/ref/circle.png b/tests/ref/circle.png
new file mode 100644
index 0000000000..8a86e1948e
Binary files /dev/null and b/tests/ref/circle.png differ
diff --git a/tests/ref/cite-footnote.png b/tests/ref/cite-footnote.png
new file mode 100644
index 0000000000..5bc6433e05
Binary files /dev/null and b/tests/ref/cite-footnote.png differ
diff --git a/tests/ref/cite-form.png b/tests/ref/cite-form.png
new file mode 100644
index 0000000000..c35a357355
Binary files /dev/null and b/tests/ref/cite-form.png differ
diff --git a/tests/ref/cite-group.png b/tests/ref/cite-group.png
new file mode 100644
index 0000000000..70feb4e1e0
Binary files /dev/null and b/tests/ref/cite-group.png differ
diff --git a/tests/ref/cite-grouping-and-ordering.png b/tests/ref/cite-grouping-and-ordering.png
new file mode 100644
index 0000000000..6a70539db6
Binary files /dev/null and b/tests/ref/cite-grouping-and-ordering.png differ
diff --git a/tests/ref/cjk-punctuation-adjustment-1.png b/tests/ref/cjk-punctuation-adjustment-1.png
new file mode 100644
index 0000000000..a68274cf31
Binary files /dev/null and b/tests/ref/cjk-punctuation-adjustment-1.png differ
diff --git a/tests/ref/cjk-punctuation-adjustment-2.png b/tests/ref/cjk-punctuation-adjustment-2.png
new file mode 100644
index 0000000000..925c0f3c58
Binary files /dev/null and b/tests/ref/cjk-punctuation-adjustment-2.png differ
diff --git a/tests/ref/cjk-punctuation-adjustment-3.png b/tests/ref/cjk-punctuation-adjustment-3.png
new file mode 100644
index 0000000000..e5eb70a941
Binary files /dev/null and b/tests/ref/cjk-punctuation-adjustment-3.png differ
diff --git a/tests/ref/closure-capture-in-lvalue.png b/tests/ref/closure-capture-in-lvalue.png
new file mode 100644
index 0000000000..5f3ab0356d
Binary files /dev/null and b/tests/ref/closure-capture-in-lvalue.png differ
diff --git a/tests/ref/closure-path-resolve-in-layout-phase.png b/tests/ref/closure-path-resolve-in-layout-phase.png
new file mode 100644
index 0000000000..e56e23a025
Binary files /dev/null and b/tests/ref/closure-path-resolve-in-layout-phase.png differ
diff --git a/tests/ref/closure-without-params-non-atomic.png b/tests/ref/closure-without-params-non-atomic.png
new file mode 100644
index 0000000000..7d01ea3cd9
Binary files /dev/null and b/tests/ref/closure-without-params-non-atomic.png differ
diff --git a/tests/ref/code-block-basic-syntax.png b/tests/ref/code-block-basic-syntax.png
new file mode 100644
index 0000000000..7b2e6045ef
Binary files /dev/null and b/tests/ref/code-block-basic-syntax.png differ
diff --git a/tests/ref/color-cmyk-ops.png b/tests/ref/color-cmyk-ops.png
new file mode 100644
index 0000000000..4f799efa19
Binary files /dev/null and b/tests/ref/color-cmyk-ops.png differ
diff --git a/tests/ref/color-luma.png b/tests/ref/color-luma.png
new file mode 100644
index 0000000000..7bacc74432
Binary files /dev/null and b/tests/ref/color-luma.png differ
diff --git a/tests/ref/color-outside-srgb-gamut.png b/tests/ref/color-outside-srgb-gamut.png
new file mode 100644
index 0000000000..3a2806c5de
Binary files /dev/null and b/tests/ref/color-outside-srgb-gamut.png differ
diff --git a/tests/ref/color-rotate-hue.png b/tests/ref/color-rotate-hue.png
new file mode 100644
index 0000000000..a21397141f
Binary files /dev/null and b/tests/ref/color-rotate-hue.png differ
diff --git a/tests/ref/color-saturation.png b/tests/ref/color-saturation.png
new file mode 100644
index 0000000000..ccac48287e
Binary files /dev/null and b/tests/ref/color-saturation.png differ
diff --git a/tests/ref/color-spaces.png b/tests/ref/color-spaces.png
new file mode 100644
index 0000000000..ade861ccb9
Binary files /dev/null and b/tests/ref/color-spaces.png differ
diff --git a/tests/ref/columns-colbreak-after-place.png b/tests/ref/columns-colbreak-after-place.png
new file mode 100644
index 0000000000..f6a8a63dc5
Binary files /dev/null and b/tests/ref/columns-colbreak-after-place.png differ
diff --git a/tests/ref/columns-empty-second-column.png b/tests/ref/columns-empty-second-column.png
new file mode 100644
index 0000000000..a00d5fb2a2
Binary files /dev/null and b/tests/ref/columns-empty-second-column.png differ
diff --git a/tests/ref/columns-in-auto-sized-rect.png b/tests/ref/columns-in-auto-sized-rect.png
new file mode 100644
index 0000000000..00088b7ebc
Binary files /dev/null and b/tests/ref/columns-in-auto-sized-rect.png differ
diff --git a/tests/ref/columns-in-fixed-size-rect.png b/tests/ref/columns-in-fixed-size-rect.png
new file mode 100644
index 0000000000..28cb97cb29
Binary files /dev/null and b/tests/ref/columns-in-fixed-size-rect.png differ
diff --git a/tests/ref/columns-more-with-gutter.png b/tests/ref/columns-more-with-gutter.png
new file mode 100644
index 0000000000..e89c6a0b2f
Binary files /dev/null and b/tests/ref/columns-more-with-gutter.png differ
diff --git a/tests/ref/columns-one.png b/tests/ref/columns-one.png
new file mode 100644
index 0000000000..02abf6590a
Binary files /dev/null and b/tests/ref/columns-one.png differ
diff --git a/tests/ref/columns-page-height-auto.png b/tests/ref/columns-page-height-auto.png
new file mode 100644
index 0000000000..9b3f1f8555
Binary files /dev/null and b/tests/ref/columns-page-height-auto.png differ
diff --git a/tests/ref/columns-page-width-auto.png b/tests/ref/columns-page-width-auto.png
new file mode 100644
index 0000000000..04d88bc10b
Binary files /dev/null and b/tests/ref/columns-page-width-auto.png differ
diff --git a/tests/ref/columns-rtl.png b/tests/ref/columns-rtl.png
new file mode 100644
index 0000000000..7efa57f57e
Binary files /dev/null and b/tests/ref/columns-rtl.png differ
diff --git a/tests/ref/columns-set-page-colbreak-pagebreak.png b/tests/ref/columns-set-page-colbreak-pagebreak.png
new file mode 100644
index 0000000000..48d2fd7b15
Binary files /dev/null and b/tests/ref/columns-set-page-colbreak-pagebreak.png differ
diff --git a/tests/ref/columns-set-page.png b/tests/ref/columns-set-page.png
new file mode 100644
index 0000000000..42b5bea704
Binary files /dev/null and b/tests/ref/columns-set-page.png differ
diff --git a/tests/ref/coma.png b/tests/ref/coma.png
index fc0f6ba1d2..96f9e4d9b6 100644
Binary files a/tests/ref/coma.png and b/tests/ref/coma.png differ
diff --git a/tests/ref/comment-end-of-line.png b/tests/ref/comment-end-of-line.png
new file mode 100644
index 0000000000..94da23cb10
Binary files /dev/null and b/tests/ref/comment-end-of-line.png differ
diff --git a/tests/ref/comments.png b/tests/ref/comments.png
new file mode 100644
index 0000000000..892ff5e474
Binary files /dev/null and b/tests/ref/comments.png differ
diff --git a/tests/ref/compiler/array.png b/tests/ref/compiler/array.png
deleted file mode 100644
index 9b6bf8b308..0000000000
Binary files a/tests/ref/compiler/array.png and /dev/null differ
diff --git a/tests/ref/compiler/block.png b/tests/ref/compiler/block.png
deleted file mode 100644
index 21a38de2c7..0000000000
Binary files a/tests/ref/compiler/block.png and /dev/null differ
diff --git a/tests/ref/compiler/break-continue.png b/tests/ref/compiler/break-continue.png
deleted file mode 100644
index 9751d39516..0000000000
Binary files a/tests/ref/compiler/break-continue.png and /dev/null differ
diff --git a/tests/ref/compiler/call.png b/tests/ref/compiler/call.png
deleted file mode 100644
index 2c5d1e78ff..0000000000
Binary files a/tests/ref/compiler/call.png and /dev/null differ
diff --git a/tests/ref/compiler/closure.png b/tests/ref/compiler/closure.png
deleted file mode 100644
index 07c171c58e..0000000000
Binary files a/tests/ref/compiler/closure.png and /dev/null differ
diff --git a/tests/ref/compiler/color.png b/tests/ref/compiler/color.png
deleted file mode 100644
index 2b416f6413..0000000000
Binary files a/tests/ref/compiler/color.png and /dev/null differ
diff --git a/tests/ref/compiler/comment.png b/tests/ref/compiler/comment.png
deleted file mode 100644
index 608df6eae0..0000000000
Binary files a/tests/ref/compiler/comment.png and /dev/null differ
diff --git a/tests/ref/compiler/construct.png b/tests/ref/compiler/construct.png
deleted file mode 100644
index f1acf66541..0000000000
Binary files a/tests/ref/compiler/construct.png and /dev/null differ
diff --git a/tests/ref/compiler/content-field.png b/tests/ref/compiler/content-field.png
deleted file mode 100644
index 3095ba8c91..0000000000
Binary files a/tests/ref/compiler/content-field.png and /dev/null differ
diff --git a/tests/ref/compiler/dict.png b/tests/ref/compiler/dict.png
deleted file mode 100644
index c97b2dbf34..0000000000
Binary files a/tests/ref/compiler/dict.png and /dev/null differ
diff --git a/tests/ref/compiler/for.png b/tests/ref/compiler/for.png
deleted file mode 100644
index 5608248f82..0000000000
Binary files a/tests/ref/compiler/for.png and /dev/null differ
diff --git a/tests/ref/compiler/highlight.png b/tests/ref/compiler/highlight.png
deleted file mode 100644
index ccbbc0560a..0000000000
Binary files a/tests/ref/compiler/highlight.png and /dev/null differ
diff --git a/tests/ref/compiler/if.png b/tests/ref/compiler/if.png
deleted file mode 100644
index bd3adc88b3..0000000000
Binary files a/tests/ref/compiler/if.png and /dev/null differ
diff --git a/tests/ref/compiler/import.png b/tests/ref/compiler/import.png
deleted file mode 100644
index 5c6132d2f7..0000000000
Binary files a/tests/ref/compiler/import.png and /dev/null differ
diff --git a/tests/ref/compiler/include.png b/tests/ref/compiler/include.png
deleted file mode 100644
index 7fdb0310b4..0000000000
Binary files a/tests/ref/compiler/include.png and /dev/null differ
diff --git a/tests/ref/compiler/label.png b/tests/ref/compiler/label.png
deleted file mode 100644
index 21764f978b..0000000000
Binary files a/tests/ref/compiler/label.png and /dev/null differ
diff --git a/tests/ref/compiler/let.png b/tests/ref/compiler/let.png
deleted file mode 100644
index 4423fe0a30..0000000000
Binary files a/tests/ref/compiler/let.png and /dev/null differ
diff --git a/tests/ref/compiler/ops.png b/tests/ref/compiler/ops.png
deleted file mode 100644
index 51fb9d1a5d..0000000000
Binary files a/tests/ref/compiler/ops.png and /dev/null differ
diff --git a/tests/ref/compiler/repr-color-gradient.png b/tests/ref/compiler/repr-color-gradient.png
deleted file mode 100644
index 11bde774ed..0000000000
Binary files a/tests/ref/compiler/repr-color-gradient.png and /dev/null differ
diff --git a/tests/ref/compiler/repr.png b/tests/ref/compiler/repr.png
deleted file mode 100644
index c90cc3cdb1..0000000000
Binary files a/tests/ref/compiler/repr.png and /dev/null differ
diff --git a/tests/ref/compiler/return.png b/tests/ref/compiler/return.png
deleted file mode 100644
index e8fa3ab2b8..0000000000
Binary files a/tests/ref/compiler/return.png and /dev/null differ
diff --git a/tests/ref/compiler/select-where-styles.png b/tests/ref/compiler/select-where-styles.png
deleted file mode 100644
index ffdc4babf0..0000000000
Binary files a/tests/ref/compiler/select-where-styles.png and /dev/null differ
diff --git a/tests/ref/compiler/selector-logical.png b/tests/ref/compiler/selector-logical.png
deleted file mode 100644
index eafa93c811..0000000000
Binary files a/tests/ref/compiler/selector-logical.png and /dev/null differ
diff --git a/tests/ref/compiler/set.png b/tests/ref/compiler/set.png
deleted file mode 100644
index 264093968a..0000000000
Binary files a/tests/ref/compiler/set.png and /dev/null differ
diff --git a/tests/ref/compiler/shorthand.png b/tests/ref/compiler/shorthand.png
deleted file mode 100644
index 4507177bcc..0000000000
Binary files a/tests/ref/compiler/shorthand.png and /dev/null differ
diff --git a/tests/ref/compiler/show-bare.png b/tests/ref/compiler/show-bare.png
deleted file mode 100644
index c6a1e1013f..0000000000
Binary files a/tests/ref/compiler/show-bare.png and /dev/null differ
diff --git a/tests/ref/compiler/show-node.png b/tests/ref/compiler/show-node.png
deleted file mode 100644
index 396e5429e8..0000000000
Binary files a/tests/ref/compiler/show-node.png and /dev/null differ
diff --git a/tests/ref/compiler/show-recursive.png b/tests/ref/compiler/show-recursive.png
deleted file mode 100644
index a5a153c07c..0000000000
Binary files a/tests/ref/compiler/show-recursive.png and /dev/null differ
diff --git a/tests/ref/compiler/show-selector-logical.png b/tests/ref/compiler/show-selector-logical.png
deleted file mode 100644
index a7a8005303..0000000000
Binary files a/tests/ref/compiler/show-selector-logical.png and /dev/null differ
diff --git a/tests/ref/compiler/show-selector.png b/tests/ref/compiler/show-selector.png
deleted file mode 100644
index 52e99c9ac7..0000000000
Binary files a/tests/ref/compiler/show-selector.png and /dev/null differ
diff --git a/tests/ref/compiler/show-set-func.png b/tests/ref/compiler/show-set-func.png
deleted file mode 100644
index c5ff24893b..0000000000
Binary files a/tests/ref/compiler/show-set-func.png and /dev/null differ
diff --git a/tests/ref/compiler/show-set-text.png b/tests/ref/compiler/show-set-text.png
deleted file mode 100644
index 27803e8a41..0000000000
Binary files a/tests/ref/compiler/show-set-text.png and /dev/null differ
diff --git a/tests/ref/compiler/show-set.png b/tests/ref/compiler/show-set.png
deleted file mode 100644
index e87fc60021..0000000000
Binary files a/tests/ref/compiler/show-set.png and /dev/null differ
diff --git a/tests/ref/compiler/show-text.png b/tests/ref/compiler/show-text.png
deleted file mode 100644
index 2026cc35ca..0000000000
Binary files a/tests/ref/compiler/show-text.png and /dev/null differ
diff --git a/tests/ref/compiler/while.png b/tests/ref/compiler/while.png
deleted file mode 100644
index d0f8647369..0000000000
Binary files a/tests/ref/compiler/while.png and /dev/null differ
diff --git a/tests/ref/compute/construct.png b/tests/ref/compute/construct.png
deleted file mode 100644
index e17174733e..0000000000
Binary files a/tests/ref/compute/construct.png and /dev/null differ
diff --git a/tests/ref/compute/data.png b/tests/ref/compute/data.png
deleted file mode 100644
index 2dab6875b7..0000000000
Binary files a/tests/ref/compute/data.png and /dev/null differ
diff --git a/tests/ref/compute/eval-path.png b/tests/ref/compute/eval-path.png
deleted file mode 100644
index c59dd2aa47..0000000000
Binary files a/tests/ref/compute/eval-path.png and /dev/null differ
diff --git a/tests/ref/compute/foundations.png b/tests/ref/compute/foundations.png
deleted file mode 100644
index 5d6ba7441a..0000000000
Binary files a/tests/ref/compute/foundations.png and /dev/null differ
diff --git a/tests/ref/container-layoutable-child.png b/tests/ref/container-layoutable-child.png
new file mode 100644
index 0000000000..972ccb6153
Binary files /dev/null and b/tests/ref/container-layoutable-child.png differ
diff --git a/tests/ref/content-field-materialized-heading.png b/tests/ref/content-field-materialized-heading.png
new file mode 100644
index 0000000000..722016278b
Binary files /dev/null and b/tests/ref/content-field-materialized-heading.png differ
diff --git a/tests/ref/content-field-materialized-query.png b/tests/ref/content-field-materialized-query.png
new file mode 100644
index 0000000000..2d2a14806f
Binary files /dev/null and b/tests/ref/content-field-materialized-query.png differ
diff --git a/tests/ref/content-field-materialized-table.png b/tests/ref/content-field-materialized-table.png
new file mode 100644
index 0000000000..09efe75389
Binary files /dev/null and b/tests/ref/content-field-materialized-table.png differ
diff --git a/tests/ref/content-fields-complex.png b/tests/ref/content-fields-complex.png
new file mode 100644
index 0000000000..624a8b332e
Binary files /dev/null and b/tests/ref/content-fields-complex.png differ
diff --git a/tests/ref/content-label-field-access.png b/tests/ref/content-label-field-access.png
new file mode 100644
index 0000000000..bdb7c0f2fe
Binary files /dev/null and b/tests/ref/content-label-field-access.png differ
diff --git a/tests/ref/content-label-fields-method.png b/tests/ref/content-label-fields-method.png
new file mode 100644
index 0000000000..bdb7c0f2fe
Binary files /dev/null and b/tests/ref/content-label-fields-method.png differ
diff --git a/tests/ref/content-label-has-method.png b/tests/ref/content-label-has-method.png
new file mode 100644
index 0000000000..bdb7c0f2fe
Binary files /dev/null and b/tests/ref/content-label-has-method.png differ
diff --git a/tests/ref/context-compatibility-locate.png b/tests/ref/context-compatibility-locate.png
new file mode 100644
index 0000000000..4c8944ab4d
Binary files /dev/null and b/tests/ref/context-compatibility-locate.png differ
diff --git a/tests/ref/context-compatibility-styling.png b/tests/ref/context-compatibility-styling.png
new file mode 100644
index 0000000000..aee16c3ab6
Binary files /dev/null and b/tests/ref/context-compatibility-styling.png differ
diff --git a/tests/ref/counter-basic-1.png b/tests/ref/counter-basic-1.png
new file mode 100644
index 0000000000..9228259480
Binary files /dev/null and b/tests/ref/counter-basic-1.png differ
diff --git a/tests/ref/counter-figure.png b/tests/ref/counter-figure.png
new file mode 100644
index 0000000000..5e4a4a5f39
Binary files /dev/null and b/tests/ref/counter-figure.png differ
diff --git a/tests/ref/counter-heading.png b/tests/ref/counter-heading.png
new file mode 100644
index 0000000000..96dafd6ad4
Binary files /dev/null and b/tests/ref/counter-heading.png differ
diff --git a/tests/ref/counter-label.png b/tests/ref/counter-label.png
new file mode 100644
index 0000000000..6fea90df7d
Binary files /dev/null and b/tests/ref/counter-label.png differ
diff --git a/tests/ref/counter-page.png b/tests/ref/counter-page.png
new file mode 100644
index 0000000000..be1653ebdd
Binary files /dev/null and b/tests/ref/counter-page.png differ
diff --git a/tests/ref/csv.png b/tests/ref/csv.png
new file mode 100644
index 0000000000..fd0c9a1cd5
Binary files /dev/null and b/tests/ref/csv.png differ
diff --git a/tests/ref/destructuring-during-loop-continue.png b/tests/ref/destructuring-during-loop-continue.png
new file mode 100644
index 0000000000..9ea8e3c1f0
Binary files /dev/null and b/tests/ref/destructuring-during-loop-continue.png differ
diff --git a/tests/ref/dict-basic-methods.png b/tests/ref/dict-basic-methods.png
new file mode 100644
index 0000000000..20410cc3bf
Binary files /dev/null and b/tests/ref/dict-basic-methods.png differ
diff --git a/tests/ref/dict-basic-syntax.png b/tests/ref/dict-basic-syntax.png
new file mode 100644
index 0000000000..02effef673
Binary files /dev/null and b/tests/ref/dict-basic-syntax.png differ
diff --git a/tests/ref/dict-remove-order.png b/tests/ref/dict-remove-order.png
new file mode 100644
index 0000000000..20410cc3bf
Binary files /dev/null and b/tests/ref/dict-remove-order.png differ
diff --git a/tests/ref/document-set-title.png b/tests/ref/document-set-title.png
new file mode 100644
index 0000000000..74bcfe191a
Binary files /dev/null and b/tests/ref/document-set-title.png differ
diff --git a/tests/ref/ellipse-auto-sizing.png b/tests/ref/ellipse-auto-sizing.png
new file mode 100644
index 0000000000..ed20152153
Binary files /dev/null and b/tests/ref/ellipse-auto-sizing.png differ
diff --git a/tests/ref/ellipse.png b/tests/ref/ellipse.png
new file mode 100644
index 0000000000..0f4e92cace
Binary files /dev/null and b/tests/ref/ellipse.png differ
diff --git a/tests/ref/emph-and-strong-call-in-word.png b/tests/ref/emph-and-strong-call-in-word.png
new file mode 100644
index 0000000000..4720f9949b
Binary files /dev/null and b/tests/ref/emph-and-strong-call-in-word.png differ
diff --git a/tests/ref/emph-double-underscore-empty-hint.png b/tests/ref/emph-double-underscore-empty-hint.png
new file mode 100644
index 0000000000..a940dfb6ab
Binary files /dev/null and b/tests/ref/emph-double-underscore-empty-hint.png differ
diff --git a/tests/ref/emph-syntax.png b/tests/ref/emph-syntax.png
new file mode 100644
index 0000000000..66f117a8e7
Binary files /dev/null and b/tests/ref/emph-syntax.png differ
diff --git a/tests/ref/empty.png b/tests/ref/empty.png
deleted file mode 100644
index db3a66950b..0000000000
Binary files a/tests/ref/empty.png and /dev/null differ
diff --git a/tests/ref/enum-built-in-loop.png b/tests/ref/enum-built-in-loop.png
new file mode 100644
index 0000000000..298518da33
Binary files /dev/null and b/tests/ref/enum-built-in-loop.png differ
diff --git a/tests/ref/enum-function-call.png b/tests/ref/enum-function-call.png
new file mode 100644
index 0000000000..a451f27c7f
Binary files /dev/null and b/tests/ref/enum-function-call.png differ
diff --git a/tests/ref/enum-number-align-2d.png b/tests/ref/enum-number-align-2d.png
new file mode 100644
index 0000000000..e205844ff5
Binary files /dev/null and b/tests/ref/enum-number-align-2d.png differ
diff --git a/tests/ref/enum-number-align-default.png b/tests/ref/enum-number-align-default.png
new file mode 100644
index 0000000000..c47f9001a6
Binary files /dev/null and b/tests/ref/enum-number-align-default.png differ
diff --git a/tests/ref/enum-number-align-specified.png b/tests/ref/enum-number-align-specified.png
new file mode 100644
index 0000000000..b2f2d619a4
Binary files /dev/null and b/tests/ref/enum-number-align-specified.png differ
diff --git a/tests/ref/enum-number-align-unaffected.png b/tests/ref/enum-number-align-unaffected.png
new file mode 100644
index 0000000000..3abcaaab79
Binary files /dev/null and b/tests/ref/enum-number-align-unaffected.png differ
diff --git a/tests/ref/enum-number-align-unfolded.png b/tests/ref/enum-number-align-unfolded.png
new file mode 100644
index 0000000000..8c4f294338
Binary files /dev/null and b/tests/ref/enum-number-align-unfolded.png differ
diff --git a/tests/ref/enum-number-override-nested.png b/tests/ref/enum-number-override-nested.png
new file mode 100644
index 0000000000..22bb7611eb
Binary files /dev/null and b/tests/ref/enum-number-override-nested.png differ
diff --git a/tests/ref/enum-number-override.png b/tests/ref/enum-number-override.png
new file mode 100644
index 0000000000..65c0f9d845
Binary files /dev/null and b/tests/ref/enum-number-override.png differ
diff --git a/tests/ref/enum-numbering-closure-nested-complex.png b/tests/ref/enum-numbering-closure-nested-complex.png
new file mode 100644
index 0000000000..a756f37cd5
Binary files /dev/null and b/tests/ref/enum-numbering-closure-nested-complex.png differ
diff --git a/tests/ref/enum-numbering-closure-nested.png b/tests/ref/enum-numbering-closure-nested.png
new file mode 100644
index 0000000000..25a5c42d59
Binary files /dev/null and b/tests/ref/enum-numbering-closure-nested.png differ
diff --git a/tests/ref/enum-numbering-closure.png b/tests/ref/enum-numbering-closure.png
new file mode 100644
index 0000000000..bf86f55418
Binary files /dev/null and b/tests/ref/enum-numbering-closure.png differ
diff --git a/tests/ref/enum-numbering-full.png b/tests/ref/enum-numbering-full.png
new file mode 100644
index 0000000000..46449e5730
Binary files /dev/null and b/tests/ref/enum-numbering-full.png differ
diff --git a/tests/ref/enum-numbering-pattern.png b/tests/ref/enum-numbering-pattern.png
new file mode 100644
index 0000000000..4ecb9e4a2d
Binary files /dev/null and b/tests/ref/enum-numbering-pattern.png differ
diff --git a/tests/ref/enum-syntax-at-start.png b/tests/ref/enum-syntax-at-start.png
new file mode 100644
index 0000000000..ce9f3967e1
Binary files /dev/null and b/tests/ref/enum-syntax-at-start.png differ
diff --git a/tests/ref/enum-syntax-edge-cases.png b/tests/ref/enum-syntax-edge-cases.png
new file mode 100644
index 0000000000..496dc8e3df
Binary files /dev/null and b/tests/ref/enum-syntax-edge-cases.png differ
diff --git a/tests/ref/escape.png b/tests/ref/escape.png
new file mode 100644
index 0000000000..0b49606ca8
Binary files /dev/null and b/tests/ref/escape.png differ
diff --git a/tests/ref/eval-in-show-rule.png b/tests/ref/eval-in-show-rule.png
new file mode 100644
index 0000000000..91a038683f
Binary files /dev/null and b/tests/ref/eval-in-show-rule.png differ
diff --git a/tests/ref/eval-mode.png b/tests/ref/eval-mode.png
new file mode 100644
index 0000000000..94357ff4ff
Binary files /dev/null and b/tests/ref/eval-mode.png differ
diff --git a/tests/ref/eval-path-resolve-in-show-rule.png b/tests/ref/eval-path-resolve-in-show-rule.png
new file mode 100644
index 0000000000..cf5c183ad8
Binary files /dev/null and b/tests/ref/eval-path-resolve-in-show-rule.png differ
diff --git a/tests/ref/eval-path-resolve.png b/tests/ref/eval-path-resolve.png
new file mode 100644
index 0000000000..cf5c183ad8
Binary files /dev/null and b/tests/ref/eval-path-resolve.png differ
diff --git a/tests/ref/field-function.png b/tests/ref/field-function.png
new file mode 100644
index 0000000000..261fb39571
Binary files /dev/null and b/tests/ref/field-function.png differ
diff --git a/tests/ref/figure-and-caption-show.png b/tests/ref/figure-and-caption-show.png
new file mode 100644
index 0000000000..daf8f2b620
Binary files /dev/null and b/tests/ref/figure-and-caption-show.png differ
diff --git a/tests/ref/figure-basic.png b/tests/ref/figure-basic.png
new file mode 100644
index 0000000000..22a841db5e
Binary files /dev/null and b/tests/ref/figure-basic.png differ
diff --git a/tests/ref/figure-breakable.png b/tests/ref/figure-breakable.png
new file mode 100644
index 0000000000..40cb3ec577
Binary files /dev/null and b/tests/ref/figure-breakable.png differ
diff --git a/tests/ref/figure-caption-separator.png b/tests/ref/figure-caption-separator.png
new file mode 100644
index 0000000000..e645f01fed
Binary files /dev/null and b/tests/ref/figure-caption-separator.png differ
diff --git a/tests/ref/figure-caption-show.png b/tests/ref/figure-caption-show.png
new file mode 100644
index 0000000000..4ed6443ac3
Binary files /dev/null and b/tests/ref/figure-caption-show.png differ
diff --git a/tests/ref/figure-caption-where-selector.png b/tests/ref/figure-caption-where-selector.png
new file mode 100644
index 0000000000..08eb46f60e
Binary files /dev/null and b/tests/ref/figure-caption-where-selector.png differ
diff --git a/tests/ref/figure-localization-fr.png b/tests/ref/figure-localization-fr.png
new file mode 100644
index 0000000000..665b35522e
Binary files /dev/null and b/tests/ref/figure-localization-fr.png differ
diff --git a/tests/ref/figure-localization-gr.png b/tests/ref/figure-localization-gr.png
new file mode 100644
index 0000000000..46b52b051c
Binary files /dev/null and b/tests/ref/figure-localization-gr.png differ
diff --git a/tests/ref/figure-localization-ru.png b/tests/ref/figure-localization-ru.png
new file mode 100644
index 0000000000..102df59718
Binary files /dev/null and b/tests/ref/figure-localization-ru.png differ
diff --git a/tests/ref/figure-localization-zh.png b/tests/ref/figure-localization-zh.png
new file mode 100644
index 0000000000..f7625b1b40
Binary files /dev/null and b/tests/ref/figure-localization-zh.png differ
diff --git a/tests/ref/figure-table.png b/tests/ref/figure-table.png
new file mode 100644
index 0000000000..9a09f65900
Binary files /dev/null and b/tests/ref/figure-table.png differ
diff --git a/tests/ref/figure-theorem.png b/tests/ref/figure-theorem.png
new file mode 100644
index 0000000000..10d6eeacb6
Binary files /dev/null and b/tests/ref/figure-theorem.png differ
diff --git a/tests/ref/float-display.png b/tests/ref/float-display.png
new file mode 100644
index 0000000000..6c33b372f0
Binary files /dev/null and b/tests/ref/float-display.png differ
diff --git a/tests/ref/float-repr.png b/tests/ref/float-repr.png
new file mode 100644
index 0000000000..8b51096973
Binary files /dev/null and b/tests/ref/float-repr.png differ
diff --git a/tests/ref/flow-first-region-counter-update-and-placed.png b/tests/ref/flow-first-region-counter-update-and-placed.png
new file mode 100644
index 0000000000..213167199c
Binary files /dev/null and b/tests/ref/flow-first-region-counter-update-and-placed.png differ
diff --git a/tests/ref/flow-first-region-counter-update-placed-and-line.png b/tests/ref/flow-first-region-counter-update-placed-and-line.png
new file mode 100644
index 0000000000..95ca518e40
Binary files /dev/null and b/tests/ref/flow-first-region-counter-update-placed-and-line.png differ
diff --git a/tests/ref/flow-first-region-counter-update.png b/tests/ref/flow-first-region-counter-update.png
new file mode 100644
index 0000000000..8e883335ae
Binary files /dev/null and b/tests/ref/flow-first-region-counter-update.png differ
diff --git a/tests/ref/flow-first-region-no-item.png b/tests/ref/flow-first-region-no-item.png
new file mode 100644
index 0000000000..e888898c01
Binary files /dev/null and b/tests/ref/flow-first-region-no-item.png differ
diff --git a/tests/ref/flow-first-region-placed.png b/tests/ref/flow-first-region-placed.png
new file mode 100644
index 0000000000..cae4aa32f6
Binary files /dev/null and b/tests/ref/flow-first-region-placed.png differ
diff --git a/tests/ref/flow-first-region-zero-sized-item.png b/tests/ref/flow-first-region-zero-sized-item.png
new file mode 100644
index 0000000000..2e75fcfe79
Binary files /dev/null and b/tests/ref/flow-first-region-zero-sized-item.png differ
diff --git a/tests/ref/flow-fr.png b/tests/ref/flow-fr.png
new file mode 100644
index 0000000000..b09a960424
Binary files /dev/null and b/tests/ref/flow-fr.png differ
diff --git a/tests/ref/flow-heading-no-orphan.png b/tests/ref/flow-heading-no-orphan.png
new file mode 100644
index 0000000000..87789ea111
Binary files /dev/null and b/tests/ref/flow-heading-no-orphan.png differ
diff --git a/tests/ref/flow-par-no-orphan-and-widow-lines.png b/tests/ref/flow-par-no-orphan-and-widow-lines.png
new file mode 100644
index 0000000000..cace5d4488
Binary files /dev/null and b/tests/ref/flow-par-no-orphan-and-widow-lines.png differ
diff --git a/tests/ref/fold-vec-order-meta.png b/tests/ref/fold-vec-order-meta.png
new file mode 100644
index 0000000000..36e3cd5155
Binary files /dev/null and b/tests/ref/fold-vec-order-meta.png differ
diff --git a/tests/ref/fold-vec-order-text-decos.png b/tests/ref/fold-vec-order-text-decos.png
new file mode 100644
index 0000000000..62c9e1af98
Binary files /dev/null and b/tests/ref/fold-vec-order-text-decos.png differ
diff --git a/tests/ref/fold-vec-order-text-features.png b/tests/ref/fold-vec-order-text-features.png
new file mode 100644
index 0000000000..f2ff6f25a4
Binary files /dev/null and b/tests/ref/fold-vec-order-text-features.png differ
diff --git a/tests/ref/footnote-basic.png b/tests/ref/footnote-basic.png
new file mode 100644
index 0000000000..3562438bd4
Binary files /dev/null and b/tests/ref/footnote-basic.png differ
diff --git a/tests/ref/footnote-break-across-pages.png b/tests/ref/footnote-break-across-pages.png
new file mode 100644
index 0000000000..8ec55418c4
Binary files /dev/null and b/tests/ref/footnote-break-across-pages.png differ
diff --git a/tests/ref/footnote-duplicate.png b/tests/ref/footnote-duplicate.png
new file mode 100644
index 0000000000..7c83b8de63
Binary files /dev/null and b/tests/ref/footnote-duplicate.png differ
diff --git a/tests/ref/footnote-entry.png b/tests/ref/footnote-entry.png
new file mode 100644
index 0000000000..e62315c464
Binary files /dev/null and b/tests/ref/footnote-entry.png differ
diff --git a/tests/ref/footnote-in-caption.png b/tests/ref/footnote-in-caption.png
new file mode 100644
index 0000000000..8d548c5962
Binary files /dev/null and b/tests/ref/footnote-in-caption.png differ
diff --git a/tests/ref/footnote-in-columns.png b/tests/ref/footnote-in-columns.png
new file mode 100644
index 0000000000..e16b4ebcfa
Binary files /dev/null and b/tests/ref/footnote-in-columns.png differ
diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png
new file mode 100644
index 0000000000..0fd0acc7f0
Binary files /dev/null and b/tests/ref/footnote-in-table.png differ
diff --git a/tests/ref/footnote-invariant.png b/tests/ref/footnote-invariant.png
new file mode 100644
index 0000000000..c49c268d44
Binary files /dev/null and b/tests/ref/footnote-invariant.png differ
diff --git a/tests/ref/footnote-nested-same-frame.png b/tests/ref/footnote-nested-same-frame.png
new file mode 100644
index 0000000000..b22276d5cc
Binary files /dev/null and b/tests/ref/footnote-nested-same-frame.png differ
diff --git a/tests/ref/footnote-nested.png b/tests/ref/footnote-nested.png
new file mode 100644
index 0000000000..fecf2e8de7
Binary files /dev/null and b/tests/ref/footnote-nested.png differ
diff --git a/tests/ref/footnote-ref-call.png b/tests/ref/footnote-ref-call.png
new file mode 100644
index 0000000000..3c795302e3
Binary files /dev/null and b/tests/ref/footnote-ref-call.png differ
diff --git a/tests/ref/footnote-ref-forward.png b/tests/ref/footnote-ref-forward.png
new file mode 100644
index 0000000000..e67671bec4
Binary files /dev/null and b/tests/ref/footnote-ref-forward.png differ
diff --git a/tests/ref/footnote-ref-in-footnote.png b/tests/ref/footnote-ref-in-footnote.png
new file mode 100644
index 0000000000..4718a0887d
Binary files /dev/null and b/tests/ref/footnote-ref-in-footnote.png differ
diff --git a/tests/ref/footnote-ref-multiple.png b/tests/ref/footnote-ref-multiple.png
new file mode 100644
index 0000000000..fc6f11cf54
Binary files /dev/null and b/tests/ref/footnote-ref-multiple.png differ
diff --git a/tests/ref/footnote-ref.png b/tests/ref/footnote-ref.png
new file mode 100644
index 0000000000..517d997aaf
Binary files /dev/null and b/tests/ref/footnote-ref.png differ
diff --git a/tests/ref/footnote-space-collapsing.png b/tests/ref/footnote-space-collapsing.png
new file mode 100644
index 0000000000..d7d02704dc
Binary files /dev/null and b/tests/ref/footnote-space-collapsing.png differ
diff --git a/tests/ref/footnote-styling.png b/tests/ref/footnote-styling.png
new file mode 100644
index 0000000000..fd7684af7a
Binary files /dev/null and b/tests/ref/footnote-styling.png differ
diff --git a/tests/ref/for-loop-basic.png b/tests/ref/for-loop-basic.png
new file mode 100644
index 0000000000..42d611eff1
Binary files /dev/null and b/tests/ref/for-loop-basic.png differ
diff --git a/tests/ref/gradient-conic-angled.png b/tests/ref/gradient-conic-angled.png
new file mode 100644
index 0000000000..163366e6c4
Binary files /dev/null and b/tests/ref/gradient-conic-angled.png differ
diff --git a/tests/ref/gradient-conic-center-shifted-1.png b/tests/ref/gradient-conic-center-shifted-1.png
new file mode 100644
index 0000000000..5964b124a7
Binary files /dev/null and b/tests/ref/gradient-conic-center-shifted-1.png differ
diff --git a/tests/ref/gradient-conic-center-shifted-2.png b/tests/ref/gradient-conic-center-shifted-2.png
new file mode 100644
index 0000000000..53e5da9822
Binary files /dev/null and b/tests/ref/gradient-conic-center-shifted-2.png differ
diff --git a/tests/ref/gradient-conic-hsl.png b/tests/ref/gradient-conic-hsl.png
new file mode 100644
index 0000000000..321a3b07cc
Binary files /dev/null and b/tests/ref/gradient-conic-hsl.png differ
diff --git a/tests/ref/gradient-conic-hsv.png b/tests/ref/gradient-conic-hsv.png
new file mode 100644
index 0000000000..648e1fb501
Binary files /dev/null and b/tests/ref/gradient-conic-hsv.png differ
diff --git a/tests/ref/gradient-conic-oklab.png b/tests/ref/gradient-conic-oklab.png
new file mode 100644
index 0000000000..e567eacc16
Binary files /dev/null and b/tests/ref/gradient-conic-oklab.png differ
diff --git a/tests/ref/gradient-conic-oklch.png b/tests/ref/gradient-conic-oklch.png
new file mode 100644
index 0000000000..f712defab1
Binary files /dev/null and b/tests/ref/gradient-conic-oklch.png differ
diff --git a/tests/ref/gradient-conic-relative-parent.png b/tests/ref/gradient-conic-relative-parent.png
new file mode 100644
index 0000000000..1685ca446e
Binary files /dev/null and b/tests/ref/gradient-conic-relative-parent.png differ
diff --git a/tests/ref/gradient-conic-relative-self.png b/tests/ref/gradient-conic-relative-self.png
new file mode 100644
index 0000000000..108fe43a61
Binary files /dev/null and b/tests/ref/gradient-conic-relative-self.png differ
diff --git a/tests/ref/gradient-conic-stroke.png b/tests/ref/gradient-conic-stroke.png
new file mode 100644
index 0000000000..ae631fd4fc
Binary files /dev/null and b/tests/ref/gradient-conic-stroke.png differ
diff --git a/tests/ref/gradient-conic-text.png b/tests/ref/gradient-conic-text.png
new file mode 100644
index 0000000000..1abef3cb84
Binary files /dev/null and b/tests/ref/gradient-conic-text.png differ
diff --git a/tests/ref/gradient-conic.png b/tests/ref/gradient-conic.png
new file mode 100644
index 0000000000..0f5f5bada5
Binary files /dev/null and b/tests/ref/gradient-conic.png differ
diff --git a/tests/ref/gradient-fill-and-stroke.png b/tests/ref/gradient-fill-and-stroke.png
new file mode 100644
index 0000000000..7856351716
Binary files /dev/null and b/tests/ref/gradient-fill-and-stroke.png differ
diff --git a/tests/ref/gradient-linear-angled.png b/tests/ref/gradient-linear-angled.png
new file mode 100644
index 0000000000..b195b1281d
Binary files /dev/null and b/tests/ref/gradient-linear-angled.png differ
diff --git a/tests/ref/gradient-linear-hsl.png b/tests/ref/gradient-linear-hsl.png
new file mode 100644
index 0000000000..7bfe958bf4
Binary files /dev/null and b/tests/ref/gradient-linear-hsl.png differ
diff --git a/tests/ref/gradient-linear-hsv.png b/tests/ref/gradient-linear-hsv.png
new file mode 100644
index 0000000000..56b446f280
Binary files /dev/null and b/tests/ref/gradient-linear-hsv.png differ
diff --git a/tests/ref/gradient-linear-line.png b/tests/ref/gradient-linear-line.png
new file mode 100644
index 0000000000..d32aba8923
Binary files /dev/null and b/tests/ref/gradient-linear-line.png differ
diff --git a/tests/ref/gradient-linear-oklab.png b/tests/ref/gradient-linear-oklab.png
new file mode 100644
index 0000000000..6f963c7729
Binary files /dev/null and b/tests/ref/gradient-linear-oklab.png differ
diff --git a/tests/ref/gradient-linear-oklch.png b/tests/ref/gradient-linear-oklch.png
new file mode 100644
index 0000000000..394d0935e3
Binary files /dev/null and b/tests/ref/gradient-linear-oklch.png differ
diff --git a/tests/ref/gradient-linear-relative-parent.png b/tests/ref/gradient-linear-relative-parent.png
new file mode 100644
index 0000000000..2ad1286e80
Binary files /dev/null and b/tests/ref/gradient-linear-relative-parent.png differ
diff --git a/tests/ref/gradient-linear-relative-self.png b/tests/ref/gradient-linear-relative-self.png
new file mode 100644
index 0000000000..d573a892fd
Binary files /dev/null and b/tests/ref/gradient-linear-relative-self.png differ
diff --git a/tests/ref/gradient-linear-repeat-and-mirror-1.png b/tests/ref/gradient-linear-repeat-and-mirror-1.png
new file mode 100644
index 0000000000..9640d5e203
Binary files /dev/null and b/tests/ref/gradient-linear-repeat-and-mirror-1.png differ
diff --git a/tests/ref/gradient-linear-repeat-and-mirror-2.png b/tests/ref/gradient-linear-repeat-and-mirror-2.png
new file mode 100644
index 0000000000..98cf254349
Binary files /dev/null and b/tests/ref/gradient-linear-repeat-and-mirror-2.png differ
diff --git a/tests/ref/gradient-linear-repeat-and-mirror-3.png b/tests/ref/gradient-linear-repeat-and-mirror-3.png
new file mode 100644
index 0000000000..641e54c9c6
Binary files /dev/null and b/tests/ref/gradient-linear-repeat-and-mirror-3.png differ
diff --git a/tests/ref/gradient-linear-sharp-and-repeat.png b/tests/ref/gradient-linear-sharp-and-repeat.png
new file mode 100644
index 0000000000..e46af7a05e
Binary files /dev/null and b/tests/ref/gradient-linear-sharp-and-repeat.png differ
diff --git a/tests/ref/gradient-linear-sharp-and-smooth.png b/tests/ref/gradient-linear-sharp-and-smooth.png
new file mode 100644
index 0000000000..5bd74d2476
Binary files /dev/null and b/tests/ref/gradient-linear-sharp-and-smooth.png differ
diff --git a/tests/ref/gradient-linear-sharp-repeat-and-mirror.png b/tests/ref/gradient-linear-sharp-repeat-and-mirror.png
new file mode 100644
index 0000000000..5b4b9817b0
Binary files /dev/null and b/tests/ref/gradient-linear-sharp-repeat-and-mirror.png differ
diff --git a/tests/ref/gradient-linear-sharp.png b/tests/ref/gradient-linear-sharp.png
new file mode 100644
index 0000000000..4d63884f75
Binary files /dev/null and b/tests/ref/gradient-linear-sharp.png differ
diff --git a/tests/ref/gradient-linear-stroke.png b/tests/ref/gradient-linear-stroke.png
new file mode 100644
index 0000000000..490ffec265
Binary files /dev/null and b/tests/ref/gradient-linear-stroke.png differ
diff --git a/tests/ref/gradient-math-cancel.png b/tests/ref/gradient-math-cancel.png
new file mode 100644
index 0000000000..0769d6d36b
Binary files /dev/null and b/tests/ref/gradient-math-cancel.png differ
diff --git a/tests/ref/gradient-math-conic.png b/tests/ref/gradient-math-conic.png
new file mode 100644
index 0000000000..88ff7a85c6
Binary files /dev/null and b/tests/ref/gradient-math-conic.png differ
diff --git a/tests/ref/gradient-math-dir.png b/tests/ref/gradient-math-dir.png
new file mode 100644
index 0000000000..5ed1918216
Binary files /dev/null and b/tests/ref/gradient-math-dir.png differ
diff --git a/tests/ref/gradient-math-frac.png b/tests/ref/gradient-math-frac.png
new file mode 100644
index 0000000000..1316dc47af
Binary files /dev/null and b/tests/ref/gradient-math-frac.png differ
diff --git a/tests/ref/gradient-math-mat.png b/tests/ref/gradient-math-mat.png
new file mode 100644
index 0000000000..aa3332b96c
Binary files /dev/null and b/tests/ref/gradient-math-mat.png differ
diff --git a/tests/ref/gradient-math-misc.png b/tests/ref/gradient-math-misc.png
new file mode 100644
index 0000000000..b8fbdd7455
Binary files /dev/null and b/tests/ref/gradient-math-misc.png differ
diff --git a/tests/ref/gradient-math-radial.png b/tests/ref/gradient-math-radial.png
new file mode 100644
index 0000000000..c9b966b29c
Binary files /dev/null and b/tests/ref/gradient-math-radial.png differ
diff --git a/tests/ref/gradient-math-root.png b/tests/ref/gradient-math-root.png
new file mode 100644
index 0000000000..4c2e4272e7
Binary files /dev/null and b/tests/ref/gradient-math-root.png differ
diff --git a/tests/ref/gradient-math-underover.png b/tests/ref/gradient-math-underover.png
new file mode 100644
index 0000000000..8909805112
Binary files /dev/null and b/tests/ref/gradient-math-underover.png differ
diff --git a/tests/ref/gradient-presets.png b/tests/ref/gradient-presets.png
new file mode 100644
index 0000000000..0c7fabdd7d
Binary files /dev/null and b/tests/ref/gradient-presets.png differ
diff --git a/tests/ref/gradient-radial-center.png b/tests/ref/gradient-radial-center.png
new file mode 100644
index 0000000000..e89e1f30bb
Binary files /dev/null and b/tests/ref/gradient-radial-center.png differ
diff --git a/tests/ref/gradient-radial-focal-center-and-radius.png b/tests/ref/gradient-radial-focal-center-and-radius.png
new file mode 100644
index 0000000000..4bc8a5d6d4
Binary files /dev/null and b/tests/ref/gradient-radial-focal-center-and-radius.png differ
diff --git a/tests/ref/gradient-radial-hsl.png b/tests/ref/gradient-radial-hsl.png
new file mode 100644
index 0000000000..4a2ded1862
Binary files /dev/null and b/tests/ref/gradient-radial-hsl.png differ
diff --git a/tests/ref/gradient-radial-radius.png b/tests/ref/gradient-radial-radius.png
new file mode 100644
index 0000000000..1037e63f23
Binary files /dev/null and b/tests/ref/gradient-radial-radius.png differ
diff --git a/tests/ref/gradient-radial-relative-parent.png b/tests/ref/gradient-radial-relative-parent.png
new file mode 100644
index 0000000000..f8addbe01b
Binary files /dev/null and b/tests/ref/gradient-radial-relative-parent.png differ
diff --git a/tests/ref/gradient-radial-relative-self.png b/tests/ref/gradient-radial-relative-self.png
new file mode 100644
index 0000000000..f5fc683640
Binary files /dev/null and b/tests/ref/gradient-radial-relative-self.png differ
diff --git a/tests/ref/gradient-radial-text.png b/tests/ref/gradient-radial-text.png
new file mode 100644
index 0000000000..6da0987801
Binary files /dev/null and b/tests/ref/gradient-radial-text.png differ
diff --git a/tests/ref/gradient-repr.png b/tests/ref/gradient-repr.png
new file mode 100644
index 0000000000..04908e5945
Binary files /dev/null and b/tests/ref/gradient-repr.png differ
diff --git a/tests/ref/gradient-text-decoration.png b/tests/ref/gradient-text-decoration.png
new file mode 100644
index 0000000000..d1713c99de
Binary files /dev/null and b/tests/ref/gradient-text-decoration.png differ
diff --git a/tests/ref/gradient-text-dir.png b/tests/ref/gradient-text-dir.png
new file mode 100644
index 0000000000..eab56d66e7
Binary files /dev/null and b/tests/ref/gradient-text-dir.png differ
diff --git a/tests/ref/gradient-text-global.png b/tests/ref/gradient-text-global.png
new file mode 100644
index 0000000000..7892fbb2c2
Binary files /dev/null and b/tests/ref/gradient-text-global.png differ
diff --git a/tests/ref/gradient-text-in-container.png b/tests/ref/gradient-text-in-container.png
new file mode 100644
index 0000000000..9122a556da
Binary files /dev/null and b/tests/ref/gradient-text-in-container.png differ
diff --git a/tests/ref/gradient-text-rotate.png b/tests/ref/gradient-text-rotate.png
new file mode 100644
index 0000000000..a32cacf851
Binary files /dev/null and b/tests/ref/gradient-text-rotate.png differ
diff --git a/tests/ref/gradient-transformed.png b/tests/ref/gradient-transformed.png
new file mode 100644
index 0000000000..2ad1286e80
Binary files /dev/null and b/tests/ref/gradient-transformed.png differ
diff --git a/tests/ref/grid-align.png b/tests/ref/grid-align.png
new file mode 100644
index 0000000000..f85abf6949
Binary files /dev/null and b/tests/ref/grid-align.png differ
diff --git a/tests/ref/grid-auto-shrink.png b/tests/ref/grid-auto-shrink.png
new file mode 100644
index 0000000000..27813e2611
Binary files /dev/null and b/tests/ref/grid-auto-shrink.png differ
diff --git a/tests/ref/grid-breaking-expand-vertically.png b/tests/ref/grid-breaking-expand-vertically.png
new file mode 100644
index 0000000000..14434d7c43
Binary files /dev/null and b/tests/ref/grid-breaking-expand-vertically.png differ
diff --git a/tests/ref/grid-calendar.png b/tests/ref/grid-calendar.png
new file mode 100644
index 0000000000..0609b84f05
Binary files /dev/null and b/tests/ref/grid-calendar.png differ
diff --git a/tests/ref/grid-cell-align-override.png b/tests/ref/grid-cell-align-override.png
new file mode 100644
index 0000000000..8ffde97f57
Binary files /dev/null and b/tests/ref/grid-cell-align-override.png differ
diff --git a/tests/ref/grid-cell-breaking.png b/tests/ref/grid-cell-breaking.png
new file mode 100644
index 0000000000..c91a399321
Binary files /dev/null and b/tests/ref/grid-cell-breaking.png differ
diff --git a/tests/ref/grid-cell-folding.png b/tests/ref/grid-cell-folding.png
new file mode 100644
index 0000000000..ce1108c69a
Binary files /dev/null and b/tests/ref/grid-cell-folding.png differ
diff --git a/tests/ref/grid-cell-override-in-header-and-footer-with-gutter.png b/tests/ref/grid-cell-override-in-header-and-footer-with-gutter.png
new file mode 100644
index 0000000000..a475bf90d4
Binary files /dev/null and b/tests/ref/grid-cell-override-in-header-and-footer-with-gutter.png differ
diff --git a/tests/ref/grid-cell-override-in-header-and-footer.png b/tests/ref/grid-cell-override-in-header-and-footer.png
new file mode 100644
index 0000000000..4d31e3796a
Binary files /dev/null and b/tests/ref/grid-cell-override-in-header-and-footer.png differ
diff --git a/tests/ref/grid-cell-override.png b/tests/ref/grid-cell-override.png
new file mode 100644
index 0000000000..d6f37d632f
Binary files /dev/null and b/tests/ref/grid-cell-override.png differ
diff --git a/tests/ref/grid-cell-position-automatic-skip-manual.png b/tests/ref/grid-cell-position-automatic-skip-manual.png
new file mode 100644
index 0000000000..ec615c9774
Binary files /dev/null and b/tests/ref/grid-cell-position-automatic-skip-manual.png differ
diff --git a/tests/ref/grid-cell-position-extra-rows.png b/tests/ref/grid-cell-position-extra-rows.png
new file mode 100644
index 0000000000..4d73c3f7c4
Binary files /dev/null and b/tests/ref/grid-cell-position-extra-rows.png differ
diff --git a/tests/ref/grid-cell-position-out-of-order.png b/tests/ref/grid-cell-position-out-of-order.png
new file mode 100644
index 0000000000..d6bdad462f
Binary files /dev/null and b/tests/ref/grid-cell-position-out-of-order.png differ
diff --git a/tests/ref/grid-cell-position-partial.png b/tests/ref/grid-cell-position-partial.png
new file mode 100644
index 0000000000..3012c5b5ef
Binary files /dev/null and b/tests/ref/grid-cell-position-partial.png differ
diff --git a/tests/ref/grid-cell-set.png b/tests/ref/grid-cell-set.png
new file mode 100644
index 0000000000..8795e53efb
Binary files /dev/null and b/tests/ref/grid-cell-set.png differ
diff --git a/tests/ref/grid-cell-show-and-override.png b/tests/ref/grid-cell-show-and-override.png
new file mode 100644
index 0000000000..6af555964c
Binary files /dev/null and b/tests/ref/grid-cell-show-and-override.png differ
diff --git a/tests/ref/grid-cell-show-based-on-position.png b/tests/ref/grid-cell-show-based-on-position.png
new file mode 100644
index 0000000000..26ad62849d
Binary files /dev/null and b/tests/ref/grid-cell-show-based-on-position.png differ
diff --git a/tests/ref/grid-cell-show-emph.png b/tests/ref/grid-cell-show-emph.png
new file mode 100644
index 0000000000..bfc03d6d32
Binary files /dev/null and b/tests/ref/grid-cell-show-emph.png differ
diff --git a/tests/ref/grid-cell-show-x-y.png b/tests/ref/grid-cell-show-x-y.png
new file mode 100644
index 0000000000..0fb4c2c55e
Binary files /dev/null and b/tests/ref/grid-cell-show-x-y.png differ
diff --git a/tests/ref/grid-cell-show.png b/tests/ref/grid-cell-show.png
new file mode 100644
index 0000000000..9ac6d2695d
Binary files /dev/null and b/tests/ref/grid-cell-show.png differ
diff --git a/tests/ref/grid-cell-various-overrides.png b/tests/ref/grid-cell-various-overrides.png
new file mode 100644
index 0000000000..74490e8464
Binary files /dev/null and b/tests/ref/grid-cell-various-overrides.png differ
diff --git a/tests/ref/grid-colspan-gutter.png b/tests/ref/grid-colspan-gutter.png
new file mode 100644
index 0000000000..2ba9c217ac
Binary files /dev/null and b/tests/ref/grid-colspan-gutter.png differ
diff --git a/tests/ref/grid-colspan-multiple-regions.png b/tests/ref/grid-colspan-multiple-regions.png
new file mode 100644
index 0000000000..22811acaeb
Binary files /dev/null and b/tests/ref/grid-colspan-multiple-regions.png differ
diff --git a/tests/ref/grid-colspan-over-all-fr-columns-page-width-auto.png b/tests/ref/grid-colspan-over-all-fr-columns-page-width-auto.png
new file mode 100644
index 0000000000..b5cf6cacf5
Binary files /dev/null and b/tests/ref/grid-colspan-over-all-fr-columns-page-width-auto.png differ
diff --git a/tests/ref/grid-colspan-over-all-fr-columns.png b/tests/ref/grid-colspan-over-all-fr-columns.png
new file mode 100644
index 0000000000..c152f3cc79
Binary files /dev/null and b/tests/ref/grid-colspan-over-all-fr-columns.png differ
diff --git a/tests/ref/grid-colspan-over-some-fr-columns.png b/tests/ref/grid-colspan-over-some-fr-columns.png
new file mode 100644
index 0000000000..5d8157c201
Binary files /dev/null and b/tests/ref/grid-colspan-over-some-fr-columns.png differ
diff --git a/tests/ref/grid-colspan-thick-stroke.png b/tests/ref/grid-colspan-thick-stroke.png
new file mode 100644
index 0000000000..7348551e07
Binary files /dev/null and b/tests/ref/grid-colspan-thick-stroke.png differ
diff --git a/tests/ref/grid-colspan.png b/tests/ref/grid-colspan.png
new file mode 100644
index 0000000000..419d23b2f4
Binary files /dev/null and b/tests/ref/grid-colspan.png differ
diff --git a/tests/ref/grid-column-sizing-auto-base.png b/tests/ref/grid-column-sizing-auto-base.png
new file mode 100644
index 0000000000..75664027d2
Binary files /dev/null and b/tests/ref/grid-column-sizing-auto-base.png differ
diff --git a/tests/ref/grid-column-sizing-fr-base.png b/tests/ref/grid-column-sizing-fr-base.png
new file mode 100644
index 0000000000..d4a44be706
Binary files /dev/null and b/tests/ref/grid-column-sizing-fr-base.png differ
diff --git a/tests/ref/grid-column-sizing-mixed-base.png b/tests/ref/grid-column-sizing-mixed-base.png
new file mode 100644
index 0000000000..dc92564d3a
Binary files /dev/null and b/tests/ref/grid-column-sizing-mixed-base.png differ
diff --git a/tests/ref/grid-columns-sizings-rect.png b/tests/ref/grid-columns-sizings-rect.png
new file mode 100644
index 0000000000..9381103d77
Binary files /dev/null and b/tests/ref/grid-columns-sizings-rect.png differ
diff --git a/tests/ref/grid-complete-rows.png b/tests/ref/grid-complete-rows.png
new file mode 100644
index 0000000000..192aa911b0
Binary files /dev/null and b/tests/ref/grid-complete-rows.png differ
diff --git a/tests/ref/grid-consecutive-rows-breaking.png b/tests/ref/grid-consecutive-rows-breaking.png
new file mode 100644
index 0000000000..6000271d83
Binary files /dev/null and b/tests/ref/grid-consecutive-rows-breaking.png differ
diff --git a/tests/ref/grid-exam.png b/tests/ref/grid-exam.png
new file mode 100644
index 0000000000..97edd52eb0
Binary files /dev/null and b/tests/ref/grid-exam.png differ
diff --git a/tests/ref/grid-fill-func.png b/tests/ref/grid-fill-func.png
new file mode 100644
index 0000000000..388a52df4f
Binary files /dev/null and b/tests/ref/grid-fill-func.png differ
diff --git a/tests/ref/grid-finance.png b/tests/ref/grid-finance.png
new file mode 100644
index 0000000000..2ea485945e
Binary files /dev/null and b/tests/ref/grid-finance.png differ
diff --git a/tests/ref/grid-footer-bare-1.png b/tests/ref/grid-footer-bare-1.png
new file mode 100644
index 0000000000..e8c8b21a35
Binary files /dev/null and b/tests/ref/grid-footer-bare-1.png differ
diff --git a/tests/ref/grid-footer-bare-2.png b/tests/ref/grid-footer-bare-2.png
new file mode 100644
index 0000000000..bad6a3dd8a
Binary files /dev/null and b/tests/ref/grid-footer-bare-2.png differ
diff --git a/tests/ref/grid-footer-below-rowspans.png b/tests/ref/grid-footer-below-rowspans.png
new file mode 100644
index 0000000000..5c3a2b26d0
Binary files /dev/null and b/tests/ref/grid-footer-below-rowspans.png differ
diff --git a/tests/ref/grid-footer-cell-with-y.png b/tests/ref/grid-footer-cell-with-y.png
new file mode 100644
index 0000000000..3237ea69d0
Binary files /dev/null and b/tests/ref/grid-footer-cell-with-y.png differ
diff --git a/tests/ref/grid-footer-expand.png b/tests/ref/grid-footer-expand.png
new file mode 100644
index 0000000000..118765d5c4
Binary files /dev/null and b/tests/ref/grid-footer-expand.png differ
diff --git a/tests/ref/grid-footer-gutter-and-no-repeat.png b/tests/ref/grid-footer-gutter-and-no-repeat.png
new file mode 100644
index 0000000000..ea36ae034e
Binary files /dev/null and b/tests/ref/grid-footer-gutter-and-no-repeat.png differ
diff --git a/tests/ref/grid-footer-hline-and-vline-1.png b/tests/ref/grid-footer-hline-and-vline-1.png
new file mode 100644
index 0000000000..a4d9a68123
Binary files /dev/null and b/tests/ref/grid-footer-hline-and-vline-1.png differ
diff --git a/tests/ref/grid-footer-hline-and-vline-2.png b/tests/ref/grid-footer-hline-and-vline-2.png
new file mode 100644
index 0000000000..0ad2bacc56
Binary files /dev/null and b/tests/ref/grid-footer-hline-and-vline-2.png differ
diff --git a/tests/ref/grid-footer-relative-row-sizes.png b/tests/ref/grid-footer-relative-row-sizes.png
new file mode 100644
index 0000000000..b533f13f32
Binary files /dev/null and b/tests/ref/grid-footer-relative-row-sizes.png differ
diff --git a/tests/ref/grid-footer-rowspan.png b/tests/ref/grid-footer-rowspan.png
new file mode 100644
index 0000000000..369e4d0795
Binary files /dev/null and b/tests/ref/grid-footer-rowspan.png differ
diff --git a/tests/ref/grid-footer-stroke-edge-cases.png b/tests/ref/grid-footer-stroke-edge-cases.png
new file mode 100644
index 0000000000..c3db98e780
Binary files /dev/null and b/tests/ref/grid-footer-stroke-edge-cases.png differ
diff --git a/tests/ref/grid-footer-top-stroke.png b/tests/ref/grid-footer-top-stroke.png
new file mode 100644
index 0000000000..ff9aa9f01c
Binary files /dev/null and b/tests/ref/grid-footer-top-stroke.png differ
diff --git a/tests/ref/grid-footer.png b/tests/ref/grid-footer.png
new file mode 100644
index 0000000000..196563c7f2
Binary files /dev/null and b/tests/ref/grid-footer.png differ
diff --git a/tests/ref/grid-funcs-gutter.png b/tests/ref/grid-funcs-gutter.png
new file mode 100644
index 0000000000..ee6723ef90
Binary files /dev/null and b/tests/ref/grid-funcs-gutter.png differ
diff --git a/tests/ref/grid-gutter-fr.png b/tests/ref/grid-gutter-fr.png
new file mode 100644
index 0000000000..2fce694920
Binary files /dev/null and b/tests/ref/grid-gutter-fr.png differ
diff --git a/tests/ref/grid-header-and-footer-containing-rowspan.png b/tests/ref/grid-header-and-footer-containing-rowspan.png
new file mode 100644
index 0000000000..705d72a470
Binary files /dev/null and b/tests/ref/grid-header-and-footer-containing-rowspan.png differ
diff --git a/tests/ref/grid-header-and-footer-empty.png b/tests/ref/grid-header-and-footer-empty.png
new file mode 100644
index 0000000000..c4e7bb0e4d
Binary files /dev/null and b/tests/ref/grid-header-and-footer-empty.png differ
diff --git a/tests/ref/grid-header-and-footer-lack-of-space.png b/tests/ref/grid-header-and-footer-lack-of-space.png
new file mode 100644
index 0000000000..78705776d5
Binary files /dev/null and b/tests/ref/grid-header-and-footer-lack-of-space.png differ
diff --git a/tests/ref/grid-header-and-footer-orphan-prevention.png b/tests/ref/grid-header-and-footer-orphan-prevention.png
new file mode 100644
index 0000000000..8253b65726
Binary files /dev/null and b/tests/ref/grid-header-and-footer-orphan-prevention.png differ
diff --git a/tests/ref/grid-header-and-rowspan-non-contiguous-1.png b/tests/ref/grid-header-and-rowspan-non-contiguous-1.png
new file mode 100644
index 0000000000..d5088a12f6
Binary files /dev/null and b/tests/ref/grid-header-and-rowspan-non-contiguous-1.png differ
diff --git a/tests/ref/grid-header-and-rowspan-non-contiguous-2.png b/tests/ref/grid-header-and-rowspan-non-contiguous-2.png
new file mode 100644
index 0000000000..4894d14186
Binary files /dev/null and b/tests/ref/grid-header-and-rowspan-non-contiguous-2.png differ
diff --git a/tests/ref/grid-header-and-rowspan-non-contiguous-3.png b/tests/ref/grid-header-and-rowspan-non-contiguous-3.png
new file mode 100644
index 0000000000..36e9a3c316
Binary files /dev/null and b/tests/ref/grid-header-and-rowspan-non-contiguous-3.png differ
diff --git a/tests/ref/grid-header-block-with-fixed-height.png b/tests/ref/grid-header-block-with-fixed-height.png
new file mode 100644
index 0000000000..b7f2eedb3e
Binary files /dev/null and b/tests/ref/grid-header-block-with-fixed-height.png differ
diff --git a/tests/ref/grid-header-cell-with-y.png b/tests/ref/grid-header-cell-with-y.png
new file mode 100644
index 0000000000..e54e35fa22
Binary files /dev/null and b/tests/ref/grid-header-cell-with-y.png differ
diff --git a/tests/ref/grid-header-containing-rowspan.png b/tests/ref/grid-header-containing-rowspan.png
new file mode 100644
index 0000000000..3cabff9e23
Binary files /dev/null and b/tests/ref/grid-header-containing-rowspan.png differ
diff --git a/tests/ref/grid-header-empty.png b/tests/ref/grid-header-empty.png
new file mode 100644
index 0000000000..20e4d92cac
Binary files /dev/null and b/tests/ref/grid-header-empty.png differ
diff --git a/tests/ref/grid-header-expand.png b/tests/ref/grid-header-expand.png
new file mode 100644
index 0000000000..465724417a
Binary files /dev/null and b/tests/ref/grid-header-expand.png differ
diff --git a/tests/ref/grid-header-footer-and-rowspan-non-contiguous-1.png b/tests/ref/grid-header-footer-and-rowspan-non-contiguous-1.png
new file mode 100644
index 0000000000..e7b153c828
Binary files /dev/null and b/tests/ref/grid-header-footer-and-rowspan-non-contiguous-1.png differ
diff --git a/tests/ref/grid-header-footer-and-rowspan-non-contiguous-2.png b/tests/ref/grid-header-footer-and-rowspan-non-contiguous-2.png
new file mode 100644
index 0000000000..525475ac5b
Binary files /dev/null and b/tests/ref/grid-header-footer-and-rowspan-non-contiguous-2.png differ
diff --git a/tests/ref/grid-header-footer-block-with-fixed-height.png b/tests/ref/grid-header-footer-block-with-fixed-height.png
new file mode 100644
index 0000000000..1f2e7c204a
Binary files /dev/null and b/tests/ref/grid-header-footer-block-with-fixed-height.png differ
diff --git a/tests/ref/grid-header-hline-and-vline.png b/tests/ref/grid-header-hline-and-vline.png
new file mode 100644
index 0000000000..a01fc00b30
Binary files /dev/null and b/tests/ref/grid-header-hline-and-vline.png differ
diff --git a/tests/ref/grid-header-hline-bottom-manually.png b/tests/ref/grid-header-hline-bottom-manually.png
new file mode 100644
index 0000000000..d944f7b5e0
Binary files /dev/null and b/tests/ref/grid-header-hline-bottom-manually.png differ
diff --git a/tests/ref/grid-header-hline-bottom.png b/tests/ref/grid-header-hline-bottom.png
new file mode 100644
index 0000000000..f13612420c
Binary files /dev/null and b/tests/ref/grid-header-hline-bottom.png differ
diff --git a/tests/ref/grid-header-lack-of-space.png b/tests/ref/grid-header-lack-of-space.png
new file mode 100644
index 0000000000..4d2b483f19
Binary files /dev/null and b/tests/ref/grid-header-lack-of-space.png differ
diff --git a/tests/ref/grid-header-last-child.png b/tests/ref/grid-header-last-child.png
new file mode 100644
index 0000000000..4fa1ff7c77
Binary files /dev/null and b/tests/ref/grid-header-last-child.png differ
diff --git a/tests/ref/grid-header-nested.png b/tests/ref/grid-header-nested.png
new file mode 100644
index 0000000000..9078090f58
Binary files /dev/null and b/tests/ref/grid-header-nested.png differ
diff --git a/tests/ref/grid-header-orphan-prevention.png b/tests/ref/grid-header-orphan-prevention.png
new file mode 100644
index 0000000000..fa903e42d8
Binary files /dev/null and b/tests/ref/grid-header-orphan-prevention.png differ
diff --git a/tests/ref/grid-header-relative-row-sizes.png b/tests/ref/grid-header-relative-row-sizes.png
new file mode 100644
index 0000000000..69ed1d1e04
Binary files /dev/null and b/tests/ref/grid-header-relative-row-sizes.png differ
diff --git a/tests/ref/grid-header-rowspan-base.png b/tests/ref/grid-header-rowspan-base.png
new file mode 100644
index 0000000000..1ab83591e0
Binary files /dev/null and b/tests/ref/grid-header-rowspan-base.png differ
diff --git a/tests/ref/grid-header-stroke-edge-cases.png b/tests/ref/grid-header-stroke-edge-cases.png
new file mode 100644
index 0000000000..b86eb63263
Binary files /dev/null and b/tests/ref/grid-header-stroke-edge-cases.png differ
diff --git a/tests/ref/grid-headers-gutter.png b/tests/ref/grid-headers-gutter.png
new file mode 100644
index 0000000000..c2a48a66e4
Binary files /dev/null and b/tests/ref/grid-headers-gutter.png differ
diff --git a/tests/ref/grid-headers-no-repeat.png b/tests/ref/grid-headers-no-repeat.png
new file mode 100644
index 0000000000..32d281a1cb
Binary files /dev/null and b/tests/ref/grid-headers-no-repeat.png differ
diff --git a/tests/ref/grid-headers.png b/tests/ref/grid-headers.png
new file mode 100644
index 0000000000..13e88dbeca
Binary files /dev/null and b/tests/ref/grid-headers.png differ
diff --git a/tests/ref/grid-inset-folding.png b/tests/ref/grid-inset-folding.png
new file mode 100644
index 0000000000..7f9942646e
Binary files /dev/null and b/tests/ref/grid-inset-folding.png differ
diff --git a/tests/ref/grid-inset.png b/tests/ref/grid-inset.png
new file mode 100644
index 0000000000..d31197d03a
Binary files /dev/null and b/tests/ref/grid-inset.png differ
diff --git a/tests/ref/grid-nested-breaking.png b/tests/ref/grid-nested-breaking.png
new file mode 100644
index 0000000000..b203c2300a
Binary files /dev/null and b/tests/ref/grid-nested-breaking.png differ
diff --git a/tests/ref/grid-nested-footers.png b/tests/ref/grid-nested-footers.png
new file mode 100644
index 0000000000..1af85a00fd
Binary files /dev/null and b/tests/ref/grid-nested-footers.png differ
diff --git a/tests/ref/grid-nested-headers.png b/tests/ref/grid-nested-headers.png
new file mode 100644
index 0000000000..e714dcc45e
Binary files /dev/null and b/tests/ref/grid-nested-headers.png differ
diff --git a/tests/ref/grid-nested-with-footers.png b/tests/ref/grid-nested-with-footers.png
new file mode 100644
index 0000000000..5ceae87707
Binary files /dev/null and b/tests/ref/grid-nested-with-footers.png differ
diff --git a/tests/ref/grid-nested-with-headers.png b/tests/ref/grid-nested-with-headers.png
new file mode 100644
index 0000000000..6b7ef14bbb
Binary files /dev/null and b/tests/ref/grid-nested-with-headers.png differ
diff --git a/tests/ref/grid-row-sizing-manual-align.png b/tests/ref/grid-row-sizing-manual-align.png
new file mode 100644
index 0000000000..68b0911ed8
Binary files /dev/null and b/tests/ref/grid-row-sizing-manual-align.png differ
diff --git a/tests/ref/grid-rowspan-block-full-height.png b/tests/ref/grid-rowspan-block-full-height.png
new file mode 100644
index 0000000000..078cbda421
Binary files /dev/null and b/tests/ref/grid-rowspan-block-full-height.png differ
diff --git a/tests/ref/grid-rowspan-block-overflow.png b/tests/ref/grid-rowspan-block-overflow.png
new file mode 100644
index 0000000000..78e26d720b
Binary files /dev/null and b/tests/ref/grid-rowspan-block-overflow.png differ
diff --git a/tests/ref/grid-rowspan-cell-coordinates.png b/tests/ref/grid-rowspan-cell-coordinates.png
new file mode 100644
index 0000000000..ebe19fd494
Binary files /dev/null and b/tests/ref/grid-rowspan-cell-coordinates.png differ
diff --git a/tests/ref/grid-rowspan-cell-order.png b/tests/ref/grid-rowspan-cell-order.png
new file mode 100644
index 0000000000..c9b1f5546a
Binary files /dev/null and b/tests/ref/grid-rowspan-cell-order.png differ
diff --git a/tests/ref/grid-rowspan-excessive-gutter.png b/tests/ref/grid-rowspan-excessive-gutter.png
new file mode 100644
index 0000000000..8688364c9b
Binary files /dev/null and b/tests/ref/grid-rowspan-excessive-gutter.png differ
diff --git a/tests/ref/grid-rowspan-excessive.png b/tests/ref/grid-rowspan-excessive.png
new file mode 100644
index 0000000000..1e6b412829
Binary files /dev/null and b/tests/ref/grid-rowspan-excessive.png differ
diff --git a/tests/ref/grid-rowspan-fixed-size.png b/tests/ref/grid-rowspan-fixed-size.png
new file mode 100644
index 0000000000..c9ae3fa126
Binary files /dev/null and b/tests/ref/grid-rowspan-fixed-size.png differ
diff --git a/tests/ref/grid-rowspan-gutter.png b/tests/ref/grid-rowspan-gutter.png
new file mode 100644
index 0000000000..b37a1cab2a
Binary files /dev/null and b/tests/ref/grid-rowspan-gutter.png differ
diff --git a/tests/ref/grid-rowspan-in-all-columns-stroke-gutter.png b/tests/ref/grid-rowspan-in-all-columns-stroke-gutter.png
new file mode 100644
index 0000000000..edad2f01f0
Binary files /dev/null and b/tests/ref/grid-rowspan-in-all-columns-stroke-gutter.png differ
diff --git a/tests/ref/grid-rowspan-in-all-columns-stroke.png b/tests/ref/grid-rowspan-in-all-columns-stroke.png
new file mode 100644
index 0000000000..135d1911aa
Binary files /dev/null and b/tests/ref/grid-rowspan-in-all-columns-stroke.png differ
diff --git a/tests/ref/grid-rowspan-over-auto-row.png b/tests/ref/grid-rowspan-over-auto-row.png
new file mode 100644
index 0000000000..450373824a
Binary files /dev/null and b/tests/ref/grid-rowspan-over-auto-row.png differ
diff --git a/tests/ref/grid-rowspan-over-fr-row-at-end.png b/tests/ref/grid-rowspan-over-fr-row-at-end.png
new file mode 100644
index 0000000000..1cf8b9fc8d
Binary files /dev/null and b/tests/ref/grid-rowspan-over-fr-row-at-end.png differ
diff --git a/tests/ref/grid-rowspan-over-fr-row-at-start.png b/tests/ref/grid-rowspan-over-fr-row-at-start.png
new file mode 100644
index 0000000000..577db9165a
Binary files /dev/null and b/tests/ref/grid-rowspan-over-fr-row-at-start.png differ
diff --git a/tests/ref/grid-rowspan-split-1.png b/tests/ref/grid-rowspan-split-1.png
new file mode 100644
index 0000000000..e99b105f93
Binary files /dev/null and b/tests/ref/grid-rowspan-split-1.png differ
diff --git a/tests/ref/grid-rowspan-split-10.png b/tests/ref/grid-rowspan-split-10.png
new file mode 100644
index 0000000000..0b907e7dfc
Binary files /dev/null and b/tests/ref/grid-rowspan-split-10.png differ
diff --git a/tests/ref/grid-rowspan-split-11.png b/tests/ref/grid-rowspan-split-11.png
new file mode 100644
index 0000000000..202665d6d0
Binary files /dev/null and b/tests/ref/grid-rowspan-split-11.png differ
diff --git a/tests/ref/grid-rowspan-split-12.png b/tests/ref/grid-rowspan-split-12.png
new file mode 100644
index 0000000000..3d8985f2d2
Binary files /dev/null and b/tests/ref/grid-rowspan-split-12.png differ
diff --git a/tests/ref/grid-rowspan-split-13.png b/tests/ref/grid-rowspan-split-13.png
new file mode 100644
index 0000000000..f4e9d694e6
Binary files /dev/null and b/tests/ref/grid-rowspan-split-13.png differ
diff --git a/tests/ref/grid-rowspan-split-14.png b/tests/ref/grid-rowspan-split-14.png
new file mode 100644
index 0000000000..1500a89b03
Binary files /dev/null and b/tests/ref/grid-rowspan-split-14.png differ
diff --git a/tests/ref/grid-rowspan-split-15.png b/tests/ref/grid-rowspan-split-15.png
new file mode 100644
index 0000000000..445f0a959e
Binary files /dev/null and b/tests/ref/grid-rowspan-split-15.png differ
diff --git a/tests/ref/grid-rowspan-split-16.png b/tests/ref/grid-rowspan-split-16.png
new file mode 100644
index 0000000000..fff83aebe9
Binary files /dev/null and b/tests/ref/grid-rowspan-split-16.png differ
diff --git a/tests/ref/grid-rowspan-split-17.png b/tests/ref/grid-rowspan-split-17.png
new file mode 100644
index 0000000000..2224c194c2
Binary files /dev/null and b/tests/ref/grid-rowspan-split-17.png differ
diff --git a/tests/ref/grid-rowspan-split-2.png b/tests/ref/grid-rowspan-split-2.png
new file mode 100644
index 0000000000..43a5eed7c8
Binary files /dev/null and b/tests/ref/grid-rowspan-split-2.png differ
diff --git a/tests/ref/grid-rowspan-split-3.png b/tests/ref/grid-rowspan-split-3.png
new file mode 100644
index 0000000000..0d7c3359c9
Binary files /dev/null and b/tests/ref/grid-rowspan-split-3.png differ
diff --git a/tests/ref/grid-rowspan-split-4.png b/tests/ref/grid-rowspan-split-4.png
new file mode 100644
index 0000000000..2af887bba7
Binary files /dev/null and b/tests/ref/grid-rowspan-split-4.png differ
diff --git a/tests/ref/grid-rowspan-split-5.png b/tests/ref/grid-rowspan-split-5.png
new file mode 100644
index 0000000000..3aa79cda33
Binary files /dev/null and b/tests/ref/grid-rowspan-split-5.png differ
diff --git a/tests/ref/grid-rowspan-split-6.png b/tests/ref/grid-rowspan-split-6.png
new file mode 100644
index 0000000000..fbf5bf28cc
Binary files /dev/null and b/tests/ref/grid-rowspan-split-6.png differ
diff --git a/tests/ref/grid-rowspan-split-7.png b/tests/ref/grid-rowspan-split-7.png
new file mode 100644
index 0000000000..00e03f025f
Binary files /dev/null and b/tests/ref/grid-rowspan-split-7.png differ
diff --git a/tests/ref/grid-rowspan-split-8.png b/tests/ref/grid-rowspan-split-8.png
new file mode 100644
index 0000000000..405b54235d
Binary files /dev/null and b/tests/ref/grid-rowspan-split-8.png differ
diff --git a/tests/ref/grid-rowspan-split-9.png b/tests/ref/grid-rowspan-split-9.png
new file mode 100644
index 0000000000..5346be718c
Binary files /dev/null and b/tests/ref/grid-rowspan-split-9.png differ
diff --git a/tests/ref/grid-rowspan-unbreakable-1.png b/tests/ref/grid-rowspan-unbreakable-1.png
new file mode 100644
index 0000000000..6112c06926
Binary files /dev/null and b/tests/ref/grid-rowspan-unbreakable-1.png differ
diff --git a/tests/ref/grid-rowspan-unbreakable-2.png b/tests/ref/grid-rowspan-unbreakable-2.png
new file mode 100644
index 0000000000..8e4a222aa6
Binary files /dev/null and b/tests/ref/grid-rowspan-unbreakable-2.png differ
diff --git a/tests/ref/grid-rowspan.png b/tests/ref/grid-rowspan.png
new file mode 100644
index 0000000000..87ad418078
Binary files /dev/null and b/tests/ref/grid-rowspan.png differ
diff --git a/tests/ref/grid-rtl-colspan-stroke.png b/tests/ref/grid-rtl-colspan-stroke.png
new file mode 100644
index 0000000000..248a575cbf
Binary files /dev/null and b/tests/ref/grid-rtl-colspan-stroke.png differ
diff --git a/tests/ref/grid-rtl-colspan.png b/tests/ref/grid-rtl-colspan.png
new file mode 100644
index 0000000000..886e276dfe
Binary files /dev/null and b/tests/ref/grid-rtl-colspan.png differ
diff --git a/tests/ref/grid-rtl-complex.png b/tests/ref/grid-rtl-complex.png
new file mode 100644
index 0000000000..a4177548d1
Binary files /dev/null and b/tests/ref/grid-rtl-complex.png differ
diff --git a/tests/ref/grid-rtl-header.png b/tests/ref/grid-rtl-header.png
new file mode 100644
index 0000000000..1ed532c386
Binary files /dev/null and b/tests/ref/grid-rtl-header.png differ
diff --git a/tests/ref/grid-rtl-multiple-regions.png b/tests/ref/grid-rtl-multiple-regions.png
new file mode 100644
index 0000000000..a9ec7340ef
Binary files /dev/null and b/tests/ref/grid-rtl-multiple-regions.png differ
diff --git a/tests/ref/grid-rtl-rowspan.png b/tests/ref/grid-rtl-rowspan.png
new file mode 100644
index 0000000000..2465164b1e
Binary files /dev/null and b/tests/ref/grid-rtl-rowspan.png differ
diff --git a/tests/ref/grid-rtl-vline-position.png b/tests/ref/grid-rtl-vline-position.png
new file mode 100644
index 0000000000..3612fc9fe5
Binary files /dev/null and b/tests/ref/grid-rtl-vline-position.png differ
diff --git a/tests/ref/grid-rtl.png b/tests/ref/grid-rtl.png
new file mode 100644
index 0000000000..c40fc58850
Binary files /dev/null and b/tests/ref/grid-rtl.png differ
diff --git a/tests/ref/grid-same-row-multiple-columns-breaking.png b/tests/ref/grid-same-row-multiple-columns-breaking.png
new file mode 100644
index 0000000000..b440f3368e
Binary files /dev/null and b/tests/ref/grid-same-row-multiple-columns-breaking.png differ
diff --git a/tests/ref/grid-stroke-array.png b/tests/ref/grid-stroke-array.png
new file mode 100644
index 0000000000..6f8e28b099
Binary files /dev/null and b/tests/ref/grid-stroke-array.png differ
diff --git a/tests/ref/grid-stroke-automatically-positioned-lines.png b/tests/ref/grid-stroke-automatically-positioned-lines.png
new file mode 100644
index 0000000000..2118112c0c
Binary files /dev/null and b/tests/ref/grid-stroke-automatically-positioned-lines.png differ
diff --git a/tests/ref/grid-stroke-border-partial.png b/tests/ref/grid-stroke-border-partial.png
new file mode 100644
index 0000000000..ffd8835f11
Binary files /dev/null and b/tests/ref/grid-stroke-border-partial.png differ
diff --git a/tests/ref/grid-stroke-complex.png b/tests/ref/grid-stroke-complex.png
new file mode 100644
index 0000000000..e68fd5f3b8
Binary files /dev/null and b/tests/ref/grid-stroke-complex.png differ
diff --git a/tests/ref/grid-stroke-field-in-show.png b/tests/ref/grid-stroke-field-in-show.png
new file mode 100644
index 0000000000..695868c045
Binary files /dev/null and b/tests/ref/grid-stroke-field-in-show.png differ
diff --git a/tests/ref/grid-stroke-folding.png b/tests/ref/grid-stroke-folding.png
new file mode 100644
index 0000000000..0f2d59602b
Binary files /dev/null and b/tests/ref/grid-stroke-folding.png differ
diff --git a/tests/ref/grid-stroke-func.png b/tests/ref/grid-stroke-func.png
new file mode 100644
index 0000000000..954e90df53
Binary files /dev/null and b/tests/ref/grid-stroke-func.png differ
diff --git a/tests/ref/grid-stroke-hline-position-bottom-gutter.png b/tests/ref/grid-stroke-hline-position-bottom-gutter.png
new file mode 100644
index 0000000000..23c7def41d
Binary files /dev/null and b/tests/ref/grid-stroke-hline-position-bottom-gutter.png differ
diff --git a/tests/ref/grid-stroke-hline-position-bottom.png b/tests/ref/grid-stroke-hline-position-bottom.png
new file mode 100644
index 0000000000..25c003c813
Binary files /dev/null and b/tests/ref/grid-stroke-hline-position-bottom.png differ
diff --git a/tests/ref/grid-stroke-hline-rowspan.png b/tests/ref/grid-stroke-hline-rowspan.png
new file mode 100644
index 0000000000..2faf707916
Binary files /dev/null and b/tests/ref/grid-stroke-hline-rowspan.png differ
diff --git a/tests/ref/grid-stroke-manually-positioned-lines.png b/tests/ref/grid-stroke-manually-positioned-lines.png
new file mode 100644
index 0000000000..a8a75ee07e
Binary files /dev/null and b/tests/ref/grid-stroke-manually-positioned-lines.png differ
diff --git a/tests/ref/grid-stroke-none.png b/tests/ref/grid-stroke-none.png
new file mode 100644
index 0000000000..3f978bd3a0
Binary files /dev/null and b/tests/ref/grid-stroke-none.png differ
diff --git a/tests/ref/grid-stroke-pattern.png b/tests/ref/grid-stroke-pattern.png
new file mode 100644
index 0000000000..15e846eafb
Binary files /dev/null and b/tests/ref/grid-stroke-pattern.png differ
diff --git a/tests/ref/grid-stroke-priority-cell.png b/tests/ref/grid-stroke-priority-cell.png
new file mode 100644
index 0000000000..2c28e9e813
Binary files /dev/null and b/tests/ref/grid-stroke-priority-cell.png differ
diff --git a/tests/ref/grid-stroke-priority-line-cell.png b/tests/ref/grid-stroke-priority-line-cell.png
new file mode 100644
index 0000000000..064dc1c98a
Binary files /dev/null and b/tests/ref/grid-stroke-priority-line-cell.png differ
diff --git a/tests/ref/grid-stroke-priority-line.png b/tests/ref/grid-stroke-priority-line.png
new file mode 100644
index 0000000000..1bcaa2ee89
Binary files /dev/null and b/tests/ref/grid-stroke-priority-line.png differ
diff --git a/tests/ref/grid-stroke-set-on-cell-and-line.png b/tests/ref/grid-stroke-set-on-cell-and-line.png
new file mode 100644
index 0000000000..d43752f05d
Binary files /dev/null and b/tests/ref/grid-stroke-set-on-cell-and-line.png differ
diff --git a/tests/ref/grid-stroke-vline-colspan.png b/tests/ref/grid-stroke-vline-colspan.png
new file mode 100644
index 0000000000..7b38143725
Binary files /dev/null and b/tests/ref/grid-stroke-vline-colspan.png differ
diff --git a/tests/ref/grid-stroke-vline-position-left-and-right.png b/tests/ref/grid-stroke-vline-position-left-and-right.png
new file mode 100644
index 0000000000..852fcf297b
Binary files /dev/null and b/tests/ref/grid-stroke-vline-position-left-and-right.png differ
diff --git a/tests/ref/grid-trailing-linebreak-region-overflow.png b/tests/ref/grid-trailing-linebreak-region-overflow.png
new file mode 100644
index 0000000000..4f7bc852f3
Binary files /dev/null and b/tests/ref/grid-trailing-linebreak-region-overflow.png differ
diff --git a/tests/ref/heading-basic.png b/tests/ref/heading-basic.png
new file mode 100644
index 0000000000..74a8f2cea7
Binary files /dev/null and b/tests/ref/heading-basic.png differ
diff --git a/tests/ref/heading-block.png b/tests/ref/heading-block.png
new file mode 100644
index 0000000000..595f18f5d1
Binary files /dev/null and b/tests/ref/heading-block.png differ
diff --git a/tests/ref/heading-offset-and-level.png b/tests/ref/heading-offset-and-level.png
new file mode 100644
index 0000000000..9277e770a4
Binary files /dev/null and b/tests/ref/heading-offset-and-level.png differ
diff --git a/tests/ref/heading-offset.png b/tests/ref/heading-offset.png
new file mode 100644
index 0000000000..3a3670cc39
Binary files /dev/null and b/tests/ref/heading-offset.png differ
diff --git a/tests/ref/heading-show-where.png b/tests/ref/heading-show-where.png
new file mode 100644
index 0000000000..609e6ec9a2
Binary files /dev/null and b/tests/ref/heading-show-where.png differ
diff --git a/tests/ref/heading-syntax-at-start.png b/tests/ref/heading-syntax-at-start.png
new file mode 100644
index 0000000000..29b824e09e
Binary files /dev/null and b/tests/ref/heading-syntax-at-start.png differ
diff --git a/tests/ref/heading-syntax-edge-cases.png b/tests/ref/heading-syntax-edge-cases.png
new file mode 100644
index 0000000000..372e1a65d7
Binary files /dev/null and b/tests/ref/heading-syntax-edge-cases.png differ
diff --git a/tests/ref/hide-image.png b/tests/ref/hide-image.png
new file mode 100644
index 0000000000..78bc690c8e
Binary files /dev/null and b/tests/ref/hide-image.png differ
diff --git a/tests/ref/hide-line.png b/tests/ref/hide-line.png
new file mode 100644
index 0000000000..7d8fa6cd27
Binary files /dev/null and b/tests/ref/hide-line.png differ
diff --git a/tests/ref/hide-list.png b/tests/ref/hide-list.png
new file mode 100644
index 0000000000..055f7b66f4
Binary files /dev/null and b/tests/ref/hide-list.png differ
diff --git a/tests/ref/hide-polygon.png b/tests/ref/hide-polygon.png
new file mode 100644
index 0000000000..5c74eb41ab
Binary files /dev/null and b/tests/ref/hide-polygon.png differ
diff --git a/tests/ref/hide-rect.png b/tests/ref/hide-rect.png
new file mode 100644
index 0000000000..62372c21f8
Binary files /dev/null and b/tests/ref/hide-rect.png differ
diff --git a/tests/ref/hide-table.png b/tests/ref/hide-table.png
new file mode 100644
index 0000000000..e3d890d7bd
Binary files /dev/null and b/tests/ref/hide-table.png differ
diff --git a/tests/ref/hide-text.png b/tests/ref/hide-text.png
new file mode 100644
index 0000000000..1136038cc3
Binary files /dev/null and b/tests/ref/hide-text.png differ
diff --git a/tests/ref/highlight-bounds.png b/tests/ref/highlight-bounds.png
new file mode 100644
index 0000000000..ed868c29b9
Binary files /dev/null and b/tests/ref/highlight-bounds.png differ
diff --git a/tests/ref/highlight-edges-bounds.png b/tests/ref/highlight-edges-bounds.png
new file mode 100644
index 0000000000..f78f3cc39b
Binary files /dev/null and b/tests/ref/highlight-edges-bounds.png differ
diff --git a/tests/ref/highlight-edges.png b/tests/ref/highlight-edges.png
new file mode 100644
index 0000000000..ca48707f03
Binary files /dev/null and b/tests/ref/highlight-edges.png differ
diff --git a/tests/ref/highlight-radius.png b/tests/ref/highlight-radius.png
new file mode 100644
index 0000000000..3baa3e6d9f
Binary files /dev/null and b/tests/ref/highlight-radius.png differ
diff --git a/tests/ref/highlight-stroke.png b/tests/ref/highlight-stroke.png
new file mode 100644
index 0000000000..5a8ad3b596
Binary files /dev/null and b/tests/ref/highlight-stroke.png differ
diff --git a/tests/ref/highlight.png b/tests/ref/highlight.png
new file mode 100644
index 0000000000..0047b7f49c
Binary files /dev/null and b/tests/ref/highlight.png differ
diff --git a/tests/ref/hyphenate-between-shape-runs.png b/tests/ref/hyphenate-between-shape-runs.png
new file mode 100644
index 0000000000..a365af24d5
Binary files /dev/null and b/tests/ref/hyphenate-between-shape-runs.png differ
diff --git a/tests/ref/hyphenate-off-temporarily.png b/tests/ref/hyphenate-off-temporarily.png
new file mode 100644
index 0000000000..48e3caa963
Binary files /dev/null and b/tests/ref/hyphenate-off-temporarily.png differ
diff --git a/tests/ref/hyphenate-punctuation.png b/tests/ref/hyphenate-punctuation.png
new file mode 100644
index 0000000000..897a15a043
Binary files /dev/null and b/tests/ref/hyphenate-punctuation.png differ
diff --git a/tests/ref/hyphenate-shy.png b/tests/ref/hyphenate-shy.png
new file mode 100644
index 0000000000..a548c711a4
Binary files /dev/null and b/tests/ref/hyphenate-shy.png differ
diff --git a/tests/ref/hyphenate.png b/tests/ref/hyphenate.png
new file mode 100644
index 0000000000..c01c902114
Binary files /dev/null and b/tests/ref/hyphenate.png differ
diff --git a/tests/ref/if-condition-complex.png b/tests/ref/if-condition-complex.png
new file mode 100644
index 0000000000..4cbebc5e02
Binary files /dev/null and b/tests/ref/if-condition-complex.png differ
diff --git a/tests/ref/if-markup.png b/tests/ref/if-markup.png
new file mode 100644
index 0000000000..57eb47da18
Binary files /dev/null and b/tests/ref/if-markup.png differ
diff --git a/tests/ref/image-baseline-with-box.png b/tests/ref/image-baseline-with-box.png
new file mode 100644
index 0000000000..4112806982
Binary files /dev/null and b/tests/ref/image-baseline-with-box.png differ
diff --git a/tests/ref/image-decode-detect-format.png b/tests/ref/image-decode-detect-format.png
new file mode 100644
index 0000000000..6f12e8b474
Binary files /dev/null and b/tests/ref/image-decode-detect-format.png differ
diff --git a/tests/ref/image-decode-specify-format.png b/tests/ref/image-decode-specify-format.png
new file mode 100644
index 0000000000..6f12e8b474
Binary files /dev/null and b/tests/ref/image-decode-specify-format.png differ
diff --git a/tests/ref/image-decode-svg.png b/tests/ref/image-decode-svg.png
new file mode 100644
index 0000000000..b7cfcb17c3
Binary files /dev/null and b/tests/ref/image-decode-svg.png differ
diff --git a/tests/ref/image-fit.png b/tests/ref/image-fit.png
new file mode 100644
index 0000000000..5a3bdec186
Binary files /dev/null and b/tests/ref/image-fit.png differ
diff --git a/tests/ref/image-jump-to-next-page.png b/tests/ref/image-jump-to-next-page.png
new file mode 100644
index 0000000000..d8f03b3f4e
Binary files /dev/null and b/tests/ref/image-jump-to-next-page.png differ
diff --git a/tests/ref/image-natural-dpi-sizing.png b/tests/ref/image-natural-dpi-sizing.png
new file mode 100644
index 0000000000..3b9f3fa5de
Binary files /dev/null and b/tests/ref/image-natural-dpi-sizing.png differ
diff --git a/tests/ref/image-rgba-png-and-jpeg.png b/tests/ref/image-rgba-png-and-jpeg.png
new file mode 100644
index 0000000000..601271705e
Binary files /dev/null and b/tests/ref/image-rgba-png-and-jpeg.png differ
diff --git a/tests/ref/image-sizing.png b/tests/ref/image-sizing.png
new file mode 100644
index 0000000000..7419de141b
Binary files /dev/null and b/tests/ref/image-sizing.png differ
diff --git a/tests/ref/image-svg-complex.png b/tests/ref/image-svg-complex.png
new file mode 100644
index 0000000000..1ac4547745
Binary files /dev/null and b/tests/ref/image-svg-complex.png differ
diff --git a/tests/ref/image-svg-text-font.png b/tests/ref/image-svg-text-font.png
new file mode 100644
index 0000000000..2e3b0a0f5c
Binary files /dev/null and b/tests/ref/image-svg-text-font.png differ
diff --git a/tests/ref/image-svg-text.png b/tests/ref/image-svg-text.png
new file mode 100644
index 0000000000..2e41f905e4
Binary files /dev/null and b/tests/ref/image-svg-text.png differ
diff --git a/tests/ref/import-basic.png b/tests/ref/import-basic.png
new file mode 100644
index 0000000000..674c4ecf95
Binary files /dev/null and b/tests/ref/import-basic.png differ
diff --git a/tests/ref/import-from-function-scope.png b/tests/ref/import-from-function-scope.png
new file mode 100644
index 0000000000..f6169d8c2e
Binary files /dev/null and b/tests/ref/import-from-function-scope.png differ
diff --git a/tests/ref/import-source-field-access.png b/tests/ref/import-source-field-access.png
new file mode 100644
index 0000000000..e42bf2091c
Binary files /dev/null and b/tests/ref/import-source-field-access.png differ
diff --git a/tests/ref/include-file.png b/tests/ref/include-file.png
new file mode 100644
index 0000000000..57c3aca1e0
Binary files /dev/null and b/tests/ref/include-file.png differ
diff --git a/tests/ref/int-display.png b/tests/ref/int-display.png
new file mode 100644
index 0000000000..bfb0464806
Binary files /dev/null and b/tests/ref/int-display.png differ
diff --git a/tests/ref/int-repr.png b/tests/ref/int-repr.png
new file mode 100644
index 0000000000..a2ee4ee08b
Binary files /dev/null and b/tests/ref/int-repr.png differ
diff --git a/tests/ref/issue-1041-smartquotes-in-outline.png b/tests/ref/issue-1041-smartquotes-in-outline.png
new file mode 100644
index 0000000000..29ba4065a5
Binary files /dev/null and b/tests/ref/issue-1041-smartquotes-in-outline.png differ
diff --git a/tests/ref/issue-1050-terms-indent.png b/tests/ref/issue-1050-terms-indent.png
new file mode 100644
index 0000000000..ca0521c76e
Binary files /dev/null and b/tests/ref/issue-1050-terms-indent.png differ
diff --git a/tests/ref/issue-1052-math-number-spacing.png b/tests/ref/issue-1052-math-number-spacing.png
new file mode 100644
index 0000000000..79df2c9f90
Binary files /dev/null and b/tests/ref/issue-1052-math-number-spacing.png differ
diff --git a/tests/ref/issue-1216-clamp-panic.png b/tests/ref/issue-1216-clamp-panic.png
new file mode 100644
index 0000000000..d51f134c20
Binary files /dev/null and b/tests/ref/issue-1216-clamp-panic.png differ
diff --git a/tests/ref/issue-1240-stack-h-fr.png b/tests/ref/issue-1240-stack-h-fr.png
new file mode 100644
index 0000000000..ae1ba41e08
Binary files /dev/null and b/tests/ref/issue-1240-stack-h-fr.png differ
diff --git a/tests/ref/issue-1240-stack-v-fr.png b/tests/ref/issue-1240-stack-v-fr.png
new file mode 100644
index 0000000000..a9ac36e8b9
Binary files /dev/null and b/tests/ref/issue-1240-stack-v-fr.png differ
diff --git a/tests/ref/issue-1368-place-pagebreak.png b/tests/ref/issue-1368-place-pagebreak.png
new file mode 100644
index 0000000000..920cd20306
Binary files /dev/null and b/tests/ref/issue-1368-place-pagebreak.png differ
diff --git a/tests/ref/issue-1373-bidi-tofus.png b/tests/ref/issue-1373-bidi-tofus.png
new file mode 100644
index 0000000000..783eb473d8
Binary files /dev/null and b/tests/ref/issue-1373-bidi-tofus.png differ
diff --git a/tests/ref/issue-1388-table-row-missing.png b/tests/ref/issue-1388-table-row-missing.png
new file mode 100644
index 0000000000..dd08eb4645
Binary files /dev/null and b/tests/ref/issue-1388-table-row-missing.png differ
diff --git a/tests/ref/issue-1398-line-align.png b/tests/ref/issue-1398-line-align.png
new file mode 100644
index 0000000000..778aa72c9f
Binary files /dev/null and b/tests/ref/issue-1398-line-align.png differ
diff --git a/tests/ref/issue-1433-footnote-in-list.png b/tests/ref/issue-1433-footnote-in-list.png
new file mode 100644
index 0000000000..28a6e77ff2
Binary files /dev/null and b/tests/ref/issue-1433-footnote-in-list.png differ
diff --git a/tests/ref/issue-1540-smartquotes-across-newlines.png b/tests/ref/issue-1540-smartquotes-across-newlines.png
new file mode 100644
index 0000000000..10fe733796
Binary files /dev/null and b/tests/ref/issue-1540-smartquotes-across-newlines.png differ
diff --git a/tests/ref/issue-1597-cite-footnote.png b/tests/ref/issue-1597-cite-footnote.png
new file mode 100644
index 0000000000..bdd9f22573
Binary files /dev/null and b/tests/ref/issue-1597-cite-footnote.png differ
diff --git a/tests/ref/issue-1825-rect-overflow.png b/tests/ref/issue-1825-rect-overflow.png
new file mode 100644
index 0000000000..70f09e1208
Binary files /dev/null and b/tests/ref/issue-1825-rect-overflow.png differ
diff --git a/tests/ref/issue-183-table-lines.png b/tests/ref/issue-183-table-lines.png
new file mode 100644
index 0000000000..e43692624b
Binary files /dev/null and b/tests/ref/issue-183-table-lines.png differ
diff --git a/tests/ref/issue-1948-math-text-break.png b/tests/ref/issue-1948-math-text-break.png
new file mode 100644
index 0000000000..6e3e9e85b4
Binary files /dev/null and b/tests/ref/issue-1948-math-text-break.png differ
diff --git a/tests/ref/issue-2044-invalid-parsed-ident.png b/tests/ref/issue-2044-invalid-parsed-ident.png
new file mode 100644
index 0000000000..7e37ce2c61
Binary files /dev/null and b/tests/ref/issue-2044-invalid-parsed-ident.png differ
diff --git a/tests/ref/issue-2051-new-cm-svg.png b/tests/ref/issue-2051-new-cm-svg.png
new file mode 100644
index 0000000000..6535286027
Binary files /dev/null and b/tests/ref/issue-2051-new-cm-svg.png differ
diff --git a/tests/ref/issue-2055-math-eval.png b/tests/ref/issue-2055-math-eval.png
new file mode 100644
index 0000000000..168b89139c
Binary files /dev/null and b/tests/ref/issue-2055-math-eval.png differ
diff --git a/tests/ref/issue-2095-pagebreak-numbering.png b/tests/ref/issue-2095-pagebreak-numbering.png
new file mode 100644
index 0000000000..e3a515b76f
Binary files /dev/null and b/tests/ref/issue-2095-pagebreak-numbering.png differ
diff --git a/tests/ref/issue-2105-linebreak-tofu.png b/tests/ref/issue-2105-linebreak-tofu.png
new file mode 100644
index 0000000000..197412b971
Binary files /dev/null and b/tests/ref/issue-2105-linebreak-tofu.png differ
diff --git a/tests/ref/issue-2128-block-width-box.png b/tests/ref/issue-2128-block-width-box.png
new file mode 100644
index 0000000000..40fe6b4f88
Binary files /dev/null and b/tests/ref/issue-2128-block-width-box.png differ
diff --git a/tests/ref/issue-2134-pagebreak-bibliography.png b/tests/ref/issue-2134-pagebreak-bibliography.png
new file mode 100644
index 0000000000..ad0fb1653f
Binary files /dev/null and b/tests/ref/issue-2134-pagebreak-bibliography.png differ
diff --git a/tests/ref/issue-2162-pagebreak-set-style.png b/tests/ref/issue-2162-pagebreak-set-style.png
new file mode 100644
index 0000000000..4ea6f56ff2
Binary files /dev/null and b/tests/ref/issue-2162-pagebreak-set-style.png differ
diff --git a/tests/ref/issue-2199-place-spacing-bottom.png b/tests/ref/issue-2199-place-spacing-bottom.png
new file mode 100644
index 0000000000..1f27559bc4
Binary files /dev/null and b/tests/ref/issue-2199-place-spacing-bottom.png differ
diff --git a/tests/ref/issue-2199-place-spacing-default.png b/tests/ref/issue-2199-place-spacing-default.png
new file mode 100644
index 0000000000..565a830283
Binary files /dev/null and b/tests/ref/issue-2199-place-spacing-default.png differ
diff --git a/tests/ref/issue-2214-baseline-math.png b/tests/ref/issue-2214-baseline-math.png
new file mode 100644
index 0000000000..9a3e6f3c6a
Binary files /dev/null and b/tests/ref/issue-2214-baseline-math.png differ
diff --git a/tests/ref/issue-2259-raw-color-overwrite.png b/tests/ref/issue-2259-raw-color-overwrite.png
new file mode 100644
index 0000000000..9cf42c4377
Binary files /dev/null and b/tests/ref/issue-2259-raw-color-overwrite.png differ
diff --git a/tests/ref/issue-2268-mat-augment-color.png b/tests/ref/issue-2268-mat-augment-color.png
new file mode 100644
index 0000000000..5aca29cae1
Binary files /dev/null and b/tests/ref/issue-2268-mat-augment-color.png differ
diff --git a/tests/ref/issue-2419-justify-hanging-indent.png b/tests/ref/issue-2419-justify-hanging-indent.png
new file mode 100644
index 0000000000..bb478ba45c
Binary files /dev/null and b/tests/ref/issue-2419-justify-hanging-indent.png differ
diff --git a/tests/ref/issue-2530-enum-item-panic.png b/tests/ref/issue-2530-enum-item-panic.png
new file mode 100644
index 0000000000..4f6130bad5
Binary files /dev/null and b/tests/ref/issue-2530-enum-item-panic.png differ
diff --git a/tests/ref/issue-2530-figure-caption-panic.png b/tests/ref/issue-2530-figure-caption-panic.png
new file mode 100644
index 0000000000..025449efeb
Binary files /dev/null and b/tests/ref/issue-2530-figure-caption-panic.png differ
diff --git a/tests/ref/issue-2530-list-item-panic.png b/tests/ref/issue-2530-list-item-panic.png
new file mode 100644
index 0000000000..14d2f57006
Binary files /dev/null and b/tests/ref/issue-2530-list-item-panic.png differ
diff --git a/tests/ref/issue-2530-term-item-panic.png b/tests/ref/issue-2530-term-item-panic.png
new file mode 100644
index 0000000000..85b3e92f6c
Binary files /dev/null and b/tests/ref/issue-2530-term-item-panic.png differ
diff --git a/tests/ref/issue-2531-cite-show-set.png b/tests/ref/issue-2531-cite-show-set.png
new file mode 100644
index 0000000000..25723f4ded
Binary files /dev/null and b/tests/ref/issue-2531-cite-show-set.png differ
diff --git a/tests/ref/issue-2538-cjk-latin-spacing-before-linebreak.png b/tests/ref/issue-2538-cjk-latin-spacing-before-linebreak.png
new file mode 100644
index 0000000000..5957164268
Binary files /dev/null and b/tests/ref/issue-2538-cjk-latin-spacing-before-linebreak.png differ
diff --git a/tests/ref/issue-2595-float-overlap.png b/tests/ref/issue-2595-float-overlap.png
new file mode 100644
index 0000000000..4b460579d5
Binary files /dev/null and b/tests/ref/issue-2595-float-overlap.png differ
diff --git a/tests/ref/issue-2650-cjk-latin-spacing-meta.png b/tests/ref/issue-2650-cjk-latin-spacing-meta.png
new file mode 100644
index 0000000000..d346b73f90
Binary files /dev/null and b/tests/ref/issue-2650-cjk-latin-spacing-meta.png differ
diff --git a/tests/ref/issue-2715-float-order.png b/tests/ref/issue-2715-float-order.png
new file mode 100644
index 0000000000..01599d250c
Binary files /dev/null and b/tests/ref/issue-2715-float-order.png differ
diff --git a/tests/ref/issue-2902-gradient-oklab-panic.png b/tests/ref/issue-2902-gradient-oklab-panic.png
new file mode 100644
index 0000000000..f8e18f7ce0
Binary files /dev/null and b/tests/ref/issue-2902-gradient-oklab-panic.png differ
diff --git a/tests/ref/issue-2902-gradient-oklch-panic.png b/tests/ref/issue-2902-gradient-oklch-panic.png
new file mode 100644
index 0000000000..1af7200e6e
Binary files /dev/null and b/tests/ref/issue-2902-gradient-oklch-panic.png differ
diff --git a/tests/ref/issue-3082-chinese-punctuation.png b/tests/ref/issue-3082-chinese-punctuation.png
new file mode 100644
index 0000000000..642013d09b
Binary files /dev/null and b/tests/ref/issue-3082-chinese-punctuation.png differ
diff --git a/tests/ref/issue-3191-raw-indent-shrink.png b/tests/ref/issue-3191-raw-indent-shrink.png
new file mode 100644
index 0000000000..e7ac73b739
Binary files /dev/null and b/tests/ref/issue-3191-raw-indent-shrink.png differ
diff --git a/tests/ref/issue-3191-raw-normal-paragraphs-still-shrink.png b/tests/ref/issue-3191-raw-normal-paragraphs-still-shrink.png
new file mode 100644
index 0000000000..1eb4999598
Binary files /dev/null and b/tests/ref/issue-3191-raw-normal-paragraphs-still-shrink.png differ
diff --git a/tests/ref/issue-3232-dict-empty.png b/tests/ref/issue-3232-dict-empty.png
new file mode 100644
index 0000000000..f8d3f324f3
Binary files /dev/null and b/tests/ref/issue-3232-dict-empty.png differ
diff --git a/tests/ref/issue-3264-rect-negative-dimensions.png b/tests/ref/issue-3264-rect-negative-dimensions.png
new file mode 100644
index 0000000000..44a046811d
Binary files /dev/null and b/tests/ref/issue-3264-rect-negative-dimensions.png differ
diff --git a/tests/ref/issue-3363-json-large-number.png b/tests/ref/issue-3363-json-large-number.png
new file mode 100644
index 0000000000..3e13dea16b
Binary files /dev/null and b/tests/ref/issue-3363-json-large-number.png differ
diff --git a/tests/ref/issue-3481-cite-location.png b/tests/ref/issue-3481-cite-location.png
new file mode 100644
index 0000000000..cfc13db512
Binary files /dev/null and b/tests/ref/issue-3481-cite-location.png differ
diff --git a/tests/ref/issue-3586-figure-caption-separator.png b/tests/ref/issue-3586-figure-caption-separator.png
new file mode 100644
index 0000000000..1d038fe3ff
Binary files /dev/null and b/tests/ref/issue-3586-figure-caption-separator.png differ
diff --git a/tests/ref/issue-3624-spacing-behaviour.png b/tests/ref/issue-3624-spacing-behaviour.png
new file mode 100644
index 0000000000..c7db67538c
Binary files /dev/null and b/tests/ref/issue-3624-spacing-behaviour.png differ
diff --git a/tests/ref/issue-3641-float-loop.png b/tests/ref/issue-3641-float-loop.png
new file mode 100644
index 0000000000..4490d30a53
Binary files /dev/null and b/tests/ref/issue-3641-float-loop.png differ
diff --git a/tests/ref/issue-3650-italic-equation.png b/tests/ref/issue-3650-italic-equation.png
new file mode 100644
index 0000000000..484457e828
Binary files /dev/null and b/tests/ref/issue-3650-italic-equation.png differ
diff --git a/tests/ref/issue-3658-math-size.png b/tests/ref/issue-3658-math-size.png
new file mode 100644
index 0000000000..db8fccf9aa
Binary files /dev/null and b/tests/ref/issue-3658-math-size.png differ
diff --git a/tests/ref/issue-3662-pdf-smartquotes.png b/tests/ref/issue-3662-pdf-smartquotes.png
new file mode 100644
index 0000000000..ff73cbc809
Binary files /dev/null and b/tests/ref/issue-3662-pdf-smartquotes.png differ
diff --git a/tests/ref/issue-3696-equation-rtl.png b/tests/ref/issue-3696-equation-rtl.png
new file mode 100644
index 0000000000..1ebf2dc217
Binary files /dev/null and b/tests/ref/issue-3696-equation-rtl.png differ
diff --git a/tests/ref/issue-3699-cite-twice-et-al.png b/tests/ref/issue-3699-cite-twice-et-al.png
new file mode 100644
index 0000000000..0fe48a48ce
Binary files /dev/null and b/tests/ref/issue-3699-cite-twice-et-al.png differ
diff --git a/tests/ref/issue-3700-deformed-stroke.png b/tests/ref/issue-3700-deformed-stroke.png
new file mode 100644
index 0000000000..9578a675dd
Binary files /dev/null and b/tests/ref/issue-3700-deformed-stroke.png differ
diff --git a/tests/ref/issue-3726-query-show-set.png b/tests/ref/issue-3726-query-show-set.png
new file mode 100644
index 0000000000..3f5c88846f
Binary files /dev/null and b/tests/ref/issue-3726-query-show-set.png differ
diff --git a/tests/ref/issue-3774-math-call-empty-2d-args.png b/tests/ref/issue-3774-math-call-empty-2d-args.png
new file mode 100644
index 0000000000..ce4b4eb8c2
Binary files /dev/null and b/tests/ref/issue-3774-math-call-empty-2d-args.png differ
diff --git a/tests/ref/issue-3820-raw-space-when-end-with-backtick.png b/tests/ref/issue-3820-raw-space-when-end-with-backtick.png
new file mode 100644
index 0000000000..1ba3fb182e
Binary files /dev/null and b/tests/ref/issue-3820-raw-space-when-end-with-backtick.png differ
diff --git a/tests/ref/issue-3841-tabs-in-raw-type-code.png b/tests/ref/issue-3841-tabs-in-raw-type-code.png
new file mode 100644
index 0000000000..b7a7b1bae4
Binary files /dev/null and b/tests/ref/issue-3841-tabs-in-raw-type-code.png differ
diff --git a/tests/ref/issue-3973-math-equation-align.png b/tests/ref/issue-3973-math-equation-align.png
new file mode 100644
index 0000000000..91b48849ac
Binary files /dev/null and b/tests/ref/issue-3973-math-equation-align.png differ
diff --git a/tests/ref/issue-622-hide-meta-cite.png b/tests/ref/issue-622-hide-meta-cite.png
new file mode 100644
index 0000000000..8918f668a6
Binary files /dev/null and b/tests/ref/issue-622-hide-meta-cite.png differ
diff --git a/tests/ref/issue-622-hide-meta-outline.png b/tests/ref/issue-622-hide-meta-outline.png
new file mode 100644
index 0000000000..72a82e4d8b
Binary files /dev/null and b/tests/ref/issue-622-hide-meta-outline.png differ
diff --git a/tests/ref/issue-785-cite-locate.png b/tests/ref/issue-785-cite-locate.png
new file mode 100644
index 0000000000..7c2a650a46
Binary files /dev/null and b/tests/ref/issue-785-cite-locate.png differ
diff --git a/tests/ref/issue-80-emoji-linebreak.png b/tests/ref/issue-80-emoji-linebreak.png
new file mode 100644
index 0000000000..d35a62b3c2
Binary files /dev/null and b/tests/ref/issue-80-emoji-linebreak.png differ
diff --git a/tests/ref/issue-852-mat-type.png b/tests/ref/issue-852-mat-type.png
new file mode 100644
index 0000000000..81af3bb5b7
Binary files /dev/null and b/tests/ref/issue-852-mat-type.png differ
diff --git a/tests/ref/issue-870-image-rotation.png b/tests/ref/issue-870-image-rotation.png
new file mode 100644
index 0000000000..c321a1a93f
Binary files /dev/null and b/tests/ref/issue-870-image-rotation.png differ
diff --git a/tests/ref/issue-886-args-sink.png b/tests/ref/issue-886-args-sink.png
new file mode 100644
index 0000000000..2ef08adf9a
Binary files /dev/null and b/tests/ref/issue-886-args-sink.png differ
diff --git a/tests/ref/issue-columns-heading.png b/tests/ref/issue-columns-heading.png
new file mode 100644
index 0000000000..700972bc06
Binary files /dev/null and b/tests/ref/issue-columns-heading.png differ
diff --git a/tests/ref/issue-flow-frame-placement.png b/tests/ref/issue-flow-frame-placement.png
new file mode 100644
index 0000000000..27469c2794
Binary files /dev/null and b/tests/ref/issue-flow-frame-placement.png differ
diff --git a/tests/ref/issue-flow-layout-index-out-of-bounds.png b/tests/ref/issue-flow-layout-index-out-of-bounds.png
new file mode 100644
index 0000000000..8746cbfc99
Binary files /dev/null and b/tests/ref/issue-flow-layout-index-out-of-bounds.png differ
diff --git a/tests/ref/issue-flow-overlarge-frames.png b/tests/ref/issue-flow-overlarge-frames.png
new file mode 100644
index 0000000000..016af525dd
Binary files /dev/null and b/tests/ref/issue-flow-overlarge-frames.png differ
diff --git a/tests/ref/issue-flow-trailing-leading.png b/tests/ref/issue-flow-trailing-leading.png
new file mode 100644
index 0000000000..4245d42fbf
Binary files /dev/null and b/tests/ref/issue-flow-trailing-leading.png differ
diff --git a/tests/ref/issue-flow-weak-spacing.png b/tests/ref/issue-flow-weak-spacing.png
new file mode 100644
index 0000000000..e37a5ae33d
Binary files /dev/null and b/tests/ref/issue-flow-weak-spacing.png differ
diff --git a/tests/ref/issue-footnotes-skip-first-page.png b/tests/ref/issue-footnotes-skip-first-page.png
new file mode 100644
index 0000000000..d24387e3ba
Binary files /dev/null and b/tests/ref/issue-footnotes-skip-first-page.png differ
diff --git a/tests/ref/issue-gradient-cmyk-encode.png b/tests/ref/issue-gradient-cmyk-encode.png
new file mode 100644
index 0000000000..065d1a3b4a
Binary files /dev/null and b/tests/ref/issue-gradient-cmyk-encode.png differ
diff --git a/tests/ref/issue-grid-base-auto-row-list.png b/tests/ref/issue-grid-base-auto-row-list.png
new file mode 100644
index 0000000000..8da3adf5d9
Binary files /dev/null and b/tests/ref/issue-grid-base-auto-row-list.png differ
diff --git a/tests/ref/issue-grid-base-auto-row.png b/tests/ref/issue-grid-base-auto-row.png
new file mode 100644
index 0000000000..0e05577de1
Binary files /dev/null and b/tests/ref/issue-grid-base-auto-row.png differ
diff --git a/tests/ref/issue-grid-double-skip.png b/tests/ref/issue-grid-double-skip.png
new file mode 100644
index 0000000000..2901f29ae9
Binary files /dev/null and b/tests/ref/issue-grid-double-skip.png differ
diff --git a/tests/ref/issue-grid-gutter-skip.png b/tests/ref/issue-grid-gutter-skip.png
new file mode 100644
index 0000000000..3404fd1014
Binary files /dev/null and b/tests/ref/issue-grid-gutter-skip.png differ
diff --git a/tests/ref/issue-grid-skip-list.png b/tests/ref/issue-grid-skip-list.png
new file mode 100644
index 0000000000..bd67433765
Binary files /dev/null and b/tests/ref/issue-grid-skip-list.png differ
diff --git a/tests/ref/issue-grid-skip.png b/tests/ref/issue-grid-skip.png
new file mode 100644
index 0000000000..1b46fd1a33
Binary files /dev/null and b/tests/ref/issue-grid-skip.png differ
diff --git a/tests/ref/issue-math-realize-hide.png b/tests/ref/issue-math-realize-hide.png
new file mode 100644
index 0000000000..729e9f00c2
Binary files /dev/null and b/tests/ref/issue-math-realize-hide.png differ
diff --git a/tests/ref/issue-math-realize-scripting.png b/tests/ref/issue-math-realize-scripting.png
new file mode 100644
index 0000000000..a29b0364de
Binary files /dev/null and b/tests/ref/issue-math-realize-scripting.png differ
diff --git a/tests/ref/issue-math-realize-show.png b/tests/ref/issue-math-realize-show.png
new file mode 100644
index 0000000000..d6b727c1de
Binary files /dev/null and b/tests/ref/issue-math-realize-show.png differ
diff --git a/tests/ref/issue-multiple-footnote-in-one-line.png b/tests/ref/issue-multiple-footnote-in-one-line.png
new file mode 100644
index 0000000000..1d8c017d43
Binary files /dev/null and b/tests/ref/issue-multiple-footnote-in-one-line.png differ
diff --git a/tests/ref/issue-non-atomic-closure.png b/tests/ref/issue-non-atomic-closure.png
new file mode 100644
index 0000000000..f60b465489
Binary files /dev/null and b/tests/ref/issue-non-atomic-closure.png differ
diff --git a/tests/ref/issue-path-in-sized-container.png b/tests/ref/issue-path-in-sized-container.png
new file mode 100644
index 0000000000..ebecc122b5
Binary files /dev/null and b/tests/ref/issue-path-in-sized-container.png differ
diff --git a/tests/ref/issue-place-base.png b/tests/ref/issue-place-base.png
new file mode 100644
index 0000000000..45517fe94e
Binary files /dev/null and b/tests/ref/issue-place-base.png differ
diff --git a/tests/ref/issue-rtl-safe-to-break-panic.png b/tests/ref/issue-rtl-safe-to-break-panic.png
new file mode 100644
index 0000000000..5cd9920cc2
Binary files /dev/null and b/tests/ref/issue-rtl-safe-to-break-panic.png differ
diff --git a/tests/ref/justify-avoid-runts.png b/tests/ref/justify-avoid-runts.png
new file mode 100644
index 0000000000..70513939cd
Binary files /dev/null and b/tests/ref/justify-avoid-runts.png differ
diff --git a/tests/ref/justify-chinese.png b/tests/ref/justify-chinese.png
new file mode 100644
index 0000000000..0284e8b9d3
Binary files /dev/null and b/tests/ref/justify-chinese.png differ
diff --git a/tests/ref/justify-code-blocks.png b/tests/ref/justify-code-blocks.png
new file mode 100644
index 0000000000..088e8b6365
Binary files /dev/null and b/tests/ref/justify-code-blocks.png differ
diff --git a/tests/ref/justify-japanese.png b/tests/ref/justify-japanese.png
new file mode 100644
index 0000000000..addeba5485
Binary files /dev/null and b/tests/ref/justify-japanese.png differ
diff --git a/tests/ref/justify-justified-linebreak.png b/tests/ref/justify-justified-linebreak.png
new file mode 100644
index 0000000000..8792e9e2ef
Binary files /dev/null and b/tests/ref/justify-justified-linebreak.png differ
diff --git a/tests/ref/justify-knuth-story.png b/tests/ref/justify-knuth-story.png
new file mode 100644
index 0000000000..9fbcc3c3e0
Binary files /dev/null and b/tests/ref/justify-knuth-story.png differ
diff --git a/tests/ref/justify-manual-linebreak.png b/tests/ref/justify-manual-linebreak.png
new file mode 100644
index 0000000000..144a62c73b
Binary files /dev/null and b/tests/ref/justify-manual-linebreak.png differ
diff --git a/tests/ref/justify-no-leading-spaces.png b/tests/ref/justify-no-leading-spaces.png
new file mode 100644
index 0000000000..9d2557b5c0
Binary files /dev/null and b/tests/ref/justify-no-leading-spaces.png differ
diff --git a/tests/ref/justify-punctuation-adjustment.png b/tests/ref/justify-punctuation-adjustment.png
new file mode 100644
index 0000000000..28d4ef0474
Binary files /dev/null and b/tests/ref/justify-punctuation-adjustment.png differ
diff --git a/tests/ref/justify-shrink-last-line.png b/tests/ref/justify-shrink-last-line.png
new file mode 100644
index 0000000000..f839e92edf
Binary files /dev/null and b/tests/ref/justify-shrink-last-line.png differ
diff --git a/tests/ref/justify-variants.png b/tests/ref/justify-variants.png
new file mode 100644
index 0000000000..81fcc700e6
Binary files /dev/null and b/tests/ref/justify-variants.png differ
diff --git a/tests/ref/justify-whitespace-adjustment.png b/tests/ref/justify-whitespace-adjustment.png
new file mode 100644
index 0000000000..4ea6829ca6
Binary files /dev/null and b/tests/ref/justify-whitespace-adjustment.png differ
diff --git a/tests/ref/justify-without-justifiables.png b/tests/ref/justify-without-justifiables.png
new file mode 100644
index 0000000000..77e5bf1b3d
Binary files /dev/null and b/tests/ref/justify-without-justifiables.png differ
diff --git a/tests/ref/justify.png b/tests/ref/justify.png
new file mode 100644
index 0000000000..4e4fdbf542
Binary files /dev/null and b/tests/ref/justify.png differ
diff --git a/tests/ref/label-after-expression.png b/tests/ref/label-after-expression.png
new file mode 100644
index 0000000000..5ceaf34202
Binary files /dev/null and b/tests/ref/label-after-expression.png differ
diff --git a/tests/ref/label-after-parbreak.png b/tests/ref/label-after-parbreak.png
new file mode 100644
index 0000000000..9339c65c27
Binary files /dev/null and b/tests/ref/label-after-parbreak.png differ
diff --git a/tests/ref/label-dynamic-show-set.png b/tests/ref/label-dynamic-show-set.png
new file mode 100644
index 0000000000..25681b9299
Binary files /dev/null and b/tests/ref/label-dynamic-show-set.png differ
diff --git a/tests/ref/label-in-block.png b/tests/ref/label-in-block.png
new file mode 100644
index 0000000000..e97bd72570
Binary files /dev/null and b/tests/ref/label-in-block.png differ
diff --git a/tests/ref/label-on-text.png b/tests/ref/label-on-text.png
new file mode 100644
index 0000000000..67fb1aa81f
Binary files /dev/null and b/tests/ref/label-on-text.png differ
diff --git a/tests/ref/label-show-where-selector.png b/tests/ref/label-show-where-selector.png
new file mode 100644
index 0000000000..61e90a9adc
Binary files /dev/null and b/tests/ref/label-show-where-selector.png differ
diff --git a/tests/ref/label-unclosed-is-text.png b/tests/ref/label-unclosed-is-text.png
new file mode 100644
index 0000000000..051db0cf92
Binary files /dev/null and b/tests/ref/label-unclosed-is-text.png differ
diff --git a/tests/ref/layout-in-fixed-size-block.png b/tests/ref/layout-in-fixed-size-block.png
new file mode 100644
index 0000000000..6cc321b361
Binary files /dev/null and b/tests/ref/layout-in-fixed-size-block.png differ
diff --git a/tests/ref/layout-in-page-call.png b/tests/ref/layout-in-page-call.png
new file mode 100644
index 0000000000..9bc75ae79d
Binary files /dev/null and b/tests/ref/layout-in-page-call.png differ
diff --git a/tests/ref/layout/align.png b/tests/ref/layout/align.png
deleted file mode 100644
index a01135976a..0000000000
Binary files a/tests/ref/layout/align.png and /dev/null differ
diff --git a/tests/ref/layout/block-sizing.png b/tests/ref/layout/block-sizing.png
deleted file mode 100644
index 7d57a0d83e..0000000000
Binary files a/tests/ref/layout/block-sizing.png and /dev/null differ
diff --git a/tests/ref/layout/block-spacing.png b/tests/ref/layout/block-spacing.png
deleted file mode 100644
index d73abac02f..0000000000
Binary files a/tests/ref/layout/block-spacing.png and /dev/null differ
diff --git a/tests/ref/layout/cjk-latin-spacing.png b/tests/ref/layout/cjk-latin-spacing.png
deleted file mode 100644
index 629145e46f..0000000000
Binary files a/tests/ref/layout/cjk-latin-spacing.png and /dev/null differ
diff --git a/tests/ref/layout/cjk-punctuation-adjustment.png b/tests/ref/layout/cjk-punctuation-adjustment.png
deleted file mode 100644
index 1da08f23ff..0000000000
Binary files a/tests/ref/layout/cjk-punctuation-adjustment.png and /dev/null differ
diff --git a/tests/ref/layout/clip.png b/tests/ref/layout/clip.png
deleted file mode 100644
index f37bf9ad15..0000000000
Binary files a/tests/ref/layout/clip.png and /dev/null differ
diff --git a/tests/ref/layout/columns.png b/tests/ref/layout/columns.png
deleted file mode 100644
index 38912f1bf6..0000000000
Binary files a/tests/ref/layout/columns.png and /dev/null differ
diff --git a/tests/ref/layout/container-fill.png b/tests/ref/layout/container-fill.png
deleted file mode 100644
index 74fdc73df5..0000000000
Binary files a/tests/ref/layout/container-fill.png and /dev/null differ
diff --git a/tests/ref/layout/container.png b/tests/ref/layout/container.png
deleted file mode 100644
index 0cd56b2dfb..0000000000
Binary files a/tests/ref/layout/container.png and /dev/null differ
diff --git a/tests/ref/layout/enum-align.png b/tests/ref/layout/enum-align.png
deleted file mode 100644
index 18e392f2f4..0000000000
Binary files a/tests/ref/layout/enum-align.png and /dev/null differ
diff --git a/tests/ref/layout/enum-numbering.png b/tests/ref/layout/enum-numbering.png
deleted file mode 100644
index e1b2103bb5..0000000000
Binary files a/tests/ref/layout/enum-numbering.png and /dev/null differ
diff --git a/tests/ref/layout/enum.png b/tests/ref/layout/enum.png
deleted file mode 100644
index 62f1e4abfe..0000000000
Binary files a/tests/ref/layout/enum.png and /dev/null differ
diff --git a/tests/ref/layout/flow-orphan.png b/tests/ref/layout/flow-orphan.png
deleted file mode 100644
index 434636c45c..0000000000
Binary files a/tests/ref/layout/flow-orphan.png and /dev/null differ
diff --git a/tests/ref/layout/grid-1.png b/tests/ref/layout/grid-1.png
deleted file mode 100644
index 9e33772c82..0000000000
Binary files a/tests/ref/layout/grid-1.png and /dev/null differ
diff --git a/tests/ref/layout/grid-2.png b/tests/ref/layout/grid-2.png
deleted file mode 100644
index ac1f7014bf..0000000000
Binary files a/tests/ref/layout/grid-2.png and /dev/null differ
diff --git a/tests/ref/layout/grid-3.png b/tests/ref/layout/grid-3.png
deleted file mode 100644
index 0f54f2ccd0..0000000000
Binary files a/tests/ref/layout/grid-3.png and /dev/null differ
diff --git a/tests/ref/layout/grid-4.png b/tests/ref/layout/grid-4.png
deleted file mode 100644
index 35a05ab52c..0000000000
Binary files a/tests/ref/layout/grid-4.png and /dev/null differ
diff --git a/tests/ref/layout/grid-5.png b/tests/ref/layout/grid-5.png
deleted file mode 100644
index 233ebb00c7..0000000000
Binary files a/tests/ref/layout/grid-5.png and /dev/null differ
diff --git a/tests/ref/layout/grid-auto-shrink.png b/tests/ref/layout/grid-auto-shrink.png
deleted file mode 100644
index 34995215bf..0000000000
Binary files a/tests/ref/layout/grid-auto-shrink.png and /dev/null differ
diff --git a/tests/ref/layout/grid-cell.png b/tests/ref/layout/grid-cell.png
deleted file mode 100644
index 07508b4001..0000000000
Binary files a/tests/ref/layout/grid-cell.png and /dev/null differ
diff --git a/tests/ref/layout/grid-colspan.png b/tests/ref/layout/grid-colspan.png
deleted file mode 100644
index e16ca347ee..0000000000
Binary files a/tests/ref/layout/grid-colspan.png and /dev/null differ
diff --git a/tests/ref/layout/grid-footers-1.png b/tests/ref/layout/grid-footers-1.png
deleted file mode 100644
index 331cf7ad5d..0000000000
Binary files a/tests/ref/layout/grid-footers-1.png and /dev/null differ
diff --git a/tests/ref/layout/grid-footers-2.png b/tests/ref/layout/grid-footers-2.png
deleted file mode 100644
index 60e9689c62..0000000000
Binary files a/tests/ref/layout/grid-footers-2.png and /dev/null differ
diff --git a/tests/ref/layout/grid-footers-3.png b/tests/ref/layout/grid-footers-3.png
deleted file mode 100644
index cc4948b804..0000000000
Binary files a/tests/ref/layout/grid-footers-3.png and /dev/null differ
diff --git a/tests/ref/layout/grid-footers-4.png b/tests/ref/layout/grid-footers-4.png
deleted file mode 100644
index 29a6430bd0..0000000000
Binary files a/tests/ref/layout/grid-footers-4.png and /dev/null differ
diff --git a/tests/ref/layout/grid-footers-5.png b/tests/ref/layout/grid-footers-5.png
deleted file mode 100644
index 6cae5592c9..0000000000
Binary files a/tests/ref/layout/grid-footers-5.png and /dev/null differ
diff --git a/tests/ref/layout/grid-headers-1.png b/tests/ref/layout/grid-headers-1.png
deleted file mode 100644
index 7ae2d8d34e..0000000000
Binary files a/tests/ref/layout/grid-headers-1.png and /dev/null differ
diff --git a/tests/ref/layout/grid-headers-2.png b/tests/ref/layout/grid-headers-2.png
deleted file mode 100644
index 3dbc07c884..0000000000
Binary files a/tests/ref/layout/grid-headers-2.png and /dev/null differ
diff --git a/tests/ref/layout/grid-headers-3.png b/tests/ref/layout/grid-headers-3.png
deleted file mode 100644
index 9ee77d50b5..0000000000
Binary files a/tests/ref/layout/grid-headers-3.png and /dev/null differ
diff --git a/tests/ref/layout/grid-headers-4.png b/tests/ref/layout/grid-headers-4.png
deleted file mode 100644
index 1f3e4b10fb..0000000000
Binary files a/tests/ref/layout/grid-headers-4.png and /dev/null differ
diff --git a/tests/ref/layout/grid-positioning.png b/tests/ref/layout/grid-positioning.png
deleted file mode 100644
index cac93f4038..0000000000
Binary files a/tests/ref/layout/grid-positioning.png and /dev/null differ
diff --git a/tests/ref/layout/grid-rowspan-basic.png b/tests/ref/layout/grid-rowspan-basic.png
deleted file mode 100644
index b464d8b40a..0000000000
Binary files a/tests/ref/layout/grid-rowspan-basic.png and /dev/null differ
diff --git a/tests/ref/layout/grid-rowspan-split-1.png b/tests/ref/layout/grid-rowspan-split-1.png
deleted file mode 100644
index 12cd5fc6f5..0000000000
Binary files a/tests/ref/layout/grid-rowspan-split-1.png and /dev/null differ
diff --git a/tests/ref/layout/grid-rowspan-split-2.png b/tests/ref/layout/grid-rowspan-split-2.png
deleted file mode 100644
index e55c5e2300..0000000000
Binary files a/tests/ref/layout/grid-rowspan-split-2.png and /dev/null differ
diff --git a/tests/ref/layout/grid-rowspan-split-3.png b/tests/ref/layout/grid-rowspan-split-3.png
deleted file mode 100644
index c3ff4bd150..0000000000
Binary files a/tests/ref/layout/grid-rowspan-split-3.png and /dev/null differ
diff --git a/tests/ref/layout/grid-rtl.png b/tests/ref/layout/grid-rtl.png
deleted file mode 100644
index d628ee8aed..0000000000
Binary files a/tests/ref/layout/grid-rtl.png and /dev/null differ
diff --git a/tests/ref/layout/grid-stroke.png b/tests/ref/layout/grid-stroke.png
deleted file mode 100644
index fbba379e85..0000000000
Binary files a/tests/ref/layout/grid-stroke.png and /dev/null differ
diff --git a/tests/ref/layout/grid-styling.png b/tests/ref/layout/grid-styling.png
deleted file mode 100644
index dc50dd9018..0000000000
Binary files a/tests/ref/layout/grid-styling.png and /dev/null differ
diff --git a/tests/ref/layout/hide.png b/tests/ref/layout/hide.png
deleted file mode 100644
index d89800494f..0000000000
Binary files a/tests/ref/layout/hide.png and /dev/null differ
diff --git a/tests/ref/layout/list-attach.png b/tests/ref/layout/list-attach.png
deleted file mode 100644
index 4a6a457372..0000000000
Binary files a/tests/ref/layout/list-attach.png and /dev/null differ
diff --git a/tests/ref/layout/list-marker.png b/tests/ref/layout/list-marker.png
deleted file mode 100644
index 19d6ed5fab..0000000000
Binary files a/tests/ref/layout/list-marker.png and /dev/null differ
diff --git a/tests/ref/layout/list.png b/tests/ref/layout/list.png
deleted file mode 100644
index 269243eb90..0000000000
Binary files a/tests/ref/layout/list.png and /dev/null differ
diff --git a/tests/ref/layout/out-of-flow-in-block.png b/tests/ref/layout/out-of-flow-in-block.png
deleted file mode 100644
index 97637145ff..0000000000
Binary files a/tests/ref/layout/out-of-flow-in-block.png and /dev/null differ
diff --git a/tests/ref/layout/pad.png b/tests/ref/layout/pad.png
deleted file mode 100644
index d228f07fb0..0000000000
Binary files a/tests/ref/layout/pad.png and /dev/null differ
diff --git a/tests/ref/layout/page-binding.png b/tests/ref/layout/page-binding.png
deleted file mode 100644
index 5b6d06576e..0000000000
Binary files a/tests/ref/layout/page-binding.png and /dev/null differ
diff --git a/tests/ref/layout/page-margin.png b/tests/ref/layout/page-margin.png
deleted file mode 100644
index f690724b28..0000000000
Binary files a/tests/ref/layout/page-margin.png and /dev/null differ
diff --git a/tests/ref/layout/page-marginals.png b/tests/ref/layout/page-marginals.png
deleted file mode 100644
index bbe6358ed6..0000000000
Binary files a/tests/ref/layout/page-marginals.png and /dev/null differ
diff --git a/tests/ref/layout/page-number-align.png b/tests/ref/layout/page-number-align.png
deleted file mode 100644
index b05ca45456..0000000000
Binary files a/tests/ref/layout/page-number-align.png and /dev/null differ
diff --git a/tests/ref/layout/page-style.png b/tests/ref/layout/page-style.png
deleted file mode 100644
index ac6b602c18..0000000000
Binary files a/tests/ref/layout/page-style.png and /dev/null differ
diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png
deleted file mode 100644
index bcf325261b..0000000000
Binary files a/tests/ref/layout/page.png and /dev/null differ
diff --git a/tests/ref/layout/pagebreak-parity.png b/tests/ref/layout/pagebreak-parity.png
deleted file mode 100644
index 0dbabe7aed..0000000000
Binary files a/tests/ref/layout/pagebreak-parity.png and /dev/null differ
diff --git a/tests/ref/layout/pagebreak-weak.png b/tests/ref/layout/pagebreak-weak.png
deleted file mode 100644
index 412c4e8d65..0000000000
Binary files a/tests/ref/layout/pagebreak-weak.png and /dev/null differ
diff --git a/tests/ref/layout/pagebreak.png b/tests/ref/layout/pagebreak.png
deleted file mode 100644
index ab05564335..0000000000
Binary files a/tests/ref/layout/pagebreak.png and /dev/null differ
diff --git a/tests/ref/layout/par-bidi.png b/tests/ref/layout/par-bidi.png
deleted file mode 100644
index 4750ccb88e..0000000000
Binary files a/tests/ref/layout/par-bidi.png and /dev/null differ
diff --git a/tests/ref/layout/par-indent.png b/tests/ref/layout/par-indent.png
deleted file mode 100644
index cceaa3b915..0000000000
Binary files a/tests/ref/layout/par-indent.png and /dev/null differ
diff --git a/tests/ref/layout/par-justify-cjk.png b/tests/ref/layout/par-justify-cjk.png
deleted file mode 100644
index a9baf14c5b..0000000000
Binary files a/tests/ref/layout/par-justify-cjk.png and /dev/null differ
diff --git a/tests/ref/layout/par-justify.png b/tests/ref/layout/par-justify.png
deleted file mode 100644
index 0cd9cbcdcf..0000000000
Binary files a/tests/ref/layout/par-justify.png and /dev/null differ
diff --git a/tests/ref/layout/par-knuth.png b/tests/ref/layout/par-knuth.png
deleted file mode 100644
index f3da17531e..0000000000
Binary files a/tests/ref/layout/par-knuth.png and /dev/null differ
diff --git a/tests/ref/layout/par-simple.png b/tests/ref/layout/par-simple.png
deleted file mode 100644
index a645bfd892..0000000000
Binary files a/tests/ref/layout/par-simple.png and /dev/null differ
diff --git a/tests/ref/layout/par.png b/tests/ref/layout/par.png
deleted file mode 100644
index f25f56d2bd..0000000000
Binary files a/tests/ref/layout/par.png and /dev/null differ
diff --git a/tests/ref/layout/place-background.png b/tests/ref/layout/place-background.png
deleted file mode 100644
index d9c1c42f35..0000000000
Binary files a/tests/ref/layout/place-background.png and /dev/null differ
diff --git a/tests/ref/layout/place-float-auto.png b/tests/ref/layout/place-float-auto.png
deleted file mode 100644
index f2e4ee92e4..0000000000
Binary files a/tests/ref/layout/place-float-auto.png and /dev/null differ
diff --git a/tests/ref/layout/place-float-columns.png b/tests/ref/layout/place-float-columns.png
deleted file mode 100644
index 186b79d147..0000000000
Binary files a/tests/ref/layout/place-float-columns.png and /dev/null differ
diff --git a/tests/ref/layout/place-float-figure.png b/tests/ref/layout/place-float-figure.png
deleted file mode 100644
index bf9d21b4f9..0000000000
Binary files a/tests/ref/layout/place-float-figure.png and /dev/null differ
diff --git a/tests/ref/layout/place-nested.png b/tests/ref/layout/place-nested.png
deleted file mode 100644
index 864830d854..0000000000
Binary files a/tests/ref/layout/place-nested.png and /dev/null differ
diff --git a/tests/ref/layout/place.png b/tests/ref/layout/place.png
deleted file mode 100644
index 2ef85a4de5..0000000000
Binary files a/tests/ref/layout/place.png and /dev/null differ
diff --git a/tests/ref/layout/repeat.png b/tests/ref/layout/repeat.png
deleted file mode 100644
index 8e21f1022d..0000000000
Binary files a/tests/ref/layout/repeat.png and /dev/null differ
diff --git a/tests/ref/layout/spacing.png b/tests/ref/layout/spacing.png
deleted file mode 100644
index 9bab536a20..0000000000
Binary files a/tests/ref/layout/spacing.png and /dev/null differ
diff --git a/tests/ref/layout/stack-1.png b/tests/ref/layout/stack-1.png
deleted file mode 100644
index 1a3133b821..0000000000
Binary files a/tests/ref/layout/stack-1.png and /dev/null differ
diff --git a/tests/ref/layout/stack-2.png b/tests/ref/layout/stack-2.png
deleted file mode 100644
index 6cb0aad2f1..0000000000
Binary files a/tests/ref/layout/stack-2.png and /dev/null differ
diff --git a/tests/ref/layout/table-cell.png b/tests/ref/layout/table-cell.png
deleted file mode 100644
index 647a2e1058..0000000000
Binary files a/tests/ref/layout/table-cell.png and /dev/null differ
diff --git a/tests/ref/layout/table.png b/tests/ref/layout/table.png
deleted file mode 100644
index ddd0e0434d..0000000000
Binary files a/tests/ref/layout/table.png and /dev/null differ
diff --git a/tests/ref/layout/terms.png b/tests/ref/layout/terms.png
deleted file mode 100644
index e0cd013ab6..0000000000
Binary files a/tests/ref/layout/terms.png and /dev/null differ
diff --git a/tests/ref/layout/transform-layout.png b/tests/ref/layout/transform-layout.png
deleted file mode 100644
index 576824f065..0000000000
Binary files a/tests/ref/layout/transform-layout.png and /dev/null differ
diff --git a/tests/ref/layout/transform.png b/tests/ref/layout/transform.png
deleted file mode 100644
index 83b7d13ab4..0000000000
Binary files a/tests/ref/layout/transform.png and /dev/null differ
diff --git a/tests/ref/let-basic.png b/tests/ref/let-basic.png
new file mode 100644
index 0000000000..ded47a5a98
Binary files /dev/null and b/tests/ref/let-basic.png differ
diff --git a/tests/ref/let-termination.png b/tests/ref/let-termination.png
new file mode 100644
index 0000000000..552bb4ce40
Binary files /dev/null and b/tests/ref/let-termination.png differ
diff --git a/tests/ref/line-basic.png b/tests/ref/line-basic.png
new file mode 100644
index 0000000000..007672e120
Binary files /dev/null and b/tests/ref/line-basic.png differ
diff --git a/tests/ref/line-positioning.png b/tests/ref/line-positioning.png
new file mode 100644
index 0000000000..65678caa64
Binary files /dev/null and b/tests/ref/line-positioning.png differ
diff --git a/tests/ref/line-stroke-dash.png b/tests/ref/line-stroke-dash.png
new file mode 100644
index 0000000000..f245e32fdc
Binary files /dev/null and b/tests/ref/line-stroke-dash.png differ
diff --git a/tests/ref/line-stroke-set.png b/tests/ref/line-stroke-set.png
new file mode 100644
index 0000000000..f82f899eb3
Binary files /dev/null and b/tests/ref/line-stroke-set.png differ
diff --git a/tests/ref/line-stroke.png b/tests/ref/line-stroke.png
new file mode 100644
index 0000000000..d0213002e2
Binary files /dev/null and b/tests/ref/line-stroke.png differ
diff --git a/tests/ref/linebreak-cite-punctuation.png b/tests/ref/linebreak-cite-punctuation.png
new file mode 100644
index 0000000000..64d930c6d1
Binary files /dev/null and b/tests/ref/linebreak-cite-punctuation.png differ
diff --git a/tests/ref/linebreak-hyphen-nbsp.png b/tests/ref/linebreak-hyphen-nbsp.png
new file mode 100644
index 0000000000..ee88ae5869
Binary files /dev/null and b/tests/ref/linebreak-hyphen-nbsp.png differ
diff --git a/tests/ref/linebreak-link-end.png b/tests/ref/linebreak-link-end.png
new file mode 100644
index 0000000000..f11e91d60e
Binary files /dev/null and b/tests/ref/linebreak-link-end.png differ
diff --git a/tests/ref/linebreak-link-justify.png b/tests/ref/linebreak-link-justify.png
new file mode 100644
index 0000000000..8007cf3e2a
Binary files /dev/null and b/tests/ref/linebreak-link-justify.png differ
diff --git a/tests/ref/linebreak-link.png b/tests/ref/linebreak-link.png
new file mode 100644
index 0000000000..d5ba8c9e30
Binary files /dev/null and b/tests/ref/linebreak-link.png differ
diff --git a/tests/ref/linebreak-manual-consecutive.png b/tests/ref/linebreak-manual-consecutive.png
new file mode 100644
index 0000000000..0dbef35b9f
Binary files /dev/null and b/tests/ref/linebreak-manual-consecutive.png differ
diff --git a/tests/ref/linebreak-manual-directly-after-automatic.png b/tests/ref/linebreak-manual-directly-after-automatic.png
new file mode 100644
index 0000000000..006e3ef263
Binary files /dev/null and b/tests/ref/linebreak-manual-directly-after-automatic.png differ
diff --git a/tests/ref/linebreak-manual-justified.png b/tests/ref/linebreak-manual-justified.png
new file mode 100644
index 0000000000..f74ea3fd0b
Binary files /dev/null and b/tests/ref/linebreak-manual-justified.png differ
diff --git a/tests/ref/linebreak-manual-trailing-multiple.png b/tests/ref/linebreak-manual-trailing-multiple.png
new file mode 100644
index 0000000000..edf3a94991
Binary files /dev/null and b/tests/ref/linebreak-manual-trailing-multiple.png differ
diff --git a/tests/ref/linebreak-manual.png b/tests/ref/linebreak-manual.png
new file mode 100644
index 0000000000..37aca398ac
Binary files /dev/null and b/tests/ref/linebreak-manual.png differ
diff --git a/tests/ref/linebreak-math-punctuation.png b/tests/ref/linebreak-math-punctuation.png
new file mode 100644
index 0000000000..93b77d2af0
Binary files /dev/null and b/tests/ref/linebreak-math-punctuation.png differ
diff --git a/tests/ref/linebreak-narrow-nbsp.png b/tests/ref/linebreak-narrow-nbsp.png
new file mode 100644
index 0000000000..81cf82f866
Binary files /dev/null and b/tests/ref/linebreak-narrow-nbsp.png differ
diff --git a/tests/ref/linebreak-overflow-double.png b/tests/ref/linebreak-overflow-double.png
new file mode 100644
index 0000000000..04a5bbaaab
Binary files /dev/null and b/tests/ref/linebreak-overflow-double.png differ
diff --git a/tests/ref/linebreak-overflow.png b/tests/ref/linebreak-overflow.png
new file mode 100644
index 0000000000..1dfcbc27ec
Binary files /dev/null and b/tests/ref/linebreak-overflow.png differ
diff --git a/tests/ref/linebreak-shape-run.png b/tests/ref/linebreak-shape-run.png
new file mode 100644
index 0000000000..ebfb87f066
Binary files /dev/null and b/tests/ref/linebreak-shape-run.png differ
diff --git a/tests/ref/linebreak-thai.png b/tests/ref/linebreak-thai.png
new file mode 100644
index 0000000000..8053a2128e
Binary files /dev/null and b/tests/ref/linebreak-thai.png differ
diff --git a/tests/ref/link-basic.png b/tests/ref/link-basic.png
new file mode 100644
index 0000000000..d16c7ef15c
Binary files /dev/null and b/tests/ref/link-basic.png differ
diff --git a/tests/ref/link-bracket-balanced.png b/tests/ref/link-bracket-balanced.png
new file mode 100644
index 0000000000..048a7c52b2
Binary files /dev/null and b/tests/ref/link-bracket-balanced.png differ
diff --git a/tests/ref/link-bracket-unbalanced-closing.png b/tests/ref/link-bracket-unbalanced-closing.png
new file mode 100644
index 0000000000..e1c1341ce1
Binary files /dev/null and b/tests/ref/link-bracket-unbalanced-closing.png differ
diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png
new file mode 100644
index 0000000000..9076983def
Binary files /dev/null and b/tests/ref/link-on-block.png differ
diff --git a/tests/ref/link-show.png b/tests/ref/link-show.png
new file mode 100644
index 0000000000..59542bad8f
Binary files /dev/null and b/tests/ref/link-show.png differ
diff --git a/tests/ref/link-to-label.png b/tests/ref/link-to-label.png
new file mode 100644
index 0000000000..f607552610
Binary files /dev/null and b/tests/ref/link-to-label.png differ
diff --git a/tests/ref/link-to-page.png b/tests/ref/link-to-page.png
new file mode 100644
index 0000000000..bbd2f10350
Binary files /dev/null and b/tests/ref/link-to-page.png differ
diff --git a/tests/ref/link-trailing-period.png b/tests/ref/link-trailing-period.png
new file mode 100644
index 0000000000..4dd11f3402
Binary files /dev/null and b/tests/ref/link-trailing-period.png differ
diff --git a/tests/ref/link-transformed.png b/tests/ref/link-transformed.png
new file mode 100644
index 0000000000..6b94b5cb35
Binary files /dev/null and b/tests/ref/link-transformed.png differ
diff --git a/tests/ref/list-attached-above-spacing.png b/tests/ref/list-attached-above-spacing.png
new file mode 100644
index 0000000000..0f499769bd
Binary files /dev/null and b/tests/ref/list-attached-above-spacing.png differ
diff --git a/tests/ref/list-attached.png b/tests/ref/list-attached.png
new file mode 100644
index 0000000000..c1735fd019
Binary files /dev/null and b/tests/ref/list-attached.png differ
diff --git a/tests/ref/list-basic.png b/tests/ref/list-basic.png
new file mode 100644
index 0000000000..edf69cac74
Binary files /dev/null and b/tests/ref/list-basic.png differ
diff --git a/tests/ref/list-content-block.png b/tests/ref/list-content-block.png
new file mode 100644
index 0000000000..18b003e577
Binary files /dev/null and b/tests/ref/list-content-block.png differ
diff --git a/tests/ref/list-indent-specifics.png b/tests/ref/list-indent-specifics.png
new file mode 100644
index 0000000000..212e45ed05
Binary files /dev/null and b/tests/ref/list-indent-specifics.png differ
diff --git a/tests/ref/list-marker-align-unaffected.png b/tests/ref/list-marker-align-unaffected.png
new file mode 100644
index 0000000000..90f9ad45b9
Binary files /dev/null and b/tests/ref/list-marker-align-unaffected.png differ
diff --git a/tests/ref/list-marker-bare-hyphen.png b/tests/ref/list-marker-bare-hyphen.png
new file mode 100644
index 0000000000..37830fd67d
Binary files /dev/null and b/tests/ref/list-marker-bare-hyphen.png differ
diff --git a/tests/ref/list-marker-closure.png b/tests/ref/list-marker-closure.png
new file mode 100644
index 0000000000..4dba3b9d8d
Binary files /dev/null and b/tests/ref/list-marker-closure.png differ
diff --git a/tests/ref/list-marker-cycle.png b/tests/ref/list-marker-cycle.png
new file mode 100644
index 0000000000..ef219f074b
Binary files /dev/null and b/tests/ref/list-marker-cycle.png differ
diff --git a/tests/ref/list-marker-dash.png b/tests/ref/list-marker-dash.png
new file mode 100644
index 0000000000..10abc8a672
Binary files /dev/null and b/tests/ref/list-marker-dash.png differ
diff --git a/tests/ref/list-mix.png b/tests/ref/list-mix.png
new file mode 100644
index 0000000000..0f2b03cf9e
Binary files /dev/null and b/tests/ref/list-mix.png differ
diff --git a/tests/ref/list-mixed-tabs-and-spaces.png b/tests/ref/list-mixed-tabs-and-spaces.png
new file mode 100644
index 0000000000..fcddff42bd
Binary files /dev/null and b/tests/ref/list-mixed-tabs-and-spaces.png differ
diff --git a/tests/ref/list-nested.png b/tests/ref/list-nested.png
new file mode 100644
index 0000000000..22f73ecb3e
Binary files /dev/null and b/tests/ref/list-nested.png differ
diff --git a/tests/ref/list-non-attached-followed-by-attached.png b/tests/ref/list-non-attached-followed-by-attached.png
new file mode 100644
index 0000000000..22db4e3881
Binary files /dev/null and b/tests/ref/list-non-attached-followed-by-attached.png differ
diff --git a/tests/ref/list-rtl.png b/tests/ref/list-rtl.png
new file mode 100644
index 0000000000..db1e754632
Binary files /dev/null and b/tests/ref/list-rtl.png differ
diff --git a/tests/ref/list-syntax-edge-cases.png b/tests/ref/list-syntax-edge-cases.png
new file mode 100644
index 0000000000..460462e3cd
Binary files /dev/null and b/tests/ref/list-syntax-edge-cases.png differ
diff --git a/tests/ref/list-tabs.png b/tests/ref/list-tabs.png
new file mode 100644
index 0000000000..1fce74c35a
Binary files /dev/null and b/tests/ref/list-tabs.png differ
diff --git a/tests/ref/list-tight-non-attached-tight.png b/tests/ref/list-tight-non-attached-tight.png
new file mode 100644
index 0000000000..96d5181330
Binary files /dev/null and b/tests/ref/list-tight-non-attached-tight.png differ
diff --git a/tests/ref/list-top-level-indent.png b/tests/ref/list-top-level-indent.png
new file mode 100644
index 0000000000..beb17ede95
Binary files /dev/null and b/tests/ref/list-top-level-indent.png differ
diff --git a/tests/ref/list-wide-cannot-attach.png b/tests/ref/list-wide-cannot-attach.png
new file mode 100644
index 0000000000..600041a77e
Binary files /dev/null and b/tests/ref/list-wide-cannot-attach.png differ
diff --git a/tests/ref/list-wide-really-cannot-attach.png b/tests/ref/list-wide-really-cannot-attach.png
new file mode 100644
index 0000000000..89680c092f
Binary files /dev/null and b/tests/ref/list-wide-really-cannot-attach.png differ
diff --git a/tests/ref/locate-element-selector.png b/tests/ref/locate-element-selector.png
new file mode 100644
index 0000000000..fc36ddff61
Binary files /dev/null and b/tests/ref/locate-element-selector.png differ
diff --git a/tests/ref/locate-position.png b/tests/ref/locate-position.png
new file mode 100644
index 0000000000..fc36ddff61
Binary files /dev/null and b/tests/ref/locate-position.png differ
diff --git a/tests/ref/loop-break-join-in-first-arg.png b/tests/ref/loop-break-join-in-first-arg.png
new file mode 100644
index 0000000000..fbad2125e6
Binary files /dev/null and b/tests/ref/loop-break-join-in-first-arg.png differ
diff --git a/tests/ref/loop-break-join-in-nested-blocks.png b/tests/ref/loop-break-join-in-nested-blocks.png
new file mode 100644
index 0000000000..6e2af47a60
Binary files /dev/null and b/tests/ref/loop-break-join-in-nested-blocks.png differ
diff --git a/tests/ref/loop-break-join-in-set-rule-args.png b/tests/ref/loop-break-join-in-set-rule-args.png
new file mode 100644
index 0000000000..37e1377320
Binary files /dev/null and b/tests/ref/loop-break-join-in-set-rule-args.png differ
diff --git a/tests/ref/loop-break-join-set-and-show.png b/tests/ref/loop-break-join-set-and-show.png
new file mode 100644
index 0000000000..8c81c1472c
Binary files /dev/null and b/tests/ref/loop-break-join-set-and-show.png differ
diff --git a/tests/ref/lorem-pars.png b/tests/ref/lorem-pars.png
new file mode 100644
index 0000000000..5ff0a34513
Binary files /dev/null and b/tests/ref/lorem-pars.png differ
diff --git a/tests/ref/lorem.png b/tests/ref/lorem.png
new file mode 100644
index 0000000000..197acb1ce2
Binary files /dev/null and b/tests/ref/lorem.png differ
diff --git a/tests/ref/math-accent-align.png b/tests/ref/math-accent-align.png
new file mode 100644
index 0000000000..84e8dc8ccd
Binary files /dev/null and b/tests/ref/math-accent-align.png differ
diff --git a/tests/ref/math-accent-bounds.png b/tests/ref/math-accent-bounds.png
new file mode 100644
index 0000000000..d987618287
Binary files /dev/null and b/tests/ref/math-accent-bounds.png differ
diff --git a/tests/ref/math-accent-func.png b/tests/ref/math-accent-func.png
new file mode 100644
index 0000000000..00821f70de
Binary files /dev/null and b/tests/ref/math-accent-func.png differ
diff --git a/tests/ref/math-accent-high-base.png b/tests/ref/math-accent-high-base.png
new file mode 100644
index 0000000000..f4d7580f1e
Binary files /dev/null and b/tests/ref/math-accent-high-base.png differ
diff --git a/tests/ref/math-accent-sized.png b/tests/ref/math-accent-sized.png
new file mode 100644
index 0000000000..76783b2547
Binary files /dev/null and b/tests/ref/math-accent-sized.png differ
diff --git a/tests/ref/math-accent-superscript.png b/tests/ref/math-accent-superscript.png
new file mode 100644
index 0000000000..8ddf113d5d
Binary files /dev/null and b/tests/ref/math-accent-superscript.png differ
diff --git a/tests/ref/math-accent-sym-call.png b/tests/ref/math-accent-sym-call.png
new file mode 100644
index 0000000000..0837a86c9e
Binary files /dev/null and b/tests/ref/math-accent-sym-call.png differ
diff --git a/tests/ref/math-accent-wide-base.png b/tests/ref/math-accent-wide-base.png
new file mode 100644
index 0000000000..af716bf45f
Binary files /dev/null and b/tests/ref/math-accent-wide-base.png differ
diff --git a/tests/ref/math-align-aligned-in-source.png b/tests/ref/math-align-aligned-in-source.png
new file mode 100644
index 0000000000..958a42c5d2
Binary files /dev/null and b/tests/ref/math-align-aligned-in-source.png differ
diff --git a/tests/ref/math-align-basic.png b/tests/ref/math-align-basic.png
new file mode 100644
index 0000000000..cf4a8d6ae6
Binary files /dev/null and b/tests/ref/math-align-basic.png differ
diff --git a/tests/ref/math-align-cases.png b/tests/ref/math-align-cases.png
new file mode 100644
index 0000000000..4ea9a264c6
Binary files /dev/null and b/tests/ref/math-align-cases.png differ
diff --git a/tests/ref/math-align-implicit.png b/tests/ref/math-align-implicit.png
new file mode 100644
index 0000000000..05a0d98d82
Binary files /dev/null and b/tests/ref/math-align-implicit.png differ
diff --git a/tests/ref/math-align-lines-mixed.png b/tests/ref/math-align-lines-mixed.png
new file mode 100644
index 0000000000..d50af28c58
Binary files /dev/null and b/tests/ref/math-align-lines-mixed.png differ
diff --git a/tests/ref/math-align-post-fix.png b/tests/ref/math-align-post-fix.png
new file mode 100644
index 0000000000..33bc3da7f4
Binary files /dev/null and b/tests/ref/math-align-post-fix.png differ
diff --git a/tests/ref/math-align-toggle.png b/tests/ref/math-align-toggle.png
new file mode 100644
index 0000000000..24448ab5ff
Binary files /dev/null and b/tests/ref/math-align-toggle.png differ
diff --git a/tests/ref/math-align-weird.png b/tests/ref/math-align-weird.png
new file mode 100644
index 0000000000..672742ec1a
Binary files /dev/null and b/tests/ref/math-align-weird.png differ
diff --git a/tests/ref/math-align-wider-first-column.png b/tests/ref/math-align-wider-first-column.png
new file mode 100644
index 0000000000..46c9c3ff16
Binary files /dev/null and b/tests/ref/math-align-wider-first-column.png differ
diff --git a/tests/ref/math-at-line-end.png b/tests/ref/math-at-line-end.png
new file mode 100644
index 0000000000..6e4173a7f9
Binary files /dev/null and b/tests/ref/math-at-line-end.png differ
diff --git a/tests/ref/math-at-line-start.png b/tests/ref/math-at-line-start.png
new file mode 100644
index 0000000000..05221db179
Binary files /dev/null and b/tests/ref/math-at-line-start.png differ
diff --git a/tests/ref/math-at-par-end.png b/tests/ref/math-at-par-end.png
new file mode 100644
index 0000000000..dd3393faa6
Binary files /dev/null and b/tests/ref/math-at-par-end.png differ
diff --git a/tests/ref/math-at-par-start.png b/tests/ref/math-at-par-start.png
new file mode 100644
index 0000000000..d69b214a8e
Binary files /dev/null and b/tests/ref/math-at-par-start.png differ
diff --git a/tests/ref/math-attach-default-placement.png b/tests/ref/math-attach-default-placement.png
new file mode 100644
index 0000000000..685fb550ac
Binary files /dev/null and b/tests/ref/math-attach-default-placement.png differ
diff --git a/tests/ref/math-attach-descender-collision.png b/tests/ref/math-attach-descender-collision.png
new file mode 100644
index 0000000000..71654916b2
Binary files /dev/null and b/tests/ref/math-attach-descender-collision.png differ
diff --git a/tests/ref/math-attach-followed-by-func-call.png b/tests/ref/math-attach-followed-by-func-call.png
new file mode 100644
index 0000000000..71d78c16e7
Binary files /dev/null and b/tests/ref/math-attach-followed-by-func-call.png differ
diff --git a/tests/ref/math-attach-force-scripts-and-limits.png b/tests/ref/math-attach-force-scripts-and-limits.png
new file mode 100644
index 0000000000..21a1050f21
Binary files /dev/null and b/tests/ref/math-attach-force-scripts-and-limits.png differ
diff --git a/tests/ref/math-attach-high.png b/tests/ref/math-attach-high.png
new file mode 100644
index 0000000000..4bb6cb29fc
Binary files /dev/null and b/tests/ref/math-attach-high.png differ
diff --git a/tests/ref/math-attach-horizontal-align.png b/tests/ref/math-attach-horizontal-align.png
new file mode 100644
index 0000000000..507cb0ffda
Binary files /dev/null and b/tests/ref/math-attach-horizontal-align.png differ
diff --git a/tests/ref/math-attach-integral.png b/tests/ref/math-attach-integral.png
new file mode 100644
index 0000000000..baebf44c6c
Binary files /dev/null and b/tests/ref/math-attach-integral.png differ
diff --git a/tests/ref/math-attach-large-operator.png b/tests/ref/math-attach-large-operator.png
new file mode 100644
index 0000000000..774b603c01
Binary files /dev/null and b/tests/ref/math-attach-large-operator.png differ
diff --git a/tests/ref/math-attach-limit.png b/tests/ref/math-attach-limit.png
new file mode 100644
index 0000000000..5f9f24d7c8
Binary files /dev/null and b/tests/ref/math-attach-limit.png differ
diff --git a/tests/ref/math-attach-mixed.png b/tests/ref/math-attach-mixed.png
new file mode 100644
index 0000000000..4be327e3e9
Binary files /dev/null and b/tests/ref/math-attach-mixed.png differ
diff --git a/tests/ref/math-attach-nested.png b/tests/ref/math-attach-nested.png
new file mode 100644
index 0000000000..8b4309cf8c
Binary files /dev/null and b/tests/ref/math-attach-nested.png differ
diff --git a/tests/ref/math-attach-postscripts.png b/tests/ref/math-attach-postscripts.png
new file mode 100644
index 0000000000..bd94e4bd77
Binary files /dev/null and b/tests/ref/math-attach-postscripts.png differ
diff --git a/tests/ref/math-attach-prescripts.png b/tests/ref/math-attach-prescripts.png
new file mode 100644
index 0000000000..cd105e9df4
Binary files /dev/null and b/tests/ref/math-attach-prescripts.png differ
diff --git a/tests/ref/math-attach-show-limit.png b/tests/ref/math-attach-show-limit.png
new file mode 100644
index 0000000000..4ce2b3fbc3
Binary files /dev/null and b/tests/ref/math-attach-show-limit.png differ
diff --git a/tests/ref/math-attach-subscript-multiline.png b/tests/ref/math-attach-subscript-multiline.png
new file mode 100644
index 0000000000..7f9aec2e4b
Binary files /dev/null and b/tests/ref/math-attach-subscript-multiline.png differ
diff --git a/tests/ref/math-attach-to-group.png b/tests/ref/math-attach-to-group.png
new file mode 100644
index 0000000000..a3d1923eb0
Binary files /dev/null and b/tests/ref/math-attach-to-group.png differ
diff --git a/tests/ref/math-binom-multiple.png b/tests/ref/math-binom-multiple.png
new file mode 100644
index 0000000000..7eb60be011
Binary files /dev/null and b/tests/ref/math-binom-multiple.png differ
diff --git a/tests/ref/math-binom.png b/tests/ref/math-binom.png
new file mode 100644
index 0000000000..85ab08f92a
Binary files /dev/null and b/tests/ref/math-binom.png differ
diff --git a/tests/ref/math-box-with-baseline.png b/tests/ref/math-box-with-baseline.png
new file mode 100644
index 0000000000..e38e6442fa
Binary files /dev/null and b/tests/ref/math-box-with-baseline.png differ
diff --git a/tests/ref/math-box-without-baseline.png b/tests/ref/math-box-without-baseline.png
new file mode 100644
index 0000000000..275495878e
Binary files /dev/null and b/tests/ref/math-box-without-baseline.png differ
diff --git a/tests/ref/math-call-2d-semicolon-priority.png b/tests/ref/math-call-2d-semicolon-priority.png
new file mode 100644
index 0000000000..18807e0bf3
Binary files /dev/null and b/tests/ref/math-call-2d-semicolon-priority.png differ
diff --git a/tests/ref/math-call-empty-args-non-func.png b/tests/ref/math-call-empty-args-non-func.png
new file mode 100644
index 0000000000..5ca90df5c0
Binary files /dev/null and b/tests/ref/math-call-empty-args-non-func.png differ
diff --git a/tests/ref/math-call-non-func.png b/tests/ref/math-call-non-func.png
new file mode 100644
index 0000000000..da46efc964
Binary files /dev/null and b/tests/ref/math-call-non-func.png differ
diff --git a/tests/ref/math-call-pass-to-box.png b/tests/ref/math-call-pass-to-box.png
new file mode 100644
index 0000000000..0ce1b3d08f
Binary files /dev/null and b/tests/ref/math-call-pass-to-box.png differ
diff --git a/tests/ref/math-cancel-angle-absolute.png b/tests/ref/math-cancel-angle-absolute.png
new file mode 100644
index 0000000000..125e59fb11
Binary files /dev/null and b/tests/ref/math-cancel-angle-absolute.png differ
diff --git a/tests/ref/math-cancel-angle-func.png b/tests/ref/math-cancel-angle-func.png
new file mode 100644
index 0000000000..54f6e75955
Binary files /dev/null and b/tests/ref/math-cancel-angle-func.png differ
diff --git a/tests/ref/math-cancel-cross.png b/tests/ref/math-cancel-cross.png
new file mode 100644
index 0000000000..49fba664e8
Binary files /dev/null and b/tests/ref/math-cancel-cross.png differ
diff --git a/tests/ref/math-cancel-customized.png b/tests/ref/math-cancel-customized.png
new file mode 100644
index 0000000000..9fa5045d7c
Binary files /dev/null and b/tests/ref/math-cancel-customized.png differ
diff --git a/tests/ref/math-cancel-display.png b/tests/ref/math-cancel-display.png
new file mode 100644
index 0000000000..30d30a5968
Binary files /dev/null and b/tests/ref/math-cancel-display.png differ
diff --git a/tests/ref/math-cancel-inline.png b/tests/ref/math-cancel-inline.png
new file mode 100644
index 0000000000..4d92bc5e3b
Binary files /dev/null and b/tests/ref/math-cancel-inline.png differ
diff --git a/tests/ref/math-cancel-inverted.png b/tests/ref/math-cancel-inverted.png
new file mode 100644
index 0000000000..129d53a053
Binary files /dev/null and b/tests/ref/math-cancel-inverted.png differ
diff --git a/tests/ref/math-cases-gap.png b/tests/ref/math-cases-gap.png
new file mode 100644
index 0000000000..e357913079
Binary files /dev/null and b/tests/ref/math-cases-gap.png differ
diff --git a/tests/ref/math-cases.png b/tests/ref/math-cases.png
new file mode 100644
index 0000000000..2e8c260a38
Binary files /dev/null and b/tests/ref/math-cases.png differ
diff --git a/tests/ref/math-class-chars.png b/tests/ref/math-class-chars.png
new file mode 100644
index 0000000000..a4f7d29b11
Binary files /dev/null and b/tests/ref/math-class-chars.png differ
diff --git a/tests/ref/math-class-content.png b/tests/ref/math-class-content.png
new file mode 100644
index 0000000000..47603fb4be
Binary files /dev/null and b/tests/ref/math-class-content.png differ
diff --git a/tests/ref/math-class-exceptions.png b/tests/ref/math-class-exceptions.png
new file mode 100644
index 0000000000..8b3ecc8161
Binary files /dev/null and b/tests/ref/math-class-exceptions.png differ
diff --git a/tests/ref/math-class-limits.png b/tests/ref/math-class-limits.png
new file mode 100644
index 0000000000..140acf956f
Binary files /dev/null and b/tests/ref/math-class-limits.png differ
diff --git a/tests/ref/math-class-nested.png b/tests/ref/math-class-nested.png
new file mode 100644
index 0000000000..5847868eae
Binary files /dev/null and b/tests/ref/math-class-nested.png differ
diff --git a/tests/ref/math-common-symbols.png b/tests/ref/math-common-symbols.png
new file mode 100644
index 0000000000..22da84b526
Binary files /dev/null and b/tests/ref/math-common-symbols.png differ
diff --git a/tests/ref/math-consecutive.png b/tests/ref/math-consecutive.png
new file mode 100644
index 0000000000..63209657fa
Binary files /dev/null and b/tests/ref/math-consecutive.png differ
diff --git a/tests/ref/math-dif.png b/tests/ref/math-dif.png
new file mode 100644
index 0000000000..dfe88b3c3b
Binary files /dev/null and b/tests/ref/math-dif.png differ
diff --git a/tests/ref/math-equation-align-numbered.png b/tests/ref/math-equation-align-numbered.png
new file mode 100644
index 0000000000..e43054c8cf
Binary files /dev/null and b/tests/ref/math-equation-align-numbered.png differ
diff --git a/tests/ref/math-equation-align-unnumbered.png b/tests/ref/math-equation-align-unnumbered.png
new file mode 100644
index 0000000000..413da12009
Binary files /dev/null and b/tests/ref/math-equation-align-unnumbered.png differ
diff --git a/tests/ref/math-equation-auto-wrapping.png b/tests/ref/math-equation-auto-wrapping.png
new file mode 100644
index 0000000000..9c600172e6
Binary files /dev/null and b/tests/ref/math-equation-auto-wrapping.png differ
diff --git a/tests/ref/math-equation-font.png b/tests/ref/math-equation-font.png
new file mode 100644
index 0000000000..b105d9e353
Binary files /dev/null and b/tests/ref/math-equation-font.png differ
diff --git a/tests/ref/math-equation-number-align-end.png b/tests/ref/math-equation-number-align-end.png
new file mode 100644
index 0000000000..f60a15ec1e
Binary files /dev/null and b/tests/ref/math-equation-number-align-end.png differ
diff --git a/tests/ref/math-equation-number-align-left.png b/tests/ref/math-equation-number-align-left.png
new file mode 100644
index 0000000000..a8ed40a5e3
Binary files /dev/null and b/tests/ref/math-equation-number-align-left.png differ
diff --git a/tests/ref/math-equation-number-align-multiline-bottom.png b/tests/ref/math-equation-number-align-multiline-bottom.png
new file mode 100644
index 0000000000..cb0e5daa97
Binary files /dev/null and b/tests/ref/math-equation-number-align-multiline-bottom.png differ
diff --git a/tests/ref/math-equation-number-align-multiline-expand.png b/tests/ref/math-equation-number-align-multiline-expand.png
new file mode 100644
index 0000000000..3c3cdc0513
Binary files /dev/null and b/tests/ref/math-equation-number-align-multiline-expand.png differ
diff --git a/tests/ref/math-equation-number-align-multiline-top-start.png b/tests/ref/math-equation-number-align-multiline-top-start.png
new file mode 100644
index 0000000000..43346de95a
Binary files /dev/null and b/tests/ref/math-equation-number-align-multiline-top-start.png differ
diff --git a/tests/ref/math-equation-number-align-multiline.png b/tests/ref/math-equation-number-align-multiline.png
new file mode 100644
index 0000000000..a46bc1e985
Binary files /dev/null and b/tests/ref/math-equation-number-align-multiline.png differ
diff --git a/tests/ref/math-equation-number-align-right.png b/tests/ref/math-equation-number-align-right.png
new file mode 100644
index 0000000000..e3d588c4e4
Binary files /dev/null and b/tests/ref/math-equation-number-align-right.png differ
diff --git a/tests/ref/math-equation-number-align-start.png b/tests/ref/math-equation-number-align-start.png
new file mode 100644
index 0000000000..67ed3c4c28
Binary files /dev/null and b/tests/ref/math-equation-number-align-start.png differ
diff --git a/tests/ref/math-equation-number-align.png b/tests/ref/math-equation-number-align.png
new file mode 100644
index 0000000000..f60a15ec1e
Binary files /dev/null and b/tests/ref/math-equation-number-align.png differ
diff --git a/tests/ref/math-equation-numbering.png b/tests/ref/math-equation-numbering.png
new file mode 100644
index 0000000000..b1e6b10e32
Binary files /dev/null and b/tests/ref/math-equation-numbering.png differ
diff --git a/tests/ref/math-equation-show-rule.png b/tests/ref/math-equation-show-rule.png
new file mode 100644
index 0000000000..26da7cd19c
Binary files /dev/null and b/tests/ref/math-equation-show-rule.png differ
diff --git a/tests/ref/math-font-fallback.png b/tests/ref/math-font-fallback.png
new file mode 100644
index 0000000000..50fa85c7e8
Binary files /dev/null and b/tests/ref/math-font-fallback.png differ
diff --git a/tests/ref/math-font-features.png b/tests/ref/math-font-features.png
new file mode 100644
index 0000000000..0fd5e6e131
Binary files /dev/null and b/tests/ref/math-font-features.png differ
diff --git a/tests/ref/math-font-switch.png b/tests/ref/math-font-switch.png
new file mode 100644
index 0000000000..4c572ea547
Binary files /dev/null and b/tests/ref/math-font-switch.png differ
diff --git a/tests/ref/math-frac-associativity.png b/tests/ref/math-frac-associativity.png
new file mode 100644
index 0000000000..a5daca5963
Binary files /dev/null and b/tests/ref/math-frac-associativity.png differ
diff --git a/tests/ref/math-frac-baseline.png b/tests/ref/math-frac-baseline.png
new file mode 100644
index 0000000000..d65e2c3376
Binary files /dev/null and b/tests/ref/math-frac-baseline.png differ
diff --git a/tests/ref/math-frac-large.png b/tests/ref/math-frac-large.png
new file mode 100644
index 0000000000..ff9520f376
Binary files /dev/null and b/tests/ref/math-frac-large.png differ
diff --git a/tests/ref/math-frac-paren-removal.png b/tests/ref/math-frac-paren-removal.png
new file mode 100644
index 0000000000..4f58f1d390
Binary files /dev/null and b/tests/ref/math-frac-paren-removal.png differ
diff --git a/tests/ref/math-frac-precedence.png b/tests/ref/math-frac-precedence.png
new file mode 100644
index 0000000000..236b9989e2
Binary files /dev/null and b/tests/ref/math-frac-precedence.png differ
diff --git a/tests/ref/math-linebreaking-after-binop-and-rel.png b/tests/ref/math-linebreaking-after-binop-and-rel.png
new file mode 100644
index 0000000000..1cdd73c4f9
Binary files /dev/null and b/tests/ref/math-linebreaking-after-binop-and-rel.png differ
diff --git a/tests/ref/math-linebreaking-after-relation-without-space.png b/tests/ref/math-linebreaking-after-relation-without-space.png
new file mode 100644
index 0000000000..7c569ad1fd
Binary files /dev/null and b/tests/ref/math-linebreaking-after-relation-without-space.png differ
diff --git a/tests/ref/math-linebreaking-between-consecutive-relations.png b/tests/ref/math-linebreaking-between-consecutive-relations.png
new file mode 100644
index 0000000000..ba222c5739
Binary files /dev/null and b/tests/ref/math-linebreaking-between-consecutive-relations.png differ
diff --git a/tests/ref/math-linebreaking-empty.png b/tests/ref/math-linebreaking-empty.png
new file mode 100644
index 0000000000..2b0917a67e
Binary files /dev/null and b/tests/ref/math-linebreaking-empty.png differ
diff --git a/tests/ref/math-linebreaking-in-box.png b/tests/ref/math-linebreaking-in-box.png
new file mode 100644
index 0000000000..e026f1a22a
Binary files /dev/null and b/tests/ref/math-linebreaking-in-box.png differ
diff --git a/tests/ref/math-linebreaking-lr.png b/tests/ref/math-linebreaking-lr.png
new file mode 100644
index 0000000000..69f08e7eac
Binary files /dev/null and b/tests/ref/math-linebreaking-lr.png differ
diff --git a/tests/ref/math-linebreaking-multiline.png b/tests/ref/math-linebreaking-multiline.png
new file mode 100644
index 0000000000..cd5f2fcea8
Binary files /dev/null and b/tests/ref/math-linebreaking-multiline.png differ
diff --git a/tests/ref/math-linebreaking-trailing-linebreak.png b/tests/ref/math-linebreaking-trailing-linebreak.png
new file mode 100644
index 0000000000..4a50832baa
Binary files /dev/null and b/tests/ref/math-linebreaking-trailing-linebreak.png differ
diff --git a/tests/ref/math-lr-call.png b/tests/ref/math-lr-call.png
new file mode 100644
index 0000000000..baf668d4d7
Binary files /dev/null and b/tests/ref/math-lr-call.png differ
diff --git a/tests/ref/math-lr-color.png b/tests/ref/math-lr-color.png
new file mode 100644
index 0000000000..6687681920
Binary files /dev/null and b/tests/ref/math-lr-color.png differ
diff --git a/tests/ref/math-lr-fences.png b/tests/ref/math-lr-fences.png
new file mode 100644
index 0000000000..32314cb4b1
Binary files /dev/null and b/tests/ref/math-lr-fences.png differ
diff --git a/tests/ref/math-lr-half.png b/tests/ref/math-lr-half.png
new file mode 100644
index 0000000000..311188b4c0
Binary files /dev/null and b/tests/ref/math-lr-half.png differ
diff --git a/tests/ref/math-lr-matching.png b/tests/ref/math-lr-matching.png
new file mode 100644
index 0000000000..e5fd4c7b50
Binary files /dev/null and b/tests/ref/math-lr-matching.png differ
diff --git a/tests/ref/math-lr-mid.png b/tests/ref/math-lr-mid.png
new file mode 100644
index 0000000000..1e14759e67
Binary files /dev/null and b/tests/ref/math-lr-mid.png differ
diff --git a/tests/ref/math-lr-shorthands.png b/tests/ref/math-lr-shorthands.png
new file mode 100644
index 0000000000..d8961672eb
Binary files /dev/null and b/tests/ref/math-lr-shorthands.png differ
diff --git a/tests/ref/math-lr-size.png b/tests/ref/math-lr-size.png
new file mode 100644
index 0000000000..09d2442152
Binary files /dev/null and b/tests/ref/math-lr-size.png differ
diff --git a/tests/ref/math-lr-symbol-unmatched.png b/tests/ref/math-lr-symbol-unmatched.png
new file mode 100644
index 0000000000..38d0a988a7
Binary files /dev/null and b/tests/ref/math-lr-symbol-unmatched.png differ
diff --git a/tests/ref/math-lr-unbalanced.png b/tests/ref/math-lr-unbalanced.png
new file mode 100644
index 0000000000..eff579ba4c
Binary files /dev/null and b/tests/ref/math-lr-unbalanced.png differ
diff --git a/tests/ref/math-lr-unmatched.png b/tests/ref/math-lr-unmatched.png
new file mode 100644
index 0000000000..9a0f3275eb
Binary files /dev/null and b/tests/ref/math-lr-unmatched.png differ
diff --git a/tests/ref/math-lr-weak-spacing.png b/tests/ref/math-lr-weak-spacing.png
new file mode 100644
index 0000000000..871aaa2eba
Binary files /dev/null and b/tests/ref/math-lr-weak-spacing.png differ
diff --git a/tests/ref/math-mat-align-complex.png b/tests/ref/math-mat-align-complex.png
new file mode 100644
index 0000000000..682fed2286
Binary files /dev/null and b/tests/ref/math-mat-align-complex.png differ
diff --git a/tests/ref/math-mat-align-explicit--alternating.png b/tests/ref/math-mat-align-explicit--alternating.png
new file mode 100644
index 0000000000..cb29eb067e
Binary files /dev/null and b/tests/ref/math-mat-align-explicit--alternating.png differ
diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png
new file mode 100644
index 0000000000..97fe0a1f42
Binary files /dev/null and b/tests/ref/math-mat-align-explicit-left.png differ
diff --git a/tests/ref/math-mat-align-explicit-right.png b/tests/ref/math-mat-align-explicit-right.png
new file mode 100644
index 0000000000..80966e5246
Binary files /dev/null and b/tests/ref/math-mat-align-explicit-right.png differ
diff --git a/tests/ref/math-mat-align-implicit.png b/tests/ref/math-mat-align-implicit.png
new file mode 100644
index 0000000000..0c14f1a790
Binary files /dev/null and b/tests/ref/math-mat-align-implicit.png differ
diff --git a/tests/ref/math-mat-align-signed-numbers.png b/tests/ref/math-mat-align-signed-numbers.png
new file mode 100644
index 0000000000..02a3c58203
Binary files /dev/null and b/tests/ref/math-mat-align-signed-numbers.png differ
diff --git a/tests/ref/math-mat-augment-set.png b/tests/ref/math-mat-augment-set.png
new file mode 100644
index 0000000000..f3827c41f0
Binary files /dev/null and b/tests/ref/math-mat-augment-set.png differ
diff --git a/tests/ref/math-mat-augment.png b/tests/ref/math-mat-augment.png
new file mode 100644
index 0000000000..3a272ce903
Binary files /dev/null and b/tests/ref/math-mat-augment.png differ
diff --git a/tests/ref/math-mat-baseline.png b/tests/ref/math-mat-baseline.png
new file mode 100644
index 0000000000..51e90a1f32
Binary files /dev/null and b/tests/ref/math-mat-baseline.png differ
diff --git a/tests/ref/math-mat-delim-direct.png b/tests/ref/math-mat-delim-direct.png
new file mode 100644
index 0000000000..b40fd36cf7
Binary files /dev/null and b/tests/ref/math-mat-delim-direct.png differ
diff --git a/tests/ref/math-mat-delim-set.png b/tests/ref/math-mat-delim-set.png
new file mode 100644
index 0000000000..fc92fd4b39
Binary files /dev/null and b/tests/ref/math-mat-delim-set.png differ
diff --git a/tests/ref/math-mat-gap.png b/tests/ref/math-mat-gap.png
new file mode 100644
index 0000000000..5eb8460d7f
Binary files /dev/null and b/tests/ref/math-mat-gap.png differ
diff --git a/tests/ref/math-mat-gaps.png b/tests/ref/math-mat-gaps.png
new file mode 100644
index 0000000000..38cf524754
Binary files /dev/null and b/tests/ref/math-mat-gaps.png differ
diff --git a/tests/ref/math-mat-semicolon.png b/tests/ref/math-mat-semicolon.png
new file mode 100644
index 0000000000..abb5d1df8b
Binary files /dev/null and b/tests/ref/math-mat-semicolon.png differ
diff --git a/tests/ref/math-mat-sparse.png b/tests/ref/math-mat-sparse.png
new file mode 100644
index 0000000000..4d077931b1
Binary files /dev/null and b/tests/ref/math-mat-sparse.png differ
diff --git a/tests/ref/math-multiline-multiple-trailing-linebreaks.png b/tests/ref/math-multiline-multiple-trailing-linebreaks.png
new file mode 100644
index 0000000000..2c6484c7a8
Binary files /dev/null and b/tests/ref/math-multiline-multiple-trailing-linebreaks.png differ
diff --git a/tests/ref/math-multiline-no-trailing-linebreak.png b/tests/ref/math-multiline-no-trailing-linebreak.png
new file mode 100644
index 0000000000..8ad6204d46
Binary files /dev/null and b/tests/ref/math-multiline-no-trailing-linebreak.png differ
diff --git a/tests/ref/math-multiline-trailing-linebreak.png b/tests/ref/math-multiline-trailing-linebreak.png
new file mode 100644
index 0000000000..364d8624df
Binary files /dev/null and b/tests/ref/math-multiline-trailing-linebreak.png differ
diff --git a/tests/ref/math-nested-normal-layout.png b/tests/ref/math-nested-normal-layout.png
new file mode 100644
index 0000000000..4ec7d46ebb
Binary files /dev/null and b/tests/ref/math-nested-normal-layout.png differ
diff --git a/tests/ref/math-non-math-content.png b/tests/ref/math-non-math-content.png
new file mode 100644
index 0000000000..66896d1864
Binary files /dev/null and b/tests/ref/math-non-math-content.png differ
diff --git a/tests/ref/math-op-call.png b/tests/ref/math-op-call.png
new file mode 100644
index 0000000000..2fcdf2cb30
Binary files /dev/null and b/tests/ref/math-op-call.png differ
diff --git a/tests/ref/math-op-custom.png b/tests/ref/math-op-custom.png
new file mode 100644
index 0000000000..fbba241d5d
Binary files /dev/null and b/tests/ref/math-op-custom.png differ
diff --git a/tests/ref/math-op-predefined.png b/tests/ref/math-op-predefined.png
new file mode 100644
index 0000000000..bfede9e73a
Binary files /dev/null and b/tests/ref/math-op-predefined.png differ
diff --git a/tests/ref/math-op-scripts-vs-limits.png b/tests/ref/math-op-scripts-vs-limits.png
new file mode 100644
index 0000000000..418974169c
Binary files /dev/null and b/tests/ref/math-op-scripts-vs-limits.png differ
diff --git a/tests/ref/math-op-styled.png b/tests/ref/math-op-styled.png
new file mode 100644
index 0000000000..c6890d74e7
Binary files /dev/null and b/tests/ref/math-op-styled.png differ
diff --git a/tests/ref/math-optical-size-frac-script-script.png b/tests/ref/math-optical-size-frac-script-script.png
new file mode 100644
index 0000000000..893b343495
Binary files /dev/null and b/tests/ref/math-optical-size-frac-script-script.png differ
diff --git a/tests/ref/math-optical-size-nested-scripts.png b/tests/ref/math-optical-size-nested-scripts.png
new file mode 100644
index 0000000000..8ca35c6e41
Binary files /dev/null and b/tests/ref/math-optical-size-nested-scripts.png differ
diff --git a/tests/ref/math-optical-size-prime-large-operator.png b/tests/ref/math-optical-size-prime-large-operator.png
new file mode 100644
index 0000000000..b38a934edc
Binary files /dev/null and b/tests/ref/math-optical-size-prime-large-operator.png differ
diff --git a/tests/ref/math-optical-size-primes.png b/tests/ref/math-optical-size-primes.png
new file mode 100644
index 0000000000..8fc199aa89
Binary files /dev/null and b/tests/ref/math-optical-size-primes.png differ
diff --git a/tests/ref/math-primes-after-code-expr.png b/tests/ref/math-primes-after-code-expr.png
new file mode 100644
index 0000000000..5ec3bc8cf0
Binary files /dev/null and b/tests/ref/math-primes-after-code-expr.png differ
diff --git a/tests/ref/math-primes-attach.png b/tests/ref/math-primes-attach.png
new file mode 100644
index 0000000000..95b77882e9
Binary files /dev/null and b/tests/ref/math-primes-attach.png differ
diff --git a/tests/ref/math-primes-complex.png b/tests/ref/math-primes-complex.png
new file mode 100644
index 0000000000..5f5558eb21
Binary files /dev/null and b/tests/ref/math-primes-complex.png differ
diff --git a/tests/ref/math-primes-limits.png b/tests/ref/math-primes-limits.png
new file mode 100644
index 0000000000..f2c5cec263
Binary files /dev/null and b/tests/ref/math-primes-limits.png differ
diff --git a/tests/ref/math-primes-scripts.png b/tests/ref/math-primes-scripts.png
new file mode 100644
index 0000000000..2a9121809b
Binary files /dev/null and b/tests/ref/math-primes-scripts.png differ
diff --git a/tests/ref/math-primes-spaces.png b/tests/ref/math-primes-spaces.png
new file mode 100644
index 0000000000..890cc34844
Binary files /dev/null and b/tests/ref/math-primes-spaces.png differ
diff --git a/tests/ref/math-primes.png b/tests/ref/math-primes.png
new file mode 100644
index 0000000000..f3323197b7
Binary files /dev/null and b/tests/ref/math-primes.png differ
diff --git a/tests/ref/math-root-basic.png b/tests/ref/math-root-basic.png
new file mode 100644
index 0000000000..b8b891eb03
Binary files /dev/null and b/tests/ref/math-root-basic.png differ
diff --git a/tests/ref/math-root-large-body.png b/tests/ref/math-root-large-body.png
new file mode 100644
index 0000000000..3dd4d848e4
Binary files /dev/null and b/tests/ref/math-root-large-body.png differ
diff --git a/tests/ref/math-root-large-index.png b/tests/ref/math-root-large-index.png
new file mode 100644
index 0000000000..8037222cb2
Binary files /dev/null and b/tests/ref/math-root-large-index.png differ
diff --git a/tests/ref/math-root-precomposed.png b/tests/ref/math-root-precomposed.png
new file mode 100644
index 0000000000..e09f187816
Binary files /dev/null and b/tests/ref/math-root-precomposed.png differ
diff --git a/tests/ref/math-root-radical-attachment.png b/tests/ref/math-root-radical-attachment.png
new file mode 100644
index 0000000000..4cb447ec09
Binary files /dev/null and b/tests/ref/math-root-radical-attachment.png differ
diff --git a/tests/ref/math-root-syntax.png b/tests/ref/math-root-syntax.png
new file mode 100644
index 0000000000..492554934c
Binary files /dev/null and b/tests/ref/math-root-syntax.png differ
diff --git a/tests/ref/math-shorthands.png b/tests/ref/math-shorthands.png
new file mode 100644
index 0000000000..ff26ce96f1
Binary files /dev/null and b/tests/ref/math-shorthands.png differ
diff --git a/tests/ref/math-size.png b/tests/ref/math-size.png
new file mode 100644
index 0000000000..b44e4c7458
Binary files /dev/null and b/tests/ref/math-size.png differ
diff --git a/tests/ref/math-spacing-basic.png b/tests/ref/math-spacing-basic.png
new file mode 100644
index 0000000000..5567b08756
Binary files /dev/null and b/tests/ref/math-spacing-basic.png differ
diff --git a/tests/ref/math-spacing-decorated.png b/tests/ref/math-spacing-decorated.png
new file mode 100644
index 0000000000..e34066ea22
Binary files /dev/null and b/tests/ref/math-spacing-decorated.png differ
diff --git a/tests/ref/math-spacing-kept-spaces.png b/tests/ref/math-spacing-kept-spaces.png
new file mode 100644
index 0000000000..bb433d4fc5
Binary files /dev/null and b/tests/ref/math-spacing-kept-spaces.png differ
diff --git a/tests/ref/math-spacing-predefined.png b/tests/ref/math-spacing-predefined.png
new file mode 100644
index 0000000000..244e764287
Binary files /dev/null and b/tests/ref/math-spacing-predefined.png differ
diff --git a/tests/ref/math-spacing-set-comprehension.png b/tests/ref/math-spacing-set-comprehension.png
new file mode 100644
index 0000000000..63ef46ca92
Binary files /dev/null and b/tests/ref/math-spacing-set-comprehension.png differ
diff --git a/tests/ref/math-spacing-weak.png b/tests/ref/math-spacing-weak.png
new file mode 100644
index 0000000000..71af322217
Binary files /dev/null and b/tests/ref/math-spacing-weak.png differ
diff --git a/tests/ref/math-style-exceptions.png b/tests/ref/math-style-exceptions.png
new file mode 100644
index 0000000000..bdeabb6732
Binary files /dev/null and b/tests/ref/math-style-exceptions.png differ
diff --git a/tests/ref/math-style-greek-exceptions.png b/tests/ref/math-style-greek-exceptions.png
new file mode 100644
index 0000000000..93ae6309bc
Binary files /dev/null and b/tests/ref/math-style-greek-exceptions.png differ
diff --git a/tests/ref/math-style-hebrew-exceptions.png b/tests/ref/math-style-hebrew-exceptions.png
new file mode 100644
index 0000000000..723466e8a3
Binary files /dev/null and b/tests/ref/math-style-hebrew-exceptions.png differ
diff --git a/tests/ref/math-style-italic-default.png b/tests/ref/math-style-italic-default.png
new file mode 100644
index 0000000000..0a25f6facb
Binary files /dev/null and b/tests/ref/math-style-italic-default.png differ
diff --git a/tests/ref/math-style.png b/tests/ref/math-style.png
new file mode 100644
index 0000000000..f514bd18dd
Binary files /dev/null and b/tests/ref/math-style.png differ
diff --git a/tests/ref/math-symbol-show-rule.png b/tests/ref/math-symbol-show-rule.png
new file mode 100644
index 0000000000..68faf937a3
Binary files /dev/null and b/tests/ref/math-symbol-show-rule.png differ
diff --git a/tests/ref/math-table.png b/tests/ref/math-table.png
new file mode 100644
index 0000000000..5eb9321815
Binary files /dev/null and b/tests/ref/math-table.png differ
diff --git a/tests/ref/math-text-color.png b/tests/ref/math-text-color.png
new file mode 100644
index 0000000000..33ff00f379
Binary files /dev/null and b/tests/ref/math-text-color.png differ
diff --git a/tests/ref/math-underover-brace.png b/tests/ref/math-underover-brace.png
new file mode 100644
index 0000000000..d4a3f4a1c0
Binary files /dev/null and b/tests/ref/math-underover-brace.png differ
diff --git a/tests/ref/math-underover-brackets.png b/tests/ref/math-underover-brackets.png
new file mode 100644
index 0000000000..03419bc3ba
Binary files /dev/null and b/tests/ref/math-underover-brackets.png differ
diff --git a/tests/ref/math-underover-line-bracket.png b/tests/ref/math-underover-line-bracket.png
new file mode 100644
index 0000000000..08d8df2027
Binary files /dev/null and b/tests/ref/math-underover-line-bracket.png differ
diff --git a/tests/ref/math-unicode.png b/tests/ref/math-unicode.png
new file mode 100644
index 0000000000..e74429ebf6
Binary files /dev/null and b/tests/ref/math-unicode.png differ
diff --git a/tests/ref/math-vec-align-explicit-alternating.png b/tests/ref/math-vec-align-explicit-alternating.png
new file mode 100644
index 0000000000..cb29eb067e
Binary files /dev/null and b/tests/ref/math-vec-align-explicit-alternating.png differ
diff --git a/tests/ref/math-vec-delim-set.png b/tests/ref/math-vec-delim-set.png
new file mode 100644
index 0000000000..8024d594c4
Binary files /dev/null and b/tests/ref/math-vec-delim-set.png differ
diff --git a/tests/ref/math-vec-gap.png b/tests/ref/math-vec-gap.png
new file mode 100644
index 0000000000..06f8cf7de2
Binary files /dev/null and b/tests/ref/math-vec-gap.png differ
diff --git a/tests/ref/math-vec-wide.png b/tests/ref/math-vec-wide.png
new file mode 100644
index 0000000000..30853a0019
Binary files /dev/null and b/tests/ref/math-vec-wide.png differ
diff --git a/tests/ref/math/accent.png b/tests/ref/math/accent.png
deleted file mode 100644
index 52a7037e3b..0000000000
Binary files a/tests/ref/math/accent.png and /dev/null differ
diff --git a/tests/ref/math/alignment.png b/tests/ref/math/alignment.png
deleted file mode 100644
index 4bf739a4f6..0000000000
Binary files a/tests/ref/math/alignment.png and /dev/null differ
diff --git a/tests/ref/math/attach-p1.png b/tests/ref/math/attach-p1.png
deleted file mode 100644
index 45c465ce57..0000000000
Binary files a/tests/ref/math/attach-p1.png and /dev/null differ
diff --git a/tests/ref/math/attach-p2.png b/tests/ref/math/attach-p2.png
deleted file mode 100644
index 3820f33e91..0000000000
Binary files a/tests/ref/math/attach-p2.png and /dev/null differ
diff --git a/tests/ref/math/attach-p3.png b/tests/ref/math/attach-p3.png
deleted file mode 100644
index 69e0a7dd71..0000000000
Binary files a/tests/ref/math/attach-p3.png and /dev/null differ
diff --git a/tests/ref/math/call.png b/tests/ref/math/call.png
deleted file mode 100644
index 907a1a2be4..0000000000
Binary files a/tests/ref/math/call.png and /dev/null differ
diff --git a/tests/ref/math/cancel.png b/tests/ref/math/cancel.png
deleted file mode 100644
index 4f0de13603..0000000000
Binary files a/tests/ref/math/cancel.png and /dev/null differ
diff --git a/tests/ref/math/cases.png b/tests/ref/math/cases.png
deleted file mode 100644
index e222ae17ae..0000000000
Binary files a/tests/ref/math/cases.png and /dev/null differ
diff --git a/tests/ref/math/class.png b/tests/ref/math/class.png
deleted file mode 100644
index a4d6e86c1a..0000000000
Binary files a/tests/ref/math/class.png and /dev/null differ
diff --git a/tests/ref/math/content.png b/tests/ref/math/content.png
deleted file mode 100644
index c27a17ea67..0000000000
Binary files a/tests/ref/math/content.png and /dev/null differ
diff --git a/tests/ref/math/delimited.png b/tests/ref/math/delimited.png
deleted file mode 100644
index 6126f58ad9..0000000000
Binary files a/tests/ref/math/delimited.png and /dev/null differ
diff --git a/tests/ref/math/equation-block-align.png b/tests/ref/math/equation-block-align.png
deleted file mode 100644
index 8736312a7c..0000000000
Binary files a/tests/ref/math/equation-block-align.png and /dev/null differ
diff --git a/tests/ref/math/equation-number.png b/tests/ref/math/equation-number.png
deleted file mode 100644
index 8ba915902f..0000000000
Binary files a/tests/ref/math/equation-number.png and /dev/null differ
diff --git a/tests/ref/math/equation-show.png b/tests/ref/math/equation-show.png
deleted file mode 100644
index 79a70dc026..0000000000
Binary files a/tests/ref/math/equation-show.png and /dev/null differ
diff --git a/tests/ref/math/font-features.png b/tests/ref/math/font-features.png
deleted file mode 100644
index 0ab2c06d01..0000000000
Binary files a/tests/ref/math/font-features.png and /dev/null differ
diff --git a/tests/ref/math/frac.png b/tests/ref/math/frac.png
deleted file mode 100644
index 3e08f7e5bf..0000000000
Binary files a/tests/ref/math/frac.png and /dev/null differ
diff --git a/tests/ref/math/linebreak.png b/tests/ref/math/linebreak.png
deleted file mode 100644
index f3212a4a10..0000000000
Binary files a/tests/ref/math/linebreak.png and /dev/null differ
diff --git a/tests/ref/math/matrix-alignment.png b/tests/ref/math/matrix-alignment.png
deleted file mode 100644
index cdf174639e..0000000000
Binary files a/tests/ref/math/matrix-alignment.png and /dev/null differ
diff --git a/tests/ref/math/matrix-gaps.png b/tests/ref/math/matrix-gaps.png
deleted file mode 100644
index 16788969f2..0000000000
Binary files a/tests/ref/math/matrix-gaps.png and /dev/null differ
diff --git a/tests/ref/math/matrix.png b/tests/ref/math/matrix.png
deleted file mode 100644
index b8ea19e206..0000000000
Binary files a/tests/ref/math/matrix.png and /dev/null differ
diff --git a/tests/ref/math/multiline.png b/tests/ref/math/multiline.png
deleted file mode 100644
index 185724af9a..0000000000
Binary files a/tests/ref/math/multiline.png and /dev/null differ
diff --git a/tests/ref/math/numbering.png b/tests/ref/math/numbering.png
deleted file mode 100644
index 813f5f8c35..0000000000
Binary files a/tests/ref/math/numbering.png and /dev/null differ
diff --git a/tests/ref/math/op.png b/tests/ref/math/op.png
deleted file mode 100644
index ab3f35f6e5..0000000000
Binary files a/tests/ref/math/op.png and /dev/null differ
diff --git a/tests/ref/math/opticalsize.png b/tests/ref/math/opticalsize.png
deleted file mode 100644
index 303f7bee7d..0000000000
Binary files a/tests/ref/math/opticalsize.png and /dev/null differ
diff --git a/tests/ref/math/prime.png b/tests/ref/math/prime.png
deleted file mode 100644
index 81a4764367..0000000000
Binary files a/tests/ref/math/prime.png and /dev/null differ
diff --git a/tests/ref/math/root.png b/tests/ref/math/root.png
deleted file mode 100644
index 51fdf2e8f2..0000000000
Binary files a/tests/ref/math/root.png and /dev/null differ
diff --git a/tests/ref/math/spacing.png b/tests/ref/math/spacing.png
deleted file mode 100644
index d8198bbfb2..0000000000
Binary files a/tests/ref/math/spacing.png and /dev/null differ
diff --git a/tests/ref/math/style.png b/tests/ref/math/style.png
deleted file mode 100644
index 5201181eee..0000000000
Binary files a/tests/ref/math/style.png and /dev/null differ
diff --git a/tests/ref/math/syntax.png b/tests/ref/math/syntax.png
deleted file mode 100644
index 3855fa9b82..0000000000
Binary files a/tests/ref/math/syntax.png and /dev/null differ
diff --git a/tests/ref/math/unbalanced.png b/tests/ref/math/unbalanced.png
deleted file mode 100644
index 84f51837ce..0000000000
Binary files a/tests/ref/math/unbalanced.png and /dev/null differ
diff --git a/tests/ref/math/underover.png b/tests/ref/math/underover.png
deleted file mode 100644
index e974302f42..0000000000
Binary files a/tests/ref/math/underover.png and /dev/null differ
diff --git a/tests/ref/math/vec.png b/tests/ref/math/vec.png
deleted file mode 100644
index f2371e5b3f..0000000000
Binary files a/tests/ref/math/vec.png and /dev/null differ
diff --git a/tests/ref/meta/bibliography-full.png b/tests/ref/meta/bibliography-full.png
deleted file mode 100644
index d8778c09e1..0000000000
Binary files a/tests/ref/meta/bibliography-full.png and /dev/null differ
diff --git a/tests/ref/meta/bibliography-ordering.png b/tests/ref/meta/bibliography-ordering.png
deleted file mode 100644
index 116c561d08..0000000000
Binary files a/tests/ref/meta/bibliography-ordering.png and /dev/null differ
diff --git a/tests/ref/meta/bibliography.png b/tests/ref/meta/bibliography.png
deleted file mode 100644
index 8fbd09bc81..0000000000
Binary files a/tests/ref/meta/bibliography.png and /dev/null differ
diff --git a/tests/ref/meta/cite-footnote.png b/tests/ref/meta/cite-footnote.png
deleted file mode 100644
index 3a7a0094da..0000000000
Binary files a/tests/ref/meta/cite-footnote.png and /dev/null differ
diff --git a/tests/ref/meta/cite-form.png b/tests/ref/meta/cite-form.png
deleted file mode 100644
index 8adeac92f0..0000000000
Binary files a/tests/ref/meta/cite-form.png and /dev/null differ
diff --git a/tests/ref/meta/cite-group.png b/tests/ref/meta/cite-group.png
deleted file mode 100644
index 8d02a9036d..0000000000
Binary files a/tests/ref/meta/cite-group.png and /dev/null differ
diff --git a/tests/ref/meta/counter-page.png b/tests/ref/meta/counter-page.png
deleted file mode 100644
index 869718bcaa..0000000000
Binary files a/tests/ref/meta/counter-page.png and /dev/null differ
diff --git a/tests/ref/meta/counter.png b/tests/ref/meta/counter.png
deleted file mode 100644
index 6c29ac17f8..0000000000
Binary files a/tests/ref/meta/counter.png and /dev/null differ
diff --git a/tests/ref/meta/document.png b/tests/ref/meta/document.png
deleted file mode 100644
index 6db265112f..0000000000
Binary files a/tests/ref/meta/document.png and /dev/null differ
diff --git a/tests/ref/meta/figure-caption.png b/tests/ref/meta/figure-caption.png
deleted file mode 100644
index 8a1d4a599f..0000000000
Binary files a/tests/ref/meta/figure-caption.png and /dev/null differ
diff --git a/tests/ref/meta/figure-localization.png b/tests/ref/meta/figure-localization.png
deleted file mode 100644
index 5fcbd2b7c1..0000000000
Binary files a/tests/ref/meta/figure-localization.png and /dev/null differ
diff --git a/tests/ref/meta/figure.png b/tests/ref/meta/figure.png
deleted file mode 100644
index bcdd0d2fa7..0000000000
Binary files a/tests/ref/meta/figure.png and /dev/null differ
diff --git a/tests/ref/meta/footnote-break.png b/tests/ref/meta/footnote-break.png
deleted file mode 100644
index 625305c88f..0000000000
Binary files a/tests/ref/meta/footnote-break.png and /dev/null differ
diff --git a/tests/ref/meta/footnote-columns.png b/tests/ref/meta/footnote-columns.png
deleted file mode 100644
index 528ec66418..0000000000
Binary files a/tests/ref/meta/footnote-columns.png and /dev/null differ
diff --git a/tests/ref/meta/footnote-container.png b/tests/ref/meta/footnote-container.png
deleted file mode 100644
index 9327e7eecc..0000000000
Binary files a/tests/ref/meta/footnote-container.png and /dev/null differ
diff --git a/tests/ref/meta/footnote-invariant.png b/tests/ref/meta/footnote-invariant.png
deleted file mode 100644
index 66b4118255..0000000000
Binary files a/tests/ref/meta/footnote-invariant.png and /dev/null differ
diff --git a/tests/ref/meta/footnote-refs.png b/tests/ref/meta/footnote-refs.png
deleted file mode 100644
index 3fab7bd5d6..0000000000
Binary files a/tests/ref/meta/footnote-refs.png and /dev/null differ
diff --git a/tests/ref/meta/footnote-table.png b/tests/ref/meta/footnote-table.png
deleted file mode 100644
index 023f8008be..0000000000
Binary files a/tests/ref/meta/footnote-table.png and /dev/null differ
diff --git a/tests/ref/meta/footnote.png b/tests/ref/meta/footnote.png
deleted file mode 100644
index 4c67bbd78b..0000000000
Binary files a/tests/ref/meta/footnote.png and /dev/null differ
diff --git a/tests/ref/meta/heading.png b/tests/ref/meta/heading.png
deleted file mode 100644
index 8467ea5387..0000000000
Binary files a/tests/ref/meta/heading.png and /dev/null differ
diff --git a/tests/ref/meta/link.png b/tests/ref/meta/link.png
deleted file mode 100644
index 3c3ecd2c09..0000000000
Binary files a/tests/ref/meta/link.png and /dev/null differ
diff --git a/tests/ref/meta/numbering.png b/tests/ref/meta/numbering.png
deleted file mode 100644
index fa5b520fa0..0000000000
Binary files a/tests/ref/meta/numbering.png and /dev/null differ
diff --git a/tests/ref/meta/outline-entry.png b/tests/ref/meta/outline-entry.png
deleted file mode 100644
index f8f5412f0a..0000000000
Binary files a/tests/ref/meta/outline-entry.png and /dev/null differ
diff --git a/tests/ref/meta/outline-indent.png b/tests/ref/meta/outline-indent.png
deleted file mode 100644
index 816d86a5e7..0000000000
Binary files a/tests/ref/meta/outline-indent.png and /dev/null differ
diff --git a/tests/ref/meta/outline.png b/tests/ref/meta/outline.png
deleted file mode 100644
index 047bcc801a..0000000000
Binary files a/tests/ref/meta/outline.png and /dev/null differ
diff --git a/tests/ref/meta/page-label.png b/tests/ref/meta/page-label.png
deleted file mode 100644
index 301d626ab3..0000000000
Binary files a/tests/ref/meta/page-label.png and /dev/null differ
diff --git a/tests/ref/meta/query-before-after.png b/tests/ref/meta/query-before-after.png
deleted file mode 100644
index 80f8fe1f73..0000000000
Binary files a/tests/ref/meta/query-before-after.png and /dev/null differ
diff --git a/tests/ref/meta/query-figure.png b/tests/ref/meta/query-figure.png
deleted file mode 100644
index 2537ebf00d..0000000000
Binary files a/tests/ref/meta/query-figure.png and /dev/null differ
diff --git a/tests/ref/meta/query-header.png b/tests/ref/meta/query-header.png
deleted file mode 100644
index c2dc468929..0000000000
Binary files a/tests/ref/meta/query-header.png and /dev/null differ
diff --git a/tests/ref/meta/ref.png b/tests/ref/meta/ref.png
deleted file mode 100644
index 51563f54b6..0000000000
Binary files a/tests/ref/meta/ref.png and /dev/null differ
diff --git a/tests/ref/meta/state.png b/tests/ref/meta/state.png
deleted file mode 100644
index 25faa0d991..0000000000
Binary files a/tests/ref/meta/state.png and /dev/null differ
diff --git a/tests/ref/newline-continuation-code.png b/tests/ref/newline-continuation-code.png
new file mode 100644
index 0000000000..46a6afd56f
Binary files /dev/null and b/tests/ref/newline-continuation-code.png differ
diff --git a/tests/ref/newline-continuation-markup.png b/tests/ref/newline-continuation-markup.png
new file mode 100644
index 0000000000..268e5f8408
Binary files /dev/null and b/tests/ref/newline-continuation-markup.png differ
diff --git a/tests/ref/numbering-chinese.png b/tests/ref/numbering-chinese.png
new file mode 100644
index 0000000000..06b3133423
Binary files /dev/null and b/tests/ref/numbering-chinese.png differ
diff --git a/tests/ref/numbering-hebrew.png b/tests/ref/numbering-hebrew.png
new file mode 100644
index 0000000000..d761422591
Binary files /dev/null and b/tests/ref/numbering-hebrew.png differ
diff --git a/tests/ref/numbering-japanese-aiueo.png b/tests/ref/numbering-japanese-aiueo.png
new file mode 100644
index 0000000000..b06d5c69fc
Binary files /dev/null and b/tests/ref/numbering-japanese-aiueo.png differ
diff --git a/tests/ref/numbering-japanese-iroha.png b/tests/ref/numbering-japanese-iroha.png
new file mode 100644
index 0000000000..2018802f2d
Binary files /dev/null and b/tests/ref/numbering-japanese-iroha.png differ
diff --git a/tests/ref/numbering-korean.png b/tests/ref/numbering-korean.png
new file mode 100644
index 0000000000..281f2ec28f
Binary files /dev/null and b/tests/ref/numbering-korean.png differ
diff --git a/tests/ref/numbering-latin.png b/tests/ref/numbering-latin.png
new file mode 100644
index 0000000000..e154735a19
Binary files /dev/null and b/tests/ref/numbering-latin.png differ
diff --git a/tests/ref/numbering-symbol-and-roman.png b/tests/ref/numbering-symbol-and-roman.png
new file mode 100644
index 0000000000..979f3b90b2
Binary files /dev/null and b/tests/ref/numbering-symbol-and-roman.png differ
diff --git a/tests/ref/numbers.png b/tests/ref/numbers.png
new file mode 100644
index 0000000000..e6e7215b58
Binary files /dev/null and b/tests/ref/numbers.png differ
diff --git a/tests/ref/ops-add-content.png b/tests/ref/ops-add-content.png
new file mode 100644
index 0000000000..bdb8cb5ea5
Binary files /dev/null and b/tests/ref/ops-add-content.png differ
diff --git a/tests/ref/ops-multiply-inf-with-length.png b/tests/ref/ops-multiply-inf-with-length.png
new file mode 100644
index 0000000000..749be0564f
Binary files /dev/null and b/tests/ref/ops-multiply-inf-with-length.png differ
diff --git a/tests/ref/outline-entry-complex.png b/tests/ref/outline-entry-complex.png
new file mode 100644
index 0000000000..c885cacc07
Binary files /dev/null and b/tests/ref/outline-entry-complex.png differ
diff --git a/tests/ref/outline-entry.png b/tests/ref/outline-entry.png
new file mode 100644
index 0000000000..94e7a5a740
Binary files /dev/null and b/tests/ref/outline-entry.png differ
diff --git a/tests/ref/outline-first-line-indent.png b/tests/ref/outline-first-line-indent.png
new file mode 100644
index 0000000000..dd995f3194
Binary files /dev/null and b/tests/ref/outline-first-line-indent.png differ
diff --git a/tests/ref/outline-indent-no-numbering.png b/tests/ref/outline-indent-no-numbering.png
new file mode 100644
index 0000000000..62bd80a3c5
Binary files /dev/null and b/tests/ref/outline-indent-no-numbering.png differ
diff --git a/tests/ref/outline-indent-numbering.png b/tests/ref/outline-indent-numbering.png
new file mode 100644
index 0000000000..6c9368271a
Binary files /dev/null and b/tests/ref/outline-indent-numbering.png differ
diff --git a/tests/ref/outline.png b/tests/ref/outline.png
new file mode 100644
index 0000000000..e5c24a9806
Binary files /dev/null and b/tests/ref/outline.png differ
diff --git a/tests/ref/overhang-lone.png b/tests/ref/overhang-lone.png
new file mode 100644
index 0000000000..b48618fb29
Binary files /dev/null and b/tests/ref/overhang-lone.png differ
diff --git a/tests/ref/overhang.png b/tests/ref/overhang.png
new file mode 100644
index 0000000000..b97ef30ce8
Binary files /dev/null and b/tests/ref/overhang.png differ
diff --git a/tests/ref/overline-background.png b/tests/ref/overline-background.png
new file mode 100644
index 0000000000..8efd147ea6
Binary files /dev/null and b/tests/ref/overline-background.png differ
diff --git a/tests/ref/pad-basic.png b/tests/ref/pad-basic.png
new file mode 100644
index 0000000000..f8c40088f8
Binary files /dev/null and b/tests/ref/pad-basic.png differ
diff --git a/tests/ref/pad-expanding-contents.png b/tests/ref/pad-expanding-contents.png
new file mode 100644
index 0000000000..1bef4a81be
Binary files /dev/null and b/tests/ref/pad-expanding-contents.png differ
diff --git a/tests/ref/pad-followed-by-content.png b/tests/ref/pad-followed-by-content.png
new file mode 100644
index 0000000000..f0f06a6ce5
Binary files /dev/null and b/tests/ref/pad-followed-by-content.png differ
diff --git a/tests/ref/page-call-followed-by-pagebreak.png b/tests/ref/page-call-followed-by-pagebreak.png
new file mode 100644
index 0000000000..87cd9735ea
Binary files /dev/null and b/tests/ref/page-call-followed-by-pagebreak.png differ
diff --git a/tests/ref/page-call-styled-empty.png b/tests/ref/page-call-styled-empty.png
new file mode 100644
index 0000000000..6a24b1bc10
Binary files /dev/null and b/tests/ref/page-call-styled-empty.png differ
diff --git a/tests/ref/page-fill.png b/tests/ref/page-fill.png
new file mode 100644
index 0000000000..0c7ab27788
Binary files /dev/null and b/tests/ref/page-fill.png differ
diff --git a/tests/ref/page-large.png b/tests/ref/page-large.png
new file mode 100644
index 0000000000..a57dceec70
Binary files /dev/null and b/tests/ref/page-large.png differ
diff --git a/tests/ref/page-margin-binding-from-text-lang.png b/tests/ref/page-margin-binding-from-text-lang.png
new file mode 100644
index 0000000000..8d12ff2f4c
Binary files /dev/null and b/tests/ref/page-margin-binding-from-text-lang.png differ
diff --git a/tests/ref/page-margin-individual.png b/tests/ref/page-margin-individual.png
new file mode 100644
index 0000000000..0bc0f51bc2
Binary files /dev/null and b/tests/ref/page-margin-individual.png differ
diff --git a/tests/ref/page-margin-inside-outside-override.png b/tests/ref/page-margin-inside-outside-override.png
new file mode 100644
index 0000000000..5aa8bf12ae
Binary files /dev/null and b/tests/ref/page-margin-inside-outside-override.png differ
diff --git a/tests/ref/page-margin-inside-with-binding.png b/tests/ref/page-margin-inside-with-binding.png
new file mode 100644
index 0000000000..5b9ec04fde
Binary files /dev/null and b/tests/ref/page-margin-inside-with-binding.png differ
diff --git a/tests/ref/page-margin-inside.png b/tests/ref/page-margin-inside.png
new file mode 100644
index 0000000000..d70b860470
Binary files /dev/null and b/tests/ref/page-margin-inside.png differ
diff --git a/tests/ref/page-margin-uniform.png b/tests/ref/page-margin-uniform.png
new file mode 100644
index 0000000000..8a06fb749d
Binary files /dev/null and b/tests/ref/page-margin-uniform.png differ
diff --git a/tests/ref/page-marginals.png b/tests/ref/page-marginals.png
new file mode 100644
index 0000000000..cab886b385
Binary files /dev/null and b/tests/ref/page-marginals.png differ
diff --git a/tests/ref/page-number-align-bottom-left.png b/tests/ref/page-number-align-bottom-left.png
new file mode 100644
index 0000000000..396f6e98fe
Binary files /dev/null and b/tests/ref/page-number-align-bottom-left.png differ
diff --git a/tests/ref/page-number-align-top-right.png b/tests/ref/page-number-align-top-right.png
new file mode 100644
index 0000000000..3c7e5579a7
Binary files /dev/null and b/tests/ref/page-number-align-top-right.png differ
diff --git a/tests/ref/page-numbering-pdf-label.png b/tests/ref/page-numbering-pdf-label.png
new file mode 100644
index 0000000000..a1cae72076
Binary files /dev/null and b/tests/ref/page-numbering-pdf-label.png differ
diff --git a/tests/ref/page-set-empty.png b/tests/ref/page-set-empty.png
new file mode 100644
index 0000000000..6a24b1bc10
Binary files /dev/null and b/tests/ref/page-set-empty.png differ
diff --git a/tests/ref/page-set-forces-break.png b/tests/ref/page-set-forces-break.png
new file mode 100644
index 0000000000..4654ef6c07
Binary files /dev/null and b/tests/ref/page-set-forces-break.png differ
diff --git a/tests/ref/page-set-only-pagebreak.png b/tests/ref/page-set-only-pagebreak.png
new file mode 100644
index 0000000000..9bf379d6e9
Binary files /dev/null and b/tests/ref/page-set-only-pagebreak.png differ
diff --git a/tests/ref/page-set-override-and-mix.png b/tests/ref/page-set-override-and-mix.png
new file mode 100644
index 0000000000..d9df6acd09
Binary files /dev/null and b/tests/ref/page-set-override-and-mix.png differ
diff --git a/tests/ref/page-set-override-thrice.png b/tests/ref/page-set-override-thrice.png
new file mode 100644
index 0000000000..99173ced31
Binary files /dev/null and b/tests/ref/page-set-override-thrice.png differ
diff --git a/tests/ref/pagebreak-around-set-page.png b/tests/ref/pagebreak-around-set-page.png
new file mode 100644
index 0000000000..2c1ce508ab
Binary files /dev/null and b/tests/ref/pagebreak-around-set-page.png differ
diff --git a/tests/ref/pagebreak-followed-by-page-call.png b/tests/ref/pagebreak-followed-by-page-call.png
new file mode 100644
index 0000000000..ee435cdcab
Binary files /dev/null and b/tests/ref/pagebreak-followed-by-page-call.png differ
diff --git a/tests/ref/pagebreak-meta.png b/tests/ref/pagebreak-meta.png
new file mode 100644
index 0000000000..7953dc51ad
Binary files /dev/null and b/tests/ref/pagebreak-meta.png differ
diff --git a/tests/ref/pagebreak-set-page-mixed.png b/tests/ref/pagebreak-set-page-mixed.png
new file mode 100644
index 0000000000..3502ee42b1
Binary files /dev/null and b/tests/ref/pagebreak-set-page-mixed.png differ
diff --git a/tests/ref/pagebreak-to-auto-sized.png b/tests/ref/pagebreak-to-auto-sized.png
new file mode 100644
index 0000000000..f3e2df45e7
Binary files /dev/null and b/tests/ref/pagebreak-to-auto-sized.png differ
diff --git a/tests/ref/pagebreak-to-multiple-pages.png b/tests/ref/pagebreak-to-multiple-pages.png
new file mode 100644
index 0000000000..a7af0a9a87
Binary files /dev/null and b/tests/ref/pagebreak-to-multiple-pages.png differ
diff --git a/tests/ref/pagebreak-to.png b/tests/ref/pagebreak-to.png
new file mode 100644
index 0000000000..62a4ee20a9
Binary files /dev/null and b/tests/ref/pagebreak-to.png differ
diff --git a/tests/ref/pagebreak-weak-after-set-page.png b/tests/ref/pagebreak-weak-after-set-page.png
new file mode 100644
index 0000000000..c8014df171
Binary files /dev/null and b/tests/ref/pagebreak-weak-after-set-page.png differ
diff --git a/tests/ref/pagebreak-weak-meta.png b/tests/ref/pagebreak-weak-meta.png
new file mode 100644
index 0000000000..aa69e606f7
Binary files /dev/null and b/tests/ref/pagebreak-weak-meta.png differ
diff --git a/tests/ref/pagebreak-weak-place.png b/tests/ref/pagebreak-weak-place.png
new file mode 100644
index 0000000000..f85bdf02b7
Binary files /dev/null and b/tests/ref/pagebreak-weak-place.png differ
diff --git a/tests/ref/pagebreak.png b/tests/ref/pagebreak.png
new file mode 100644
index 0000000000..d07473d90a
Binary files /dev/null and b/tests/ref/pagebreak.png differ
diff --git a/tests/ref/par-basic.png b/tests/ref/par-basic.png
new file mode 100644
index 0000000000..ffd9de9a36
Binary files /dev/null and b/tests/ref/par-basic.png differ
diff --git a/tests/ref/par-first-line-indent.png b/tests/ref/par-first-line-indent.png
new file mode 100644
index 0000000000..e6d7ed761b
Binary files /dev/null and b/tests/ref/par-first-line-indent.png differ
diff --git a/tests/ref/par-hanging-indent-manual-linebreak.png b/tests/ref/par-hanging-indent-manual-linebreak.png
new file mode 100644
index 0000000000..e9c666cd76
Binary files /dev/null and b/tests/ref/par-hanging-indent-manual-linebreak.png differ
diff --git a/tests/ref/par-hanging-indent-rtl.png b/tests/ref/par-hanging-indent-rtl.png
new file mode 100644
index 0000000000..849e0a012f
Binary files /dev/null and b/tests/ref/par-hanging-indent-rtl.png differ
diff --git a/tests/ref/par-hanging-indent.png b/tests/ref/par-hanging-indent.png
new file mode 100644
index 0000000000..49455a78db
Binary files /dev/null and b/tests/ref/par-hanging-indent.png differ
diff --git a/tests/ref/par-leading-and-block-spacing.png b/tests/ref/par-leading-and-block-spacing.png
new file mode 100644
index 0000000000..faaa31164a
Binary files /dev/null and b/tests/ref/par-leading-and-block-spacing.png differ
diff --git a/tests/ref/par-spacing-and-first-line-indent.png b/tests/ref/par-spacing-and-first-line-indent.png
new file mode 100644
index 0000000000..c322f63094
Binary files /dev/null and b/tests/ref/par-spacing-and-first-line-indent.png differ
diff --git a/tests/ref/parser-backtracking-destructuring-whitespace.png b/tests/ref/parser-backtracking-destructuring-whitespace.png
new file mode 100644
index 0000000000..d5d72888ed
Binary files /dev/null and b/tests/ref/parser-backtracking-destructuring-whitespace.png differ
diff --git a/tests/ref/path.png b/tests/ref/path.png
new file mode 100644
index 0000000000..9643a476c0
Binary files /dev/null and b/tests/ref/path.png differ
diff --git a/tests/ref/pattern-line.png b/tests/ref/pattern-line.png
new file mode 100644
index 0000000000..b891b6d7cc
Binary files /dev/null and b/tests/ref/pattern-line.png differ
diff --git a/tests/ref/pattern-lines.png b/tests/ref/pattern-lines.png
new file mode 100644
index 0000000000..008d357e7b
Binary files /dev/null and b/tests/ref/pattern-lines.png differ
diff --git a/tests/ref/pattern-relative-parent.png b/tests/ref/pattern-relative-parent.png
new file mode 100644
index 0000000000..786057ef3b
Binary files /dev/null and b/tests/ref/pattern-relative-parent.png differ
diff --git a/tests/ref/pattern-relative-self.png b/tests/ref/pattern-relative-self.png
new file mode 100644
index 0000000000..2840808117
Binary files /dev/null and b/tests/ref/pattern-relative-self.png differ
diff --git a/tests/ref/pattern-small.png b/tests/ref/pattern-small.png
new file mode 100644
index 0000000000..0406252d82
Binary files /dev/null and b/tests/ref/pattern-small.png differ
diff --git a/tests/ref/pattern-spacing-negative.png b/tests/ref/pattern-spacing-negative.png
new file mode 100644
index 0000000000..659c22837e
Binary files /dev/null and b/tests/ref/pattern-spacing-negative.png differ
diff --git a/tests/ref/pattern-spacing-positive.png b/tests/ref/pattern-spacing-positive.png
new file mode 100644
index 0000000000..3e475eeea8
Binary files /dev/null and b/tests/ref/pattern-spacing-positive.png differ
diff --git a/tests/ref/pattern-spacing-zero.png b/tests/ref/pattern-spacing-zero.png
new file mode 100644
index 0000000000..5118471ab1
Binary files /dev/null and b/tests/ref/pattern-spacing-zero.png differ
diff --git a/tests/ref/pattern-stroke.png b/tests/ref/pattern-stroke.png
new file mode 100644
index 0000000000..8b03783b5d
Binary files /dev/null and b/tests/ref/pattern-stroke.png differ
diff --git a/tests/ref/pattern-text.png b/tests/ref/pattern-text.png
new file mode 100644
index 0000000000..de9bfc2ec8
Binary files /dev/null and b/tests/ref/pattern-text.png differ
diff --git a/tests/ref/place-background.png b/tests/ref/place-background.png
new file mode 100644
index 0000000000..7d732717b2
Binary files /dev/null and b/tests/ref/place-background.png differ
diff --git a/tests/ref/place-basic.png b/tests/ref/place-basic.png
new file mode 100644
index 0000000000..07642c347d
Binary files /dev/null and b/tests/ref/place-basic.png differ
diff --git a/tests/ref/place-block-spacing.png b/tests/ref/place-block-spacing.png
new file mode 100644
index 0000000000..fb01d1b69d
Binary files /dev/null and b/tests/ref/place-block-spacing.png differ
diff --git a/tests/ref/place-bottom-in-box.png b/tests/ref/place-bottom-in-box.png
new file mode 100644
index 0000000000..fdd8c010ef
Binary files /dev/null and b/tests/ref/place-bottom-in-box.png differ
diff --git a/tests/ref/place-bottom-right-in-box.png b/tests/ref/place-bottom-right-in-box.png
new file mode 100644
index 0000000000..49c4088625
Binary files /dev/null and b/tests/ref/place-bottom-right-in-box.png differ
diff --git a/tests/ref/place-float-columns.png b/tests/ref/place-float-columns.png
new file mode 100644
index 0000000000..97065b68fe
Binary files /dev/null and b/tests/ref/place-float-columns.png differ
diff --git a/tests/ref/place-float-figure.png b/tests/ref/place-float-figure.png
new file mode 100644
index 0000000000..5411178a0a
Binary files /dev/null and b/tests/ref/place-float-figure.png differ
diff --git a/tests/ref/place-float.png b/tests/ref/place-float.png
new file mode 100644
index 0000000000..ddd49c47f4
Binary files /dev/null and b/tests/ref/place-float.png differ
diff --git a/tests/ref/place-horizon-in-boxes.png b/tests/ref/place-horizon-in-boxes.png
new file mode 100644
index 0000000000..b6d333bfcd
Binary files /dev/null and b/tests/ref/place-horizon-in-boxes.png differ
diff --git a/tests/ref/place-top-left-in-box.png b/tests/ref/place-top-left-in-box.png
new file mode 100644
index 0000000000..914ffa58d5
Binary files /dev/null and b/tests/ref/place-top-left-in-box.png differ
diff --git a/tests/ref/polygon-line-join.png b/tests/ref/polygon-line-join.png
new file mode 100644
index 0000000000..0f65fa7045
Binary files /dev/null and b/tests/ref/polygon-line-join.png differ
diff --git a/tests/ref/polygon.png b/tests/ref/polygon.png
new file mode 100644
index 0000000000..1dc110831b
Binary files /dev/null and b/tests/ref/polygon.png differ
diff --git a/tests/ref/query-and-or.png b/tests/ref/query-and-or.png
new file mode 100644
index 0000000000..39cfd0765c
Binary files /dev/null and b/tests/ref/query-and-or.png differ
diff --git a/tests/ref/query-before-after.png b/tests/ref/query-before-after.png
new file mode 100644
index 0000000000..33fde9859e
Binary files /dev/null and b/tests/ref/query-before-after.png differ
diff --git a/tests/ref/query-complex.png b/tests/ref/query-complex.png
new file mode 100644
index 0000000000..f71dcce565
Binary files /dev/null and b/tests/ref/query-complex.png differ
diff --git a/tests/ref/query-list-of-figures.png b/tests/ref/query-list-of-figures.png
new file mode 100644
index 0000000000..c94ccd00e6
Binary files /dev/null and b/tests/ref/query-list-of-figures.png differ
diff --git a/tests/ref/query-running-header.png b/tests/ref/query-running-header.png
new file mode 100644
index 0000000000..210c781036
Binary files /dev/null and b/tests/ref/query-running-header.png differ
diff --git a/tests/ref/quote-block-spacing.png b/tests/ref/quote-block-spacing.png
new file mode 100644
index 0000000000..3efae5abeb
Binary files /dev/null and b/tests/ref/quote-block-spacing.png differ
diff --git a/tests/ref/quote-cite-format-author-date.png b/tests/ref/quote-cite-format-author-date.png
new file mode 100644
index 0000000000..43816f8ccb
Binary files /dev/null and b/tests/ref/quote-cite-format-author-date.png differ
diff --git a/tests/ref/quote-cite-format-label-or-numeric.png b/tests/ref/quote-cite-format-label-or-numeric.png
new file mode 100644
index 0000000000..f0f5f90f3a
Binary files /dev/null and b/tests/ref/quote-cite-format-label-or-numeric.png differ
diff --git a/tests/ref/quote-cite-format-note.png b/tests/ref/quote-cite-format-note.png
new file mode 100644
index 0000000000..1092ffdbfa
Binary files /dev/null and b/tests/ref/quote-cite-format-note.png differ
diff --git a/tests/ref/quote-dir-align.png b/tests/ref/quote-dir-align.png
new file mode 100644
index 0000000000..0341f87c99
Binary files /dev/null and b/tests/ref/quote-dir-align.png differ
diff --git a/tests/ref/quote-dir-author-pos.png b/tests/ref/quote-dir-author-pos.png
new file mode 100644
index 0000000000..842796a251
Binary files /dev/null and b/tests/ref/quote-dir-author-pos.png differ
diff --git a/tests/ref/quote-inline.png b/tests/ref/quote-inline.png
new file mode 100644
index 0000000000..4dbc9720e1
Binary files /dev/null and b/tests/ref/quote-inline.png differ
diff --git a/tests/ref/quote-nesting-custom.png b/tests/ref/quote-nesting-custom.png
new file mode 100644
index 0000000000..e26b6258ec
Binary files /dev/null and b/tests/ref/quote-nesting-custom.png differ
diff --git a/tests/ref/quote-nesting.png b/tests/ref/quote-nesting.png
new file mode 100644
index 0000000000..dcd1e3780e
Binary files /dev/null and b/tests/ref/quote-nesting.png differ
diff --git a/tests/ref/raw-align-default.png b/tests/ref/raw-align-default.png
new file mode 100644
index 0000000000..84c5122974
Binary files /dev/null and b/tests/ref/raw-align-default.png differ
diff --git a/tests/ref/raw-align-specified.png b/tests/ref/raw-align-specified.png
new file mode 100644
index 0000000000..18b48deca0
Binary files /dev/null and b/tests/ref/raw-align-specified.png differ
diff --git a/tests/ref/raw-block-no-parbreaks.png b/tests/ref/raw-block-no-parbreaks.png
new file mode 100644
index 0000000000..401cc5a90d
Binary files /dev/null and b/tests/ref/raw-block-no-parbreaks.png differ
diff --git a/tests/ref/raw-consecutive-single-backticks.png b/tests/ref/raw-consecutive-single-backticks.png
new file mode 100644
index 0000000000..159d0edac2
Binary files /dev/null and b/tests/ref/raw-consecutive-single-backticks.png differ
diff --git a/tests/ref/raw-dedent-empty-line.png b/tests/ref/raw-dedent-empty-line.png
new file mode 100644
index 0000000000..c3c88e7ab8
Binary files /dev/null and b/tests/ref/raw-dedent-empty-line.png differ
diff --git a/tests/ref/raw-dedent-first-line.png b/tests/ref/raw-dedent-first-line.png
new file mode 100644
index 0000000000..c6eee5ce2d
Binary files /dev/null and b/tests/ref/raw-dedent-first-line.png differ
diff --git a/tests/ref/raw-dedent-last-line.png b/tests/ref/raw-dedent-last-line.png
new file mode 100644
index 0000000000..2b1fe747f3
Binary files /dev/null and b/tests/ref/raw-dedent-last-line.png differ
diff --git a/tests/ref/raw-empty.png b/tests/ref/raw-empty.png
new file mode 100644
index 0000000000..a47eb85561
Binary files /dev/null and b/tests/ref/raw-empty.png differ
diff --git a/tests/ref/raw-highlight-typ.png b/tests/ref/raw-highlight-typ.png
new file mode 100644
index 0000000000..f80bbf898f
Binary files /dev/null and b/tests/ref/raw-highlight-typ.png differ
diff --git a/tests/ref/raw-highlight.png b/tests/ref/raw-highlight.png
new file mode 100644
index 0000000000..2f99b4507b
Binary files /dev/null and b/tests/ref/raw-highlight.png differ
diff --git a/tests/ref/raw-inline-multiline.png b/tests/ref/raw-inline-multiline.png
new file mode 100644
index 0000000000..7db3126d85
Binary files /dev/null and b/tests/ref/raw-inline-multiline.png differ
diff --git a/tests/ref/raw-line-alternating-fill.png b/tests/ref/raw-line-alternating-fill.png
new file mode 100644
index 0000000000..b805312920
Binary files /dev/null and b/tests/ref/raw-line-alternating-fill.png differ
diff --git a/tests/ref/raw-line-text-fill.png b/tests/ref/raw-line-text-fill.png
new file mode 100644
index 0000000000..5b3c4d199b
Binary files /dev/null and b/tests/ref/raw-line-text-fill.png differ
diff --git a/tests/ref/raw-line.png b/tests/ref/raw-line.png
new file mode 100644
index 0000000000..c8ada95d30
Binary files /dev/null and b/tests/ref/raw-line.png differ
diff --git a/tests/ref/raw-more-backticks.png b/tests/ref/raw-more-backticks.png
new file mode 100644
index 0000000000..ab836011a6
Binary files /dev/null and b/tests/ref/raw-more-backticks.png differ
diff --git a/tests/ref/raw-show-set.png b/tests/ref/raw-show-set.png
new file mode 100644
index 0000000000..8a82c2e952
Binary files /dev/null and b/tests/ref/raw-show-set.png differ
diff --git a/tests/ref/raw-single-backtick-lang.png b/tests/ref/raw-single-backtick-lang.png
new file mode 100644
index 0000000000..b420627e32
Binary files /dev/null and b/tests/ref/raw-single-backtick-lang.png differ
diff --git a/tests/ref/raw-syntaxes.png b/tests/ref/raw-syntaxes.png
new file mode 100644
index 0000000000..4e14cd0661
Binary files /dev/null and b/tests/ref/raw-syntaxes.png differ
diff --git a/tests/ref/raw-tab-size.png b/tests/ref/raw-tab-size.png
new file mode 100644
index 0000000000..132a10b32a
Binary files /dev/null and b/tests/ref/raw-tab-size.png differ
diff --git a/tests/ref/raw-theme.png b/tests/ref/raw-theme.png
new file mode 100644
index 0000000000..78561ac660
Binary files /dev/null and b/tests/ref/raw-theme.png differ
diff --git a/tests/ref/raw-trimming.png b/tests/ref/raw-trimming.png
new file mode 100644
index 0000000000..58d90b7fed
Binary files /dev/null and b/tests/ref/raw-trimming.png differ
diff --git a/tests/ref/raw-typst-lang.png b/tests/ref/raw-typst-lang.png
new file mode 100644
index 0000000000..3dcafafbdd
Binary files /dev/null and b/tests/ref/raw-typst-lang.png differ
diff --git a/tests/ref/rect-customization.png b/tests/ref/rect-customization.png
new file mode 100644
index 0000000000..93808920c6
Binary files /dev/null and b/tests/ref/rect-customization.png differ
diff --git a/tests/ref/rect-fill-stroke.png b/tests/ref/rect-fill-stroke.png
new file mode 100644
index 0000000000..28a47c122e
Binary files /dev/null and b/tests/ref/rect-fill-stroke.png differ
diff --git a/tests/ref/rect-stroke.png b/tests/ref/rect-stroke.png
new file mode 100644
index 0000000000..7d59c049bb
Binary files /dev/null and b/tests/ref/rect-stroke.png differ
diff --git a/tests/ref/rect.png b/tests/ref/rect.png
new file mode 100644
index 0000000000..04e435ed4c
Binary files /dev/null and b/tests/ref/rect.png differ
diff --git a/tests/ref/ref-basic.png b/tests/ref/ref-basic.png
new file mode 100644
index 0000000000..94d9478940
Binary files /dev/null and b/tests/ref/ref-basic.png differ
diff --git a/tests/ref/ref-supplements.png b/tests/ref/ref-supplements.png
new file mode 100644
index 0000000000..46d1524a1c
Binary files /dev/null and b/tests/ref/ref-supplements.png differ
diff --git a/tests/ref/repeat-align-and-dir.png b/tests/ref/repeat-align-and-dir.png
new file mode 100644
index 0000000000..16797d04bb
Binary files /dev/null and b/tests/ref/repeat-align-and-dir.png differ
diff --git a/tests/ref/repeat-basic.png b/tests/ref/repeat-basic.png
new file mode 100644
index 0000000000..61e7f50f20
Binary files /dev/null and b/tests/ref/repeat-basic.png differ
diff --git a/tests/ref/repeat-dots-rtl.png b/tests/ref/repeat-dots-rtl.png
new file mode 100644
index 0000000000..a0f1a9192b
Binary files /dev/null and b/tests/ref/repeat-dots-rtl.png differ
diff --git a/tests/ref/repeat-empty.png b/tests/ref/repeat-empty.png
new file mode 100644
index 0000000000..c23d7fa4d3
Binary files /dev/null and b/tests/ref/repeat-empty.png differ
diff --git a/tests/ref/repeat-unboxed.png b/tests/ref/repeat-unboxed.png
new file mode 100644
index 0000000000..91678ceaaf
Binary files /dev/null and b/tests/ref/repeat-unboxed.png differ
diff --git a/tests/ref/repr-color.png b/tests/ref/repr-color.png
new file mode 100644
index 0000000000..3425f7d4de
Binary files /dev/null and b/tests/ref/repr-color.png differ
diff --git a/tests/ref/repr-literals.png b/tests/ref/repr-literals.png
new file mode 100644
index 0000000000..1e8e85a410
Binary files /dev/null and b/tests/ref/repr-literals.png differ
diff --git a/tests/ref/repr-misc.png b/tests/ref/repr-misc.png
new file mode 100644
index 0000000000..f4ac7c5fd8
Binary files /dev/null and b/tests/ref/repr-misc.png differ
diff --git a/tests/ref/repr-numerical.png b/tests/ref/repr-numerical.png
new file mode 100644
index 0000000000..f1a6fe8e52
Binary files /dev/null and b/tests/ref/repr-numerical.png differ
diff --git a/tests/ref/return-in-nested-content-block.png b/tests/ref/return-in-nested-content-block.png
new file mode 100644
index 0000000000..d688741c23
Binary files /dev/null and b/tests/ref/return-in-nested-content-block.png differ
diff --git a/tests/ref/set-if.png b/tests/ref/set-if.png
new file mode 100644
index 0000000000..08dc5e8263
Binary files /dev/null and b/tests/ref/set-if.png differ
diff --git a/tests/ref/set-instantiation-site-markup.png b/tests/ref/set-instantiation-site-markup.png
new file mode 100644
index 0000000000..180444b99a
Binary files /dev/null and b/tests/ref/set-instantiation-site-markup.png differ
diff --git a/tests/ref/set-instantiation-site.png b/tests/ref/set-instantiation-site.png
new file mode 100644
index 0000000000..593d3e2df3
Binary files /dev/null and b/tests/ref/set-instantiation-site.png differ
diff --git a/tests/ref/set-scoped-in-code-block.png b/tests/ref/set-scoped-in-code-block.png
new file mode 100644
index 0000000000..8941f6c4a4
Binary files /dev/null and b/tests/ref/set-scoped-in-code-block.png differ
diff --git a/tests/ref/set-text-override.png b/tests/ref/set-text-override.png
new file mode 100644
index 0000000000..8362387609
Binary files /dev/null and b/tests/ref/set-text-override.png differ
diff --git a/tests/ref/set-vs-construct-1.png b/tests/ref/set-vs-construct-1.png
new file mode 100644
index 0000000000..597e967495
Binary files /dev/null and b/tests/ref/set-vs-construct-1.png differ
diff --git a/tests/ref/set-vs-construct-2.png b/tests/ref/set-vs-construct-2.png
new file mode 100644
index 0000000000..2fedd0b427
Binary files /dev/null and b/tests/ref/set-vs-construct-2.png differ
diff --git a/tests/ref/set-vs-construct-3.png b/tests/ref/set-vs-construct-3.png
new file mode 100644
index 0000000000..dff0c8af6b
Binary files /dev/null and b/tests/ref/set-vs-construct-3.png differ
diff --git a/tests/ref/set-vs-construct-4.png b/tests/ref/set-vs-construct-4.png
new file mode 100644
index 0000000000..1f6834ef5d
Binary files /dev/null and b/tests/ref/set-vs-construct-4.png differ
diff --git a/tests/ref/shaping-emoji-bad-zwj.png b/tests/ref/shaping-emoji-bad-zwj.png
new file mode 100644
index 0000000000..544d64eea8
Binary files /dev/null and b/tests/ref/shaping-emoji-bad-zwj.png differ
diff --git a/tests/ref/shaping-emoji-basic.png b/tests/ref/shaping-emoji-basic.png
new file mode 100644
index 0000000000..090ea61118
Binary files /dev/null and b/tests/ref/shaping-emoji-basic.png differ
diff --git a/tests/ref/shaping-font-fallback.png b/tests/ref/shaping-font-fallback.png
new file mode 100644
index 0000000000..813e39151d
Binary files /dev/null and b/tests/ref/shaping-font-fallback.png differ
diff --git a/tests/ref/shaping-forced-script-font-feature-enabled.png b/tests/ref/shaping-forced-script-font-feature-enabled.png
new file mode 100644
index 0000000000..0a10087a3c
Binary files /dev/null and b/tests/ref/shaping-forced-script-font-feature-enabled.png differ
diff --git a/tests/ref/shaping-forced-script-font-feature-inhibited.png b/tests/ref/shaping-forced-script-font-feature-inhibited.png
new file mode 100644
index 0000000000..77d8010ea8
Binary files /dev/null and b/tests/ref/shaping-forced-script-font-feature-inhibited.png differ
diff --git a/tests/ref/shaping-script-separation.png b/tests/ref/shaping-script-separation.png
new file mode 100644
index 0000000000..68170dd998
Binary files /dev/null and b/tests/ref/shaping-script-separation.png differ
diff --git a/tests/ref/shorthand-dashes.png b/tests/ref/shorthand-dashes.png
new file mode 100644
index 0000000000..f8b4191fef
Binary files /dev/null and b/tests/ref/shorthand-dashes.png differ
diff --git a/tests/ref/shorthand-ellipsis.png b/tests/ref/shorthand-ellipsis.png
new file mode 100644
index 0000000000..df9a9241ed
Binary files /dev/null and b/tests/ref/shorthand-ellipsis.png differ
diff --git a/tests/ref/shorthand-nbsp-and-shy-hyphen.png b/tests/ref/shorthand-nbsp-and-shy-hyphen.png
new file mode 100644
index 0000000000..e8c81aaa6e
Binary files /dev/null and b/tests/ref/shorthand-nbsp-and-shy-hyphen.png differ
diff --git a/tests/ref/shorthand-nbsp-width.png b/tests/ref/shorthand-nbsp-width.png
new file mode 100644
index 0000000000..a92988cf13
Binary files /dev/null and b/tests/ref/shorthand-nbsp-width.png differ
diff --git a/tests/ref/shorthands-math.png b/tests/ref/shorthands-math.png
new file mode 100644
index 0000000000..0514fa6227
Binary files /dev/null and b/tests/ref/shorthands-math.png differ
diff --git a/tests/ref/show-bare-basic.png b/tests/ref/show-bare-basic.png
new file mode 100644
index 0000000000..e389b50632
Binary files /dev/null and b/tests/ref/show-bare-basic.png differ
diff --git a/tests/ref/show-bare-content-block.png b/tests/ref/show-bare-content-block.png
new file mode 100644
index 0000000000..2631092bb6
Binary files /dev/null and b/tests/ref/show-bare-content-block.png differ
diff --git a/tests/ref/show-bare-replace-with-content.png b/tests/ref/show-bare-replace-with-content.png
new file mode 100644
index 0000000000..51e36a4921
Binary files /dev/null and b/tests/ref/show-bare-replace-with-content.png differ
diff --git a/tests/ref/show-bare-vs-set-text.png b/tests/ref/show-bare-vs-set-text.png
new file mode 100644
index 0000000000..b1e15d98ff
Binary files /dev/null and b/tests/ref/show-bare-vs-set-text.png differ
diff --git a/tests/ref/show-function-order-with-set.png b/tests/ref/show-function-order-with-set.png
new file mode 100644
index 0000000000..a59f727416
Binary files /dev/null and b/tests/ref/show-function-order-with-set.png differ
diff --git a/tests/ref/show-function-set-on-it.png b/tests/ref/show-function-set-on-it.png
new file mode 100644
index 0000000000..6c545e9544
Binary files /dev/null and b/tests/ref/show-function-set-on-it.png differ
diff --git a/tests/ref/show-in-show.png b/tests/ref/show-in-show.png
new file mode 100644
index 0000000000..65280ad7b6
Binary files /dev/null and b/tests/ref/show-in-show.png differ
diff --git a/tests/ref/show-multiple-rules.png b/tests/ref/show-multiple-rules.png
new file mode 100644
index 0000000000..c92b6269ef
Binary files /dev/null and b/tests/ref/show-multiple-rules.png differ
diff --git a/tests/ref/show-nested-scopes.png b/tests/ref/show-nested-scopes.png
new file mode 100644
index 0000000000..ac0a812560
Binary files /dev/null and b/tests/ref/show-nested-scopes.png differ
diff --git a/tests/ref/show-recursive-identity.png b/tests/ref/show-recursive-identity.png
new file mode 100644
index 0000000000..6c545e9544
Binary files /dev/null and b/tests/ref/show-recursive-identity.png differ
diff --git a/tests/ref/show-recursive-multiple.png b/tests/ref/show-recursive-multiple.png
new file mode 100644
index 0000000000..b56b089c98
Binary files /dev/null and b/tests/ref/show-recursive-multiple.png differ
diff --git a/tests/ref/show-rule-in-function.png b/tests/ref/show-rule-in-function.png
new file mode 100644
index 0000000000..97aa2845e0
Binary files /dev/null and b/tests/ref/show-rule-in-function.png differ
diff --git a/tests/ref/show-selector-basic.png b/tests/ref/show-selector-basic.png
new file mode 100644
index 0000000000..870166d9e6
Binary files /dev/null and b/tests/ref/show-selector-basic.png differ
diff --git a/tests/ref/show-selector-discard.png b/tests/ref/show-selector-discard.png
new file mode 100644
index 0000000000..13c9f0d655
Binary files /dev/null and b/tests/ref/show-selector-discard.png differ
diff --git a/tests/ref/show-selector-element-or-label.png b/tests/ref/show-selector-element-or-label.png
new file mode 100644
index 0000000000..32cd992d20
Binary files /dev/null and b/tests/ref/show-selector-element-or-label.png differ
diff --git a/tests/ref/show-selector-or-elements-with-set.png b/tests/ref/show-selector-or-elements-with-set.png
new file mode 100644
index 0000000000..f561cad887
Binary files /dev/null and b/tests/ref/show-selector-or-elements-with-set.png differ
diff --git a/tests/ref/show-selector-realistic.png b/tests/ref/show-selector-realistic.png
new file mode 100644
index 0000000000..ae4f4a9a8b
Binary files /dev/null and b/tests/ref/show-selector-realistic.png differ
diff --git a/tests/ref/show-selector-replace-and-show-set.png b/tests/ref/show-selector-replace-and-show-set.png
new file mode 100644
index 0000000000..47a7ae3367
Binary files /dev/null and b/tests/ref/show-selector-replace-and-show-set.png differ
diff --git a/tests/ref/show-selector-replace.png b/tests/ref/show-selector-replace.png
new file mode 100644
index 0000000000..c00a88e8c8
Binary files /dev/null and b/tests/ref/show-selector-replace.png differ
diff --git a/tests/ref/show-selector-where.png b/tests/ref/show-selector-where.png
new file mode 100644
index 0000000000..4cb02efd07
Binary files /dev/null and b/tests/ref/show-selector-where.png differ
diff --git a/tests/ref/show-set-on-layoutable-element.png b/tests/ref/show-set-on-layoutable-element.png
new file mode 100644
index 0000000000..701bea5087
Binary files /dev/null and b/tests/ref/show-set-on-layoutable-element.png differ
diff --git a/tests/ref/show-set-on-same-element.png b/tests/ref/show-set-on-same-element.png
new file mode 100644
index 0000000000..9459fca0c8
Binary files /dev/null and b/tests/ref/show-set-on-same-element.png differ
diff --git a/tests/ref/show-set-override.png b/tests/ref/show-set-override.png
new file mode 100644
index 0000000000..e7831b90a9
Binary files /dev/null and b/tests/ref/show-set-override.png differ
diff --git a/tests/ref/show-set-same-element-and-order.png b/tests/ref/show-set-same-element-and-order.png
new file mode 100644
index 0000000000..d55d5e14fb
Binary files /dev/null and b/tests/ref/show-set-same-element-and-order.png differ
diff --git a/tests/ref/show-set-same-element-matched-field.png b/tests/ref/show-set-same-element-matched-field.png
new file mode 100644
index 0000000000..aa44baee96
Binary files /dev/null and b/tests/ref/show-set-same-element-matched-field.png differ
diff --git a/tests/ref/show-set-same-element-matching-interaction.png b/tests/ref/show-set-same-element-matching-interaction.png
new file mode 100644
index 0000000000..bc06103850
Binary files /dev/null and b/tests/ref/show-set-same-element-matching-interaction.png differ
diff --git a/tests/ref/show-set-same-element-synthesized-matched-field.png b/tests/ref/show-set-same-element-synthesized-matched-field.png
new file mode 100644
index 0000000000..c3918e8f31
Binary files /dev/null and b/tests/ref/show-set-same-element-synthesized-matched-field.png differ
diff --git a/tests/ref/show-set-text-order-adjacent-1.png b/tests/ref/show-set-text-order-adjacent-1.png
new file mode 100644
index 0000000000..1bc95e3b9b
Binary files /dev/null and b/tests/ref/show-set-text-order-adjacent-1.png differ
diff --git a/tests/ref/show-set-text-order-adjacent-2.png b/tests/ref/show-set-text-order-adjacent-2.png
new file mode 100644
index 0000000000..caada91a04
Binary files /dev/null and b/tests/ref/show-set-text-order-adjacent-2.png differ
diff --git a/tests/ref/show-set-text-order-contained-1.png b/tests/ref/show-set-text-order-contained-1.png
new file mode 100644
index 0000000000..8deaaacdf5
Binary files /dev/null and b/tests/ref/show-set-text-order-contained-1.png differ
diff --git a/tests/ref/show-set-text-order-contained-2.png b/tests/ref/show-set-text-order-contained-2.png
new file mode 100644
index 0000000000..00ea3fb83d
Binary files /dev/null and b/tests/ref/show-set-text-order-contained-2.png differ
diff --git a/tests/ref/show-set-text-order-contained-3.png b/tests/ref/show-set-text-order-contained-3.png
new file mode 100644
index 0000000000..1bc95e3b9b
Binary files /dev/null and b/tests/ref/show-set-text-order-contained-3.png differ
diff --git a/tests/ref/show-set-text-order-contained-4.png b/tests/ref/show-set-text-order-contained-4.png
new file mode 100644
index 0000000000..0946f92218
Binary files /dev/null and b/tests/ref/show-set-text-order-contained-4.png differ
diff --git a/tests/ref/show-set-text-order-overlapping-1.png b/tests/ref/show-set-text-order-overlapping-1.png
new file mode 100644
index 0000000000..71222567bf
Binary files /dev/null and b/tests/ref/show-set-text-order-overlapping-1.png differ
diff --git a/tests/ref/show-set-text-order-overlapping-2.png b/tests/ref/show-set-text-order-overlapping-2.png
new file mode 100644
index 0000000000..f1b658f203
Binary files /dev/null and b/tests/ref/show-set-text-order-overlapping-2.png differ
diff --git a/tests/ref/show-set-vs-construct.png b/tests/ref/show-set-vs-construct.png
new file mode 100644
index 0000000000..a0ec96bf68
Binary files /dev/null and b/tests/ref/show-set-vs-construct.png differ
diff --git a/tests/ref/show-set-where-override.png b/tests/ref/show-set-where-override.png
new file mode 100644
index 0000000000..7f1ec60d4c
Binary files /dev/null and b/tests/ref/show-set-where-override.png differ
diff --git a/tests/ref/show-text-basic.png b/tests/ref/show-text-basic.png
new file mode 100644
index 0000000000..29bb5840ce
Binary files /dev/null and b/tests/ref/show-text-basic.png differ
diff --git a/tests/ref/show-text-cyclic-raw.png b/tests/ref/show-text-cyclic-raw.png
new file mode 100644
index 0000000000..b7521c4492
Binary files /dev/null and b/tests/ref/show-text-cyclic-raw.png differ
diff --git a/tests/ref/show-text-cyclic.png b/tests/ref/show-text-cyclic.png
new file mode 100644
index 0000000000..4c4c48868e
Binary files /dev/null and b/tests/ref/show-text-cyclic.png differ
diff --git a/tests/ref/show-text-exactly-once.png b/tests/ref/show-text-exactly-once.png
new file mode 100644
index 0000000000..f681f72160
Binary files /dev/null and b/tests/ref/show-text-exactly-once.png differ
diff --git a/tests/ref/show-text-get-text-on-it.png b/tests/ref/show-text-get-text-on-it.png
new file mode 100644
index 0000000000..5c75b9de9c
Binary files /dev/null and b/tests/ref/show-text-get-text-on-it.png differ
diff --git a/tests/ref/show-text-in-other-show.png b/tests/ref/show-text-in-other-show.png
new file mode 100644
index 0000000000..f29de999b6
Binary files /dev/null and b/tests/ref/show-text-in-other-show.png differ
diff --git a/tests/ref/show-text-indirectly-cyclic.png b/tests/ref/show-text-indirectly-cyclic.png
new file mode 100644
index 0000000000..de166dcaa4
Binary files /dev/null and b/tests/ref/show-text-indirectly-cyclic.png differ
diff --git a/tests/ref/show-text-path-resolving.png b/tests/ref/show-text-path-resolving.png
new file mode 100644
index 0000000000..1a04f9e6e7
Binary files /dev/null and b/tests/ref/show-text-path-resolving.png differ
diff --git a/tests/ref/show-text-regex-case-insensitive.png b/tests/ref/show-text-regex-case-insensitive.png
new file mode 100644
index 0000000000..70d70d343e
Binary files /dev/null and b/tests/ref/show-text-regex-case-insensitive.png differ
diff --git a/tests/ref/show-text-regex-character-class.png b/tests/ref/show-text-regex-character-class.png
new file mode 100644
index 0000000000..946c5d2259
Binary files /dev/null and b/tests/ref/show-text-regex-character-class.png differ
diff --git a/tests/ref/show-text-regex-word-boundary.png b/tests/ref/show-text-regex-word-boundary.png
new file mode 100644
index 0000000000..c171ac0270
Binary files /dev/null and b/tests/ref/show-text-regex-word-boundary.png differ
diff --git a/tests/ref/show-text-regex.png b/tests/ref/show-text-regex.png
new file mode 100644
index 0000000000..85db10a334
Binary files /dev/null and b/tests/ref/show-text-regex.png differ
diff --git a/tests/ref/show-where-folding-stroke.png b/tests/ref/show-where-folding-stroke.png
new file mode 100644
index 0000000000..186ce68103
Binary files /dev/null and b/tests/ref/show-where-folding-stroke.png differ
diff --git a/tests/ref/show-where-folding-text-size.png b/tests/ref/show-where-folding-text-size.png
new file mode 100644
index 0000000000..9fbe3ff980
Binary files /dev/null and b/tests/ref/show-where-folding-text-size.png differ
diff --git a/tests/ref/show-where-optional-field-raw.png b/tests/ref/show-where-optional-field-raw.png
new file mode 100644
index 0000000000..dd3816108a
Binary files /dev/null and b/tests/ref/show-where-optional-field-raw.png differ
diff --git a/tests/ref/show-where-optional-field-text.png b/tests/ref/show-where-optional-field-text.png
new file mode 100644
index 0000000000..b1367d0922
Binary files /dev/null and b/tests/ref/show-where-optional-field-text.png differ
diff --git a/tests/ref/show-where-resolving-hyphenate.png b/tests/ref/show-where-resolving-hyphenate.png
new file mode 100644
index 0000000000..052a2eda5c
Binary files /dev/null and b/tests/ref/show-where-resolving-hyphenate.png differ
diff --git a/tests/ref/show-where-resolving-length.png b/tests/ref/show-where-resolving-length.png
new file mode 100644
index 0000000000..4c77f2acd2
Binary files /dev/null and b/tests/ref/show-where-resolving-length.png differ
diff --git a/tests/ref/smallcaps.png b/tests/ref/smallcaps.png
new file mode 100644
index 0000000000..b5ee12b790
Binary files /dev/null and b/tests/ref/smallcaps.png differ
diff --git a/tests/ref/smartquote-apostrophe.png b/tests/ref/smartquote-apostrophe.png
new file mode 100644
index 0000000000..d2cc1ebf76
Binary files /dev/null and b/tests/ref/smartquote-apostrophe.png differ
diff --git a/tests/ref/smartquote-custom-complex.png b/tests/ref/smartquote-custom-complex.png
new file mode 100644
index 0000000000..7204a997b5
Binary files /dev/null and b/tests/ref/smartquote-custom-complex.png differ
diff --git a/tests/ref/smartquote-custom.png b/tests/ref/smartquote-custom.png
new file mode 100644
index 0000000000..6a6bd9d1a0
Binary files /dev/null and b/tests/ref/smartquote-custom.png differ
diff --git a/tests/ref/smartquote-disable.png b/tests/ref/smartquote-disable.png
new file mode 100644
index 0000000000..0218b7acba
Binary files /dev/null and b/tests/ref/smartquote-disable.png differ
diff --git a/tests/ref/smartquote-disabled-temporarily.png b/tests/ref/smartquote-disabled-temporarily.png
new file mode 100644
index 0000000000..84bc5e32b7
Binary files /dev/null and b/tests/ref/smartquote-disabled-temporarily.png differ
diff --git a/tests/ref/smartquote-empty.png b/tests/ref/smartquote-empty.png
new file mode 100644
index 0000000000..f9f19989cf
Binary files /dev/null and b/tests/ref/smartquote-empty.png differ
diff --git a/tests/ref/smartquote-escape.png b/tests/ref/smartquote-escape.png
new file mode 100644
index 0000000000..45d8f6027e
Binary files /dev/null and b/tests/ref/smartquote-escape.png differ
diff --git a/tests/ref/smartquote-nesting.png b/tests/ref/smartquote-nesting.png
new file mode 100644
index 0000000000..1f38c09758
Binary files /dev/null and b/tests/ref/smartquote-nesting.png differ
diff --git a/tests/ref/smartquote.png b/tests/ref/smartquote.png
new file mode 100644
index 0000000000..070e04876a
Binary files /dev/null and b/tests/ref/smartquote.png differ
diff --git a/tests/ref/space-collapsing-comments.png b/tests/ref/space-collapsing-comments.png
new file mode 100644
index 0000000000..b35d9fec9a
Binary files /dev/null and b/tests/ref/space-collapsing-comments.png differ
diff --git a/tests/ref/space-collapsing-linebreaks.png b/tests/ref/space-collapsing-linebreaks.png
new file mode 100644
index 0000000000..b1f4a3af92
Binary files /dev/null and b/tests/ref/space-collapsing-linebreaks.png differ
diff --git a/tests/ref/space-collapsing-stringy-linebreak.png b/tests/ref/space-collapsing-stringy-linebreak.png
new file mode 100644
index 0000000000..ceec6da7cb
Binary files /dev/null and b/tests/ref/space-collapsing-stringy-linebreak.png differ
diff --git a/tests/ref/space-collapsing-with-h.png b/tests/ref/space-collapsing-with-h.png
new file mode 100644
index 0000000000..c2e253e71b
Binary files /dev/null and b/tests/ref/space-collapsing-with-h.png differ
diff --git a/tests/ref/space-collapsing.png b/tests/ref/space-collapsing.png
new file mode 100644
index 0000000000..32bd6039de
Binary files /dev/null and b/tests/ref/space-collapsing.png differ
diff --git a/tests/ref/space-ideographic-kept.png b/tests/ref/space-ideographic-kept.png
new file mode 100644
index 0000000000..cd292e2da0
Binary files /dev/null and b/tests/ref/space-ideographic-kept.png differ
diff --git a/tests/ref/space-thin-kept.png b/tests/ref/space-thin-kept.png
new file mode 100644
index 0000000000..6ed3504bef
Binary files /dev/null and b/tests/ref/space-thin-kept.png differ
diff --git a/tests/ref/space-trailing-linebreak.png b/tests/ref/space-trailing-linebreak.png
new file mode 100644
index 0000000000..42b2826430
Binary files /dev/null and b/tests/ref/space-trailing-linebreak.png differ
diff --git a/tests/ref/spacing-h-and-v.png b/tests/ref/spacing-h-and-v.png
new file mode 100644
index 0000000000..2c9a296067
Binary files /dev/null and b/tests/ref/spacing-h-and-v.png differ
diff --git a/tests/ref/spacing-rtl.png b/tests/ref/spacing-rtl.png
new file mode 100644
index 0000000000..a9cbbca6e1
Binary files /dev/null and b/tests/ref/spacing-rtl.png differ
diff --git a/tests/ref/square-auto-sized.png b/tests/ref/square-auto-sized.png
new file mode 100644
index 0000000000..a2c4a36e12
Binary files /dev/null and b/tests/ref/square-auto-sized.png differ
diff --git a/tests/ref/square-base.png b/tests/ref/square-base.png
new file mode 100644
index 0000000000..3ef753f237
Binary files /dev/null and b/tests/ref/square-base.png differ
diff --git a/tests/ref/square-circle-alignment.png b/tests/ref/square-circle-alignment.png
new file mode 100644
index 0000000000..3fff9e66f6
Binary files /dev/null and b/tests/ref/square-circle-alignment.png differ
diff --git a/tests/ref/square-circle-overspecified.png b/tests/ref/square-circle-overspecified.png
new file mode 100644
index 0000000000..6dde5e511d
Binary files /dev/null and b/tests/ref/square-circle-overspecified.png differ
diff --git a/tests/ref/square-contents-overflow.png b/tests/ref/square-contents-overflow.png
new file mode 100644
index 0000000000..ae65b0a8c2
Binary files /dev/null and b/tests/ref/square-contents-overflow.png differ
diff --git a/tests/ref/square-height-limited-stack.png b/tests/ref/square-height-limited-stack.png
new file mode 100644
index 0000000000..f52c608d62
Binary files /dev/null and b/tests/ref/square-height-limited-stack.png differ
diff --git a/tests/ref/square-height-limited.png b/tests/ref/square-height-limited.png
new file mode 100644
index 0000000000..c01dc426a6
Binary files /dev/null and b/tests/ref/square-height-limited.png differ
diff --git a/tests/ref/square-overflow.png b/tests/ref/square-overflow.png
new file mode 100644
index 0000000000..6169f305ba
Binary files /dev/null and b/tests/ref/square-overflow.png differ
diff --git a/tests/ref/square-rect-rounded.png b/tests/ref/square-rect-rounded.png
new file mode 100644
index 0000000000..678ba819b1
Binary files /dev/null and b/tests/ref/square-rect-rounded.png differ
diff --git a/tests/ref/square-relative-size.png b/tests/ref/square-relative-size.png
new file mode 100644
index 0000000000..96e744e6b7
Binary files /dev/null and b/tests/ref/square-relative-size.png differ
diff --git a/tests/ref/square-relatively-sized-child.png b/tests/ref/square-relatively-sized-child.png
new file mode 100644
index 0000000000..3ffe310571
Binary files /dev/null and b/tests/ref/square-relatively-sized-child.png differ
diff --git a/tests/ref/square.png b/tests/ref/square.png
new file mode 100644
index 0000000000..e6f8f5c8d3
Binary files /dev/null and b/tests/ref/square.png differ
diff --git a/tests/ref/stack-basic.png b/tests/ref/stack-basic.png
new file mode 100644
index 0000000000..b5f38a833b
Binary files /dev/null and b/tests/ref/stack-basic.png differ
diff --git a/tests/ref/stack-fr.png b/tests/ref/stack-fr.png
new file mode 100644
index 0000000000..e34dd9b11a
Binary files /dev/null and b/tests/ref/stack-fr.png differ
diff --git a/tests/ref/stack-overflow.png b/tests/ref/stack-overflow.png
new file mode 100644
index 0000000000..43b3625afb
Binary files /dev/null and b/tests/ref/stack-overflow.png differ
diff --git a/tests/ref/stack-rtl-align-and-fr.png b/tests/ref/stack-rtl-align-and-fr.png
new file mode 100644
index 0000000000..653ade6fda
Binary files /dev/null and b/tests/ref/stack-rtl-align-and-fr.png differ
diff --git a/tests/ref/stack-spacing.png b/tests/ref/stack-spacing.png
new file mode 100644
index 0000000000..9667f65770
Binary files /dev/null and b/tests/ref/stack-spacing.png differ
diff --git a/tests/ref/state-basic.png b/tests/ref/state-basic.png
new file mode 100644
index 0000000000..0c67a751db
Binary files /dev/null and b/tests/ref/state-basic.png differ
diff --git a/tests/ref/state-multiple-calls-same-key.png b/tests/ref/state-multiple-calls-same-key.png
new file mode 100644
index 0000000000..077b67929d
Binary files /dev/null and b/tests/ref/state-multiple-calls-same-key.png differ
diff --git a/tests/ref/state-nested.png b/tests/ref/state-nested.png
new file mode 100644
index 0000000000..cc7016001e
Binary files /dev/null and b/tests/ref/state-nested.png differ
diff --git a/tests/ref/state-no-convergence.png b/tests/ref/state-no-convergence.png
new file mode 100644
index 0000000000..dd44b9e179
Binary files /dev/null and b/tests/ref/state-no-convergence.png differ
diff --git a/tests/ref/strike-background.png b/tests/ref/strike-background.png
new file mode 100644
index 0000000000..01861d255a
Binary files /dev/null and b/tests/ref/strike-background.png differ
diff --git a/tests/ref/strike-with.png b/tests/ref/strike-with.png
new file mode 100644
index 0000000000..59a84150ed
Binary files /dev/null and b/tests/ref/strike-with.png differ
diff --git a/tests/ref/stroke-composition.png b/tests/ref/stroke-composition.png
new file mode 100644
index 0000000000..a6c7ce70fe
Binary files /dev/null and b/tests/ref/stroke-composition.png differ
diff --git a/tests/ref/stroke-folding.png b/tests/ref/stroke-folding.png
new file mode 100644
index 0000000000..b4f1b1a9a3
Binary files /dev/null and b/tests/ref/stroke-folding.png differ
diff --git a/tests/ref/stroke-text.png b/tests/ref/stroke-text.png
new file mode 100644
index 0000000000..ac09053a99
Binary files /dev/null and b/tests/ref/stroke-text.png differ
diff --git a/tests/ref/stroke-zero-thickness.png b/tests/ref/stroke-zero-thickness.png
new file mode 100644
index 0000000000..6d305eaf40
Binary files /dev/null and b/tests/ref/stroke-zero-thickness.png differ
diff --git a/tests/ref/strong-delta.png b/tests/ref/strong-delta.png
new file mode 100644
index 0000000000..d32459f60d
Binary files /dev/null and b/tests/ref/strong-delta.png differ
diff --git a/tests/ref/strong-double-star-empty-hint.png b/tests/ref/strong-double-star-empty-hint.png
new file mode 100644
index 0000000000..29cbb90f4e
Binary files /dev/null and b/tests/ref/strong-double-star-empty-hint.png differ
diff --git a/tests/ref/sub-super-non-typographic.png b/tests/ref/sub-super-non-typographic.png
new file mode 100644
index 0000000000..e5a8b67330
Binary files /dev/null and b/tests/ref/sub-super-non-typographic.png differ
diff --git a/tests/ref/sub-super.png b/tests/ref/sub-super.png
new file mode 100644
index 0000000000..9359cf015c
Binary files /dev/null and b/tests/ref/sub-super.png differ
diff --git a/tests/ref/super-underline.png b/tests/ref/super-underline.png
new file mode 100644
index 0000000000..99c1c30908
Binary files /dev/null and b/tests/ref/super-underline.png differ
diff --git a/tests/ref/symbol-constructor.png b/tests/ref/symbol-constructor.png
new file mode 100644
index 0000000000..e6db9491d1
Binary files /dev/null and b/tests/ref/symbol-constructor.png differ
diff --git a/tests/ref/symbol.png b/tests/ref/symbol.png
new file mode 100644
index 0000000000..37339d591a
Binary files /dev/null and b/tests/ref/symbol.png differ
diff --git a/tests/ref/table-align-array.png b/tests/ref/table-align-array.png
new file mode 100644
index 0000000000..9242ae1208
Binary files /dev/null and b/tests/ref/table-align-array.png differ
diff --git a/tests/ref/table-cell-align-override.png b/tests/ref/table-cell-align-override.png
new file mode 100644
index 0000000000..dfab2bb038
Binary files /dev/null and b/tests/ref/table-cell-align-override.png differ
diff --git a/tests/ref/table-cell-folding.png b/tests/ref/table-cell-folding.png
new file mode 100644
index 0000000000..94897a9278
Binary files /dev/null and b/tests/ref/table-cell-folding.png differ
diff --git a/tests/ref/table-cell-override.png b/tests/ref/table-cell-override.png
new file mode 100644
index 0000000000..d6f37d632f
Binary files /dev/null and b/tests/ref/table-cell-override.png differ
diff --git a/tests/ref/table-cell-set.png b/tests/ref/table-cell-set.png
new file mode 100644
index 0000000000..ef720ab36c
Binary files /dev/null and b/tests/ref/table-cell-set.png differ
diff --git a/tests/ref/table-cell-show-and-override.png b/tests/ref/table-cell-show-and-override.png
new file mode 100644
index 0000000000..df74580223
Binary files /dev/null and b/tests/ref/table-cell-show-and-override.png differ
diff --git a/tests/ref/table-cell-show-based-on-position.png b/tests/ref/table-cell-show-based-on-position.png
new file mode 100644
index 0000000000..db46e2600d
Binary files /dev/null and b/tests/ref/table-cell-show-based-on-position.png differ
diff --git a/tests/ref/table-cell-show-emph.png b/tests/ref/table-cell-show-emph.png
new file mode 100644
index 0000000000..1afc833ba0
Binary files /dev/null and b/tests/ref/table-cell-show-emph.png differ
diff --git a/tests/ref/table-cell-show.png b/tests/ref/table-cell-show.png
new file mode 100644
index 0000000000..9ac6d2695d
Binary files /dev/null and b/tests/ref/table-cell-show.png differ
diff --git a/tests/ref/table-cell-various-overrides.png b/tests/ref/table-cell-various-overrides.png
new file mode 100644
index 0000000000..c8540dfed1
Binary files /dev/null and b/tests/ref/table-cell-various-overrides.png differ
diff --git a/tests/ref/table-fill-basic.png b/tests/ref/table-fill-basic.png
new file mode 100644
index 0000000000..bc12f8ae8d
Binary files /dev/null and b/tests/ref/table-fill-basic.png differ
diff --git a/tests/ref/table-gutters.png b/tests/ref/table-gutters.png
new file mode 100644
index 0000000000..697ddd488c
Binary files /dev/null and b/tests/ref/table-gutters.png differ
diff --git a/tests/ref/table-inset-fold.png b/tests/ref/table-inset-fold.png
new file mode 100644
index 0000000000..f2985c9ed8
Binary files /dev/null and b/tests/ref/table-inset-fold.png differ
diff --git a/tests/ref/table-inset.png b/tests/ref/table-inset.png
new file mode 100644
index 0000000000..a8a9adda62
Binary files /dev/null and b/tests/ref/table-inset.png differ
diff --git a/tests/ref/table-newlines.png b/tests/ref/table-newlines.png
new file mode 100644
index 0000000000..a4da25f3d8
Binary files /dev/null and b/tests/ref/table-newlines.png differ
diff --git a/tests/ref/table-stroke-vline-position-left-and-right.png b/tests/ref/table-stroke-vline-position-left-and-right.png
new file mode 100644
index 0000000000..53b48a101a
Binary files /dev/null and b/tests/ref/table-stroke-vline-position-left-and-right.png differ
diff --git a/tests/ref/terms-built-in-loop.png b/tests/ref/terms-built-in-loop.png
new file mode 100644
index 0000000000..dc103af92d
Binary files /dev/null and b/tests/ref/terms-built-in-loop.png differ
diff --git a/tests/ref/terms-constructor.png b/tests/ref/terms-constructor.png
new file mode 100644
index 0000000000..fe16150587
Binary files /dev/null and b/tests/ref/terms-constructor.png differ
diff --git a/tests/ref/terms-grid.png b/tests/ref/terms-grid.png
new file mode 100644
index 0000000000..6142becfc4
Binary files /dev/null and b/tests/ref/terms-grid.png differ
diff --git a/tests/ref/terms-multiline.png b/tests/ref/terms-multiline.png
new file mode 100644
index 0000000000..b5baea4add
Binary files /dev/null and b/tests/ref/terms-multiline.png differ
diff --git a/tests/ref/terms-rtl.png b/tests/ref/terms-rtl.png
new file mode 100644
index 0000000000..538571ddbb
Binary files /dev/null and b/tests/ref/terms-rtl.png differ
diff --git a/tests/ref/terms-style-change-interrupted.png b/tests/ref/terms-style-change-interrupted.png
new file mode 100644
index 0000000000..846e45e14e
Binary files /dev/null and b/tests/ref/terms-style-change-interrupted.png differ
diff --git a/tests/ref/terms-syntax-edge-cases.png b/tests/ref/terms-syntax-edge-cases.png
new file mode 100644
index 0000000000..e2a557c1aa
Binary files /dev/null and b/tests/ref/terms-syntax-edge-cases.png differ
diff --git a/tests/ref/text-alternates-and-stylistic-sets.png b/tests/ref/text-alternates-and-stylistic-sets.png
new file mode 100644
index 0000000000..877542fc23
Binary files /dev/null and b/tests/ref/text-alternates-and-stylistic-sets.png differ
diff --git a/tests/ref/text-call-body.png b/tests/ref/text-call-body.png
new file mode 100644
index 0000000000..24cdeb9f90
Binary files /dev/null and b/tests/ref/text-call-body.png differ
diff --git a/tests/ref/text-chinese-basic.png b/tests/ref/text-chinese-basic.png
new file mode 100644
index 0000000000..ea4a0b829a
Binary files /dev/null and b/tests/ref/text-chinese-basic.png differ
diff --git a/tests/ref/text-cjk-latin-spacing.png b/tests/ref/text-cjk-latin-spacing.png
new file mode 100644
index 0000000000..1906bf761e
Binary files /dev/null and b/tests/ref/text-cjk-latin-spacing.png differ
diff --git a/tests/ref/text-copy-paste-ligatures.png b/tests/ref/text-copy-paste-ligatures.png
new file mode 100644
index 0000000000..f0f36a8691
Binary files /dev/null and b/tests/ref/text-copy-paste-ligatures.png differ
diff --git a/tests/ref/text-edge.png b/tests/ref/text-edge.png
new file mode 100644
index 0000000000..0953ededf8
Binary files /dev/null and b/tests/ref/text-edge.png differ
diff --git a/tests/ref/text-features.png b/tests/ref/text-features.png
new file mode 100644
index 0000000000..7b0b391f77
Binary files /dev/null and b/tests/ref/text-features.png differ
diff --git a/tests/ref/text-font-change-after-space.png b/tests/ref/text-font-change-after-space.png
new file mode 100644
index 0000000000..83d2ceb621
Binary files /dev/null and b/tests/ref/text-font-change-after-space.png differ
diff --git a/tests/ref/text-font-just-a-space.png b/tests/ref/text-font-just-a-space.png
new file mode 100644
index 0000000000..3c91db3ce0
Binary files /dev/null and b/tests/ref/text-font-just-a-space.png differ
diff --git a/tests/ref/text-font-properties.png b/tests/ref/text-font-properties.png
new file mode 100644
index 0000000000..3c65fa33c2
Binary files /dev/null and b/tests/ref/text-font-properties.png differ
diff --git a/tests/ref/text-kerning.png b/tests/ref/text-kerning.png
new file mode 100644
index 0000000000..1bd3a00122
Binary files /dev/null and b/tests/ref/text-kerning.png differ
diff --git a/tests/ref/text-lang-hyphenate.png b/tests/ref/text-lang-hyphenate.png
new file mode 100644
index 0000000000..6315d6e217
Binary files /dev/null and b/tests/ref/text-lang-hyphenate.png differ
diff --git a/tests/ref/text-lang-region.png b/tests/ref/text-lang-region.png
new file mode 100644
index 0000000000..a273657845
Binary files /dev/null and b/tests/ref/text-lang-region.png differ
diff --git a/tests/ref/text-lang-script-shaping.png b/tests/ref/text-lang-script-shaping.png
new file mode 100644
index 0000000000..6beaece405
Binary files /dev/null and b/tests/ref/text-lang-script-shaping.png differ
diff --git a/tests/ref/text-lang-shaping.png b/tests/ref/text-lang-shaping.png
new file mode 100644
index 0000000000..b892fcd5dc
Binary files /dev/null and b/tests/ref/text-lang-shaping.png differ
diff --git a/tests/ref/text-lang-unknown-region.png b/tests/ref/text-lang-unknown-region.png
new file mode 100644
index 0000000000..de63013eca
Binary files /dev/null and b/tests/ref/text-lang-unknown-region.png differ
diff --git a/tests/ref/text-lang.png b/tests/ref/text-lang.png
new file mode 100644
index 0000000000..de63013eca
Binary files /dev/null and b/tests/ref/text-lang.png differ
diff --git a/tests/ref/text-ligatures.png b/tests/ref/text-ligatures.png
new file mode 100644
index 0000000000..6f0e286c69
Binary files /dev/null and b/tests/ref/text-ligatures.png differ
diff --git a/tests/ref/text-number-type.png b/tests/ref/text-number-type.png
new file mode 100644
index 0000000000..beb6ba6c48
Binary files /dev/null and b/tests/ref/text-number-type.png differ
diff --git a/tests/ref/text-number-width.png b/tests/ref/text-number-width.png
new file mode 100644
index 0000000000..62d8c61b24
Binary files /dev/null and b/tests/ref/text-number-width.png differ
diff --git a/tests/ref/text-size-em-nesting.png b/tests/ref/text-size-em-nesting.png
new file mode 100644
index 0000000000..34ae35fe1a
Binary files /dev/null and b/tests/ref/text-size-em-nesting.png differ
diff --git a/tests/ref/text-size-em.png b/tests/ref/text-size-em.png
new file mode 100644
index 0000000000..944bdd2987
Binary files /dev/null and b/tests/ref/text-size-em.png differ
diff --git a/tests/ref/text-slashed-zero-and-fractions.png b/tests/ref/text-slashed-zero-and-fractions.png
new file mode 100644
index 0000000000..a25ca02326
Binary files /dev/null and b/tests/ref/text-slashed-zero-and-fractions.png differ
diff --git a/tests/ref/text-spacing-relative.png b/tests/ref/text-spacing-relative.png
new file mode 100644
index 0000000000..ccd2f14057
Binary files /dev/null and b/tests/ref/text-spacing-relative.png differ
diff --git a/tests/ref/text-spacing.png b/tests/ref/text-spacing.png
new file mode 100644
index 0000000000..240c69c077
Binary files /dev/null and b/tests/ref/text-spacing.png differ
diff --git a/tests/ref/text-tracking-arabic.png b/tests/ref/text-tracking-arabic.png
new file mode 100644
index 0000000000..a4e450ff65
Binary files /dev/null and b/tests/ref/text-tracking-arabic.png differ
diff --git a/tests/ref/text-tracking-changed-temporarily.png b/tests/ref/text-tracking-changed-temporarily.png
new file mode 100644
index 0000000000..f27849b43e
Binary files /dev/null and b/tests/ref/text-tracking-changed-temporarily.png differ
diff --git a/tests/ref/text-tracking-mark-placement.png b/tests/ref/text-tracking-mark-placement.png
new file mode 100644
index 0000000000..7fc8bb19eb
Binary files /dev/null and b/tests/ref/text-tracking-mark-placement.png differ
diff --git a/tests/ref/text-tracking-negative.png b/tests/ref/text-tracking-negative.png
new file mode 100644
index 0000000000..9658988780
Binary files /dev/null and b/tests/ref/text-tracking-negative.png differ
diff --git a/tests/ref/text/baseline.png b/tests/ref/text/baseline.png
deleted file mode 100644
index dcd6eb121b..0000000000
Binary files a/tests/ref/text/baseline.png and /dev/null differ
diff --git a/tests/ref/text/chinese.png b/tests/ref/text/chinese.png
deleted file mode 100644
index 0c3ddd0024..0000000000
Binary files a/tests/ref/text/chinese.png and /dev/null differ
diff --git a/tests/ref/text/copy-paste.png b/tests/ref/text/copy-paste.png
deleted file mode 100644
index ae4a5ad992..0000000000
Binary files a/tests/ref/text/copy-paste.png and /dev/null differ
diff --git a/tests/ref/text/deco.png b/tests/ref/text/deco.png
deleted file mode 100644
index 3a11e72f27..0000000000
Binary files a/tests/ref/text/deco.png and /dev/null differ
diff --git a/tests/ref/text/edge.png b/tests/ref/text/edge.png
deleted file mode 100644
index 1daf4c2fc7..0000000000
Binary files a/tests/ref/text/edge.png and /dev/null differ
diff --git a/tests/ref/text/em.png b/tests/ref/text/em.png
deleted file mode 100644
index 04cccd5303..0000000000
Binary files a/tests/ref/text/em.png and /dev/null differ
diff --git a/tests/ref/text/emoji.png b/tests/ref/text/emoji.png
deleted file mode 100644
index 1dbbba79aa..0000000000
Binary files a/tests/ref/text/emoji.png and /dev/null differ
diff --git a/tests/ref/text/emphasis.png b/tests/ref/text/emphasis.png
deleted file mode 100644
index c19f6ebb01..0000000000
Binary files a/tests/ref/text/emphasis.png and /dev/null differ
diff --git a/tests/ref/text/escape.png b/tests/ref/text/escape.png
deleted file mode 100644
index c94bc52f91..0000000000
Binary files a/tests/ref/text/escape.png and /dev/null differ
diff --git a/tests/ref/text/fallback.png b/tests/ref/text/fallback.png
deleted file mode 100644
index 7f1e3e385a..0000000000
Binary files a/tests/ref/text/fallback.png and /dev/null differ
diff --git a/tests/ref/text/features.png b/tests/ref/text/features.png
deleted file mode 100644
index 566694c68c..0000000000
Binary files a/tests/ref/text/features.png and /dev/null differ
diff --git a/tests/ref/text/font.png b/tests/ref/text/font.png
deleted file mode 100644
index 39c8a951bd..0000000000
Binary files a/tests/ref/text/font.png and /dev/null differ
diff --git a/tests/ref/text/hyphenate.png b/tests/ref/text/hyphenate.png
deleted file mode 100644
index 7b386a5127..0000000000
Binary files a/tests/ref/text/hyphenate.png and /dev/null differ
diff --git a/tests/ref/text/lang-with-region.png b/tests/ref/text/lang-with-region.png
deleted file mode 100644
index c7753104ad..0000000000
Binary files a/tests/ref/text/lang-with-region.png and /dev/null differ
diff --git a/tests/ref/text/lang.png b/tests/ref/text/lang.png
deleted file mode 100644
index a5ae89796a..0000000000
Binary files a/tests/ref/text/lang.png and /dev/null differ
diff --git a/tests/ref/text/linebreak-link.png b/tests/ref/text/linebreak-link.png
deleted file mode 100644
index ffe39caa39..0000000000
Binary files a/tests/ref/text/linebreak-link.png and /dev/null differ
diff --git a/tests/ref/text/linebreak-obj.png b/tests/ref/text/linebreak-obj.png
deleted file mode 100644
index 127ee6872a..0000000000
Binary files a/tests/ref/text/linebreak-obj.png and /dev/null differ
diff --git a/tests/ref/text/linebreak.png b/tests/ref/text/linebreak.png
deleted file mode 100644
index 3dd2fc156a..0000000000
Binary files a/tests/ref/text/linebreak.png and /dev/null differ
diff --git a/tests/ref/text/lorem.png b/tests/ref/text/lorem.png
deleted file mode 100644
index 9d55df22e6..0000000000
Binary files a/tests/ref/text/lorem.png and /dev/null differ
diff --git a/tests/ref/text/microtype.png b/tests/ref/text/microtype.png
deleted file mode 100644
index 87622b0f8f..0000000000
Binary files a/tests/ref/text/microtype.png and /dev/null differ
diff --git a/tests/ref/text/numbers.png b/tests/ref/text/numbers.png
deleted file mode 100644
index 9fc76aae22..0000000000
Binary files a/tests/ref/text/numbers.png and /dev/null differ
diff --git a/tests/ref/text/quote-nesting.png b/tests/ref/text/quote-nesting.png
deleted file mode 100644
index fb16002de9..0000000000
Binary files a/tests/ref/text/quote-nesting.png and /dev/null differ
diff --git a/tests/ref/text/quote.png b/tests/ref/text/quote.png
deleted file mode 100644
index 653f2d17aa..0000000000
Binary files a/tests/ref/text/quote.png and /dev/null differ
diff --git a/tests/ref/text/quotes.png b/tests/ref/text/quotes.png
deleted file mode 100644
index 535c28297f..0000000000
Binary files a/tests/ref/text/quotes.png and /dev/null differ
diff --git a/tests/ref/text/raw-align.png b/tests/ref/text/raw-align.png
deleted file mode 100644
index 6d1044f7e7..0000000000
Binary files a/tests/ref/text/raw-align.png and /dev/null differ
diff --git a/tests/ref/text/raw-code.png b/tests/ref/text/raw-code.png
deleted file mode 100644
index d3373f5f76..0000000000
Binary files a/tests/ref/text/raw-code.png and /dev/null differ
diff --git a/tests/ref/text/raw-line.png b/tests/ref/text/raw-line.png
deleted file mode 100644
index b76eb8087c..0000000000
Binary files a/tests/ref/text/raw-line.png and /dev/null differ
diff --git a/tests/ref/text/raw-syntaxes.png b/tests/ref/text/raw-syntaxes.png
deleted file mode 100644
index ada751e09b..0000000000
Binary files a/tests/ref/text/raw-syntaxes.png and /dev/null differ
diff --git a/tests/ref/text/raw-tabs.png b/tests/ref/text/raw-tabs.png
deleted file mode 100644
index cac265e982..0000000000
Binary files a/tests/ref/text/raw-tabs.png and /dev/null differ
diff --git a/tests/ref/text/raw-theme.png b/tests/ref/text/raw-theme.png
deleted file mode 100644
index 0ce1776059..0000000000
Binary files a/tests/ref/text/raw-theme.png and /dev/null differ
diff --git a/tests/ref/text/raw.png b/tests/ref/text/raw.png
deleted file mode 100644
index 27120d746b..0000000000
Binary files a/tests/ref/text/raw.png and /dev/null differ
diff --git a/tests/ref/text/shaping.png b/tests/ref/text/shaping.png
deleted file mode 100644
index 69cba132d4..0000000000
Binary files a/tests/ref/text/shaping.png and /dev/null differ
diff --git a/tests/ref/text/shift.png b/tests/ref/text/shift.png
deleted file mode 100644
index 09d68bacd5..0000000000
Binary files a/tests/ref/text/shift.png and /dev/null differ
diff --git a/tests/ref/text/smartquotes.png b/tests/ref/text/smartquotes.png
deleted file mode 100644
index a6a8cbb564..0000000000
Binary files a/tests/ref/text/smartquotes.png and /dev/null differ
diff --git a/tests/ref/text/space.png b/tests/ref/text/space.png
deleted file mode 100644
index bae0e0a833..0000000000
Binary files a/tests/ref/text/space.png and /dev/null differ
diff --git a/tests/ref/text/stroke.png b/tests/ref/text/stroke.png
deleted file mode 100644
index d6d85c28e7..0000000000
Binary files a/tests/ref/text/stroke.png and /dev/null differ
diff --git a/tests/ref/text/symbol.png b/tests/ref/text/symbol.png
deleted file mode 100644
index 04d9d77f1c..0000000000
Binary files a/tests/ref/text/symbol.png and /dev/null differ
diff --git a/tests/ref/text/tracking-spacing.png b/tests/ref/text/tracking-spacing.png
deleted file mode 100644
index 68d802130f..0000000000
Binary files a/tests/ref/text/tracking-spacing.png and /dev/null differ
diff --git a/tests/ref/transform-rotate-and-scale.png b/tests/ref/transform-rotate-and-scale.png
new file mode 100644
index 0000000000..0dcf67ed2e
Binary files /dev/null and b/tests/ref/transform-rotate-and-scale.png differ
diff --git a/tests/ref/transform-rotate-origin.png b/tests/ref/transform-rotate-origin.png
new file mode 100644
index 0000000000..152b1e1f82
Binary files /dev/null and b/tests/ref/transform-rotate-origin.png differ
diff --git a/tests/ref/transform-rotate-relative-sizing.png b/tests/ref/transform-rotate-relative-sizing.png
new file mode 100644
index 0000000000..9b81c3865d
Binary files /dev/null and b/tests/ref/transform-rotate-relative-sizing.png differ
diff --git a/tests/ref/transform-rotate.png b/tests/ref/transform-rotate.png
new file mode 100644
index 0000000000..3990ed5b8e
Binary files /dev/null and b/tests/ref/transform-rotate.png differ
diff --git a/tests/ref/transform-scale-origin.png b/tests/ref/transform-scale-origin.png
new file mode 100644
index 0000000000..10e1cfe280
Binary files /dev/null and b/tests/ref/transform-scale-origin.png differ
diff --git a/tests/ref/transform-scale-relative-sizing.png b/tests/ref/transform-scale-relative-sizing.png
new file mode 100644
index 0000000000..d10bd3ff4f
Binary files /dev/null and b/tests/ref/transform-scale-relative-sizing.png differ
diff --git a/tests/ref/transform-scale.png b/tests/ref/transform-scale.png
new file mode 100644
index 0000000000..c95b90f1d8
Binary files /dev/null and b/tests/ref/transform-scale.png differ
diff --git a/tests/ref/transform-tex-logo.png b/tests/ref/transform-tex-logo.png
new file mode 100644
index 0000000000..5d16ffb4f4
Binary files /dev/null and b/tests/ref/transform-tex-logo.png differ
diff --git a/tests/ref/underline-background.png b/tests/ref/underline-background.png
new file mode 100644
index 0000000000..33ba381a20
Binary files /dev/null and b/tests/ref/underline-background.png differ
diff --git a/tests/ref/underline-overline-strike.png b/tests/ref/underline-overline-strike.png
new file mode 100644
index 0000000000..2567fca4e6
Binary files /dev/null and b/tests/ref/underline-overline-strike.png differ
diff --git a/tests/ref/underline-stroke-folding.png b/tests/ref/underline-stroke-folding.png
new file mode 100644
index 0000000000..32119e5c59
Binary files /dev/null and b/tests/ref/underline-stroke-folding.png differ
diff --git a/tests/ref/visualize/gradient-conic.png b/tests/ref/visualize/gradient-conic.png
deleted file mode 100644
index ff4a0ca2e6..0000000000
Binary files a/tests/ref/visualize/gradient-conic.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-dir.png b/tests/ref/visualize/gradient-dir.png
deleted file mode 100644
index bda3eb171f..0000000000
Binary files a/tests/ref/visualize/gradient-dir.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-hue-rotation.png b/tests/ref/visualize/gradient-hue-rotation.png
deleted file mode 100644
index 2d786f710f..0000000000
Binary files a/tests/ref/visualize/gradient-hue-rotation.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-math.png b/tests/ref/visualize/gradient-math.png
deleted file mode 100644
index 470e6138af..0000000000
Binary files a/tests/ref/visualize/gradient-math.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-presets.png b/tests/ref/visualize/gradient-presets.png
deleted file mode 100644
index e6f7f73a1c..0000000000
Binary files a/tests/ref/visualize/gradient-presets.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-radial.png b/tests/ref/visualize/gradient-radial.png
deleted file mode 100644
index 2e8e9af3e0..0000000000
Binary files a/tests/ref/visualize/gradient-radial.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-relative-conic.png b/tests/ref/visualize/gradient-relative-conic.png
deleted file mode 100644
index 232c5f0af4..0000000000
Binary files a/tests/ref/visualize/gradient-relative-conic.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-relative-linear.png b/tests/ref/visualize/gradient-relative-linear.png
deleted file mode 100644
index 56e46119e1..0000000000
Binary files a/tests/ref/visualize/gradient-relative-linear.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-relative-radial.png b/tests/ref/visualize/gradient-relative-radial.png
deleted file mode 100644
index 210ea7b0f8..0000000000
Binary files a/tests/ref/visualize/gradient-relative-radial.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-repeat.png b/tests/ref/visualize/gradient-repeat.png
deleted file mode 100644
index 6be7dc660f..0000000000
Binary files a/tests/ref/visualize/gradient-repeat.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-sharp.png b/tests/ref/visualize/gradient-sharp.png
deleted file mode 100644
index b7698cfa45..0000000000
Binary files a/tests/ref/visualize/gradient-sharp.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-stroke.png b/tests/ref/visualize/gradient-stroke.png
deleted file mode 100644
index 69317f7325..0000000000
Binary files a/tests/ref/visualize/gradient-stroke.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-text-decorations.png b/tests/ref/visualize/gradient-text-decorations.png
deleted file mode 100644
index 887cd50086..0000000000
Binary files a/tests/ref/visualize/gradient-text-decorations.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-text-other.png b/tests/ref/visualize/gradient-text-other.png
deleted file mode 100644
index 78555b18f2..0000000000
Binary files a/tests/ref/visualize/gradient-text-other.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-text.png b/tests/ref/visualize/gradient-text.png
deleted file mode 100644
index 478a058635..0000000000
Binary files a/tests/ref/visualize/gradient-text.png and /dev/null differ
diff --git a/tests/ref/visualize/gradient-transform.png b/tests/ref/visualize/gradient-transform.png
deleted file mode 100644
index a55ad91e81..0000000000
Binary files a/tests/ref/visualize/gradient-transform.png and /dev/null differ
diff --git a/tests/ref/visualize/image-scale.png b/tests/ref/visualize/image-scale.png
deleted file mode 100644
index 95e9157ec3..0000000000
Binary files a/tests/ref/visualize/image-scale.png and /dev/null differ
diff --git a/tests/ref/visualize/image.png b/tests/ref/visualize/image.png
deleted file mode 100644
index ec53fa980d..0000000000
Binary files a/tests/ref/visualize/image.png and /dev/null differ
diff --git a/tests/ref/visualize/line.png b/tests/ref/visualize/line.png
deleted file mode 100644
index d19dea0ea1..0000000000
Binary files a/tests/ref/visualize/line.png and /dev/null differ
diff --git a/tests/ref/visualize/path.png b/tests/ref/visualize/path.png
deleted file mode 100644
index c7f710c942..0000000000
Binary files a/tests/ref/visualize/path.png and /dev/null differ
diff --git a/tests/ref/visualize/pattern-relative.png b/tests/ref/visualize/pattern-relative.png
deleted file mode 100644
index 7958bf7f64..0000000000
Binary files a/tests/ref/visualize/pattern-relative.png and /dev/null differ
diff --git a/tests/ref/visualize/pattern-simple.png b/tests/ref/visualize/pattern-simple.png
deleted file mode 100644
index ac473a7560..0000000000
Binary files a/tests/ref/visualize/pattern-simple.png and /dev/null differ
diff --git a/tests/ref/visualize/pattern-small.png b/tests/ref/visualize/pattern-small.png
deleted file mode 100644
index 6af592dd8d..0000000000
Binary files a/tests/ref/visualize/pattern-small.png and /dev/null differ
diff --git a/tests/ref/visualize/pattern-spacing.png b/tests/ref/visualize/pattern-spacing.png
deleted file mode 100644
index 4c95a3b027..0000000000
Binary files a/tests/ref/visualize/pattern-spacing.png and /dev/null differ
diff --git a/tests/ref/visualize/pattern-stroke.png b/tests/ref/visualize/pattern-stroke.png
deleted file mode 100644
index d71f1c920f..0000000000
Binary files a/tests/ref/visualize/pattern-stroke.png and /dev/null differ
diff --git a/tests/ref/visualize/pattern-text.png b/tests/ref/visualize/pattern-text.png
deleted file mode 100644
index 2ecf2fdabc..0000000000
Binary files a/tests/ref/visualize/pattern-text.png and /dev/null differ
diff --git a/tests/ref/visualize/polygon.png b/tests/ref/visualize/polygon.png
deleted file mode 100644
index 234aeb1488..0000000000
Binary files a/tests/ref/visualize/polygon.png and /dev/null differ
diff --git a/tests/ref/visualize/shape-aspect.png b/tests/ref/visualize/shape-aspect.png
deleted file mode 100644
index 918a5e7315..0000000000
Binary files a/tests/ref/visualize/shape-aspect.png and /dev/null differ
diff --git a/tests/ref/visualize/shape-circle.png b/tests/ref/visualize/shape-circle.png
deleted file mode 100644
index a2ee279df2..0000000000
Binary files a/tests/ref/visualize/shape-circle.png and /dev/null differ
diff --git a/tests/ref/visualize/shape-ellipse.png b/tests/ref/visualize/shape-ellipse.png
deleted file mode 100644
index 6de5e9f61d..0000000000
Binary files a/tests/ref/visualize/shape-ellipse.png and /dev/null differ
diff --git a/tests/ref/visualize/shape-fill-stroke.png b/tests/ref/visualize/shape-fill-stroke.png
deleted file mode 100644
index d4a4817af2..0000000000
Binary files a/tests/ref/visualize/shape-fill-stroke.png and /dev/null differ
diff --git a/tests/ref/visualize/shape-rect.png b/tests/ref/visualize/shape-rect.png
deleted file mode 100644
index 3eda642f8f..0000000000
Binary files a/tests/ref/visualize/shape-rect.png and /dev/null differ
diff --git a/tests/ref/visualize/shape-rounded.png b/tests/ref/visualize/shape-rounded.png
deleted file mode 100644
index ec926d0a56..0000000000
Binary files a/tests/ref/visualize/shape-rounded.png and /dev/null differ
diff --git a/tests/ref/visualize/shape-square.png b/tests/ref/visualize/shape-square.png
deleted file mode 100644
index 46e243e1f8..0000000000
Binary files a/tests/ref/visualize/shape-square.png and /dev/null differ
diff --git a/tests/ref/visualize/stroke.png b/tests/ref/visualize/stroke.png
deleted file mode 100644
index bdfcae9f76..0000000000
Binary files a/tests/ref/visualize/stroke.png and /dev/null differ
diff --git a/tests/ref/visualize/svg-text.png b/tests/ref/visualize/svg-text.png
deleted file mode 100644
index b2bbe320b3..0000000000
Binary files a/tests/ref/visualize/svg-text.png and /dev/null differ
diff --git a/tests/ref/while-loop-basic.png b/tests/ref/while-loop-basic.png
new file mode 100644
index 0000000000..3a0e6d242b
Binary files /dev/null and b/tests/ref/while-loop-basic.png differ
diff --git a/tests/src/args.rs b/tests/src/args.rs
new file mode 100644
index 0000000000..ddd088facd
--- /dev/null
+++ b/tests/src/args.rs
@@ -0,0 +1,64 @@
+use clap::{Parser, Subcommand};
+
+/// Typst's test runner.
+#[derive(Debug, Clone, Parser)]
+#[clap(name = "typst-test", author)]
+pub struct CliArguments {
+ /// The command to run.
+ #[command(subcommand)]
+ pub command: Option,
+ /// All the tests that contain the filter string will be run.
+ pub filter: Vec,
+ /// Runs only the tests with the exact specified `filter` names.
+ #[arg(short, long)]
+ pub exact: bool,
+ /// Whether to update the reference images of non-passing tests.
+ #[arg(short, long)]
+ pub update: bool,
+ /// The scaling factor to render the output image with.
+ ///
+ /// Does not affect the comparison or the reference image.
+ #[arg(short, long, default_value_t = 1.0)]
+ pub scale: f32,
+ /// Whether to run the tests in extended mode, including PDF and SVG
+ /// export.
+ ///
+ /// This is used in CI.
+ #[arg(long, env = "TYPST_TESTS_EXTENDED")]
+ pub extended: bool,
+ /// Runs PDF export.
+ #[arg(long)]
+ pub pdf: bool,
+ /// Runs SVG export.
+ #[arg(long)]
+ pub svg: bool,
+ /// Whether to display the syntax tree.
+ #[arg(long)]
+ pub syntax: bool,
+ /// Prevents the terminal from being cleared of test names.
+ #[arg(short, long)]
+ pub verbose: bool,
+ /// How many threads to spawn when running the tests.
+ #[arg(short = 'j', long)]
+ pub num_threads: Option,
+}
+
+impl CliArguments {
+ /// Whether to run PDF export.
+ pub fn pdf(&self) -> bool {
+ self.pdf || self.extended
+ }
+
+ /// Whether to run SVG export.
+ pub fn svg(&self) -> bool {
+ self.svg || self.extended
+ }
+}
+
+/// What to do.
+#[derive(Debug, Clone, Subcommand)]
+#[command()]
+pub enum Command {
+ /// Clears the on-disk test artifact store.
+ Clean,
+}
diff --git a/tests/src/collect.rs b/tests/src/collect.rs
new file mode 100644
index 0000000000..44a325f20c
--- /dev/null
+++ b/tests/src/collect.rs
@@ -0,0 +1,420 @@
+use std::collections::{HashMap, HashSet};
+use std::fmt::{self, Display, Formatter};
+use std::ops::Range;
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+
+use ecow::{eco_format, EcoString};
+use typst::syntax::package::PackageVersion;
+use typst::syntax::{is_id_continue, is_ident, is_newline, FileId, Source, VirtualPath};
+use unscanny::Scanner;
+
+/// Collects all tests from all files.
+///
+/// Returns:
+/// - the tests and the number of skipped tests in the success case.
+/// - parsing errors in the failure case.
+pub fn collect() -> Result<(Vec, usize), Vec> {
+ Collector::new().collect()
+}
+
+/// A single test.
+pub struct Test {
+ pub pos: FilePos,
+ pub name: EcoString,
+ pub source: Source,
+ pub notes: Vec,
+ pub large: bool,
+}
+
+impl Display for Test {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{} ({})", self.name, self.pos)
+ }
+}
+
+/// A position in a file.
+#[derive(Clone)]
+pub struct FilePos {
+ pub path: PathBuf,
+ pub line: usize,
+}
+
+impl FilePos {
+ fn new(path: impl Into, line: usize) -> Self {
+ Self { path: path.into(), line }
+ }
+}
+
+impl Display for FilePos {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{}:{}", self.path.display(), self.line)
+ }
+}
+
+/// The size of a file.
+pub struct FileSize(pub usize);
+
+impl Display for FileSize {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{:.2} KiB", (self.0 as f64) / 1024.0)
+ }
+}
+
+/// An annotation like `// Error: 2-6 message` in a test.
+pub struct Note {
+ pub pos: FilePos,
+ pub kind: NoteKind,
+ pub range: Option>,
+ pub message: String,
+}
+
+/// A kind of annotation in a test.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum NoteKind {
+ Error,
+ Warning,
+ Hint,
+}
+
+impl FromStr for NoteKind {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result {
+ Ok(match s {
+ "Error" => Self::Error,
+ "Warning" => Self::Warning,
+ "Hint" => Self::Hint,
+ _ => return Err(()),
+ })
+ }
+}
+
+impl Display for NoteKind {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ f.pad(match self {
+ Self::Error => "Error",
+ Self::Warning => "Warning",
+ Self::Hint => "Hint",
+ })
+ }
+}
+
+/// Collects all tests from all files.
+struct Collector {
+ tests: Vec,
+ errors: Vec,
+ seen: HashMap,
+ large: HashSet,
+ skipped: usize,
+}
+
+impl Collector {
+ /// Creates a new test collector.
+ fn new() -> Self {
+ Self {
+ tests: vec![],
+ errors: vec![],
+ seen: HashMap::new(),
+ large: HashSet::new(),
+ skipped: 0,
+ }
+ }
+
+ /// Collects tests from all files.
+ fn collect(mut self) -> Result<(Vec, usize), Vec> {
+ self.walk_files();
+ self.walk_references();
+
+ if self.errors.is_empty() {
+ Ok((self.tests, self.skipped))
+ } else {
+ Err(self.errors)
+ }
+ }
+
+ /// Walks through all test files and collects the tests.
+ fn walk_files(&mut self) {
+ for entry in walkdir::WalkDir::new(crate::SUITE_PATH).sort_by_file_name() {
+ let entry = entry.unwrap();
+ let path = entry.path();
+ if !path.extension().is_some_and(|ext| ext == "typ") {
+ continue;
+ }
+
+ let text = std::fs::read_to_string(path).unwrap();
+ if text.starts_with("// SKIP") {
+ continue;
+ }
+
+ Parser::new(self, path, &text).parse();
+ }
+ }
+
+ /// Walks through all reference images and ensure that a test exists for
+ /// each one.
+ fn walk_references(&mut self) {
+ for entry in walkdir::WalkDir::new(crate::REF_PATH).sort_by_file_name() {
+ let entry = entry.unwrap();
+ let path = entry.path();
+ if !path.extension().is_some_and(|ext| ext == "png") {
+ continue;
+ }
+
+ let stem = path.file_stem().unwrap().to_string_lossy();
+ let name = &*stem;
+
+ let Some(pos) = self.seen.get(name) else {
+ self.errors.push(TestParseError {
+ pos: FilePos::new(path, 0),
+ message: "dangling reference image".into(),
+ });
+ continue;
+ };
+
+ let len = path.metadata().unwrap().len() as usize;
+ if !self.large.contains(name) && len > crate::REF_LIMIT {
+ self.errors.push(TestParseError {
+ pos: pos.clone(),
+ message: format!(
+ "reference image size exceeds {}, but the test is not marked as `// LARGE`",
+ FileSize(crate::REF_LIMIT),
+ ),
+ });
+ }
+ }
+ }
+}
+
+/// Parses a single test file.
+struct Parser<'a> {
+ collector: &'a mut Collector,
+ path: &'a Path,
+ s: Scanner<'a>,
+ test_start_line: usize,
+ line: usize,
+}
+
+impl<'a> Parser<'a> {
+ /// Creates a new parser for a file.
+ fn new(collector: &'a mut Collector, path: &'a Path, source: &'a str) -> Self {
+ Self {
+ collector,
+ path,
+ s: Scanner::new(source),
+ test_start_line: 1,
+ line: 1,
+ }
+ }
+
+ /// Parses an individual file.
+ fn parse(&mut self) {
+ self.skip_preamble();
+
+ while !self.s.done() {
+ let mut name = EcoString::new();
+ let mut notes = vec![];
+ if self.s.eat_if("---") {
+ self.s.eat_while(' ');
+ name = self.s.eat_until(char::is_whitespace).into();
+ self.s.eat_while(' ');
+
+ if name.is_empty() {
+ self.error("expected test name");
+ } else if !is_ident(&name) {
+ self.error(format!("test name `{name}` is not a valid identifier"));
+ } else if !self.s.eat_if("---") {
+ self.error("expected closing ---");
+ }
+ } else {
+ self.error("expected opening ---");
+ }
+
+ if self.collector.seen.contains_key(&name) {
+ self.error(format!("duplicate test {name}"));
+ }
+
+ if self.s.eat_newline() {
+ self.line += 1;
+ }
+
+ let start = self.s.cursor();
+ self.test_start_line = self.line;
+
+ let pos = FilePos::new(self.path, self.test_start_line);
+ self.collector.seen.insert(name.clone(), pos.clone());
+
+ while !self.s.done() && !self.s.at("---") {
+ self.s.eat_until(is_newline);
+ if self.s.eat_newline() {
+ self.line += 1;
+ }
+ }
+
+ let text = self.s.from(start);
+ let large = text.starts_with("// LARGE");
+ if large {
+ self.collector.large.insert(name.clone());
+ }
+
+ if !filtered(&name) {
+ self.collector.skipped += 1;
+ continue;
+ }
+
+ let vpath = VirtualPath::new(self.path);
+ let source = Source::new(FileId::new(None, vpath), text.into());
+
+ self.s.jump(start);
+ self.line = self.test_start_line;
+
+ while !self.s.done() && !self.s.at("---") {
+ self.s.eat_while(' ');
+ if self.s.eat_if("// ") {
+ notes.extend(self.parse_note(&source));
+ }
+
+ self.s.eat_until(is_newline);
+ if self.s.eat_newline() {
+ self.line += 1;
+ }
+ }
+
+ self.collector.tests.push(Test { pos, name, source, notes, large });
+ }
+ }
+
+ /// Skips the preamble of a test.
+ fn skip_preamble(&mut self) {
+ let mut errored = false;
+ while !self.s.done() && !self.s.at("---") {
+ let line = self.s.eat_until(is_newline).trim();
+ if !errored && !line.is_empty() && !line.starts_with("//") {
+ self.error("test preamble may only contain comments and blank lines");
+ errored = true;
+ }
+ if self.s.eat_newline() {
+ self.line += 1;
+ }
+ }
+ }
+
+ /// Parses an annotation in a test.
+ fn parse_note(&mut self, source: &Source) -> Option {
+ let head = self.s.eat_while(is_id_continue);
+ if !self.s.eat_if(':') {
+ return None;
+ }
+
+ let kind: NoteKind = head.parse().ok()?;
+ self.s.eat_if(' ');
+
+ let mut range = None;
+ if self.s.at('-') || self.s.at(char::is_numeric) {
+ range = self.parse_range(source);
+ if range.is_none() {
+ self.error("range is malformed");
+ return None;
+ }
+ }
+
+ let message = self
+ .s
+ .eat_until(is_newline)
+ .trim()
+ .replace("VERSION", &eco_format!("{}", PackageVersion::compiler()));
+
+ Some(Note {
+ pos: FilePos::new(self.path, self.line),
+ kind,
+ range,
+ message,
+ })
+ }
+
+ /// Parse a range, optionally abbreviated as just a position if the range
+ /// is empty.
+ fn parse_range(&mut self, source: &Source) -> Option> {
+ let start = self.parse_position(source)?;
+ let end = if self.s.eat_if('-') { self.parse_position(source)? } else { start };
+ Some(start..end)
+ }
+
+ /// Parses a relative `(line:)?column` position.
+ fn parse_position(&mut self, source: &Source) -> Option {
+ let first = self.parse_number()?;
+ let (line_delta, column) =
+ if self.s.eat_if(':') { (first, self.parse_number()?) } else { (1, first) };
+
+ let text = source.text();
+ let line_idx_in_test = self.line - self.test_start_line;
+ let comments = text
+ .lines()
+ .skip(line_idx_in_test + 1)
+ .take_while(|line| line.trim().starts_with("//"))
+ .count();
+
+ let line_idx = (line_idx_in_test + comments).checked_add_signed(line_delta)?;
+ let column_idx = if column < 0 {
+ // Negative column index is from the back.
+ let range = source.line_to_range(line_idx)?;
+ text[range].chars().count().saturating_add_signed(column)
+ } else {
+ usize::try_from(column).ok()?.checked_sub(1)?
+ };
+
+ source.line_column_to_byte(line_idx, column_idx)
+ }
+
+ /// Parse a number.
+ fn parse_number(&mut self) -> Option {
+ let start = self.s.cursor();
+ self.s.eat_if('-');
+ self.s.eat_while(char::is_numeric);
+ self.s.from(start).parse().ok()
+ }
+
+ /// Stores a test parsing error.
+ fn error(&mut self, message: impl Into) {
+ self.collector.errors.push(TestParseError {
+ pos: FilePos::new(self.path, self.line),
+ message: message.into(),
+ });
+ }
+}
+
+/// Whether a test is within the filtered set.
+fn filtered(name: &str) -> bool {
+ let exact = crate::ARGS.exact;
+ let filter = &crate::ARGS.filter;
+ filter.is_empty()
+ || filter
+ .iter()
+ .any(|v| if exact { name == v } else { name.contains(v) })
+}
+
+/// An error in a test file.
+pub struct TestParseError {
+ pos: FilePos,
+ message: String,
+}
+
+impl Display for TestParseError {
+ fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ write!(f, "{} ({})", self.message, self.pos)
+ }
+}
+
+trait ScannerExt {
+ fn eat_newline(&mut self) -> bool;
+}
+
+impl ScannerExt for Scanner<'_> {
+ fn eat_newline(&mut self) -> bool {
+ let ate = self.eat_if(is_newline);
+ if ate && self.before().ends_with('\r') {
+ self.eat_if('\n');
+ }
+ ate
+ }
+}
diff --git a/tests/src/logger.rs b/tests/src/logger.rs
new file mode 100644
index 0000000000..c48650a763
--- /dev/null
+++ b/tests/src/logger.rs
@@ -0,0 +1,141 @@
+use std::io::{self, IsTerminal, StderrLock, Write};
+use std::time::{Duration, Instant};
+
+use crate::collect::Test;
+use crate::run::TestResult;
+
+/// Receives status updates by individual test runs.
+pub struct Logger<'a> {
+ filtered: usize,
+ passed: usize,
+ failed: usize,
+ skipped: usize,
+ mismatched_image: bool,
+ active: Vec<&'a Test>,
+ last_change: Instant,
+ temp_lines: usize,
+ terminal: bool,
+}
+
+impl<'a> Logger<'a> {
+ /// Create a new logger.
+ pub fn new(filtered: usize, skipped: usize) -> Self {
+ Self {
+ filtered,
+ passed: 0,
+ failed: 0,
+ skipped,
+ mismatched_image: false,
+ active: vec![],
+ temp_lines: 0,
+ last_change: Instant::now(),
+ terminal: std::io::stderr().is_terminal(),
+ }
+ }
+
+ /// Register the start of a test.
+ pub fn start(&mut self, test: &'a Test) {
+ self.active.push(test);
+ self.last_change = Instant::now();
+ self.refresh();
+ }
+
+ /// Register a finished test.
+ pub fn end(&mut self, test: &'a Test, result: std::thread::Result) {
+ self.active.retain(|t| t.name != test.name);
+
+ let result = match result {
+ Ok(result) => result,
+ Err(_) => {
+ self.failed += 1;
+ self.temp_lines = 0;
+ self.print(move |out| {
+ writeln!(out, "❌ {test} panicked")?;
+ Ok(())
+ })
+ .unwrap();
+ return;
+ }
+ };
+
+ if result.is_ok() {
+ self.passed += 1;
+ } else {
+ self.failed += 1;
+ }
+
+ self.mismatched_image |= result.mismatched_image;
+ self.last_change = Instant::now();
+
+ self.print(move |out| {
+ if !result.errors.is_empty() {
+ writeln!(out, "❌ {test}")?;
+ for line in result.errors.lines() {
+ writeln!(out, " {line}")?;
+ }
+ } else if crate::ARGS.verbose || !result.infos.is_empty() {
+ writeln!(out, "✅ {test}")?;
+ }
+ for line in result.infos.lines() {
+ writeln!(out, " {line}")?;
+ }
+ Ok(())
+ })
+ .unwrap();
+ }
+
+ /// Prints a summary and returns whether the test suite passed.
+ pub fn finish(&self) -> bool {
+ let Self { filtered, passed, failed, skipped, .. } = *self;
+
+ eprintln!("{passed} passed, {failed} failed, {skipped} skipped");
+ assert_eq!(filtered, passed + failed, "not all tests were executed succesfully");
+
+ if self.mismatched_image {
+ eprintln!(" pass the --update flag to update the reference images");
+ }
+
+ self.failed == 0
+ }
+
+ /// Refresh the status.
+ pub fn refresh(&mut self) {
+ self.print(|_| Ok(())).unwrap();
+ }
+
+ /// Refresh the status print.
+ fn print(
+ &mut self,
+ inner: impl FnOnce(&mut StderrLock<'_>) -> io::Result<()>,
+ ) -> io::Result<()> {
+ let mut out = std::io::stderr().lock();
+
+ // Clear the status lines.
+ for _ in 0..self.temp_lines {
+ write!(out, "\x1B[1F\x1B[0J")?;
+ self.temp_lines = 0;
+ }
+
+ // Print the result of a finished test.
+ inner(&mut out)?;
+
+ // Print the status line.
+ let done = self.failed + self.passed;
+ if done < self.filtered {
+ if self.last_change.elapsed() > Duration::from_secs(2) {
+ for test in &self.active {
+ writeln!(out, "⏰ {test} is taking a long time ...")?;
+ if self.terminal {
+ self.temp_lines += 1;
+ }
+ }
+ }
+ if self.terminal {
+ writeln!(out, "💨 {done} / {}", self.filtered)?;
+ self.temp_lines += 1;
+ }
+ }
+
+ Ok(())
+ }
+}
diff --git a/tests/src/metadata.rs b/tests/src/metadata.rs
deleted file mode 100644
index 53cbbdffb9..0000000000
--- a/tests/src/metadata.rs
+++ /dev/null
@@ -1,334 +0,0 @@
-use std::collections::HashSet;
-use std::fmt::{self, Display, Formatter};
-use std::ops::Range;
-use std::str::FromStr;
-
-use ecow::EcoString;
-use typst::syntax::package::PackageVersion;
-use typst::syntax::Source;
-use unscanny::Scanner;
-
-/// Each test and subset may contain metadata.
-#[derive(Debug)]
-pub struct TestMetadata {
- /// Configures how the test is run.
- pub config: TestConfig,
- /// Declares properties that must hold for a test.
- ///
- /// For instance, `// Warning: 1-3 no text within underscores`
- /// will fail the test if the warning isn't generated by your test.
- pub annotations: HashSet,
-}
-
-/// Configuration of a test or subtest.
-#[derive(Debug, Default)]
-pub struct TestConfig {
- /// Reference images will be generated and compared.
- ///
- /// Defaults to `true`, can be disabled with `Ref: false`.
- pub compare_ref: Option,
- /// Hint annotations will be compared to compiler hints.
- ///
- /// Defaults to `true`, can be disabled with `Hints: false`.
- pub validate_hints: Option,
- /// Autocompletion annotations will be validated against autocompletions.
- /// Mutually exclusive with error and hint annotations.
- ///
- /// Defaults to `false`, can be enabled with `Autocomplete: true`.
- pub validate_autocomplete: Option,
-}
-
-/// Parsing error when the metadata is invalid.
-pub(crate) enum InvalidMetadata {
- /// An invalid annotation and it's error message.
- InvalidAnnotation(Annotation, String),
- /// Setting metadata can only be done with `true` or `false` as a value.
- InvalidSet(String),
-}
-
-impl InvalidMetadata {
- pub(crate) fn write(
- invalid_data: Vec,
- output: &mut String,
- print_annotation: &mut impl FnMut(&Annotation, &mut String),
- ) {
- use std::fmt::Write;
- for data in invalid_data.into_iter() {
- let (annotation, error) = match data {
- InvalidMetadata::InvalidAnnotation(a, e) => (Some(a), e),
- InvalidMetadata::InvalidSet(e) => (None, e),
- };
- write!(output, "{error}",).unwrap();
- if let Some(annotation) = annotation {
- print_annotation(&annotation, output)
- } else {
- writeln!(output).unwrap();
- }
- }
- }
-}
-
-/// Annotation of the form `// KIND: RANGE TEXT`.
-#[derive(Debug, Clone, Eq, PartialEq, Hash)]
-pub struct Annotation {
- /// Which kind of annotation this is.
- pub kind: AnnotationKind,
- /// May be written as:
- /// - `{line}:{col}-{line}:{col}`, e.g. `0:4-0:6`.
- /// - `{col}-{col}`, e.g. `4-6`:
- /// The line is assumed to be the line after the annotation.
- /// - `-1`: Produces a range of length zero at the end of the next line.
- /// Mostly useful for autocompletion tests which require an index.
- pub range: Option>,
- /// The raw text after the annotation.
- pub text: EcoString,
-}
-
-/// The different kinds of in-test annotations.
-#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
-pub enum AnnotationKind {
- Error,
- Warning,
- Hint,
- AutocompleteContains,
- AutocompleteExcludes,
-}
-
-impl AnnotationKind {
- /// Returns the user-facing string for this annotation.
- pub fn as_str(self) -> &'static str {
- match self {
- AnnotationKind::Error => "Error",
- AnnotationKind::Warning => "Warning",
- AnnotationKind::Hint => "Hint",
- AnnotationKind::AutocompleteContains => "Autocomplete contains",
- AnnotationKind::AutocompleteExcludes => "Autocomplete excludes",
- }
- }
-}
-
-impl FromStr for AnnotationKind {
- type Err = &'static str;
-
- fn from_str(s: &str) -> Result {
- Ok(match s {
- "Error" => AnnotationKind::Error,
- "Warning" => AnnotationKind::Warning,
- "Hint" => AnnotationKind::Hint,
- "Autocomplete contains" => AnnotationKind::AutocompleteContains,
- "Autocomplete excludes" => AnnotationKind::AutocompleteExcludes,
- _ => return Err("invalid annotatino"),
- })
- }
-}
-
-impl Display for AnnotationKind {
- fn fmt(&self, f: &mut Formatter) -> fmt::Result {
- f.pad(self.as_str())
- }
-}
-
-/// Parse metadata for a test.
-pub fn parse_part_metadata(
- source: &Source,
- is_header: bool,
-) -> Result> {
- let mut config = TestConfig::default();
- let mut annotations = HashSet::default();
- let mut invalid_data = vec![];
-
- let lines = source_to_lines(source);
-
- for (i, line) in lines.iter().enumerate() {
- if let Some((key, value)) = parse_metadata_line(line) {
- let key = key.trim();
- match key {
- "Ref" => validate_set_annotation(
- value,
- &mut config.compare_ref,
- &mut invalid_data,
- ),
- "Hints" => validate_set_annotation(
- value,
- &mut config.validate_hints,
- &mut invalid_data,
- ),
- "Autocomplete" => validate_set_annotation(
- value,
- &mut config.validate_autocomplete,
- &mut invalid_data,
- ),
- annotation_key => {
- let Ok(kind) = AnnotationKind::from_str(annotation_key) else {
- continue;
- };
- let mut s = Scanner::new(value);
- let range = parse_range(&mut s, i, source);
- let rest = if range.is_some() { s.after() } else { s.string() };
- let message = rest
- .trim()
- .replace("VERSION", &PackageVersion::compiler().to_string())
- .into();
-
- let annotation =
- Annotation { kind, range: range.clone(), text: message };
-
- if is_header {
- invalid_data.push(InvalidMetadata::InvalidAnnotation(
- annotation,
- format!(
- "Error: header may not contain annotations of type {kind}"
- ),
- ));
- continue;
- }
-
- if matches!(
- kind,
- AnnotationKind::AutocompleteContains
- | AnnotationKind::AutocompleteExcludes
- ) {
- if let Some(range) = range {
- if range.start != range.end {
- invalid_data.push(InvalidMetadata::InvalidAnnotation(
- annotation,
- "Error: found range in Autocomplete annotation where range.start != range.end, range.end would be ignored."
- .to_string()
- ));
- continue;
- }
- } else {
- invalid_data.push(InvalidMetadata::InvalidAnnotation(
- annotation,
- "Error: autocomplete annotation but no range specified"
- .to_string(),
- ));
- continue;
- }
- }
- annotations.insert(annotation);
- }
- }
- }
- }
- if invalid_data.is_empty() {
- Ok(TestMetadata { config, annotations })
- } else {
- Err(invalid_data)
- }
-}
-
-/// Extract key and value for a metadata line of the form: `// KEY: VALUE`.
-fn parse_metadata_line(line: &str) -> Option<(&str, &str)> {
- let mut s = Scanner::new(line);
- if !s.eat_if("// ") {
- return None;
- }
-
- let key = s.eat_until(':').trim();
- if !s.eat_if(':') {
- return None;
- }
-
- let value = s.eat_until('\n').trim();
- Some((key, value))
-}
-
-/// Parse a quoted string.
-fn parse_string<'a>(s: &mut Scanner<'a>) -> Option<&'a str> {
- if !s.eat_if('"') {
- return None;
- }
- let sub = s.eat_until('"');
- if !s.eat_if('"') {
- return None;
- }
-
- Some(sub)
-}
-
-/// Parse a number.
-fn parse_num(s: &mut Scanner) -> Option {
- let mut first = true;
- let n = &s.eat_while(|c: char| {
- let valid = first && c == '-' || c.is_numeric();
- first = false;
- valid
- });
- n.parse().ok()
-}
-
-/// Parse a comma-separated list of strings.
-pub fn parse_string_list(text: &str) -> HashSet<&str> {
- let mut s = Scanner::new(text);
- let mut result = HashSet::new();
- while let Some(sub) = parse_string(&mut s) {
- result.insert(sub);
- s.eat_whitespace();
- if !s.eat_if(',') {
- break;
- }
- s.eat_whitespace();
- }
- result
-}
-
-/// Parse a position.
-fn parse_pos(s: &mut Scanner, i: usize, source: &Source) -> Option {
- let first = parse_num(s)? - 1;
- let (delta, column) =
- if s.eat_if(':') { (first, parse_num(s)? - 1) } else { (0, first) };
- let line = (i + comments_until_code(source, i)).checked_add_signed(delta)?;
- source.line_column_to_byte(line, usize::try_from(column).ok()?)
-}
-
-/// Parse a range.
-fn parse_range(s: &mut Scanner, i: usize, source: &Source) -> Option> {
- let lines = source_to_lines(source);
- s.eat_whitespace();
- if s.eat_if("-1") {
- let mut add = 1;
- while let Some(line) = lines.get(i + add) {
- if !line.starts_with("//") {
- break;
- }
- add += 1;
- }
- let next_line = lines.get(i + add)?;
- let col = next_line.chars().count();
-
- let index = source.line_column_to_byte(i + add, col)?;
- s.eat_whitespace();
- return Some(index..index);
- }
- let start = parse_pos(s, i, source)?;
- let end = if s.eat_if('-') { parse_pos(s, i, source)? } else { start };
- s.eat_whitespace();
- Some(start..end)
-}
-
-/// Returns the number of lines of comment from line i to next line of code.
-fn comments_until_code(source: &Source, i: usize) -> usize {
- source_to_lines(source)[i..]
- .iter()
- .take_while(|line| line.starts_with("//"))
- .count()
-}
-
-fn source_to_lines(source: &Source) -> Vec<&str> {
- source.text().lines().map(str::trim).collect()
-}
-
-fn validate_set_annotation(
- value: &str,
- flag: &mut Option,
- invalid_data: &mut Vec,
-) {
- let value = value.trim();
- if value != "false" && value != "true" {
- invalid_data.push(
- InvalidMetadata::InvalidSet(format!("Error: trying to set Ref, Hints, or Autocomplete with value {value:?} != true, != false.")))
- } else {
- *flag = Some(value == "true")
- }
-}
diff --git a/tests/src/run.rs b/tests/src/run.rs
new file mode 100644
index 0000000000..d0d86ea6be
--- /dev/null
+++ b/tests/src/run.rs
@@ -0,0 +1,442 @@
+use std::fmt::Write;
+use std::ops::Range;
+use std::path::Path;
+
+use ecow::eco_vec;
+use tiny_skia as sk;
+use typst::diag::SourceDiagnostic;
+use typst::eval::Tracer;
+use typst::foundations::Smart;
+use typst::introspection::Meta;
+use typst::layout::{Abs, Frame, FrameItem, Page, Transform};
+use typst::model::Document;
+use typst::visualize::Color;
+use typst::WorldExt;
+
+use crate::collect::{FileSize, NoteKind, Test};
+use crate::world::TestWorld;
+
+/// Runs a single test.
+///
+/// Returns whether the test passed.
+pub fn run(test: &Test) -> TestResult {
+ Runner::new(test).run()
+}
+
+/// The result of running a single test.
+pub struct TestResult {
+ /// The error log for this test. If empty, the test passed.
+ pub errors: String,
+ /// The info log for this test.
+ pub infos: String,
+ /// Whether the image was mismatched.
+ pub mismatched_image: bool,
+}
+
+impl TestResult {
+ /// Whether the test passed.
+ pub fn is_ok(&self) -> bool {
+ self.errors.is_empty()
+ }
+}
+
+/// Write a line to a log sink, defaulting to the test's error log.
+macro_rules! log {
+ (into: $sink:expr, $($tts:tt)*) => {
+ writeln!($sink, $($tts)*).unwrap();
+ };
+ ($runner:expr, $($tts:tt)*) => {
+ writeln!(&mut $runner.result.errors, $($tts)*).unwrap();
+ };
+}
+
+/// Runs a single test.
+pub struct Runner<'a> {
+ test: &'a Test,
+ world: TestWorld,
+ seen: Vec,
+ result: TestResult,
+ not_annotated: String,
+}
+
+impl<'a> Runner<'a> {
+ /// Create a new test runner.
+ fn new(test: &'a Test) -> Self {
+ Self {
+ test,
+ world: TestWorld::new(test.source.clone()),
+ seen: vec![false; test.notes.len()],
+ result: TestResult {
+ errors: String::new(),
+ infos: String::new(),
+ mismatched_image: false,
+ },
+ not_annotated: String::new(),
+ }
+ }
+
+ /// Run the test.
+ fn run(mut self) -> TestResult {
+ if crate::ARGS.syntax {
+ log!(into: self.result.infos, "tree: {:#?}", self.test.source.root());
+ }
+
+ let mut tracer = Tracer::new();
+ let (doc, errors) = match typst::compile(&self.world, &mut tracer) {
+ Ok(doc) => (Some(doc), eco_vec![]),
+ Err(errors) => (None, errors),
+ };
+
+ let warnings = tracer.warnings();
+ if doc.is_none() && errors.is_empty() {
+ log!(self, "no document, but also no errors");
+ }
+
+ self.check_document(doc.as_ref());
+
+ for error in &errors {
+ self.check_diagnostic(NoteKind::Error, error);
+ }
+
+ for warning in &warnings {
+ self.check_diagnostic(NoteKind::Warning, warning);
+ }
+
+ self.handle_not_emitted();
+ self.handle_not_annotated();
+
+ self.result
+ }
+
+ /// Handle errors that weren't annotated.
+ fn handle_not_annotated(&mut self) {
+ if !self.not_annotated.is_empty() {
+ log!(self, "not annotated");
+ self.result.errors.push_str(&self.not_annotated);
+ }
+ }
+
+ /// Handle notes that weren't handled before.
+ fn handle_not_emitted(&mut self) {
+ let mut first = true;
+ for (note, &seen) in self.test.notes.iter().zip(&self.seen) {
+ if seen {
+ continue;
+ }
+ let note_range = self.format_range(¬e.range);
+ if first {
+ log!(self, "not emitted");
+ first = false;
+ }
+ log!(self, " {}: {note_range} {} ({})", note.kind, note.message, note.pos,);
+ }
+ }
+
+ /// Check that the document output is correct.
+ fn check_document(&mut self, document: Option<&Document>) {
+ let live_path = format!("{}/render/{}.png", crate::STORE_PATH, self.test.name);
+ let ref_path = format!("{}/{}.png", crate::REF_PATH, self.test.name);
+ let has_ref = Path::new(&ref_path).exists();
+
+ let Some(document) = document else {
+ if has_ref {
+ log!(self, "missing document");
+ log!(self, " ref | {ref_path}");
+ }
+ return;
+ };
+
+ let skippable = match document.pages.as_slice() {
+ [page] => skippable(page),
+ _ => false,
+ };
+
+ // Tests without visible output and no reference image don't need to be
+ // compared.
+ if skippable && !has_ref {
+ std::fs::remove_file(&live_path).ok();
+ return;
+ }
+
+ // Render the live version.
+ let pixmap = render(document, 1.0);
+
+ // Save live version, possibly rerendering if different scale is
+ // requested.
+ let mut pixmap_live = &pixmap;
+ let slot;
+ let scale = crate::ARGS.scale;
+ if scale != 1.0 {
+ slot = render(document, scale);
+ pixmap_live = &slot;
+ }
+ let data = pixmap_live.encode_png().unwrap();
+ std::fs::write(&live_path, data).unwrap();
+
+ // Write PDF if requested.
+ if crate::ARGS.pdf() {
+ let pdf_path = format!("{}/pdf/{}.pdf", crate::STORE_PATH, self.test.name);
+ let pdf = typst_pdf::pdf(document, Smart::Auto, None);
+ std::fs::write(pdf_path, pdf).unwrap();
+ }
+
+ // Write SVG if requested.
+ if crate::ARGS.svg() {
+ let svg_path = format!("{}/svg/{}.svg", crate::STORE_PATH, self.test.name);
+ let svg = typst_svg::svg_merged(document, Abs::pt(5.0));
+ std::fs::write(svg_path, svg).unwrap();
+ }
+
+ // Compare against reference image if available.
+ let equal = has_ref && {
+ let ref_data = std::fs::read(&ref_path).unwrap();
+ let ref_pixmap = sk::Pixmap::decode_png(&ref_data).unwrap();
+ approx_equal(&pixmap, &ref_pixmap)
+ };
+
+ // Test that is ok doesn't need to be updated.
+ if equal {
+ return;
+ }
+
+ if crate::ARGS.update {
+ if skippable {
+ std::fs::remove_file(&ref_path).unwrap();
+ log!(
+ into: self.result.infos,
+ "removed reference image ({ref_path})"
+ );
+ } else {
+ let opts = oxipng::Options::max_compression();
+ let data = pixmap.encode_png().unwrap();
+ let ref_data = oxipng::optimize_from_memory(&data, &opts).unwrap();
+ if !self.test.large && ref_data.len() > crate::REF_LIMIT {
+ log!(self, "reference image would exceed maximum size");
+ log!(self, " maximum | {}", FileSize(crate::REF_LIMIT));
+ log!(self, " size | {}", FileSize(ref_data.len()));
+ log!(self, "please try to minimize the size of the test (smaller pages, less text, etc.)");
+ log!(self, "if you think the test cannot be reasonably minimized, mark it as `// LARGE`");
+ return;
+ }
+ std::fs::write(&ref_path, &ref_data).unwrap();
+ log!(
+ into: self.result.infos,
+ "Updated reference image ({ref_path}, {})",
+ FileSize(ref_data.len()),
+ );
+ }
+ } else {
+ self.result.mismatched_image = true;
+ if has_ref {
+ log!(self, "mismatched rendering");
+ log!(self, " live | {live_path}");
+ log!(self, " ref | {ref_path}");
+ } else {
+ log!(self, "missing reference image");
+ log!(self, " live | {live_path}");
+ }
+ }
+ }
+
+ /// Compare a subset of notes with a given kind against diagnostics of
+ /// that same kind.
+ fn check_diagnostic(&mut self, kind: NoteKind, diag: &SourceDiagnostic) {
+ // Ignore diagnostics from other sources than the test file itself.
+ if diag.span.id().is_some_and(|id| id != self.test.source.id()) {
+ return;
+ }
+
+ let message = diag.message.replace("\\", "/");
+ let range = self.world.range(diag.span);
+ self.validate_note(kind, range.clone(), &message);
+
+ // Check hints.
+ for hint in &diag.hints {
+ self.validate_note(NoteKind::Hint, range.clone(), hint);
+ }
+ }
+
+ /// Try to find a matching note for the given `kind`, `range`, and
+ /// `message`.
+ ///
+ /// - If found, marks it as seen and returns it.
+ /// - If none was found, emits a "Not annotated" error and returns nothing.
+ fn validate_note(
+ &mut self,
+ kind: NoteKind,
+ range: Option>,
+ message: &str,
+ ) {
+ // Try to find perfect match.
+ if let Some((i, _)) = self.test.notes.iter().enumerate().find(|&(i, note)| {
+ !self.seen[i]
+ && note.kind == kind
+ && note.range == range
+ && note.message == message
+ }) {
+ self.seen[i] = true;
+ return;
+ }
+
+ // Try to find closely matching annotation. If the note has the same
+ // range or message, it's most likely the one we're interested in.
+ let Some((i, note)) = self.test.notes.iter().enumerate().find(|&(i, note)| {
+ !self.seen[i]
+ && note.kind == kind
+ && (note.range == range || note.message == message)
+ }) else {
+ // Not even a close match, diagnostic is not annotated.
+ let diag_range = self.format_range(&range);
+ log!(into: self.not_annotated, " {kind}: {diag_range} {}", message);
+ return;
+ };
+
+ // Mark this annotation as visited and return it.
+ self.seen[i] = true;
+
+ // Range is wrong.
+ if range != note.range {
+ let note_range = self.format_range(¬e.range);
+ let note_text = self.text_for_range(¬e.range);
+ let diag_range = self.format_range(&range);
+ let diag_text = self.text_for_range(&range);
+ log!(self, "mismatched range ({}):", note.pos);
+ log!(self, " message | {}", note.message);
+ log!(self, " annotated | {note_range:<9} | {note_text}");
+ log!(self, " emitted | {diag_range:<9} | {diag_text}");
+ }
+
+ // Message is wrong.
+ if message != note.message {
+ log!(self, "mismatched message ({}):", note.pos);
+ log!(self, " annotated | {}", note.message);
+ log!(self, " emitted | {message}");
+ }
+ }
+
+ /// Display the text for a range.
+ fn text_for_range(&self, range: &Option>) -> String {
+ let Some(range) = range else { return "No text".into() };
+ if range.is_empty() {
+ "(empty)".into()
+ } else {
+ format!("`{}`", self.test.source.text()[range.clone()].replace('\n', "\\n"))
+ }
+ }
+
+ /// Display a byte range as a line:column range.
+ fn format_range(&self, range: &Option>) -> String {
+ let Some(range) = range else { return "No range".into() };
+ if range.start == range.end {
+ self.format_pos(range.start)
+ } else {
+ format!("{}-{}", self.format_pos(range.start,), self.format_pos(range.end,))
+ }
+ }
+
+ /// Display a position as a line:column pair.
+ fn format_pos(&self, pos: usize) -> String {
+ if let (Some(line_idx), Some(column_idx)) =
+ (self.test.source.byte_to_line(pos), self.test.source.byte_to_column(pos))
+ {
+ let line = self.test.pos.line + line_idx;
+ let column = column_idx + 1;
+ if line == 1 {
+ format!("{column}")
+ } else {
+ format!("{line}:{column}")
+ }
+ } else {
+ "oob".into()
+ }
+ }
+}
+
+/// Draw all frames into one image with padding in between.
+fn render(document: &Document, pixel_per_pt: f32) -> sk::Pixmap {
+ for page in &document.pages {
+ let limit = Abs::cm(100.0);
+ if page.frame.width() > limit || page.frame.height() > limit {
+ panic!("overlarge frame: {:?}", page.frame.size());
+ }
+ }
+
+ let gap = Abs::pt(1.0);
+ let mut pixmap = typst_render::render_merged(
+ document,
+ pixel_per_pt,
+ Color::WHITE,
+ gap,
+ Color::BLACK,
+ );
+
+ let gap = (pixel_per_pt * gap.to_pt() as f32).round();
+
+ let mut y = 0.0;
+ for page in &document.pages {
+ let ts =
+ sk::Transform::from_scale(pixel_per_pt, pixel_per_pt).post_translate(0.0, y);
+ render_links(&mut pixmap, ts, &page.frame);
+ y += (pixel_per_pt * page.frame.height().to_pt() as f32).round().max(1.0) + gap;
+ }
+
+ pixmap
+}
+
+/// Draw extra boxes for links so we can see whether they are there.
+fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) {
+ for (pos, item) in frame.items() {
+ let ts = ts.pre_translate(pos.x.to_pt() as f32, pos.y.to_pt() as f32);
+ match *item {
+ FrameItem::Group(ref group) => {
+ let ts = ts.pre_concat(to_sk_transform(&group.transform));
+ render_links(canvas, ts, &group.frame);
+ }
+ FrameItem::Meta(Meta::Link(_), size) => {
+ let w = size.x.to_pt() as f32;
+ let h = size.y.to_pt() as f32;
+ let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
+ let mut paint = sk::Paint::default();
+ paint.set_color_rgba8(40, 54, 99, 40);
+ canvas.fill_rect(rect, &paint, ts, None);
+ }
+ _ => {}
+ }
+ }
+}
+
+/// Whether rendering of a frame can be skipped.
+fn skippable(page: &Page) -> bool {
+ page.frame.width().approx_eq(Abs::pt(120.0))
+ && page.frame.height().approx_eq(Abs::pt(20.0))
+ && skippable_frame(&page.frame)
+}
+
+/// Whether rendering of a frame can be skipped.
+fn skippable_frame(frame: &Frame) -> bool {
+ frame.items().all(|(_, item)| match item {
+ FrameItem::Group(group) => skippable_frame(&group.frame),
+ FrameItem::Meta(..) => true,
+ _ => false,
+ })
+}
+
+/// Whether to pixel images are approximately equal.
+fn approx_equal(a: &sk::Pixmap, b: &sk::Pixmap) -> bool {
+ a.width() == b.width()
+ && a.height() == b.height()
+ && a.data().iter().zip(b.data()).all(|(&a, &b)| a.abs_diff(b) <= 1)
+}
+
+/// Convert a Typst transform to a tiny-skia transform.
+fn to_sk_transform(transform: &Transform) -> sk::Transform {
+ let Transform { sx, ky, kx, sy, tx, ty } = *transform;
+ sk::Transform::from_row(
+ sx.get() as _,
+ ky.get() as _,
+ kx.get() as _,
+ sy.get() as _,
+ tx.to_pt() as f32,
+ ty.to_pt() as f32,
+ )
+}
diff --git a/tests/src/tests.rs b/tests/src/tests.rs
index e4f60bb656..6d58e969e9 100644
--- a/tests/src/tests.rs
+++ b/tests/src/tests.rs
@@ -1,1127 +1,112 @@
-/*! This is Typst's test runner.
+//! Typst's test runner.
-Tests are Typst files composed of a header part followed by subtests.
+mod args;
+mod collect;
+mod logger;
+mod run;
+mod world;
-The header may contain:
-- a small description `// tests that features X works well`
-- metadata (see [metadata::TestConfiguration])
-
-The subtests may use extra testing functions defined in [library], most
-importantly, `test(x, y)` which will fail the test `if x != y`.
-*/
-
-#![allow(clippy::comparison_chain)]
-mod metadata;
-
-use self::metadata::*;
-
-use std::borrow::Cow;
-use std::collections::{HashMap, HashSet};
-use std::ffi::OsStr;
-use std::fmt::Write as _;
-use std::io::{self, IsTerminal, Write as _};
-use std::ops::Range;
-use std::path::{Path, PathBuf, MAIN_SEPARATOR_STR};
-use std::sync::{OnceLock, RwLock};
-use std::{env, fs};
+use std::path::Path;
+use std::time::Duration;
use clap::Parser;
-use comemo::{Prehashed, Track};
-use oxipng::{InFile, Options, OutFile};
+use once_cell::sync::Lazy;
+use parking_lot::Mutex;
use rayon::iter::{ParallelBridge, ParallelIterator};
-use tiny_skia as sk;
-use typst::diag::{bail, FileError, FileResult, Severity, SourceDiagnostic, StrResult};
-use typst::eval::Tracer;
-use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value};
-use typst::introspection::Meta;
-use typst::layout::{Abs, Frame, FrameItem, Margin, Page, PageElem, Transform};
-use typst::model::Document;
-use typst::syntax::{FileId, Source, SyntaxNode, VirtualPath};
-use typst::text::{Font, FontBook, TextElem, TextSize};
-use typst::visualize::Color;
-use typst::{Library, World, WorldExt};
-use walkdir::WalkDir;
-// These directories are all relative to the tests/ directory.
-const TYP_DIR: &str = "typ";
-const REF_DIR: &str = "ref";
-const PNG_DIR: &str = "png";
-const PDF_DIR: &str = "pdf";
-const SVG_DIR: &str = "svg";
+use crate::args::{CliArguments, Command};
+use crate::logger::Logger;
-/// Arguments that modify test behaviour.
-///
-/// Specify them like this when developing:
-/// `cargo test --workspace --test tests -- --help`
-#[derive(Debug, Clone, Parser)]
-#[clap(name = "typst-test", author)]
-struct Args {
- /// All the tests that contains a filter string will be run (unless
- /// `--exact` is specified, which is even stricter).
- filter: Vec,
- /// Runs only the specified subtest.
- #[arg(short, long)]
- #[arg(allow_hyphen_values = true)]
- subtest: Option,
- /// Runs only the test with the exact name specified in your command.
- ///
- /// Example:
- /// `cargo test --workspace --test tests -- compiler/bytes.typ --exact`
- #[arg(long)]
- exact: bool,
- /// Updates the reference images in `tests/ref`.
- #[arg(long, default_value_t = env::var_os("UPDATE_EXPECT").is_some())]
- update: bool,
- /// Exports the tests as PDF into `tests/pdf`.
- #[arg(long)]
- pdf: bool,
- /// Configuration of what to print.
- #[command(flatten)]
- print: PrintConfig,
- /// Running `cargo test --workspace -- --nocapture` for the unit tests would
- /// fail the test runner without argument.
- // TODO: would it really still happen?
- #[arg(long)]
- nocapture: bool,
- /// Prevents the terminal from being cleared of test names and includes
- /// non-essential test messages.
- #[arg(short, long)]
- verbose: bool,
-}
+/// The parsed command line arguments.
+static ARGS: Lazy = Lazy::new(CliArguments::parse);
-/// Which things to print out for debugging.
-#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Parser)]
-struct PrintConfig {
- /// Print the syntax tree.
- #[arg(long)]
- syntax: bool,
- /// Print the content model.
- #[arg(long)]
- model: bool,
- /// Print the layouted frames.
- #[arg(long)]
- frames: bool,
-}
+/// The directory where the test suite is located.
+const SUITE_PATH: &str = "tests/suite";
-impl Args {
- fn matches(&self, canonicalized_path: &Path) -> bool {
- let path = canonicalized_path.to_string_lossy();
- if !self.exact {
- return self.filter.is_empty()
- || self.filter.iter().any(|v| path.contains(v));
- }
+/// The directory where the full test results are stored.
+const STORE_PATH: &str = "tests/store";
- self.filter.iter().any(|v| match path.strip_suffix(v) {
- None => false,
- Some(residual) => {
- residual.is_empty() || residual.ends_with(MAIN_SEPARATOR_STR)
- }
- })
- }
-}
-
-/// Tests all test files and prints a summary.
-fn main() {
- let args = Args::parse();
-
- // Create loader and context.
- let world = TestWorld::new(args.print);
-
- println!("Running tests...");
- let results = WalkDir::new(TYP_DIR)
- .sort_by_file_name()
- .into_iter()
- .par_bridge()
- .filter_map(|entry| {
- let entry = entry.unwrap();
- if entry.depth() == 0 {
- return None;
- }
+/// The directory where the reference images are stored.
+const REF_PATH: &str = "tests/ref";
- if entry.path().starts_with("typ/benches") {
- return None;
- }
-
- let src_path = entry.into_path(); // Relative to TYP_DIR.
- if src_path.extension() != Some(OsStr::new("typ")) {
- return None;
- }
-
- if args.matches(&src_path.canonicalize().unwrap()) {
- Some(src_path)
- } else {
- None
- }
- })
- .map_with(world, |world, src_path| {
- let path = src_path.strip_prefix(TYP_DIR).unwrap();
- let png_path = Path::new(PNG_DIR).join(path).with_extension("png");
- let ref_path = Path::new(REF_DIR).join(path).with_extension("png");
- let svg_path = Path::new(SVG_DIR).join(path).with_extension("svg");
- let pdf_path =
- args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
-
- test(
- world,
- &src_path,
- &png_path,
- &ref_path,
- pdf_path.as_deref(),
- &svg_path,
- &args,
- ) as usize
- })
- .collect::>();
-
- let len = results.len();
- let ok = results.iter().sum::();
- if len > 0 {
- println!("{ok} / {len} test{} passed.", if len > 1 { "s" } else { "" });
- } else {
- println!("No test ran.");
- }
-
- if ok != len {
- println!(
- "Set the UPDATE_EXPECT environment variable or pass the \
- --update flag to update the reference image(s)."
- );
- }
-
- if ok < len {
- std::process::exit(1);
- }
-}
+/// The maximum size of reference images that aren't marked as `// LARGE`.
+const REF_LIMIT: usize = 20 * 1024;
-fn library() -> Library {
- #[func]
- fn test(lhs: Value, rhs: Value) -> StrResult {
- if lhs != rhs {
- bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
- }
- Ok(NoneValue)
- }
-
- #[func]
- fn test_repr(lhs: Value, rhs: Value) -> StrResult {
- if lhs.repr() != rhs.repr() {
- bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
- }
- Ok(NoneValue)
- }
-
- #[func]
- fn print(#[variadic] values: Vec) -> NoneValue {
- let mut stdout = io::stdout().lock();
- write!(stdout, "> ").unwrap();
- for (i, value) in values.into_iter().enumerate() {
- if i > 0 {
- write!(stdout, ", ").unwrap();
- }
- write!(stdout, "{value:?}").unwrap();
- }
- writeln!(stdout).unwrap();
- NoneValue
- }
-
- // Set page width to 120pt with 10pt margins, so that the inner page is
- // exactly 100pt wide. Page height is unbounded and font size is 10pt so
- // that it multiplies to nice round numbers.
- let mut lib = Library::default();
- lib.styles
- .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
- lib.styles.set(PageElem::set_height(Smart::Auto));
- lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
- Abs::pt(10.0).into(),
- )))));
- lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
-
- // Hook up helpers into the global scope.
- lib.global.scope_mut().define_func::();
- lib.global.scope_mut().define_func::();
- lib.global.scope_mut().define_func::();
- lib.global
- .scope_mut()
- .define("conifer", Color::from_u8(0x9f, 0xEB, 0x52, 0xFF));
- lib.global
- .scope_mut()
- .define("forest", Color::from_u8(0x43, 0xA1, 0x27, 0xFF));
-
- lib
-}
-
-/// A world that provides access to the tests environment.
-struct TestWorld {
- print: PrintConfig,
- main: FileId,
- library: Prehashed,
- book: Prehashed,
- fonts: Vec,
- slots: RwLock>,
-}
-
-#[derive(Clone)]
-struct FileSlot {
- source: OnceLock>,
- buffer: OnceLock>,
-}
-
-impl TestWorld {
- fn new(print: PrintConfig) -> Self {
- let fonts: Vec<_> = typst_assets::fonts()
- .chain(typst_dev_assets::fonts())
- .flat_map(|data| Font::iter(Bytes::from_static(data)))
- .collect();
-
- Self {
- print,
- main: FileId::new(None, VirtualPath::new("main.typ")),
- library: Prehashed::new(library()),
- book: Prehashed::new(FontBook::from_fonts(&fonts)),
- fonts,
- slots: RwLock::new(HashMap::new()),
- }
- }
-}
-
-impl World for TestWorld {
- fn library(&self) -> &Prehashed {
- &self.library
- }
-
- fn book(&self) -> &Prehashed {
- &self.book
- }
-
- fn main(&self) -> Source {
- self.source(self.main).unwrap()
- }
-
- fn source(&self, id: FileId) -> FileResult {
- self.slot(id, |slot| {
- slot.source
- .get_or_init(|| {
- let buf = read(&system_path(id)?)?;
- let text = String::from_utf8(buf.into_owned())?;
- Ok(Source::new(id, text))
- })
- .clone()
- })
- }
-
- fn file(&self, id: FileId) -> FileResult {
- self.slot(id, |slot| {
- slot.buffer
- .get_or_init(|| {
- read(&system_path(id)?).map(|cow| match cow {
- Cow::Owned(buf) => buf.into(),
- Cow::Borrowed(buf) => Bytes::from_static(buf),
- })
- })
- .clone()
- })
- }
-
- fn font(&self, id: usize) -> Option {
- Some(self.fonts[id].clone())
- }
-
- fn today(&self, _: Option) -> Option {
- Some(Datetime::from_ymd(1970, 1, 1).unwrap())
- }
-}
-
-impl TestWorld {
- fn set(&mut self, path: &Path, text: String) -> Source {
- self.main = FileId::new(None, VirtualPath::new(path));
- let source = Source::new(self.main, text);
- self.slot(self.main, |slot| {
- slot.source = OnceLock::from(Ok(source.clone()));
- source
- })
- }
-
- fn slot(&self, id: FileId, f: F) -> T
- where
- F: FnOnce(&mut FileSlot) -> T,
- {
- f(self.slots.write().unwrap().entry(id).or_insert_with(|| FileSlot {
- source: OnceLock::new(),
- buffer: OnceLock::new(),
- }))
- }
-}
+fn main() {
+ setup();
-impl Clone for TestWorld {
- fn clone(&self) -> Self {
- Self {
- print: self.print,
- main: self.main,
- library: self.library.clone(),
- book: self.book.clone(),
- fonts: self.fonts.clone(),
- slots: RwLock::new(self.slots.read().unwrap().clone()),
- }
+ match &ARGS.command {
+ None => test(),
+ Some(Command::Clean) => std::fs::remove_dir_all(STORE_PATH).unwrap(),
}
}
-/// The file system path for a file ID.
-fn system_path(id: FileId) -> FileResult {
- let root: PathBuf = match id.package() {
- Some(spec) => format!("packages/{}-{}", spec.name, spec.version).into(),
- None => PathBuf::new(),
- };
-
- id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
-}
-
-/// Read a file.
-fn read(path: &Path) -> FileResult> {
- // Basically symlinks `assets/files` to `tests/files` so that the assets
- // are within the test project root.
- let resolved = path.to_path_buf();
- if let Ok(suffix) = path.strip_prefix("assets/") {
- return typst_dev_assets::get(&suffix.to_string_lossy())
- .map(Cow::Borrowed)
- .ok_or_else(|| FileError::NotFound(path.into()));
- }
-
- let f = |e| FileError::from_io(e, path);
- if fs::metadata(&resolved).map_err(f)?.is_dir() {
- Err(FileError::IsDirectory)
- } else {
- fs::read(&resolved).map(Cow::Owned).map_err(f)
- }
-}
+fn setup() {
+ // Make all paths relative to the workspace. That's nicer for IDEs when
+ // clicking on paths printed to the terminal.
+ std::env::set_current_dir("..").unwrap();
-/// Tests a test file and prints the result.
-///
-/// Also tests that the header of each test is written correctly.
-/// See [parse_part_metadata] for more details.
-fn test(
- world: &mut TestWorld,
- src_path: &Path,
- png_path: &Path,
- ref_path: &Path,
- pdf_path: Option<&Path>,
- svg_path: &Path,
- args: &Args,
-) -> bool {
- struct PanicGuard<'a>(&'a Path);
- impl Drop for PanicGuard<'_> {
- fn drop(&mut self) {
- if std::thread::panicking() {
- println!("Panicked in {}", self.0.display());
- }
- }
+ // Create the storage.
+ for ext in ["render", "pdf", "svg"] {
+ std::fs::create_dir_all(Path::new(STORE_PATH).join(ext)).unwrap();
}
- let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
- let text = fs::read_to_string(src_path).unwrap();
- let _guard = PanicGuard(name);
-
- let mut output = String::new();
- let mut ok = true;
- let mut updated = false;
- let mut pages = vec![];
- let mut line = 0;
- let mut header_configuration = None;
- let mut compare_ever = false;
- let mut rng = LinearShift::new();
-
- let parts: Vec<_> = text
- .split("\n---")
- .map(|s| s.strip_suffix('\r').unwrap_or(s))
- .collect();
-
- for (i, &part) in parts.iter().enumerate() {
- if let Some(x) = args.subtest {
- let x = usize::try_from(
- x.rem_euclid(isize::try_from(parts.len()).unwrap_or_default()),
- )
+ // Set up the thread pool.
+ if let Some(num_threads) = ARGS.num_threads {
+ rayon::ThreadPoolBuilder::new()
+ .num_threads(num_threads)
+ .build_global()
.unwrap();
- if x != i {
- writeln!(output, " Skipped subtest {i}.").unwrap();
- continue;
- }
- }
- let is_header = i == 0
- && parts.len() > 1
- && part
- .lines()
- .all(|s| s.starts_with("//") || s.chars().all(|c| c.is_whitespace()));
-
- if is_header {
- let source = Source::detached(part.to_string());
- let metadata = parse_part_metadata(&source, true);
- match metadata {
- Ok(metadata) => {
- header_configuration = Some(metadata.config);
- }
- Err(invalid_data) => {
- ok = false;
- writeln!(
- output,
- " Test {}: invalid metadata in header, failing the test:",
- name.display()
- )
- .unwrap();
- InvalidMetadata::write(
- invalid_data,
- &mut output,
- &mut |annotation, output| {
- print_annotation(output, &source, line, annotation)
- },
- );
- }
- }
- } else {
- let (part_ok, compare_here, part_frames) = test_part(
- &mut output,
- world,
- src_path,
- part.into(),
- line,
- i,
- header_configuration.as_ref().unwrap_or(&Default::default()),
- &mut rng,
- args.verbose,
- );
-
- ok &= part_ok;
- compare_ever |= compare_here;
- pages.extend(part_frames);
- }
-
- line += part.lines().count() + 1;
- }
-
- let document = Document { pages, ..Default::default() };
- if compare_ever {
- if let Some(pdf_path) = pdf_path {
- let pdf_data = typst_pdf::pdf(
- &document,
- Smart::Custom(&format!("typst-test: {}", name.display())),
- world.today(Some(0)),
- );
- fs::create_dir_all(pdf_path.parent().unwrap()).unwrap();
- fs::write(pdf_path, pdf_data).unwrap();
- }
-
- if world.print.frames {
- for frame in &document.pages {
- writeln!(output, "{frame:#?}\n").unwrap();
- }
- }
-
- let canvas = render(&document);
- fs::create_dir_all(png_path.parent().unwrap()).unwrap();
- canvas.save_png(png_path).unwrap();
-
- let svg = typst_svg::svg_merged(&document, Abs::pt(5.0));
-
- fs::create_dir_all(svg_path.parent().unwrap()).unwrap();
- std::fs::write(svg_path, svg.as_bytes()).unwrap();
-
- if let Ok(ref_pixmap) = sk::Pixmap::load_png(ref_path) {
- if canvas.width() != ref_pixmap.width()
- || canvas.height() != ref_pixmap.height()
- || canvas
- .data()
- .iter()
- .zip(ref_pixmap.data())
- .any(|(&a, &b)| a.abs_diff(b) > 2)
- {
- if args.update {
- update_image(png_path, ref_path);
- updated = true;
- } else {
- writeln!(output, " Does not match reference image.").unwrap();
- ok = false;
- }
- }
- } else if !document.pages.is_empty() {
- if args.update {
- update_image(png_path, ref_path);
- updated = true;
- } else {
- writeln!(output, " Failed to open reference image.").unwrap();
- ok = false;
- }
- }
}
-
- {
- let mut stdout = io::stdout().lock();
- stdout.write_all(name.to_string_lossy().as_bytes()).unwrap();
- if ok {
- writeln!(stdout, " ✔").unwrap();
- // Don't clear the line when in verbose mode or when the reference image
- // was updated, to show in the output which test had its image updated.
- if !updated && !args.verbose && stdout.is_terminal() {
- // ANSI escape codes: cursor moves up and clears the line.
- write!(stdout, "\x1b[1A\x1b[2K").unwrap();
- }
- } else {
- writeln!(stdout, " ❌").unwrap();
- }
- if updated {
- writeln!(stdout, " Updated reference image.").unwrap();
- }
- if !output.is_empty() {
- stdout.write_all(output.as_bytes()).unwrap();
- }
- }
-
- ok
}
-fn update_image(png_path: &Path, ref_path: &Path) {
- oxipng::optimize(
- &InFile::Path(png_path.to_owned()),
- &OutFile::from_path(ref_path.to_owned()),
- &Options::max_compression(),
- )
- .unwrap();
-}
-
-#[allow(clippy::too_many_arguments)]
-fn test_part(
- output: &mut String,
- world: &mut TestWorld,
- src_path: &Path,
- text: String,
- line: usize,
- i: usize,
- header_configuration: &TestConfig,
- rng: &mut LinearShift,
- verbose: bool,
-) -> (bool, bool, Vec) {
- let source = world.set(src_path, text);
- if world.print.syntax {
- writeln!(output, "Syntax Tree:\n{:#?}\n", source.root()).unwrap();
- }
-
- if world.print.model {
- print_model(world, &source, output);
- }
-
- let mut tracer = Tracer::new();
- let (mut frames, diagnostics) = match typst::compile(world, &mut tracer) {
- Ok(document) => (document.pages, tracer.warnings()),
+fn test() {
+ let (tests, skipped) = match crate::collect::collect() {
+ Ok(output) => output,
Err(errors) => {
- let mut warnings = tracer.warnings();
- warnings.extend(errors);
- (vec![], warnings)
- }
- };
-
- let metadata = parse_part_metadata(&source, false);
- match metadata {
- Ok(metadata) => {
- let mut ok = true;
- let compare_ref = metadata
- .config
- .compare_ref
- .unwrap_or(header_configuration.compare_ref.unwrap_or(true));
- let validate_hints = metadata
- .config
- .validate_hints
- .unwrap_or(header_configuration.validate_hints.unwrap_or(true));
- let validate_autocomplete = metadata
- .config
- .validate_autocomplete
- .unwrap_or(header_configuration.validate_autocomplete.unwrap_or(false));
-
- if verbose {
- writeln!(output, "Subtest {i} runs with compare_ref={compare_ref}; validate_hints={validate_hints}; validate_autocomplete={validate_autocomplete};").unwrap();
- }
- ok &= test_spans(output, source.root());
- ok &= test_reparse(output, source.text(), i, rng);
-
- // Don't retain frames if we don't want to compare with reference images.
- if !compare_ref {
- frames.clear();
- }
-
- // we never check autocomplete and error at the same time
-
- let diagnostic_annotations = metadata
- .annotations
- .iter()
- .filter(|a| {
- !matches!(
- a.kind,
- AnnotationKind::AutocompleteContains
- | AnnotationKind::AutocompleteExcludes
- )
- })
- .cloned()
- .collect::>();
-
- if validate_autocomplete {
- // warns and ignores diagnostics
- if !diagnostic_annotations.is_empty() {
- writeln!(
- output,
- " Subtest {i} contains diagnostics but is in autocomplete mode."
- )
- .unwrap();
- for annotation in diagnostic_annotations {
- write!(output, " Ignored | ").unwrap();
- print_annotation(output, &source, line, &annotation);
- }
- }
-
- test_autocomplete(
- output,
- world,
- &source,
- line,
- i,
- &mut ok,
- metadata.annotations.iter(),
- );
- } else {
- test_diagnostics(
- output,
- world,
- &source,
- line,
- i,
- &mut ok,
- validate_hints,
- diagnostics.iter(),
- &diagnostic_annotations,
- );
- }
-
- (ok, compare_ref, frames)
- }
- Err(invalid_data) => {
- writeln!(output, " Subtest {i} has invalid metadata, failing the test:")
- .unwrap();
- InvalidMetadata::write(
- invalid_data,
- output,
- &mut |annotation: &Annotation, output: &mut String| {
- print_annotation(output, &source, line, annotation)
- },
- );
-
- (false, false, frames)
- }
- }
-}
-
-#[allow(clippy::too_many_arguments)]
-fn test_autocomplete<'a>(
- output: &mut String,
- world: &mut TestWorld,
- source: &Source,
- line: usize,
- i: usize,
- ok: &mut bool,
- annotations: impl Iterator- ,
-) {
- for annotation in annotations.filter(|a| {
- matches!(
- a.kind,
- AnnotationKind::AutocompleteContains | AnnotationKind::AutocompleteExcludes
- )
- }) {
- // Ok cause we checked in parsing that range was Some for this annotation
- let cursor = annotation.range.as_ref().unwrap().start;
-
- // todo, use document if is_some to test labels autocomplete
- let completions = typst_ide::autocomplete(world, None, source, cursor, true)
- .map(|(_, c)| c)
- .unwrap_or_default()
- .into_iter()
- .map(|c| c.label.to_string())
- .collect::
>();
- let completions =
- completions.iter().map(|s| s.as_str()).collect::>();
-
- let must_contain_or_exclude = parse_string_list(&annotation.text);
- let missing =
- must_contain_or_exclude.difference(&completions).collect::>();
-
- if !missing.is_empty()
- && matches!(annotation.kind, AnnotationKind::AutocompleteContains)
- {
- writeln!(output, " Subtest {i} does not match expected completions.")
- .unwrap();
- write!(output, " for annotation | ").unwrap();
- print_annotation(output, source, line, annotation);
-
- write!(output, " Not contained // ").unwrap();
- for item in missing {
- write!(output, "{item:?}, ").unwrap()
+ eprintln!("failed to collect tests");
+ for error in errors {
+ eprintln!("❌ {error}");
}
- writeln!(output).unwrap();
- *ok = false;
+ std::process::exit(1);
}
-
- let undesired =
- must_contain_or_exclude.intersection(&completions).collect::>();
-
- if !undesired.is_empty()
- && matches!(annotation.kind, AnnotationKind::AutocompleteExcludes)
- {
- writeln!(output, " Subtest {i} does not match expected completions.")
- .unwrap();
- write!(output, " for annotation | ").unwrap();
- print_annotation(output, source, line, annotation);
-
- write!(output, " Not excluded // ").unwrap();
- for item in undesired {
- write!(output, "{item:?}, ").unwrap()
- }
- writeln!(output).unwrap();
- *ok = false;
- }
- }
-}
-
-#[allow(clippy::too_many_arguments)]
-fn test_diagnostics<'a>(
- output: &mut String,
- world: &mut TestWorld,
- source: &Source,
- line: usize,
- i: usize,
- ok: &mut bool,
- validate_hints: bool,
- diagnostics: impl Iterator- ,
- diagnostic_annotations: &HashSet
,
-) {
- // Map diagnostics to range and message format, discard traces and errors from
- // other files, collect hints.
- //
- // This has one caveat: due to the format of the expected hints, we can not
- // verify if a hint belongs to a diagnostic or not. That should be irrelevant
- // however, as the line of the hint is still verified.
- let mut actual_diagnostics = HashSet::new();
- for diagnostic in diagnostics {
- // Ignore diagnostics from other files.
- if diagnostic.span.id().is_some_and(|id| id != source.id()) {
- continue;
- }
-
- let annotation = Annotation {
- kind: match diagnostic.severity {
- Severity::Error => AnnotationKind::Error,
- Severity::Warning => AnnotationKind::Warning,
- },
- range: world.range(diagnostic.span),
- text: diagnostic.message.replace("\\", "/"),
- };
-
- if validate_hints {
- for hint in &diagnostic.hints {
- actual_diagnostics.insert(Annotation {
- kind: AnnotationKind::Hint,
- text: hint.clone(),
- range: annotation.range.clone(),
- });
- }
- }
-
- actual_diagnostics.insert(annotation);
- }
-
- // Basically symmetric_difference, but we need to know where an item is coming from.
- let mut unexpected_outputs = actual_diagnostics
- .difference(diagnostic_annotations)
- .collect::>();
- let mut missing_outputs = diagnostic_annotations
- .difference(&actual_diagnostics)
- .collect::>();
-
- unexpected_outputs.sort_by_key(|&v| v.range.as_ref().map(|r| r.start));
- missing_outputs.sort_by_key(|&v| v.range.as_ref().map(|r| r.start));
-
- // This prints all unexpected emits first, then all missing emits.
- // Is this reasonable or subject to change?
- if !(unexpected_outputs.is_empty() && missing_outputs.is_empty()) {
- writeln!(output, " Subtest {i} does not match expected errors.").unwrap();
- *ok = false;
-
- for unexpected in unexpected_outputs {
- write!(output, " Not annotated // ").unwrap();
- print_annotation(output, source, line, unexpected)
- }
-
- for missing in missing_outputs {
- write!(output, " Not emitted // ").unwrap();
- print_annotation(output, source, line, missing)
- }
- }
-}
-
-fn print_model(world: &mut TestWorld, source: &Source, output: &mut String) {
- let world = (world as &dyn World).track();
- let route = typst::engine::Route::default();
- let mut tracer = typst::eval::Tracer::new();
-
- let module =
- typst::eval::eval(world, route.track(), tracer.track_mut(), source).unwrap();
- writeln!(output, "Model:\n{:#?}\n", module.content()).unwrap();
-}
-
-fn print_annotation(
- output: &mut String,
- source: &Source,
- line: usize,
- annotation: &Annotation,
-) {
- let Annotation { range, text, kind } = annotation;
- write!(output, "{kind}: ").unwrap();
- if let Some(range) = range {
- let start_line = 1 + line + source.byte_to_line(range.start).unwrap();
- let start_col = 1 + source.byte_to_column(range.start).unwrap();
- let end_line = 1 + line + source.byte_to_line(range.end).unwrap();
- let end_col = 1 + source.byte_to_column(range.end).unwrap();
- write!(output, "{start_line}:{start_col}-{end_line}:{end_col} ").unwrap();
- }
- writeln!(output, "{text}").unwrap();
-}
-
-/// Pseudorandomly edit the source file and test whether a reparse produces the
-/// same result as a clean parse.
-///
-/// The method will first inject 10 strings once every 400 source characters
-/// and then select 5 leaf node boundaries to inject an additional, randomly
-/// chosen string from the injection list.
-fn test_reparse(
- output: &mut String,
- text: &str,
- i: usize,
- rng: &mut LinearShift,
-) -> bool {
- let supplements = [
- "[",
- "]",
- "{",
- "}",
- "(",
- ")",
- "#rect()",
- "a word",
- ", a: 1",
- "10.0",
- ":",
- "if i == 0 {true}",
- "for",
- "* hello *",
- "//",
- "/*",
- "\\u{12e4}",
- "```typst",
- " ",
- "trees",
- "\\",
- "$ a $",
- "2.",
- "-",
- "5",
- ];
-
- let mut ok = true;
- let mut apply = |replace: Range, with| {
- let mut incr_source = Source::detached(text);
- if incr_source.root().len() != text.len() {
- println!(
- " Subtest {i} tree length {} does not match string length {} ❌",
- incr_source.root().len(),
- text.len(),
- );
- return false;
- }
-
- incr_source.edit(replace.clone(), with);
-
- let edited_src = incr_source.text();
- let ref_source = Source::detached(edited_src);
- let ref_root = ref_source.root();
- let incr_root = incr_source.root();
-
- // Ensures that the span numbering invariants hold.
- let spans_ok = test_spans(output, ref_root) && test_spans(output, incr_root);
-
- // Ensure that the reference and incremental trees are the same.
- let tree_ok = ref_root.spanless_eq(incr_root);
-
- if !tree_ok {
- writeln!(
- output,
- " Subtest {i} reparse differs from clean parse when inserting '{with}' at {}-{} ❌\n",
- replace.start, replace.end,
- ).unwrap();
- writeln!(output, " Expected reference tree:\n{ref_root:#?}\n").unwrap();
- writeln!(output, " Found incremental tree:\n{incr_root:#?}").unwrap();
- writeln!(
- output,
- " Full source ({}):\n\"{edited_src:?}\"",
- edited_src.len()
- )
- .unwrap();
- }
-
- spans_ok && tree_ok
- };
-
- let mut pick = |range: Range| {
- let ratio = rng.next();
- (range.start as f64 + ratio * (range.end - range.start) as f64).floor() as usize
};
- let insertions = (text.len() as f64 / 400.0).ceil() as usize;
- for _ in 0..insertions {
- let supplement = supplements[pick(0..supplements.len())];
- let start = pick(0..text.len());
- let end = pick(start..text.len());
-
- if !text.is_char_boundary(start) || !text.is_char_boundary(end) {
- continue;
- }
-
- ok &= apply(start..end, supplement);
- }
-
- let source = Source::detached(text);
- let leafs = leafs(source.root());
- let start = source.find(leafs[pick(0..leafs.len())].span()).unwrap().offset();
- let supplement = supplements[pick(0..supplements.len())];
- ok &= apply(start..start, supplement);
-
- ok
-}
-
-/// Returns all leaf descendants of a node (may include itself).
-fn leafs(node: &SyntaxNode) -> Vec {
- if node.children().len() == 0 {
- vec![node.clone()]
- } else {
- node.children().flat_map(leafs).collect()
- }
-}
-
-/// Ensure that all spans are properly ordered (and therefore unique).
-#[track_caller]
-fn test_spans(output: &mut String, root: &SyntaxNode) -> bool {
- test_spans_impl(output, root, 0..u64::MAX)
-}
-
-#[track_caller]
-fn test_spans_impl(output: &mut String, node: &SyntaxNode, within: Range) -> bool {
- if !within.contains(&node.span().number()) {
- writeln!(output, " Node: {node:#?}").unwrap();
- writeln!(
- output,
- " Wrong span order: {} not in {within:?} ❌",
- node.span().number()
- )
- .unwrap();
- }
-
- let start = node.span().number() + 1;
- let mut children = node.children().peekable();
- while let Some(child) = children.next() {
- let end = children.peek().map_or(within.end, |next| next.span().number());
- if !test_spans_impl(output, child, start..end) {
- return false;
- }
- }
-
- true
-}
-
-/// Draw all frames into one image with padding in between.
-fn render(document: &Document) -> sk::Pixmap {
- let pixel_per_pt = 2.0;
- let padding = Abs::pt(5.0);
-
- for page in &document.pages {
- let limit = Abs::cm(100.0);
- if page.frame.width() > limit || page.frame.height() > limit {
- panic!("overlarge frame: {:?}", page.frame.size());
- }
- }
-
- let mut pixmap = typst_render::render_merged(
- document,
- pixel_per_pt,
- Color::WHITE,
- padding,
- Color::BLACK,
- );
-
- let padding = (pixel_per_pt * padding.to_pt() as f32).round();
- let [x, mut y] = [padding; 2];
- for page in &document.pages {
- let ts =
- sk::Transform::from_scale(pixel_per_pt, pixel_per_pt).post_translate(x, y);
- render_links(&mut pixmap, ts, &page.frame);
- y += (pixel_per_pt * page.frame.height().to_pt() as f32).round().max(1.0)
- + padding;
- }
-
- pixmap
-}
-
-/// Draw extra boxes for links so we can see whether they are there.
-fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) {
- for (pos, item) in frame.items() {
- let ts = ts.pre_translate(pos.x.to_pt() as f32, pos.y.to_pt() as f32);
- match *item {
- FrameItem::Group(ref group) => {
- let ts = ts.pre_concat(to_sk_transform(&group.transform));
- render_links(canvas, ts, &group.frame);
- }
- FrameItem::Meta(Meta::Link(_), size) => {
- let w = size.x.to_pt() as f32;
- let h = size.y.to_pt() as f32;
- let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
- let mut paint = sk::Paint::default();
- paint.set_color_rgba8(40, 54, 99, 40);
- canvas.fill_rect(rect, &paint, ts, None);
- }
- _ => {}
- }
- }
-}
-
-fn to_sk_transform(transform: &Transform) -> sk::Transform {
- let Transform { sx, ky, kx, sy, tx, ty } = *transform;
- sk::Transform::from_row(
- sx.get() as _,
- ky.get() as _,
- kx.get() as _,
- sy.get() as _,
- tx.to_pt() as f32,
- ty.to_pt() as f32,
- )
-}
-
-/// A Linear-feedback shift register using XOR as its shifting function.
-/// Can be used as PRNG.
-struct LinearShift(u64);
-
-impl LinearShift {
- /// Initialize the shift register with a pre-set seed.
- pub fn new() -> Self {
- Self(0xACE5)
- }
-
- /// Return a pseudo-random number between `0.0` and `1.0`.
- pub fn next(&mut self) -> f64 {
- self.0 ^= self.0 >> 3;
- self.0 ^= self.0 << 14;
- self.0 ^= self.0 >> 28;
- self.0 ^= self.0 << 36;
- self.0 ^= self.0 >> 52;
- self.0 as f64 / u64::MAX as f64
+ let filtered = tests.len();
+ if filtered == 0 {
+ eprintln!("no test selected");
+ return;
+ }
+
+ // Run the tests.
+ let logger = Mutex::new(Logger::new(filtered, skipped));
+ std::thread::scope(|scope| {
+ let logger = &logger;
+ let (sender, receiver) = std::sync::mpsc::channel();
+
+ // Regularly refresh the logger in case we make no progress.
+ scope.spawn(move || {
+ while receiver.recv_timeout(Duration::from_millis(500)).is_err() {
+ logger.lock().refresh();
+ }
+ });
+
+ // Run the tests.
+ //
+ // We use `par_bridge` instead of `par_iter` because the former
+ // results in a stack overflow during PDF export. Probably related
+ // to `typst::util::Deferred` yielding.
+ tests.iter().par_bridge().for_each(|test| {
+ logger.lock().start(test);
+ let result = std::panic::catch_unwind(|| run::run(test));
+ logger.lock().end(test, result);
+ });
+
+ sender.send(()).unwrap();
+ });
+
+ let passed = logger.into_inner().finish();
+ if !passed {
+ std::process::exit(1);
}
}
diff --git a/tests/src/world.rs b/tests/src/world.rs
new file mode 100644
index 0000000000..86ee8da6ac
--- /dev/null
+++ b/tests/src/world.rs
@@ -0,0 +1,229 @@
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::fs;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::sync::OnceLock;
+
+use comemo::Prehashed;
+use once_cell::sync::Lazy;
+use parking_lot::Mutex;
+use typst::diag::{bail, FileError, FileResult, StrResult};
+use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value};
+use typst::layout::{Abs, Margin, PageElem};
+use typst::syntax::{FileId, Source};
+use typst::text::{Font, FontBook, TextElem, TextSize};
+use typst::visualize::Color;
+use typst::{Library, World};
+
+/// A world that provides access to the tests environment.
+#[derive(Clone)]
+pub struct TestWorld {
+ main: Source,
+ base: &'static TestBase,
+}
+
+impl TestWorld {
+ /// Create a new world for a single test.
+ ///
+ /// This is cheap because the shared base for all test runs is lazily
+ /// initialized just once.
+ pub fn new(source: Source) -> Self {
+ static BASE: Lazy = Lazy::new(TestBase::default);
+ Self { main: source, base: &*BASE }
+ }
+}
+
+impl World for TestWorld {
+ fn library(&self) -> &Prehashed {
+ &self.base.library
+ }
+
+ fn book(&self) -> &Prehashed {
+ &self.base.book
+ }
+
+ fn main(&self) -> Source {
+ self.main.clone()
+ }
+
+ fn source(&self, id: FileId) -> FileResult {
+ if id == self.main.id() {
+ Ok(self.main.clone())
+ } else {
+ self.slot(id, FileSlot::source)
+ }
+ }
+
+ fn file(&self, id: FileId) -> FileResult {
+ self.slot(id, FileSlot::file)
+ }
+
+ fn font(&self, index: usize) -> Option {
+ Some(self.base.fonts[index].clone())
+ }
+
+ fn today(&self, _: Option) -> Option {
+ Some(Datetime::from_ymd(1970, 1, 1).unwrap())
+ }
+}
+
+impl TestWorld {
+ /// Access the canonical slot for the given file id.
+ fn slot(&self, id: FileId, f: F) -> T
+ where
+ F: FnOnce(&mut FileSlot) -> T,
+ {
+ let mut map = self.base.slots.lock();
+ f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
+ }
+}
+
+/// Shared foundation of all test worlds.
+struct TestBase {
+ library: Prehashed,
+ book: Prehashed,
+ fonts: Vec,
+ slots: Mutex>,
+}
+
+impl Default for TestBase {
+ fn default() -> Self {
+ let fonts: Vec<_> = typst_assets::fonts()
+ .chain(typst_dev_assets::fonts())
+ .flat_map(|data| Font::iter(Bytes::from_static(data)))
+ .collect();
+
+ Self {
+ library: Prehashed::new(library()),
+ book: Prehashed::new(FontBook::from_fonts(&fonts)),
+ fonts,
+ slots: Mutex::new(HashMap::new()),
+ }
+ }
+}
+
+/// Holds the processed data for a file ID.
+#[derive(Clone)]
+struct FileSlot {
+ id: FileId,
+ source: OnceLock>,
+ file: OnceLock>,
+}
+
+impl FileSlot {
+ /// Create a new file slot.
+ fn new(id: FileId) -> Self {
+ Self { id, file: OnceLock::new(), source: OnceLock::new() }
+ }
+
+ /// Retrieve the source for this file.
+ fn source(&mut self) -> FileResult {
+ self.source
+ .get_or_init(|| {
+ let buf = read(&system_path(self.id)?)?;
+ let text = String::from_utf8(buf.into_owned())?;
+ Ok(Source::new(self.id, text))
+ })
+ .clone()
+ }
+
+ /// Retrieve the file's bytes.
+ fn file(&mut self) -> FileResult {
+ self.file
+ .get_or_init(|| {
+ read(&system_path(self.id)?).map(|cow| match cow {
+ Cow::Owned(buf) => buf.into(),
+ Cow::Borrowed(buf) => Bytes::from_static(buf),
+ })
+ })
+ .clone()
+ }
+}
+
+/// The file system path for a file ID.
+fn system_path(id: FileId) -> FileResult {
+ let root: PathBuf = match id.package() {
+ Some(spec) => format!("tests/packages/{}-{}", spec.name, spec.version).into(),
+ None => PathBuf::new(),
+ };
+
+ id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
+}
+
+/// Read a file.
+fn read(path: &Path) -> FileResult> {
+ // Resolve asset.
+ if let Ok(suffix) = path.strip_prefix("assets/") {
+ return typst_dev_assets::get(&suffix.to_string_lossy())
+ .map(Cow::Borrowed)
+ .ok_or_else(|| FileError::NotFound(path.into()));
+ }
+
+ let f = |e| FileError::from_io(e, path);
+ if fs::metadata(path).map_err(f)?.is_dir() {
+ Err(FileError::IsDirectory)
+ } else {
+ fs::read(path).map(Cow::Owned).map_err(f)
+ }
+}
+
+/// The extended standard library for testing.
+fn library() -> Library {
+ // Set page width to 120pt with 10pt margins, so that the inner page is
+ // exactly 100pt wide. Page height is unbounded and font size is 10pt so
+ // that it multiplies to nice round numbers.
+ let mut lib = Library::default();
+
+ #[func]
+ fn test(lhs: Value, rhs: Value) -> StrResult {
+ if lhs != rhs {
+ bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
+ }
+ Ok(NoneValue)
+ }
+
+ #[func]
+ fn test_repr(lhs: Value, rhs: Value) -> StrResult {
+ if lhs.repr() != rhs.repr() {
+ bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
+ }
+ Ok(NoneValue)
+ }
+
+ #[func]
+ fn print(#[variadic] values: Vec) -> NoneValue {
+ let mut out = std::io::stdout().lock();
+ write!(out, "> ").unwrap();
+ for (i, value) in values.into_iter().enumerate() {
+ if i > 0 {
+ write!(out, ", ").unwrap();
+ }
+ write!(out, "{value:?}").unwrap();
+ }
+ writeln!(out).unwrap();
+ NoneValue
+ }
+
+ // Hook up helpers into the global scope.
+ lib.global.scope_mut().define_func::();
+ lib.global.scope_mut().define_func::();
+ lib.global.scope_mut().define_func::();
+ lib.global
+ .scope_mut()
+ .define("conifer", Color::from_u8(0x9f, 0xEB, 0x52, 0xFF));
+ lib.global
+ .scope_mut()
+ .define("forest", Color::from_u8(0x43, 0xA1, 0x27, 0xFF));
+
+ // Hook up default styles.
+ lib.styles
+ .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
+ lib.styles.set(PageElem::set_height(Smart::Auto));
+ lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
+ Abs::pt(10.0).into(),
+ )))));
+ lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
+
+ lib
+}
diff --git a/tests/suite/foundations/array.typ b/tests/suite/foundations/array.typ
new file mode 100644
index 0000000000..d4a2f151ee
--- /dev/null
+++ b/tests/suite/foundations/array.typ
@@ -0,0 +1,472 @@
+// Test arrays.
+
+--- array-basic-syntax ---
+#set page(width: 150pt)
+
+// Empty.
+#()
+
+// Not an array, just a parenthesized expression.
+#(1)
+
+// One item and trailing comma.
+#(-1,)
+
+// No trailing comma.
+#(true, false)
+
+// Multiple lines and items and trailing comma.
+#("1"
+ , rgb("002")
+ ,)
+
+--- array-bad-token ---
+// Error: 4-6 unexpected end of block comment
+#(1*/2)
+
+--- array-bad-number-suffix ---
+// Error: 6-8 invalid number suffix: u
+#(1, 1u 2)
+
+--- array-leading-comma ---
+// Error: 3-4 unexpected comma
+#(,1)
+
+--- array-incomplete-pair ---
+// Missing expression makes named pair incomplete, making this an empty array.
+// Error: 5 expected expression
+#(a:)
+
+--- array-named-pair ---
+// Named pair after this is already identified as an array.
+// Error: 6-10 expected expression, found named pair
+#(1, b: 2)
+
+--- array-keyed-pair ---
+// Keyed pair after this is already identified as an array.
+// Error: 6-14 expected expression, found keyed pair
+#(1, "key": 2)
+
+--- array-bad-conversion-from-string ---
+// Error: 8-15 expected array, bytes, or version, found string
+#array("hello")
+
+--- spread-into-array ---
+// Test spreading into array and dictionary.
+#{
+ let l = (1, 2, 3)
+ let r = (5, 6, 7)
+ test((..l, 4, ..r), range(1, 8))
+ test((..none), ())
+}
+
+--- spread-dict-into-array ---
+// Error: 9-17 cannot spread dictionary into array
+#(1, 2, ..(a: 1))
+
+--- array-len ---
+// Test the `len` method.
+#test(().len(), 0)
+#test(("A", "B", "C").len(), 3)
+
+--- array-at-lvalue ---
+// Test lvalue and rvalue access.
+#{
+ let array = (1, 2)
+ array.at(1) += 5 + array.at(0)
+ test(array, (1, 8))
+}
+
+--- array-first-and-at-lvalue ---
+// Test different lvalue method.
+#{
+ let array = (1, 2, 3)
+ array.first() = 7
+ array.at(1) *= 8
+ test(array, (7, 16, 3))
+}
+
+--- array-at-out-of-bounds ---
+// Test rvalue out of bounds.
+// Error: 2-17 array index out of bounds (index: 5, len: 3) and no default value was specified
+#(1, 2, 3).at(5)
+
+--- array-at-out-of-bounds-negative ---
+// Error: 2-18 array index out of bounds (index: -4, len: 3) and no default value was specified
+#(1, 2, 3).at(-4)
+
+--- array-at-out-of-bounds-lvalue ---
+// Test lvalue out of bounds.
+#{
+ let array = (1, 2, 3)
+ // Error: 3-14 array index out of bounds (index: 3, len: 3)
+ array.at(3) = 5
+}
+
+--- array-at-with-default ---
+// Test default value.
+#test((1, 2, 3).at(2, default: 5), 3)
+#test((1, 2, 3).at(3, default: 5), 5)
+
+--- array-remove-with-default ---
+// Test remove with default value.
+
+#{
+ let array = (1, 2, 3)
+ test(array.remove(2, default: 5), 3)
+}
+
+#{
+ let array = (1, 2, 3)
+ test(array.remove(3, default: 5), 5)
+}
+
+--- array-range ---
+// Test the `range` function.
+#test(range(4), (0, 1, 2, 3))
+#test(range(1, 4), (1, 2, 3))
+#test(range(-4, 2), (-4, -3, -2, -1, 0, 1))
+#test(range(10, 5), ())
+#test(range(10, step: 3), (0, 3, 6, 9))
+#test(range(1, 4, step: 1), (1, 2, 3))
+#test(range(1, 8, step: 2), (1, 3, 5, 7))
+#test(range(5, 2, step: -1), (5, 4, 3))
+#test(range(10, 0, step: -3), (10, 7, 4, 1))
+
+--- array-range-end-missing ---
+// Error: 2-9 missing argument: end
+#range()
+
+--- array-range-float-invalid ---
+// Error: 11-14 expected integer, found float
+#range(1, 2.0)
+
+--- array-range-bad-step-type ---
+// Error: 17-22 expected integer, found string
+#range(4, step: "one")
+
+--- array-range-step-zero ---
+// Error: 18-19 number must not be zero
+#range(10, step: 0)
+
+--- array-bad-method-lvalue ---
+// Test bad lvalue.
+// Error: 2:3-2:14 cannot mutate a temporary value
+#let array = (1, 2, 3)
+#(array.len() = 4)
+
+--- array-unknown-method-lvalue ---
+// Test bad lvalue.
+// Error: 2:9-2:13 type array has no method `yolo`
+#let array = (1, 2, 3)
+#(array.yolo() = 4)
+
+--- array-negative-indices ---
+// Test negative indices.
+#{
+ let array = (1, 2, 3, 4)
+ test(array.at(0), 1)
+ test(array.at(-1), 4)
+ test(array.at(-2), 3)
+ test(array.at(-3), 2)
+ test(array.at(-4), 1)
+}
+
+--- array-first-and-last ---
+// The the `first` and `last` methods.
+#test((1,).first(), 1)
+#test((2,).last(), 2)
+#test((1, 2, 3).first(), 1)
+#test((1, 2, 3).last(), 3)
+
+--- array-first-empty ---
+// Error: 2-12 array is empty
+#().first()
+
+--- array-last-empty ---
+// Error: 2-11 array is empty
+#().last()
+
+--- array-push-and-pop ---
+// Test the `push` and `pop` methods.
+#{
+ let tasks = (a: (1, 2, 3), b: (4, 5, 6))
+ test(tasks.at("a").pop(), 3)
+ tasks.b.push(7)
+ test(tasks.a, (1, 2))
+ test(tasks.at("b"), (4, 5, 6, 7))
+}
+
+--- array-insert-and-remove ---
+// Test the `insert` and `remove` methods.
+#{
+ let array = (0, 1, 2, 4, 5)
+ array.insert(3, 3)
+ test(array, range(6))
+ array.remove(1)
+ test(array, (0, 2, 3, 4, 5))
+}
+
+--- array-insert-missing-index ---
+// Error: 2:2-2:18 missing argument: index
+#let numbers = ()
+#numbers.insert()
+
+--- array-slice ---
+// Test the `slice` method.
+#test((1, 2, 3, 4).slice(2), (3, 4))
+#test(range(10).slice(2, 6), (2, 3, 4, 5))
+#test(range(10).slice(4, count: 3), (4, 5, 6))
+#test(range(10).slice(-5, count: 2), (5, 6))
+#test((1, 2, 3).slice(2, -2), ())
+#test((1, 2, 3).slice(-2, 2), (2,))
+#test((1, 2, 3).slice(-3, 2), (1, 2))
+#test("ABCD".split("").slice(1, -1).join("-"), "A-B-C-D")
+
+--- array-slice-out-of-bounds ---
+// Error: 2-30 array index out of bounds (index: 12, len: 10)
+#range(10).slice(9, count: 3)
+
+--- array-slice-out-of-bounds-negative ---
+// Error: 2-24 array index out of bounds (index: -4, len: 3)
+#(1, 2, 3).slice(0, -4)
+
+--- array-position ---
+// Test the `position` method.
+#test(("Hi", "❤️", "Love").position(s => s == "❤️"), 1)
+#test(("Bye", "💘", "Apart").position(s => s == "❤️"), none)
+#test(("A", "B", "CDEF", "G").position(v => v.len() > 2), 2)
+
+--- array-filter ---
+// Test the `filter` method.
+#test(().filter(calc.even), ())
+#test((1, 2, 3, 4).filter(calc.even), (2, 4))
+#test((7, 3, 2, 5, 1).filter(x => x < 5), (3, 2, 1))
+
+--- array-map ---
+// Test the `map` method.
+#test(().map(x => x * 2), ())
+#test((2, 3).map(x => x * 2), (4, 6))
+
+--- array-fold ---
+// Test the `fold` method.
+#test(().fold("hi", grid), "hi")
+#test((1, 2, 3, 4).fold(0, (s, x) => s + x), 10)
+
+--- array-fold-closure-without-params ---
+// Error: 20-22 unexpected argument
+#(1, 2, 3).fold(0, () => none)
+
+--- array-sum ---
+// Test the `sum` method.
+#test(().sum(default: 0), 0)
+#test(().sum(default: []), [])
+#test((1, 2, 3).sum(), 6)
+
+--- array-sum-empty ---
+// Error: 2-10 cannot calculate sum of empty array with no default
+#().sum()
+
+--- array-product ---
+// Test the `product` method.
+#test(().product(default: 0), 0)
+#test(().product(default: []), [])
+#test(([ab], 3).product(), [ab]*3)
+#test((1, 2, 3).product(), 6)
+
+--- array-product-empty ---
+// Error: 2-14 cannot calculate product of empty array with no default
+#().product()
+
+--- array-rev ---
+// Test the `rev` method.
+#test(range(3).rev(), (2, 1, 0))
+
+--- array-join ---
+// Test the `join` method.
+#test(().join(), none)
+#test((1,).join(), 1)
+#test(("a", "b", "c").join(), "abc")
+#test("(" + ("a", "b", "c").join(", ") + ")", "(a, b, c)")
+
+--- array-join-bad-values ---
+// Error: 2-22 cannot join boolean with boolean
+#(true, false).join()
+
+--- array-join-bad-separator ---
+// Error: 2-20 cannot join string with integer
+#("a", "b").join(1)
+
+--- array-join-content ---
+// Test joining content.
+#([One], [Two], [Three]).join([, ], last: [ and ]).
+
+--- array-intersperse ---
+// Test the `intersperse` method
+#test(().intersperse("a"), ())
+#test((1,).intersperse("a"), (1,))
+#test((1, 2).intersperse("a"), (1, "a", 2))
+#test((1, 2, "b").intersperse("a"), (1, "a", 2, "a", "b"))
+
+--- array-chunks ---
+// Test the `chunks` method.
+#test(().chunks(10), ())
+#test((1, 2, 3).chunks(10), ((1, 2, 3),))
+#test((1, 2, 3, 4, 5, 6).chunks(3), ((1, 2, 3), (4, 5, 6)))
+#test((1, 2, 3, 4, 5, 6, 7, 8).chunks(3), ((1, 2, 3), (4, 5, 6), (7, 8)))
+
+#test(().chunks(10, exact: true), ())
+#test((1, 2, 3).chunks(10, exact: true), ())
+#test((1, 2, 3, 4, 5, 6).chunks(3, exact: true), ((1, 2, 3), (4, 5, 6)))
+#test((1, 2, 3, 4, 5, 6, 7, 8).chunks(3, exact: true), ((1, 2, 3), (4, 5, 6)))
+
+--- array-chunks-size-zero ---
+// Error: 19-20 number must be positive
+#(1, 2, 3).chunks(0)
+
+--- array-chunks-size-negative ---
+// Error: 19-21 number must be positive
+#(1, 2, 3).chunks(-5)
+
+--- array-sorted ---
+// Test the `sorted` method.
+#test(().sorted(), ())
+#test(().sorted(key: x => x), ())
+#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10)
+#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the"))
+#test(("I", "the", "hi", "text").sorted(key: x => x), ("I", "hi", "text", "the"))
+#test(("I", "the", "hi", "text").sorted(key: x => x.len()), ("I", "hi", "the", "text"))
+#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10))
+#test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x), (-10, -7, -5, 1, 2, 2, 3, 6, 8))
+#test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x * x), (1, 2, 2, 3, -5, 6, -7, 8, -10))
+
+--- array-sorted-key-function-positional-1 ---
+// Error: 12-18 unexpected argument
+#().sorted(x => x)
+
+--- array-zip ---
+// Test the `zip` method.
+#test(().zip(()), ())
+#test((1,).zip(()), ())
+#test((1,).zip((2,)), ((1, 2),))
+#test((1, 2).zip((3, 4)), ((1, 3), (2, 4)))
+#test((1, 2, 3, 4).zip((5, 6)), ((1, 5), (2, 6)))
+#test(((1, 2), 3).zip((4, 5)), (((1, 2), 4), (3, 5)))
+#test((1, "hi").zip((true, false)), ((1, true), ("hi", false)))
+#test((1, 2, 3).zip((3, 4, 5), (6, 7, 8)), ((1, 3, 6), (2, 4, 7), (3, 5, 8)))
+#test(().zip((), ()), ())
+#test((1,).zip((2,), (3,)), ((1, 2, 3),))
+#test((1, 2, 3).zip(), ((1,), (2,), (3,)))
+#test(array.zip(()), ())
+
+--- array-enumerate ---
+// Test the `enumerate` method.
+#test(().enumerate(), ())
+#test(().enumerate(start: 5), ())
+#test(("a", "b", "c").enumerate(), ((0, "a"), (1, "b"), (2, "c")))
+#test(("a", "b", "c").enumerate(start: 1), ((1, "a"), (2, "b"), (3, "c")))
+#test(("a", "b", "c").enumerate(start: 42), ((42, "a"), (43, "b"), (44, "c")))
+#test(("a", "b", "c").enumerate(start: -7), ((-7, "a"), (-6, "b"), (-5, "c")))
+
+--- array-dedup ---
+// Test the `dedup` method.
+#test(().dedup(), ())
+#test((1,).dedup(), (1,))
+#test((1, 1).dedup(), (1,))
+#test((1, 2, 1).dedup(), (1, 2))
+#test(("Jane", "John", "Eric").dedup(), ("Jane", "John", "Eric"))
+#test(("Jane", "John", "Eric", "John").dedup(), ("Jane", "John", "Eric"))
+
+--- array-dedup-key ---
+// Test the `dedup` method with the `key` argument.
+#test((1, 2, 3, 4, 5, 6).dedup(key: x => calc.rem(x, 2)), (1, 2))
+#test((1, 2, 3, 4, 5, 6).dedup(key: x => calc.rem(x, 3)), (1, 2, 3))
+#test(("Hello", "World", "Hi", "There").dedup(key: x => x.len()), ("Hello", "Hi"))
+#test(("Hello", "World", "Hi", "There").dedup(key: x => x.at(0)), ("Hello", "World", "There"))
+
+--- array-zip-positional-and-named-argument ---
+// Error: 13-30 unexpected argument: val
+#().zip((), val: "applicable")
+
+--- array-sorted-bad-key ---
+// Error: 32-37 cannot divide by zero
+#(1, 2, 0, 3).sorted(key: x => 5 / x)
+
+--- array-sorted-uncomparable ---
+// Error: 2-26 cannot compare content and content
+#([Hi], [There]).sorted()
+
+--- array-sorted-uncomparable-lengths ---
+// Error: 2-26 cannot compare 3em with 2pt
+#(1pt, 2pt, 3em).sorted()
+
+--- array-sorted-key-function-positional-2 ---
+// Error: 42-52 unexpected argument
+#((k: "a", v: 2), (k: "b", v: 1)).sorted(it => it.v)
+
+--- issue-3014-mix-array-dictionary ---
+// Error: 8-17 expected expression, found named pair
+#(box, fill: red)
+
+--- issue-3154-array-first-empty ---
+#{
+ let array = ()
+ // Error: 3-16 array is empty
+ array.first()
+}
+
+--- issue-3154-array-first-mutable-empty ---
+#{
+ let array = ()
+ // Error: 3-16 array is empty
+ array.first() = 9
+}
+
+--- issue-3154-array-last-empty ---
+#{
+ let array = ()
+ // Error: 3-15 array is empty
+ array.last()
+}
+
+--- issue-3154-array-last-mutable-empty ---
+#{
+ let array = ()
+ // Error: 3-15 array is empty
+ array.last() = 9
+}
+
+--- issue-3154-array-at-out-of-bounds ---
+#{
+ let array = (1,)
+ // Error: 3-14 array index out of bounds (index: 1, len: 1) and no default value was specified
+ array.at(1)
+}
+
+--- issue-3154-array-at-out-of-bounds-default ---
+#{
+ let array = (1,)
+ test(array.at(1, default: 0), 0)
+}
+
+--- issue-3154-array-at-out-of-bounds-mutable ---
+#{
+ let array = (1,)
+ // Error: 3-14 array index out of bounds (index: 1, len: 1)
+ array.at(1) = 9
+}
+
+--- issue-3154-array-at-out-of-bounds-mutable-default ---
+#{
+ let array = (1,)
+ // Error: 3-26 array index out of bounds (index: 1, len: 1)
+ array.at(1, default: 0) = 9
+}
+
+--- array-unopened ---
+// Error: 2-3 unclosed delimiter
+#{)}
+
+--- array-unclosed ---
+// Error: 3-4 unclosed delimiter
+#{(}
diff --git a/tests/suite/foundations/assert.typ b/tests/suite/foundations/assert.typ
new file mode 100644
index 0000000000..5de0f3878a
--- /dev/null
+++ b/tests/suite/foundations/assert.typ
@@ -0,0 +1,40 @@
+--- assert-fail ---
+// Test failing assertions.
+// Error: 2-16 assertion failed
+#assert(1 == 2)
+
+--- assert-fail-message ---
+// Test failing assertions.
+// Error: 2-51 assertion failed: two is smaller than one
+#assert(2 < 1, message: "two is smaller than one")
+
+--- assert-bad-type ---
+// Test failing assertions.
+// Error: 9-15 expected boolean, found string
+#assert("true")
+
+--- assert-eq-fail ---
+// Test failing assertions.
+// Error: 2-19 equality assertion failed: value 10 was not equal to 11
+#assert.eq(10, 11)
+
+--- assert-eq-fail-message ---
+// Test failing assertions.
+// Error: 2-55 equality assertion failed: 10 and 12 are not equal
+#assert.eq(10, 12, message: "10 and 12 are not equal")
+
+--- assert-ne-fail ---
+// Test failing assertions.
+// Error: 2-19 inequality assertion failed: value 11 was equal to 11
+#assert.ne(11, 11)
+
+--- assert-ne-fail-message ---
+// Test failing assertions.
+// Error: 2-57 inequality assertion failed: must be different from 11
+#assert.ne(11, 11, message: "must be different from 11")
+
+--- assert-success ---
+// Test successful assertions.
+#assert(5 > 3)
+#assert.eq(15, 15)
+#assert.ne(10, 12)
diff --git a/tests/typ/compiler/bytes.typ b/tests/suite/foundations/bytes.typ
similarity index 80%
rename from tests/typ/compiler/bytes.typ
rename to tests/suite/foundations/bytes.typ
index a9249bddd0..c708927804 100644
--- a/tests/typ/compiler/bytes.typ
+++ b/tests/suite/foundations/bytes.typ
@@ -1,32 +1,31 @@
// Test the bytes type.
-// Ref: false
----
+--- bytes-basic ---
#let data = read("/assets/images/rhino.png", encoding: none)
#test(data.len(), 232243)
#test(data.slice(0, count: 5), bytes((137, 80, 78, 71, 13)))
#test(str(data.slice(1, 4)), "PNG")
#test(repr(data), "bytes(232243)")
----
+--- bytes-string-conversion ---
#test(str(bytes(range(0x41, 0x50))), "ABCDEFGHIJKLMNO")
+
+--- bytes-array-conversion ---
#test(array(bytes("Hello")), (0x48, 0x65, 0x6C, 0x6C, 0x6F))
----
+--- bytes-addition ---
// Test addition and joining.
#test(bytes((1, 2)) + bytes(()), bytes((1, 2)))
#test(bytes((1, 2)) + bytes((3, 4)), bytes((1, 2, 3, 4)))
#test(bytes(()) + bytes((3, 4)), bytes((3, 4)))
+
+--- bytes-joining ---
#test(str({
bytes("Hello")
bytes((0x20,))
bytes("World")
}), "Hello World")
----
+--- bytes-bad-conversion-from-dict ---
// Error: 8-14 expected string, array, or bytes, found dictionary
#bytes((a: 1))
-
----
-// Error: 8-15 expected array, bytes, or version, found string
-#array("hello")
diff --git a/tests/suite/foundations/calc.typ b/tests/suite/foundations/calc.typ
new file mode 100644
index 0000000000..e702be9f6e
--- /dev/null
+++ b/tests/suite/foundations/calc.typ
@@ -0,0 +1,261 @@
+--- calc-round ---
+#test(calc.round(calc.e, digits: 2), 2.72)
+#test(calc.round(calc.pi, digits: 2), 3.14)
+
+--- calc-abs ---
+// Test the `abs` function.
+#test(calc.abs(-3), 3)
+#test(calc.abs(3), 3)
+#test(calc.abs(-0.0), 0.0)
+#test(calc.abs(0.0), -0.0)
+#test(calc.abs(-3.14), 3.14)
+#test(calc.abs(50%), 50%)
+#test(calc.abs(-25%), 25%)
+
+--- cals-abs-bad-type ---
+// Error: 11-22 expected integer, float, length, angle, ratio, or fraction, found string
+#calc.abs("no number")
+
+--- calc-even-and-odd ---
+// Test the `even` and `odd` functions.
+#test(calc.even(2), true)
+#test(calc.odd(2), false)
+#test(calc.odd(-1), true)
+#test(calc.even(-11), false)
+
+--- calc-rem ---
+// Test the `rem` function.
+#test(calc.rem(1, 1), 0)
+#test(calc.rem(5, 3), 2)
+#test(calc.rem(5, -3), 2)
+#test(calc.rem(22.5, 10), 2.5)
+#test(calc.rem(9, 4.5), 0)
+
+--- calc-rem-divisor-zero-1 ---
+// Error: 14-15 divisor must not be zero
+#calc.rem(5, 0)
+
+--- calc-rem-divisor-zero-2 ---
+// Error: 16-19 divisor must not be zero
+#calc.rem(3.0, 0.0)
+
+--- calc-div-euclid ---
+// Test the `div-euclid` function.
+#test(calc.div-euclid(7, 3), 2)
+#test(calc.div-euclid(7, -3), -2)
+#test(calc.div-euclid(-7, 3), -3)
+#test(calc.div-euclid(-7, -3), 3)
+#test(calc.div-euclid(2.5, 2), 1)
+
+--- calc-div-euclid-divisor-zero-1 ---
+// Error: 21-22 divisor must not be zero
+#calc.div-euclid(5, 0)
+
+--- calc-div-euclid-divisor-zero-2 ---
+// Error: 23-26 divisor must not be zero
+#calc.div-euclid(3.0, 0.0)
+
+--- calc-rem-euclid ---
+// Test the `rem-euclid` function.
+#test(calc.rem-euclid(7, 3), 1)
+#test(calc.rem-euclid(7, -3), 1)
+#test(calc.rem-euclid(-7, 3), 2)
+#test(calc.rem-euclid(-7, -3), 2)
+#test(calc.rem-euclid(2.5, 2), 0.5)
+
+--- calc-rem-euclid-divisor-zero-1 ---
+// Error: 21-22 divisor must not be zero
+#calc.rem-euclid(5, 0)
+
+--- calc-rem-euclid-divisor-zero-2 ---
+// Error: 23-26 divisor must not be zero
+#calc.rem-euclid(3.0, 0.0)
+
+--- calc-quo ---
+// Test the `quo` function.
+#test(calc.quo(1, 1), 1)
+#test(calc.quo(5, 3), 1)
+#test(calc.quo(5, -3), -1)
+#test(calc.quo(22.5, 10), 2)
+#test(calc.quo(9, 4.5), 2)
+
+--- calc-quo-divisor-zero-1 ---
+// Error: 14-15 divisor must not be zero
+#calc.quo(5, 0)
+
+--- calc-quo-divisor-zero-2 ---
+// Error: 16-19 divisor must not be zero
+#calc.quo(3.0, 0.0)
+
+--- calc-min-and-max ---
+// Test the `min` and `max` functions.
+#test(calc.min(2, -4), -4)
+#test(calc.min(3.5, 1e2, -0.1, 3), -0.1)
+#test(calc.max(-3, 11), 11)
+#test(calc.min("hi"), "hi")
+
+--- calc-pow-log-exp-ln ---
+// Test the `pow`, `log`, `exp`, and `ln` functions.
+#test(calc.pow(10, 0), 1)
+#test(calc.pow(2, 4), 16)
+#test(calc.exp(2), calc.pow(calc.e, 2))
+#test(calc.ln(10), calc.log(10, base: calc.e))
+
+--- calc-bit-logical ---
+// Test the `bit-not`, `bit-and`, `bit-or` and `bit-xor` functions.
+#test(64.bit-not(), -65)
+#test(0.bit-not(), -1)
+#test((-56).bit-not(), 55)
+#test(128.bit-and(192), 128)
+#test(192.bit-and(224), 192)
+#test((-1).bit-and(325532), 325532)
+#test(0.bit-and(-53), 0)
+#test(0.bit-or(-1), -1)
+#test(5.bit-or(3), 7)
+#test((-50).bit-or(3), -49)
+#test(64.bit-or(32), 96)
+#test((-1).bit-xor(1), -2)
+#test(64.bit-xor(96), 32)
+#test((-1).bit-xor(-7), 6)
+#test(0.bit-xor(492), 492)
+
+--- calc-bit-shift ---
+// Test the `bit-lshift` and `bit-rshift` functions.
+#test(32.bit-lshift(2), 128)
+#test(694.bit-lshift(0), 694)
+#test(128.bit-rshift(2), 32)
+#test(128.bit-rshift(12345), 0)
+#test((-7).bit-rshift(2), -2)
+#test((-7).bit-rshift(12345), -1)
+#test(128.bit-rshift(2, logical: true), 32)
+#test((-7).bit-rshift(61, logical: true), 7)
+#test(128.bit-rshift(12345, logical: true), 0)
+#test((-7).bit-rshift(12345, logical: true), 0)
+
+--- calc-bit-shift-too-large ---
+// Error: 2-18 the result is too large
+#1.bit-lshift(64)
+
+--- calc-bit-lshift-negative ---
+// Error: 15-17 number must be at least zero
+#1.bit-lshift(-1)
+
+--- calc-bit-rshift-negative ---
+// Error: 15-17 number must be at least zero
+#1.bit-rshift(-1)
+
+--- calc-pow-zero-to-power-of-zero ---
+// Error: 2-16 zero to the power of zero is undefined
+#calc.pow(0, 0)
+
+--- calc-pow-exponent-too-large ---
+// Error: 14-31 exponent is too large
+#calc.pow(2, 10000000000000000)
+
+--- calc-pow-too-large ---
+// Error: 2-25 the result is too large
+#calc.pow(2, 2147483647)
+
+--- calc-pow-bad-exponent ---
+// Error: 14-36 exponent may not be infinite, subnormal, or NaN
+#calc.pow(2, calc.pow(2.0, 10000.0))
+
+--- calc-pow-not-real ---
+// Error: 2-19 the result is not a real number
+#calc.pow(-1, 0.5)
+
+--- calc-sqrt-not-real ---
+// Error: 12-14 cannot take square root of negative number
+#calc.sqrt(-1)
+
+--- calc-root ---
+#test(calc.root(12.0, 1), 12.0)
+#test(calc.root(9.0, 2), 3.0)
+#test(calc.root(27.0, 3), 3.0)
+#test(calc.root(-27.0, 3), -3.0)
+// 100^(-1/2) = (100^(1/2))^-1 = 1/sqrt(100)
+#test(calc.root(100.0, -2), 0.1)
+
+--- calc-root-zeroth ---
+// Error: 17-18 cannot take the 0th root of a number
+#calc.root(1.0, 0)
+
+--- calc-root-negative-even ---
+// Error: 24-25 negative numbers do not have a real nth root when n is even
+#test(calc.root(-27.0, 4), -3.0)
+
+--- calc-log-negative ---
+// Error: 11-13 value must be strictly positive
+#calc.log(-1)
+
+--- calc-log-bad-base ---
+// Error: 20-21 base may not be zero, NaN, infinite, or subnormal
+#calc.log(1, base: 0)
+
+--- calc-log-not-real ---
+// Error: 2-24 the result is not a real number
+#calc.log(10, base: -1)
+
+--- calc-fact ---
+// Test the `fact` function.
+#test(calc.fact(0), 1)
+#test(calc.fact(5), 120)
+
+--- calc-fact-too-large ---
+// Error: 2-15 the result is too large
+#calc.fact(21)
+
+--- calc-perm ---
+// Test the `perm` function.
+#test(calc.perm(0, 0), 1)
+#test(calc.perm(5, 3), 60)
+#test(calc.perm(5, 5), 120)
+#test(calc.perm(5, 6), 0)
+
+--- calc-perm-too-large ---
+// Error: 2-19 the result is too large
+#calc.perm(21, 21)
+
+--- calc-binom ---
+// Test the `binom` function.
+#test(calc.binom(0, 0), 1)
+#test(calc.binom(5, 3), 10)
+#test(calc.binom(5, 5), 1)
+#test(calc.binom(5, 6), 0)
+#test(calc.binom(6, 2), 15)
+
+--- calc-gcd ---
+// Test the `gcd` function.
+#test(calc.gcd(112, 77), 7)
+#test(calc.gcd(12, 96), 12)
+#test(calc.gcd(13, 9), 1)
+#test(calc.gcd(13, -9), 1)
+#test(calc.gcd(272557, 272557), 272557)
+#test(calc.gcd(0, 0), 0)
+#test(calc.gcd(7, 0), 7)
+
+--- calc-lcm ---
+// Test the `lcm` function.
+#test(calc.lcm(112, 77), 1232)
+#test(calc.lcm(12, 96), 96)
+#test(calc.lcm(13, 9), 117)
+#test(calc.lcm(13, -9), 117)
+#test(calc.lcm(272557, 272557), 272557)
+#test(calc.lcm(0, 0), 0)
+#test(calc.lcm(8, 0), 0)
+
+--- calc-lcm-too-large ---
+// Error: 2-41 the result is too large
+#calc.lcm(15486487489457, 4874879896543)
+
+--- calc-min-nothing ---
+// Error: 2-12 expected at least one value
+#calc.min()
+
+--- calc-min-uncomparable ---
+// Error: 14-18 cannot compare string and integer
+#calc.min(1, "hi")
+
+--- calc-max-uncomparable ---
+// Error: 16-19 cannot compare 1pt with 1em
+#calc.max(1em, 1pt)
diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ
new file mode 100644
index 0000000000..afecc124f8
--- /dev/null
+++ b/tests/suite/foundations/content.typ
@@ -0,0 +1,120 @@
+--- content-at-default ---
+// Test .at() default values for content.
+#test(auto, [a].at("doesn't exist", default: auto))
+
+--- content-field-syntax ---
+// Test fields on elements.
+#show list: it => {
+ test(it.children.len(), 3)
+}
+
+- A
+- B
+- C
+
+--- content-field-missing ---
+// Error: 25-28 content does not contain field "fun"
+#show heading: it => it.fun
+= A
+
+--- content-fields ---
+// Test content fields method.
+#test([a].fields(), (text: "a"))
+#test([a *b*].fields(), (children: ([a], [ ], strong[b])))
+
+--- content-fields-mutable-invalid ---
+#{
+ let object = [hi]
+ // Error: 3-9 cannot mutate fields on content
+ object.property = "value"
+}
+
+--- content-field-materialized-table ---
+// Ensure that fields from set rules are materialized into the element before
+// a show rule runs.
+#set table(columns: (10pt, auto))
+#show table: it => it.columns
+#table[A][B][C][D]
+
+--- content-field-materialized-heading ---
+// Test it again with a different element.
+#set heading(numbering: "(I)")
+#show heading: set text(size: 11pt, weight: "regular")
+#show heading: it => it.numbering
+= Heading
+
+--- content-field-materialized-query ---
+// Test it with query.
+#set raw(lang: "rust")
+#context query().first().lang
+`raw`
+
+--- content-fields-complex ---
+// Integrated test for content fields.
+#let compute(equation, ..vars) = {
+ let vars = vars.named()
+ let f(elem) = {
+ let func = elem.func()
+ if func == text {
+ let text = elem.text
+ if regex("^\d+$") in text {
+ int(text)
+ } else if text in vars {
+ int(vars.at(text))
+ } else {
+ panic("unknown math variable: " + text)
+ }
+ } else if func == math.attach {
+ let value = f(elem.base)
+ if elem.has("t") {
+ value = calc.pow(value, f(elem.t))
+ }
+ value
+ } else if elem.has("children") {
+ elem
+ .children
+ .filter(v => v != [ ])
+ .split[+]
+ .map(xs => xs.fold(1, (prod, v) => prod * f(v)))
+ .fold(0, (sum, v) => sum + v)
+ }
+ }
+ let result = f(equation.body)
+ [With ]
+ vars
+ .pairs()
+ .map(p => $#p.first() = #p.last()$)
+ .join(", ", last: " and ")
+ [ we have:]
+ $ equation = result $
+}
+
+#compute($x y + y^2$, x: 2, y: 3)
+
+--- content-label-has-method ---
+// Test whether the label is accessible through the `has` method.
+#show heading: it => {
+ assert(it.has("label"))
+ it
+}
+
+= Hello, world!
+
+--- content-label-field-access ---
+// Test whether the label is accessible through field syntax.
+#show heading: it => {
+ assert(str(it.label) == "my-label")
+ it
+}
+
+= Hello, world!
+
+--- content-label-fields-method ---
+// Test whether the label is accessible through the fields method.
+#show heading: it => {
+ assert("label" in it.fields())
+ assert(str(it.fields().label) == "my-label")
+ it
+}
+
+= Hello, world!
diff --git a/tests/suite/foundations/context.typ b/tests/suite/foundations/context.typ
new file mode 100644
index 0000000000..fea9f544c6
--- /dev/null
+++ b/tests/suite/foundations/context.typ
@@ -0,0 +1,65 @@
+// Test context expressions.
+
+--- context-body-atomic-in-markup ---
+// Test that context body is parsed as atomic expression.
+#let c = [#context "hello".]
+#test(c.children.first().func(), (context none).func())
+#test(c.children.last(), [.])
+
+--- context-element-constructor-forbidden ---
+// Test that manual construction is forbidden.
+// Error: 2-25 cannot be constructed manually
+#(context none).func()()
+
+--- context-in-show-rule ---
+// Test that show rule establishes context.
+#set heading(numbering: "1.")
+#show heading: it => test(
+ counter(heading).get(),
+ (intro: (1,), back: (2,)).at(str(it.label)),
+)
+
+= Introduction
+= Background
+
+--- context-in-show-rule-query ---
+// Test that show rule on non-locatable element allows `query`.
+// Error: 18-47 Assertion failed: 2 != 3
+#show emph: _ => test(query(heading).len(), 3)
+#show strong: _ => test(query(heading).len(), 2)
+= Introduction
+= Background
+*Hi* _there_
+
+--- context-assign-to-captured-variable ---
+// Test error when captured variable is assigned to.
+#let i = 0
+// Error: 11-12 variables from outside the context expression are read-only and cannot be modified
+#context (i = 1)
+
+--- context-compatibility-locate ---
+#let s = state("x", 0)
+#let compute(expr) = [
+ #s.update(x =>
+ eval(expr.replace("x", str(x)))
+ )
+ New value is #s.display().
+]
+
+#locate(loc => {
+ let elem = query(, loc).first()
+ test(s.at(elem.location()), 13)
+})
+
+#compute("10") \
+#compute("x + 3") \
+*Here.* \
+#compute("x * 2") \
+#compute("x - 5")
+
+--- context-compatibility-styling ---
+#style(styles => measure([it], styles).width < 20pt)
+
+--- context-compatibility-counter-display ---
+#counter(heading).update(10)
+#counter(heading).display(n => test(n, 10))
diff --git a/tests/suite/foundations/datetime.typ b/tests/suite/foundations/datetime.typ
new file mode 100644
index 0000000000..b54c11f3f3
--- /dev/null
+++ b/tests/suite/foundations/datetime.typ
@@ -0,0 +1,93 @@
+--- datetime-constructor-empty ---
+// Error: 2-12 at least one of date or time must be fully specified
+#datetime()
+
+--- datetime-constructor-time-invalid ---
+// Error: 2-42 time is invalid
+#datetime(hour: 25, minute: 0, second: 0)
+
+--- datetime-constructor-date-invalid ---
+// Error: 2-41 date is invalid
+#datetime(year: 2000, month: 2, day: 30)
+
+--- datetime-display ---
+// Test displaying of dates.
+#test(datetime(year: 2023, month: 4, day: 29).display(), "2023-04-29")
+#test(datetime(year: 2023, month: 4, day: 29).display("[year]"), "2023")
+#test(
+ datetime(year: 2023, month: 4, day: 29)
+ .display("[year repr:last_two]"),
+ "23",
+)
+#test(
+ datetime(year: 2023, month: 4, day: 29)
+ .display("[year] [month repr:long] [day] [week_number] [weekday]"),
+ "2023 April 29 17 Saturday",
+)
+
+// Test displaying of times
+#test(datetime(hour: 14, minute: 26, second: 50).display(), "14:26:50")
+#test(datetime(hour: 14, minute: 26, second: 50).display("[hour]"), "14")
+#test(
+ datetime(hour: 14, minute: 26, second: 50)
+ .display("[hour repr:12 padding:none]"),
+ "2",
+)
+#test(
+ datetime(hour: 14, minute: 26, second: 50)
+ .display("[hour], [minute], [second]"), "14, 26, 50",
+)
+
+// Test displaying of datetimes
+#test(
+ datetime(year: 2023, month: 4, day: 29, hour: 14, minute: 26, second: 50).display(),
+ "2023-04-29 14:26:50",
+)
+
+// Test getting the year/month/day etc. of a datetime
+#let d = datetime(year: 2023, month: 4, day: 29, hour: 14, minute: 26, second: 50)
+#test(d.year(), 2023)
+#test(d.month(), 4)
+#test(d.weekday(), 6)
+#test(d.day(), 29)
+#test(d.hour(), 14)
+#test(d.minute(), 26)
+#test(d.second(), 50)
+
+#let e = datetime(year: 2023, month: 4, day: 29)
+#test(e.hour(), none)
+#test(e.minute(), none)
+#test(e.second(), none)
+
+// Test today
+#test(datetime.today().display(), "1970-01-01")
+#test(datetime.today(offset: auto).display(), "1970-01-01")
+#test(datetime.today(offset: 2).display(), "1970-01-01")
+
+--- datetime-ordinal ---
+// Test date methods.
+#test(datetime(day: 1, month: 1, year: 2000).ordinal(), 1);
+#test(datetime(day: 1, month: 3, year: 2000).ordinal(), 31 + 29 + 1);
+#test(datetime(day: 31, month: 12, year: 2000).ordinal(), 366);
+#test(datetime(day: 1, month: 3, year: 2001).ordinal(), 31 + 28 + 1);
+#test(datetime(day: 31, month: 12, year: 2001).ordinal(), 365);
+
+--- datetime-display-missing-closing-bracket ---
+// Error: 27-34 missing closing bracket for bracket at index 0
+#datetime.today().display("[year")
+
+--- datetime-display-invalid-component ---
+// Error: 27-38 invalid component name 'nothing' at index 1
+#datetime.today().display("[nothing]")
+
+--- datetime-display-invalid-modifier ---
+// Error: 27-50 invalid modifier 'wrong' at index 6
+#datetime.today().display("[year wrong:last_two]")
+
+--- datetime-display-expected-component ---
+// Error: 27-33 expected component name at index 2
+#datetime.today().display(" []")
+
+--- datetime-display-insufficient-information ---
+// Error: 2-36 failed to format datetime (insufficient information)
+#datetime.today().display("[hour]")
diff --git a/tests/suite/foundations/dict.typ b/tests/suite/foundations/dict.typ
new file mode 100644
index 0000000000..d75b916297
--- /dev/null
+++ b/tests/suite/foundations/dict.typ
@@ -0,0 +1,266 @@
+// Test dictionaries.
+
+--- dict-basic-syntax ---
+
+// Empty
+#(:)
+
+// Two pairs and string key.
+#let dict = (normal: 1, "spacy key": 2)
+#dict
+
+#test(dict.normal, 1)
+#test(dict.at("spacy key"), 2)
+
+--- dict-fields ---
+// Test field on dictionary.
+#let dict = (nothing: "ness", hello: "world")
+#test(dict.nothing, "ness")
+#{
+ let world = dict
+ .hello
+
+ test(world, "world")
+}
+
+--- dict-missing-field ---
+// Error: 6-13 dictionary does not contain key "invalid"
+#(:).invalid
+
+--- dict-bad-key ---
+// Error: 3-7 expected string, found boolean
+// Error: 16-18 expected string, found integer
+#(true: false, 42: 3)
+
+--- dict-duplicate-key ---
+// Error: 24-29 duplicate key: first
+#(first: 1, second: 2, first: 3)
+
+--- dict-duplicate-key-stringy ---
+// Error: 17-20 duplicate key: a
+#(a: 1, "b": 2, "a": 3)
+
+--- dict-bad-expression ---
+// Simple expression after already being identified as a dictionary.
+// Error: 9-10 expected named or keyed pair, found identifier
+#(a: 1, b)
+
+--- dict-leading-colon ---
+// Identified as dictionary due to initial colon.
+// The boolean key is allowed for now since it will only cause an error at the evaluation stage.
+// Error: 4-5 expected named or keyed pair, found integer
+// Error: 17 expected expression
+#(:1 b:"", true:)
+
+--- spread-into-dict ---
+#{
+ let x = (a: 1)
+ let y = (b: 2)
+ let z = (a: 3)
+ test((:..x, ..y, ..z), (a: 3, b: 2))
+ test((..(a: 1), b: 2), (a: 1, b: 2))
+}
+
+--- spread-array-into-dict ---
+// Error: 3-11 cannot spread array into dictionary
+#(..(1, 2), a: 1)
+
+--- dict-at-lvalue ---
+// Test lvalue and rvalue access.
+#{
+ let dict = (a: 1, "b b": 1)
+ dict.at("b b") += 1
+ dict.state = (ok: true, err: false)
+ test(dict, (a: 1, "b b": 2, state: (ok: true, err: false)))
+ test(dict.state.ok, true)
+ dict.at("state").ok = false
+ test(dict.state.ok, false)
+ test(dict.state.err, false)
+}
+
+--- dict-at-missing-key ---
+// Test rvalue missing key.
+#{
+ let dict = (a: 1, b: 2)
+ // Error: 11-23 dictionary does not contain key "c" and no default value was specified
+ let x = dict.at("c")
+}
+
+--- dict-at-default ---
+// Test default value.
+#test((a: 1, b: 2).at("b", default: 3), 2)
+#test((a: 1, b: 2).at("c", default: 3), 3)
+
+--- dict-insert ---
+// Test insert.
+#{
+ let dict = (a: 1, b: 2)
+ dict.insert("b", 3)
+ test(dict, (a: 1, b: 3))
+ dict.insert("c", 5)
+ test(dict, (a: 1, b: 3, c: 5))
+}
+
+--- dict-remove-with-default ---
+// Test remove with default value.
+#{
+ let dict = (a: 1, b: 2)
+ test(dict.remove("b", default: 3), 2)
+}
+
+#{
+ let dict = (a: 1, b: 2)
+ test(dict.remove("c", default: 3), 3)
+}
+
+--- dict-missing-lvalue ---
+// Missing lvalue is not automatically none-initialized.
+#{
+ let dict = (:)
+ // Error: 3-9 dictionary does not contain key "b"
+ // Hint: 3-9 use `insert` to add or update values
+ dict.b += 1
+}
+
+--- dict-basic-methods ---
+// Test dictionary methods.
+#let dict = (a: 3, c: 2, b: 1)
+#test("c" in dict, true)
+#test(dict.len(), 3)
+#test(dict.values(), (3, 2, 1))
+#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3c2b1")
+
+#dict.remove("c")
+#test("c" in dict, false)
+#test(dict, (a: 3, b: 1))
+
+--- dict-from-module ---
+// Test dictionary constructor
+#test(type(dictionary(sys).at("version")), version)
+#test(dictionary(sys).at("no-crash", default: none), none)
+
+--- dict-remove-order ---
+// Test that removal keeps order.
+#let dict = (a: 1, b: 2, c: 3, d: 4)
+#dict.remove("b")
+#test(dict.keys(), ("a", "c", "d"))
+
+--- dict-temporary-lvalue ---
+// Error: 3-15 cannot mutate a temporary value
+#((key: "val").other = "some")
+
+--- dict-function-item-not-a-method ---
+#{
+ let dict = (
+ call-me: () => 1,
+ )
+ // Error: 8-15 type dictionary has no method `call-me`
+ // Hint: 8-15 to call the function stored in the dictionary, surround the field access with parentheses, e.g. `(dict.call-me)(..)`
+ dict.call-me()
+}
+
+--- dict-item-missing-method ---
+#{
+ let dict = (
+ nonfunc: 1
+ )
+
+ // Error: 8-15 type dictionary has no method `nonfunc`
+ // Hint: 8-15 did you mean to access the field `nonfunc`?
+ dict.nonfunc()
+}
+
+--- dict-dynamic-uplicate-key ---
+#let a = "hello"
+#let b = "world"
+#let c = "value"
+#let d = "conflict"
+
+#assert.eq(((a): b), ("hello": "world"))
+#assert.eq(((a): 1, (a): 2), ("hello": 2))
+#assert.eq((hello: 1, (a): 2), ("hello": 2))
+#assert.eq((a + b: c, (a + b): d, (a): "value2", a: "value3"), ("helloworld": "conflict", "hello": "value2", "a": "value3"))
+
+--- issue-1338-dictionary-underscore ---
+#let foo = "foo"
+#let bar = "bar"
+// Error: 8-9 expected expression, found underscore
+// Error: 16-17 expected expression, found underscore
+#(foo: _, bar: _)
+
+--- issue-1342-dictionary-bare-expressions ---
+// Error: 5-8 expected named or keyed pair, found identifier
+// Error: 10-13 expected named or keyed pair, found identifier
+#(: foo, bar)
+
+--- issue-3154-dict-at-not-contained ---
+#{
+ let dict = (a: 1)
+ // Error: 3-15 dictionary does not contain key "b" and no default value was specified
+ dict.at("b")
+}
+
+--- issue-3154-dict-at-missing-default ---
+#{
+ let dict = (a: 1)
+ test(dict.at("b", default: 0), 0)
+}
+
+--- issue-3154-dict-at-missing-mutable ---
+#{
+ let dict = (a: 1)
+ // Error: 3-15 dictionary does not contain key "b"
+ // Hint: 3-15 use `insert` to add or update values
+ dict.at("b") = 9
+}
+
+--- issue-3154-dict-at-missing-mutable-default ---
+#{
+ let dict = (a: 1)
+ // Error: 3-27 dictionary does not contain key "b"
+ // Hint: 3-27 use `insert` to add or update values
+ dict.at("b", default: 0) = 9
+}
+
+--- issue-3154-dict-syntax-missing ---
+#{
+ let dict = (a: 1)
+ // Error: 8-9 dictionary does not contain key "b"
+ dict.b
+}
+
+--- issue-3154-dict-syntax-missing-mutable ---
+#{
+ let dict = (a: 1)
+ dict.b = 9
+ test(dict, (a: 1, b: 9))
+}
+
+--- issue-3154-dict-syntax-missing-add-assign ---
+#{
+ let dict = (a: 1)
+ // Error: 3-9 dictionary does not contain key "b"
+ // Hint: 3-9 use `insert` to add or update values
+ dict.b += 9
+}
+
+--- issue-3232-dict-unexpected-keys-sides ---
+// Confusing "expected relative length or dictionary, found dictionary"
+// Error: 16-58 unexpected keys "unexpected" and "unexpected-too"
+#block(outset: (unexpected: 0.5em, unexpected-too: 0.2em), [Hi])
+
+--- issue-3232-dict-unexpected-keys-corners ---
+// Error: 14-56 unexpected keys "unexpected" and "unexpected-too"
+#box(radius: (unexpected: 0.5em, unexpected-too: 0.5em), [Hi])
+
+--- issue-3232-dict-unexpected-key-sides ---
+// Error: 16-49 unexpected key "unexpected", valid keys are "left", "top", "right", "bottom", "x", "y", and "rest"
+#block(outset: (unexpected: 0.2em, right: 0.5em), [Hi]) // The 1st key is unexpected
+
+--- issue-3232-dict-unexpected-key-corners ---
+// Error: 14-50 unexpected key "unexpected", valid keys are "top-left", "top-right", "bottom-right", "bottom-left", "left", "top", "right", "bottom", and "rest"
+#box(radius: (top-left: 0.5em, unexpected: 0.5em), [Hi]) // The 2nd key is unexpected
+
+--- issue-3232-dict-empty ---
+#block(outset: (:), [Hi]) // Ok
+#box(radius: (:), [Hi]) // Ok
diff --git a/tests/typ/compiler/duration.typ b/tests/suite/foundations/duration.typ
similarity index 93%
rename from tests/typ/compiler/duration.typ
rename to tests/suite/foundations/duration.typ
index 1d831a6fa8..7e53f70369 100644
--- a/tests/typ/compiler/duration.typ
+++ b/tests/suite/foundations/duration.typ
@@ -1,17 +1,16 @@
// Test durations.
-// Ref: false
----
+--- duration-negate ---
// Test negating durations.
#test(-duration(hours: 2), duration(hours: -2))
----
+--- duration-add-and-subtract ---
// Test adding and subtracting durations.
#test(duration(weeks: 1, hours: 1), duration(weeks: 1) + duration(hours: 1))
#test(duration(weeks: 1, hours: -1), duration(weeks: 1) - duration(hours: 1))
#test(duration(days: 6, hours: 23), duration(weeks: 1) - duration(hours: 1))
----
+--- duration-add-and-subtract-dates ---
// Test adding and subtracting durations and dates.
#let d = datetime(day: 1, month: 1, year: 2000)
#let d2 = datetime(day: 1, month: 2, year: 2000)
@@ -33,7 +32,7 @@
datetime(day: 1, month: 1, year: 2001),
)
----
+--- duration-add-and-subtract-times ---
// Test adding and subtracting durations and times.
#let a = datetime(hour: 12, minute: 0, second: 0)
#test(a + duration(hours: 1, minutes: -60), datetime(hour: 12, minute: 0, second: 0))
@@ -51,7 +50,7 @@
datetime(hour: 13, minute: 13, second: 13),
)
----
+--- duration-add-and-subtract-datetimes ---
// Test adding and subtracting durations and datetimes.
#test(
datetime(day: 1, month: 1, year: 2000, hour: 12, minute: 0, second: 0)
@@ -65,7 +64,7 @@
datetime(day: 10, month: 1, year: 2000, hour: 23, minute: 9, second: 50),
)
----
+--- duration-from-date-subtraction ---
// Test subtracting dates.
#let a = datetime(hour: 12, minute: 0, second: 0)
#let b = datetime(day: 1, month: 1, year: 2000)
@@ -75,7 +74,7 @@
#test(datetime(day: 1, month: 2, year: 2000) - b, duration(days: 31))
#test(datetime(day: 15, month: 1, year: 2000) - b, duration(weeks: 2))
----
+--- duration-multiply-with-number ---
// Test multiplying and dividing durations with numbers.
#test(duration(minutes: 10) * 6, duration(hours: 1))
#test(duration(minutes: 10) * 2, duration(minutes: 20))
@@ -83,13 +82,13 @@
#test(duration(minutes: 10) / 2, duration(minutes: 5))
#test(duration(minutes: 10) / 2.5, duration(minutes: 4))
----
+--- duration-divide ---
// Test dividing durations with durations
#test(duration(minutes: 20) / duration(hours: 1), 1 / 3)
#test(duration(minutes: 20) / duration(minutes: 10), 2)
#test(duration(minutes: 20) / duration(minutes: 8), 2.5)
----
+--- duration-compare ---
// Test comparing durations
#test(duration(minutes: 20) > duration(minutes: 10), true)
#test(duration(minutes: 20) >= duration(minutes: 10), true)
diff --git a/tests/suite/foundations/eval.typ b/tests/suite/foundations/eval.typ
new file mode 100644
index 0000000000..f85146b235
--- /dev/null
+++ b/tests/suite/foundations/eval.typ
@@ -0,0 +1,54 @@
+--- eval ---
+// Test the eval function.
+#test(eval("1 + 2"), 3)
+#test(eval("1 + x", scope: (x: 3)), 4)
+#test(eval("let x = x + 1; x + 1", scope: (x: 1)), 3)
+
+--- eval-mode ---
+// Test evaluation in other modes.
+#eval("[_Hello" + " World!_]") \
+#eval("_Hello" + " World!_", mode: "markup") \
+#eval("RR_1^NN", mode: "math", scope: (RR: math.NN, NN: math.RR))
+
+--- eval-syntax-error-1 ---
+// Error: 7-12 expected pattern
+#eval("let")
+
+--- eval-in-show-rule ---
+#show raw: it => text(font: "PT Sans", eval("[" + it.text + "]"))
+
+Interacting
+```
+#set text(blue)
+Blue #move(dy: -0.15em)[🌊]
+```
+
+--- eval-runtime-error ---
+// Error: 7-17 cannot continue outside of loop
+#eval("continue")
+
+--- eval-syntax-error-2 ---
+// Error: 7-12 expected semicolon or line break
+#eval("1 2")
+
+--- eval-path-resolve ---
+// Test absolute path.
+#eval("image(\"/assets/images/tiger.jpg\", width: 50%)")
+
+--- eval-path-resolve-in-show-rule ---
+#show raw: it => eval(it.text, mode: "markup")
+
+```
+#show emph: image("/assets/images/tiger.jpg", width: 50%)
+_Tiger!_
+```
+
+--- eval-path-resolve-relative ---
+// Test relative path.
+#test(eval(`"HELLO" in read("./eval.typ")`.text), true)
+
+--- issue-2055-math-eval ---
+// Evaluating a math expr should renders the same as an equation
+#eval(mode: "math", "f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)")
+
+$f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)$
diff --git a/tests/suite/foundations/float.typ b/tests/suite/foundations/float.typ
new file mode 100644
index 0000000000..770533b9a6
--- /dev/null
+++ b/tests/suite/foundations/float.typ
@@ -0,0 +1,66 @@
+--- float-constructor ---
+#test(float(10), 10.0)
+#test(float(50% * 30%), 0.15)
+#test(float("31.4e-1"), 3.14)
+#test(float("31.4e\u{2212}1"), 3.14)
+#test(float("3.1415"), 3.1415)
+#test(float("-7654.321"), -7654.321)
+#test(float("\u{2212}7654.321"), -7654.321)
+#test(type(float(10)), float)
+
+--- float-constructor-bad-type ---
+// Error: 8-13 expected float, boolean, integer, ratio, or string, found type
+#float(float)
+
+--- float-constructor-bad-value ---
+// Error: 8-15 invalid float: 1.2.3
+#float("1.2.3")
+
+--- float-is-nan ---
+// Test float `is-nan()`.
+#test(float(calc.nan).is-nan(), true)
+#test(float(10).is-nan(), false)
+
+--- float-is-infinite ---
+// Test float `is-infinite()`.
+#test(float(calc.inf).is-infinite(), true)
+#test(float(-calc.inf).is-infinite(), true)
+#test(float(10).is-infinite(), false)
+#test(float(-10).is-infinite(), false)
+
+--- float-signum ---
+// Test float `signum()`
+#test(float(0.0).signum(), 1.0)
+#test(float(1.0).signum(), 1.0)
+#test(float(-1.0).signum(), -1.0)
+#test(float(10.0).signum(), 1.0)
+#test(float(-10.0).signum(), -1.0)
+#test(float(calc.nan).signum().is-nan(), true)
+
+--- float-repr ---
+// Test the `repr` function with floats.
+#repr(12.0) \
+#repr(3.14) \
+#repr(1234567890.0) \
+#repr(0123456789.0) \
+#repr(0.0) \
+#repr(-0.0) \
+#repr(-1.0) \
+#repr(-9876543210.0) \
+#repr(-0987654321.0) \
+#repr(-3.14) \
+#repr(4.0 - 8.0)
+
+--- float-display ---
+// Test floats.
+#12.0 \
+#3.14 \
+#1234567890.0 \
+#0123456789.0 \
+#0.0 \
+#(-0.0) \
+#(-1.0) \
+#(-9876543210.0) \
+#(-0987654321.0) \
+#(-3.14) \
+#(4.0 - 8.0)
diff --git a/tests/suite/foundations/int.typ b/tests/suite/foundations/int.typ
new file mode 100644
index 0000000000..0c85dcabab
--- /dev/null
+++ b/tests/suite/foundations/int.typ
@@ -0,0 +1,73 @@
+--- int-base-alternative ---
+// Test numbers with alternative bases.
+#test(0x10, 16)
+#test(0b1101, 13)
+#test(0xA + 0xa, 0x14)
+
+--- int-base-binary-invalid ---
+// Error: 2-7 invalid binary number: 0b123
+#0b123
+
+--- int-base-hex-invalid ---
+// Error: 2-8 invalid hexadecimal number: 0x123z
+#0x123z
+
+--- int-constructor ---
+// Test conversion to numbers.
+#test(int(false), 0)
+#test(int(true), 1)
+#test(int(10), 10)
+#test(int("150"), 150)
+#test(int("-834"), -834)
+#test(int("\u{2212}79"), -79)
+#test(int(10 / 3), 3)
+
+--- int-constructor-bad-type ---
+// Error: 6-10 expected integer, boolean, float, or string, found length
+#int(10pt)
+
+--- int-constructor-bad-value ---
+// Error: 6-12 invalid integer: nope
+#int("nope")
+
+--- int-signum ---
+// Test int `signum()`
+#test(int(0).signum(), 0)
+#test(int(1.0).signum(), 1)
+#test(int(-1.0).signum(), -1)
+#test(int(10.0).signum(), 1)
+#test(int(-10.0).signum(), -1)
+
+--- int-repr ---
+// Test the `repr` function with integers.
+#repr(12) \
+#repr(1234567890) \
+#repr(0123456789) \
+#repr(0) \
+#repr(-0) \
+#repr(-1) \
+#repr(-9876543210) \
+#repr(-0987654321) \
+#repr(4 - 8)
+
+--- int-display ---
+// Test integers.
+#12 \
+#1234567890 \
+#0123456789 \
+#0 \
+#(-0) \
+#(-1) \
+#(-9876543210) \
+#(-0987654321) \
+#(4 - 8)
+
+--- issue-int-constructor ---
+// Test that integer -> integer conversion doesn't do a roundtrip through float.
+#let x = 9223372036854775800
+#test(type(x), int)
+#test(int(x), x)
+
+--- number-invalid-suffix ---
+// Error: 2-4 invalid number suffix: u
+#1u
diff --git a/tests/typ/compiler/label.typ b/tests/suite/foundations/label.typ
similarity index 81%
rename from tests/typ/compiler/label.typ
rename to tests/suite/foundations/label.typ
index fabbac80f1..2cde102c3a 100644
--- a/tests/typ/compiler/label.typ
+++ b/tests/suite/foundations/label.typ
@@ -1,6 +1,6 @@
// Test labels.
----
+--- label-show-where-selector ---
// Test labelled headings.
#show heading: set text(10pt)
#show heading.where(label: ): underline
@@ -11,7 +11,7 @@ The beginning.
= Conclusion
The end.
----
+--- label-after-expression ---
// Test label after expression.
#show strong.where(label: ): set text(red)
@@ -19,7 +19,7 @@ The end.
#let b = [*B*]
#a #b
----
+--- label-on-text ---
// Test labelled text.
#show "t": it => {
set text(blue) if it.has("label") and it.label ==
@@ -28,14 +28,14 @@ The end.
This is a thing #[that ] happened.
----
+--- label-dynamic-show-set ---
// Test abusing dynamic labels for styling.
#show : set text(red)
#show : set text(blue)
*A* *B* *C* #label("bl" + "ue") *D*
----
+--- label-after-parbreak ---
// Test that label ignores parbreak.
#show : none
@@ -47,26 +47,24 @@ _Hidden_
_Visible_
----
+--- label-in-block ---
// Test that label only works within one content block.
#show : strike
*This is* #[] *protected.*
*This is not.*
----
+--- label-unclosed-is-text ---
// Test that incomplete label is text.
1 < 2 is #if 1 < 2 [not] a label.
----
+--- label-text-styled-and-sequence ---
// Test label on text, styled, and sequence.
-// Ref: false
#test([Hello].label, )
#test([#[A *B* C]].label, )
#test([#text(red)[Hello]].label, )
----
+--- label-string-conversion ---
// Test getting the name of a label.
-// Ref: false
#test(str(), "hey")
#test(str(label("hey")), "hey")
#test(str([Hmm].label), "hey")
diff --git a/tests/suite/foundations/panic.typ b/tests/suite/foundations/panic.typ
new file mode 100644
index 0000000000..5d9d40468e
--- /dev/null
+++ b/tests/suite/foundations/panic.typ
@@ -0,0 +1,14 @@
+--- panic ---
+// Test panic.
+// Error: 2-9 panicked
+#panic()
+
+--- panic-with-int ---
+// Test panic.
+// Error: 2-12 panicked with: 123
+#panic(123)
+
+--- panic-with-str ---
+// Test panic.
+// Error: 2-24 panicked with: "this is wrong"
+#panic("this is wrong")
diff --git a/tests/suite/foundations/plugin.typ b/tests/suite/foundations/plugin.typ
new file mode 100644
index 0000000000..0842980ecd
--- /dev/null
+++ b/tests/suite/foundations/plugin.typ
@@ -0,0 +1,47 @@
+// Test WebAssembly plugins.
+
+--- plugin-basic ---
+#let p = plugin("/assets/plugins/hello.wasm")
+#test(p.hello(), bytes("Hello from wasm!!!"))
+#test(p.double_it(bytes("hey!")), bytes("hey!.hey!"))
+#test(
+ p.shuffle(bytes("value1"), bytes("value2"), bytes("value3")),
+ bytes("value3-value1-value2"),
+)
+
+--- plugin-wrong-number-of-arguments ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 2-20 plugin function takes 0 arguments, but 1 was given
+#p.hello(bytes(""))
+
+--- plugin-wrong-argument-type ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 10-14 expected bytes, found boolean
+// Error: 27-29 expected bytes, found integer
+#p.hello(true, bytes(()), 10)
+
+--- plugin-error ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 2-17 plugin errored with: This is an `Err`
+#p.returns_err()
+
+--- plugin-panic ---
+#let p = plugin("/assets/plugins/hello.wasm")
+
+// Error: 2-16 plugin panicked: wasm `unreachable` instruction executed
+#p.will_panic()
+
+--- plugin-out-of-bounds-read ---
+#let p = plugin("/assets/plugins/plugin-oob.wasm")
+
+// Error: 2-14 plugin tried to read out of bounds: pointer 0x40000000 is out of bounds for read of length 1
+#p.read_oob()
+
+--- plugin-out-of-bounds-write ---
+#let p = plugin("/assets/plugins/plugin-oob.wasm")
+
+// Error: 2-27 plugin tried to write out of bounds: pointer 0x40000000 is out of bounds for write of length 3
+#p.write_oob(bytes("xyz"))
diff --git a/tests/typ/compiler/repr.typ b/tests/suite/foundations/repr.typ
similarity index 78%
rename from tests/typ/compiler/repr.typ
rename to tests/suite/foundations/repr.typ
index 13593a868d..f9346faacb 100644
--- a/tests/typ/compiler/repr.typ
+++ b/tests/suite/foundations/repr.typ
@@ -1,13 +1,15 @@
-// Test representation of values in the document.
+--- repr ---
+#test(repr(ltr), "ltr")
+#test(repr((1, 2, false, )), "(1, 2, false)")
----
+--- repr-literals ---
// Literal values.
#auto \
#none (empty) \
#true \
#false
----
+--- repr-numerical ---
// Numerical values.
#1 \
#1.0e-4 \
@@ -24,7 +26,7 @@
#(2em + 10pt) \
#2.3fr
----
+--- repr-misc ---
// Colors and strokes.
#set text(0.8em)
#rgb("f7a205") \
diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ
new file mode 100644
index 0000000000..025ec53d36
--- /dev/null
+++ b/tests/suite/foundations/str.typ
@@ -0,0 +1,315 @@
+// Test the string methods.
+
+--- str-constructor ---
+// Test conversion to string.
+#test(str(123), "123")
+#test(str(123, base: 3), "11120")
+#test(str(-123, base: 16), "−7b")
+#test(str(9223372036854775807, base: 36), "1y2p0ij32e8e7")
+#test(str(50.14), "50.14")
+#test(str(10 / 3).len() > 10, true)
+
+--- str-from-float ---
+// Test the `str` function with floats.
+#test(str(12.0), "12")
+#test(str(3.14), "3.14")
+#test(str(1234567890.0), "1234567890")
+#test(str(0123456789.0), "123456789")
+#test(str(0.0), "0")
+#test(str(-0.0), "0")
+#test(str(-1.0), "−1")
+#test(str(-9876543210.0), "−9876543210")
+#test(str(-0987654321.0), "−987654321")
+#test(str(-3.14), "−3.14")
+#test(str(4.0 - 8.0), "−4")
+
+--- str-from-int ---
+// Test the `str` function with integers.
+#test(str(12), "12")
+#test(str(1234567890), "1234567890")
+#test(str(0123456789), "123456789")
+#test(str(0), "0")
+#test(str(-0), "0")
+#test(str(-1), "−1")
+#test(str(-9876543210), "−9876543210")
+#test(str(-0987654321), "−987654321")
+#test(str(4 - 8), "−4")
+
+--- str-constructor-bad-type ---
+// Error: 6-8 expected integer, float, version, bytes, label, type, or string, found content
+#str([])
+
+--- str-constructor-bad-base ---
+// Error: 17-19 base must be between 2 and 36
+#str(123, base: 99)
+
+--- str-constructor-unsupported-base ---
+// Error: 18-19 base is only supported for integers
+#str(1.23, base: 2)
+
+--- str-from-and-to-unicode ---
+// Test the unicode function.
+#test(str.from-unicode(97), "a")
+#test(str.to-unicode("a"), 97)
+
+--- str-from-unicode-bad-type ---
+// Error: 19-22 expected integer, found content
+#str.from-unicode([a])
+
+--- str-to-unicode-bad-type ---
+// Error: 17-21 expected exactly one character
+#str.to-unicode("ab")
+
+--- str-from-unicode-negative ---
+// Error: 19-21 number must be at least zero
+#str.from-unicode(-1)
+
+--- str-from-unicode-bad-value ---
+// Error: 2-28 0x110000 is not a valid codepoint
+#str.from-unicode(0x110000) // 0x10ffff is the highest valid code point
+
+--- string-len ---
+// Test the `len` method.
+#test("Hello World!".len(), 12)
+
+--- string-first-and-last ---
+// Test the `first` and `last` methods.
+#test("Hello".first(), "H")
+#test("Hello".last(), "o")
+#test("🏳️🌈A🏳️⚧️".first(), "🏳️🌈")
+#test("🏳️🌈A🏳️⚧️".last(), "🏳️⚧️")
+
+--- string-first-empty ---
+// Error: 2-12 string is empty
+#"".first()
+
+--- string-last-empty ---
+// Error: 2-11 string is empty
+#"".last()
+
+--- string-at ---
+// Test the `at` method.
+#test("Hello".at(1), "e")
+#test("Hello".at(4), "o")
+#test("Hello".at(-1), "o")
+#test("Hello".at(-2), "l")
+#test("Hey: 🏳️🌈 there!".at(5), "🏳️🌈")
+
+--- string-at-default ---
+// Test `at`'s 'default' parameter.
+#test("z", "Hello".at(5, default: "z"))
+
+--- string-at-not-a-char-boundary ---
+// Error: 2-14 string index 2 is not a character boundary
+#"🏳️🌈".at(2)
+
+--- string-at-out-of-bounds ---
+// Error: 2-15 no default value was specified and string index out of bounds (index: 5, len: 5)
+#"Hello".at(5)
+
+--- string-at-at-default-other-type ---
+#test("Hello".at(5, default: (a: 10)), (a: 10))
+
+--- string-slice ---
+// Test the `slice` method.
+#test("abc".slice(1, 2), "b")
+#test("abc🏡def".slice(2, 7), "c🏡")
+#test("abc🏡def".slice(2, -2), "c🏡d")
+#test("abc🏡def".slice(-3, -1), "de")
+
+--- string-slice-not-a-char-boundary ---
+// Error: 2-21 string index -1 is not a character boundary
+#"🏳️🌈".slice(0, -1)
+
+--- string-clusters ---
+// Test the `clusters` and `codepoints` methods.
+#test("abc".clusters(), ("a", "b", "c"))
+#test("abc".clusters(), ("a", "b", "c"))
+#test("🏳️🌈!".clusters(), ("🏳️🌈", "!"))
+
+--- string-codepoints ---
+#test("🏳️🌈!".codepoints(), ("🏳", "\u{fe0f}", "\u{200d}", "🌈", "!"))
+
+--- string-contains ---
+// Test the `contains` method.
+#test("abc".contains("b"), true)
+#test("b" in "abc", true)
+#test("1234f".contains(regex("\d")), true)
+#test(regex("\d") in "1234f", true)
+#test("abc".contains("d"), false)
+#test("1234g" in "1234f", false)
+#test("abc".contains(regex("^[abc]$")), false)
+#test("abc".contains(regex("^[abc]+$")), true)
+
+--- string-starts-with ---
+// Test the `starts-with` and `ends-with` methods.
+#test("Typst".starts-with("Ty"), true)
+#test("Typst".starts-with(regex("[Tt]ys")), false)
+#test("Typst".starts-with("st"), false)
+
+--- string-ends-with ---
+#test("Typst".ends-with("st"), true)
+#test("Typst".ends-with(regex("\d*")), true)
+#test("Typst".ends-with(regex("\d+")), false)
+#test("Typ12".ends-with(regex("\d+")), true)
+#test("typst13".ends-with(regex("1[0-9]")), true)
+#test("typst113".ends-with(regex("1[0-9]")), true)
+#test("typst23".ends-with(regex("1[0-9]")), false)
+
+--- string-find-and-position ---
+// Test the `find` and `position` methods.
+#let date = regex("\d{2}:\d{2}")
+#test("Hello World".find("World"), "World")
+#test("Hello World".position("World"), 6)
+#test("It's 12:13 now".find(date), "12:13")
+#test("It's 12:13 now".position(date), 5)
+
+--- string-match ---
+// Test the `match` method.
+#test("Is there a".match("for this?"), none)
+#test(
+ "The time of my life.".match(regex("[mit]+e")),
+ (start: 4, end: 8, text: "time", captures: ()),
+)
+
+--- string-matches ---
+// Test the `matches` method.
+#test("Hello there".matches("\d"), ())
+#test("Day by Day.".matches("Day"), (
+ (start: 0, end: 3, text: "Day", captures: ()),
+ (start: 7, end: 10, text: "Day", captures: ()),
+))
+
+// Compute the sum of all timestamps in the text.
+#let timesum(text) = {
+ let time = 0
+ for match in text.matches(regex("(\d+):(\d+)")) {
+ let caps = match.captures
+ time += 60 * int(caps.at(0)) + int(caps.at(1))
+ }
+ str(int(time / 60)) + ":" + str(calc.rem(time, 60))
+}
+
+#test(timesum(""), "0:0")
+#test(timesum("2:70"), "3:10")
+#test(timesum("1:20, 2:10, 0:40"), "4:10")
+
+--- stgring-replace ---
+// Test the `replace` method with `Str` replacements.
+#test("ABC".replace("", "-"), "-A-B-C-")
+#test("Ok".replace("Ok", "Nope", count: 0), "Ok")
+#test("to add?".replace("", "How ", count: 1), "How to add?")
+#test("AB C DEF GH J".replace(" ", ",", count: 2), "AB,C,DEF GH J")
+#test("Walcemo"
+ .replace("o", "k")
+ .replace("e", "o")
+ .replace("k", "e")
+ .replace("a", "e"),
+ "Welcome"
+)
+#test("123".replace(regex("\d$"), "_"), "12_")
+#test("123".replace(regex("\d{1,2}$"), "__"), "1__")
+
+--- string-replace-function ---
+// Test the `replace` method with `Func` replacements.
+
+#test("abc".replace(regex("[a-z]"), m => {
+ str(m.start) + m.text + str(m.end)
+}), "0a11b22c3")
+#test("abcd, efgh".replace(regex("\w+"), m => {
+ upper(m.text)
+}), "ABCD, EFGH")
+#test("hello : world".replace(regex("^(.+)\s*(:)\s*(.+)$"), m => {
+ upper(m.captures.at(0)) + m.captures.at(1) + " " + upper(m.captures.at(2))
+}), "HELLO : WORLD")
+#test("hello world, lorem ipsum".replace(regex("(\w+) (\w+)"), m => {
+ m.captures.at(1) + " " + m.captures.at(0)
+}), "world hello, ipsum lorem")
+#test("hello world, lorem ipsum".replace(regex("(\w+) (\w+)"), count: 1, m => {
+ m.captures.at(1) + " " + m.captures.at(0)
+}), "world hello, lorem ipsum")
+#test("123 456".replace(regex("[a-z]+"), "a"), "123 456")
+
+#test("abc".replace("", m => "-"), "-a-b-c-")
+#test("abc".replace("", m => "-", count: 1), "-abc")
+#test("123".replace("abc", m => ""), "123")
+#test("123".replace("abc", m => "", count: 2), "123")
+#test("a123b123c".replace("123", m => {
+ str(m.start) + "-" + str(m.end)
+}), "a1-4b5-8c")
+#test("halla warld".replace("a", m => {
+ if m.start == 1 { "e" }
+ else if m.start == 4 or m.start == 7 { "o" }
+}), "hello world")
+#test("aaa".replace("a", m => str(m.captures.len())), "000")
+
+--- string-replace-function-bad-type ---
+// Error: 23-24 expected string, found integer
+#"123".replace("123", m => 1)
+
+--- string-replace-bad-type ---
+// Error: 23-32 expected string or function, found array
+#"123".replace("123", (1, 2, 3))
+
+--- string-trim-basic ---
+// Test the `trim` method; the pattern is not provided.
+#let str = "Typst, LaTeX, Word, InDesign"
+#let array = ("Typst", "LaTeX", "Word", "InDesign")
+#test(str.split(",").map(s => s.trim()), array)
+#test("".trim(), "")
+#test(" ".trim(), "")
+#test("\t".trim(), "")
+#test("\n".trim(), "")
+#test("\t \n".trim(), "")
+#test(" abc ".trim(at: start), "abc ")
+#test("\tabc ".trim(at: start), "abc ")
+#test("abc\n".trim(at: end), "abc")
+#test(" abc ".trim(at: end, repeat: true), " abc")
+#test(" abc".trim(at: start, repeat: false), "abc")
+
+--- string-trim-pattern-str ---
+// Test the `trim` method; the pattern is a string.
+#test("aabcaa".trim("a", repeat: false), "abca")
+#test("aabca".trim("a", at: start), "bca")
+#test("aabcaa".trim("a", at: end, repeat: false), "aabca")
+#test(" abc\n".trim("\n"), " abc")
+#test("whole".trim("whole", at: start), "")
+
+--- string-trim-pattern-regex ---
+// Test the `trim` method; the pattern is a regex.
+#test("".trim(regex(".")), "")
+#test("123abc456".trim(regex("\d")), "abc")
+#test("123abc456".trim(regex("\d"), repeat: false), "23abc45")
+#test("123a4b5c678".trim(regex("\d"), repeat: true), "a4b5c")
+#test("123a4b5c678".trim(regex("\d"), repeat: false), "23a4b5c67")
+#test("123abc456".trim(regex("\d"), at: start), "abc456")
+#test("123abc456".trim(regex("\d"), at: end), "123abc")
+#test("123abc456".trim(regex("\d+"), at: end, repeat: false), "123abc")
+#test("123abc456".trim(regex("\d{1,2}$"), repeat: false), "123abc4")
+#test("hello world".trim(regex(".")), "")
+#test("12306".trim(regex("\d"), at: start), "")
+#test("12306abc".trim(regex("\d"), at: start), "abc")
+#test("whole".trim(regex("whole"), at: start), "")
+#test("12306".trim(regex("\d"), at: end), "")
+#test("abc12306".trim(regex("\d"), at: end), "abc")
+#test("whole".trim(regex("whole"), at: end), "")
+
+--- string-trim-at-bad-alignment ---
+// Error: 17-21 expected either `start` or `end`
+#"abc".trim(at: left)
+
+--- string-split ---
+// Test the `split` method.
+#test("abc".split(""), ("", "a", "b", "c", ""))
+#test("abc".split("b"), ("a", "c"))
+#test("a123c".split(regex("\d")), ("a", "", "", "c"))
+#test("a123c".split(regex("\d+")), ("a", "c"))
+
+--- string-rev ---
+// Test the `rev` method.
+#test("abc".rev(), "cba")
+#test("ax̂e".rev(), "ex̂a")
+
+--- string-unclosed ---
+// Error: 2-2:1 unclosed string
+#"hello\"
diff --git a/tests/suite/foundations/type.typ b/tests/suite/foundations/type.typ
new file mode 100644
index 0000000000..f2a9884506
--- /dev/null
+++ b/tests/suite/foundations/type.typ
@@ -0,0 +1,25 @@
+--- type ---
+#test(type(1), int)
+#test(type(ltr), direction)
+#test(type(10 / 3), float)
+
+--- type-string-compatibility ---
+#test(type(10), int)
+#test(type(10), "integer")
+#test("is " + type(10), "is integer")
+#test(int in ("integer", "string"), true)
+#test(int in "integers or strings", true)
+#test(str in "integers or strings", true)
+
+--- issue-3110-type-constructor ---
+// Let the error message report the type name.
+// Error: 2-9 type content does not have a constructor
+#content()
+
+--- issue-3110-associated-field ---
+// Error: 6-12 type integer does not contain field `MAXVAL`
+#int.MAXVAL
+
+--- issue-3110-associated-function ---
+// Error: 6-18 type string does not contain field `from-unïcode`
+#str.from-unïcode(97)
diff --git a/tests/typ/compute/version.typ b/tests/suite/foundations/version.typ
similarity index 77%
rename from tests/typ/compute/version.typ
rename to tests/suite/foundations/version.typ
index e33eeb6f60..bf2cadb18f 100644
--- a/tests/typ/compute/version.typ
+++ b/tests/suite/foundations/version.typ
@@ -1,27 +1,27 @@
// Test versions.
-// Ref: false
----
+--- version-constructor ---
// Test version constructor.
// Empty.
#version()
// Plain.
-#version(1, 2)
+#test(version(1, 2).major, 1)
// Single Array argument.
-#version((1, 2))
+#test(version((1, 2)).minor, 2)
// Mixed arguments.
-#version(1, (2, 3), 4, (5, 6), 7)
+#test(version(1, (2, 3), 4, (5, 6), 7).at(5), 6)
----
+--- version-equality ---
// Test equality of different-length versions
#test(version(), version(0))
#test(version(0), version(0, 0))
#test(version(1, 2), version(1, 2, 0, 0, 0, 0))
----
+
+--- version-at ---
// Test `version.at`.
// Non-negative index in bounds
@@ -36,12 +36,12 @@
// Error: 2-22 component index out of bounds (index: -3, len: 2)
#version(1, 2).at(-3)
----
+--- version-fields ---
// Test version fields.
#test(version(1, 2, 3).major, 1)
#test(version(1, 2, 3).minor, 2)
#test(version(1, 2, 3).patch, 3)
----
+--- version-type ---
// Test the type of `sys.version`
#test(type(sys.version), version)
diff --git a/tests/suite/introspection/counter.typ b/tests/suite/introspection/counter.typ
new file mode 100644
index 0000000000..8a5315f956
--- /dev/null
+++ b/tests/suite/introspection/counter.typ
@@ -0,0 +1,78 @@
+// Test counters.
+
+--- counter-basic-1 ---
+// Count with string key.
+#let mine = counter("mine!")
+
+Final: #context mine.final().at(0) \
+#mine.step()
+First: #context mine.display() \
+#mine.update(7)
+#context mine.display("1 of 1", both: true) \
+#mine.step()
+#mine.step()
+Second: #context mine.display("I")
+#mine.update(n => n * 2)
+#mine.step()
+
+--- counter-basic-2 ---
+// Test `counter`.
+#let c = counter("heading")
+#c.update(2)
+#c.update(n => n + 2)
+#context test(c.get(), (4,))
+#c.update(n => n - 3)
+#context test(c.at(here()), (1,))
+
+--- counter-label ---
+// Count labels.
+#let label =
+#let count = context counter(label).display()
+#let elem(it) = [#box(it) #label]
+
+#elem[hey, there!] #count \
+#elem[more here!] #count
+
+--- counter-heading ---
+// Count headings.
+#set heading(numbering: "1.a.")
+#show heading: set text(10pt)
+#counter(heading).step()
+
+= Alpha
+In #context counter(heading).display()
+== Beta
+
+#set heading(numbering: none)
+= Gamma
+#heading(numbering: "I.")[Delta]
+
+At Beta, it was #context {
+ let it = query(heading).find(it => it.body == [Beta])
+ numbering(it.numbering, ..counter(heading).at(it.location()))
+}
+
+--- counter-page ---
+#set page(height: 50pt, margin: (bottom: 20pt, rest: 10pt))
+#lorem(12)
+#set page(numbering: "(i)")
+#lorem(6)
+#pagebreak()
+#set page(numbering: "1 / 1")
+#counter(page).update(1)
+#lorem(20)
+
+--- counter-figure ---
+// Count figures.
+#figure(numbering: "A", caption: [Four 'A's], kind: image, supplement: "Figure")[_AAAA!_]
+#figure(numbering: none, caption: [Four 'B's], kind: image, supplement: "Figure")[_BBBB!_]
+#figure(caption: [Four 'C's], kind: image, supplement: "Figure")[_CCCC!_]
+#counter(figure.where(kind: image)).update(n => n + 3)
+#figure(caption: [Four 'D's], kind: image, supplement: "Figure")[_DDDD!_]
+
+--- counter-at-no-context ---
+// Test `counter.at` outside of context.
+// Error: 2-28 can only be used when context is known
+// Hint: 2-28 try wrapping this in a `context` expression
+// Hint: 2-28 the `context` expression should wrap everything that depends on this function
+#counter("key").at()
diff --git a/tests/suite/introspection/here.typ b/tests/suite/introspection/here.typ
new file mode 100644
index 0000000000..18fff43957
--- /dev/null
+++ b/tests/suite/introspection/here.typ
@@ -0,0 +1,3 @@
+--- here-position ---
+// Test `context` + `here`.
+#context test(here().position().y, 10pt)
diff --git a/tests/suite/introspection/locate.typ b/tests/suite/introspection/locate.typ
new file mode 100644
index 0000000000..981f8c4656
--- /dev/null
+++ b/tests/suite/introspection/locate.typ
@@ -0,0 +1,32 @@
+--- locate-position ---
+// Test `locate`.
+#v(10pt)
+= Introduction
+#context test(locate().position().y, 20pt)
+
+--- locate-missing-label ---
+// Error: 10-25 label `` does not exist in the document
+#context locate()
+
+--- locate-duplicate-label ---
+= Introduction
+= Introduction
+
+// Error: 10-25 label `` occurs multiple times in the document
+#context locate()
+
+--- locate-element-selector ---
+#v(10pt)
+= Introduction
+#context test(locate(heading).position().y, 20pt)
+
+--- locate-element-selector-no-match ---
+// Error: 10-25 selector does not match any element
+#context locate(heading)
+
+--- locate-element-selector-multiple-matches ---
+= Introduction
+= Introduction
+
+// Error: 10-25 selector matches multiple elements
+#context locate(heading)
diff --git a/tests/suite/introspection/query.typ b/tests/suite/introspection/query.typ
new file mode 100644
index 0000000000..6cdd4bab06
--- /dev/null
+++ b/tests/suite/introspection/query.typ
@@ -0,0 +1,283 @@
+// Test creating a header with the query function.
+
+--- query-here ---
+// Test that `here()` yields the context element's location.
+#context test(query(here()).first().func(), (context none).func())
+
+--- query-running-header ---
+#set page(
+ paper: "a8",
+ margin: (y: 1cm, x: 0.5cm),
+ header: context {
+ smallcaps[Typst Academy]
+ h(1fr)
+ let after = query(selector(heading).after(here()))
+ let before = query(selector(heading).before(here()))
+ let elem = if before.len() != 0 {
+ before.last()
+ } else if after.len() != 0 {
+ after.first()
+ }
+ emph(elem.body)
+ }
+)
+
+#outline()
+
+= Introduction
+#v(1cm)
+
+= Background
+#v(2cm)
+
+= Approach
+
+--- query-list-of-figures ---
+#set page(
+ paper: "a8",
+ numbering: "1 / 1",
+ margin: (bottom: 1cm, rest: 0.5cm),
+)
+
+#set figure(numbering: "I")
+#show figure: set image(width: 80%)
+
+= List of Figures
+#context {
+ let elements = query(selector(figure).after(here()))
+ for it in elements [
+ Figure
+ #numbering(it.numbering,
+ ..counter(figure).at(it.location())):
+ #it.caption.body
+ #box(width: 1fr, repeat[.])
+ #counter(page).at(it.location()).first() \
+ ]
+}
+
+#figure(
+ image("/assets/images/cylinder.svg", width: 50%),
+ caption: [Cylinder],
+)
+
+#figure(
+ rect[Just some stand-in text],
+ kind: image,
+ supplement: "Figure",
+ caption: [Stand-in text],
+)
+
+#figure(
+ image("/assets/images/tetrahedron.svg", width: 50%),
+ caption: [Tetrahedron],
+)
+
+--- query-before-after ---
+// LARGE
+#set page(
+ paper: "a7",
+ numbering: "1 / 1",
+ margin: (bottom: 1cm, rest: 0.5cm),
+)
+
+#show heading.where(level: 1, outlined: true): it => [
+ #it
+
+ #set text(size: 12pt, weight: "regular")
+ #outline(
+ title: "Chapter outline",
+ indent: true,
+ target: heading
+ .where(level: 1)
+ .or(heading.where(level: 2))
+ .after(it.location(), inclusive: true)
+ .before(
+ heading
+ .where(level: 1, outlined: true)
+ .after(it.location(), inclusive: false),
+ inclusive: false,
+ )
+ )
+]
+
+#set heading(outlined: true, numbering: "1.")
+
+= Section 1
+== Subsection 1
+== Subsection 2
+=== Subsubsection 1
+=== Subsubsection 2
+== Subsection 3
+
+= Section 2
+== Subsection 1
+== Subsection 2
+
+= Section 3
+== Subsection 1
+== Subsection 2
+=== Subsubsection 1
+=== Subsubsection 2
+=== Subsubsection 3
+== Subsection 3
+
+--- query-and-or ---
+#set page(
+ paper: "a7",
+ numbering: "1 / 1",
+ margin: (bottom: 1cm, rest: 0.5cm),
+)
+
+#set heading(outlined: true, numbering: "1.")
+
+#context [
+ Non-outlined elements:
+ #(query(selector(heading).and(heading.where(outlined: false)))
+ .map(it => it.body).join(", "))
+]
+
+#heading("A", outlined: false)
+#heading("B", outlined: true)
+#heading("C", outlined: true)
+#heading("D", outlined: false)
+
+--- query-complex ---
+= A
+== B
+#figure([Cat], kind: "cat", supplement: [Other])
+=== D
+= E
+#figure([Frog], kind: "frog", supplement: none)
+#figure([Giraffe], kind: "giraffe", supplement: none)
+#figure([GiraffeCat], kind: "cat", supplement: [Other])
+= H
+#figure([Iguana], kind: "iguana", supplement: none)
+== I
+
+#let test-selector(selector, ref) = context {
+ test(query(selector).map(e => e.body), ref)
+}
+
+// Test `or`.
+#test-selector(
+ heading.where(level: 1).or(heading.where(level: 3)),
+ ([A], [D], [E], [H]),
+)
+
+#test-selector(
+ heading.where(level: 1).or(
+ heading.where(level: 3),
+ figure.where(kind: "frog"),
+ ),
+ ([A], [D], [E], [Frog], [H]),
+)
+
+#test-selector(
+ heading.where(level: 1).or(
+ heading.where(level: 2),
+ figure.where(kind: "frog"),
+ figure.where(kind: "cat"),
+ ),
+ ([A], [B], [Cat], [E], [Frog], [GiraffeCat], [H], [I]),
+)
+
+#test-selector(
+ figure.where(kind: "dog").or(heading.where(level: 3)),
+ ([D],),
+)
+
+#test-selector(
+ figure.where(kind: "dog").or(figure.where(kind: "fish")),
+ (),
+)
+
+// Test `or` duplicates removal.
+#test-selector(
+ heading.where(level: 1).or(heading.where(level: 1)),
+ ([A], [E], [H]),
+)
+
+// Test `and`.
+#test-selector(
+ figure.where(kind: "cat").and(figure.where(kind: "frog")),
+ (),
+)
+
+// Test `or` with `before`/`after`
+#test-selector(
+ selector(heading)
+ .before()
+ .or(selector(figure).before()),
+ ([A], [B], [Cat], [D], [E]),
+)
+
+#test-selector(
+ heading.where(level: 2)
+ .after()
+ .or(selector(figure).after()),
+ ([Frog], [Giraffe], [GiraffeCat], [Iguana], [I]),
+)
+
+// Test `and` with `after`
+#test-selector(
+ figure.where(kind: "cat")
+ .and(figure.where(supplement: [Other]))
+ .after(),
+ ([GiraffeCat],),
+)
+
+// Test `and` (with nested `or`)
+#test-selector(
+ heading.where(level: 2)
+ .or(heading.where(level: 3))
+ .and(heading.where(level: 2).or(heading.where(level: 1))),
+ ([B], [I]),
+)
+
+#test-selector(
+ heading.where(level: 2)
+ .or(heading.where(level: 3), heading.where(level:1))
+ .and(
+ heading.where(level: 2).or(heading.where(level: 1)),
+ heading.where(level: 3).or(heading.where(level: 1)),
+ ),
+ ([A], [E], [H]),
+)
+
+// Test `and` with `or` and `before`/`after`
+#test-selector(
+ heading.where(level: 1).before()
+ .or(heading.where(level: 3).before())
+ .and(
+ heading.where(level: 1).before()
+ .or(heading.where(level: 2).before())
+ ),
+ ([A], [E]),
+)
+
+#test-selector(
+ heading.where(level: 1).before(, inclusive: false)
+ .or(selector(figure).after())
+ .and(figure.where(kind: "iguana").or(
+ figure.where(kind: "frog"),
+ figure.where(kind: "cat"),
+ heading.where(level: 1).after(),
+ )),
+ ([Frog], [GiraffeCat], [Iguana])
+)
+
+--- issue-3726-query-show-set ---
+// Test that show rules apply to queried elements, i.e. that the content
+// returned from `query` isn't yet marked as prepared.
+#set heading(numbering: "1.")
+#show heading: underline
+= Hi
+
+#set heading(numbering: "I.")
+#show heading: set text(blue)
+#show heading: highlight.with(fill: aqua.lighten(50%))
+= Bye
+
+// New show rules apply to this, but its location and the materialized fields
+// from the original are retained.
+#context query(heading).join()
diff --git a/tests/suite/introspection/state.typ b/tests/suite/introspection/state.typ
new file mode 100644
index 0000000000..208a4ea285
--- /dev/null
+++ b/tests/suite/introspection/state.typ
@@ -0,0 +1,63 @@
+// Test state.
+
+--- state-basic ---
+#let s = state("hey", "a")
+#let double(it) = 2 * it
+
+#s.update(double)
+#s.update(double)
+$ 2 + 3 $
+#s.update(double)
+
+Is: #context s.get(),
+Was: #context {
+ let it = query(math.equation).first()
+ s.at(it.location())
+}.
+
+--- state-multiple-calls-same-key ---
+// Try same key with different initial value.
+#context state("key", 2).get()
+#state("key").update(x => x + 1)
+#context state("key", 2).get()
+#context state("key", 3).get()
+#state("key").update(x => x + 1)
+#context state("key", 2).get()
+
+--- state-nested ---
+#set page(width: 200pt)
+#set text(8pt)
+
+#let ls = state("lorem", lorem(1000).split("."))
+#let loremum(count) = {
+ context ls.get().slice(0, count).join(".").trim() + "."
+ ls.update(list => list.slice(count))
+}
+
+#let fs = state("fader", red)
+#let trait(title) = block[
+ #context text(fill: fs.get())[
+ *#title:* #loremum(1)
+ ]
+ #fs.update(color => color.lighten(30%))
+]
+
+#trait[Boldness]
+#trait[Adventure]
+#trait[Fear]
+#trait[Anger]
+
+--- state-no-convergence ---
+// Make sure that a warning is produced if the layout fails to converge.
+// Warning: layout did not converge within 5 attempts
+// Hint: check if any states or queries are updating themselves
+#let s = state("s", 1)
+#context s.update(s.final() + 1)
+#context s.get()
+
+--- state-at-no-context ---
+// Test `state.at` outside of context.
+// Error: 2-26 can only be used when context is known
+// Hint: 2-26 try wrapping this in a `context` expression
+// Hint: 2-26 the `context` expression should wrap everything that depends on this function
+#state("key").at()
diff --git a/tests/suite/layout/align.typ b/tests/suite/layout/align.typ
new file mode 100644
index 0000000000..61b7997578
--- /dev/null
+++ b/tests/suite/layout/align.typ
@@ -0,0 +1,142 @@
+// Test alignment.
+
+--- align-right ---
+// Test ragged-left.
+#set align(right)
+To the right! Where the sunlight peeks behind the mountain.
+
+--- align-in-stack ---
+#set page(height: 100pt)
+#stack(dir: ltr,
+ align(left, square(size: 15pt, fill: eastern)),
+ align(center, square(size: 20pt, fill: eastern)),
+ align(right, square(size: 15pt, fill: eastern)),
+)
+#align(center + horizon, rect(fill: eastern, height: 10pt))
+#align(bottom, stack(
+ align(center, rect(fill: conifer, height: 10pt)),
+ rect(fill: forest, height: 10pt, width: 100%),
+))
+
+--- align-center-in-flow ---
+// Test that multiple paragraphs in subflow also respect alignment.
+#align(center)[
+ Lorem Ipsum
+
+ Dolor
+]
+
+--- align-start-and-end ---
+// Test start and end alignment.
+#rotate(-30deg, origin: end + horizon)[Hello]
+
+#set text(lang: "de")
+#align(start)[Start]
+#align(end)[Ende]
+
+#set text(lang: "ar")
+#align(start)[يبدأ]
+#align(end)[نهاية]
+
+--- alignment-fields-x ---
+// Test 2d alignment 'horizontal' field.
+#test((start + top).x, start)
+#test((end + top).x, end)
+#test((left + top).x, left)
+#test((right + top).x, right)
+#test((center + top).x, center)
+#test((start + bottom).x, start)
+#test((end + bottom).x, end)
+#test((left + bottom).x, left)
+#test((right + bottom).x, right)
+#test((center + bottom).x, center)
+#test((start + horizon).x, start)
+#test((end + horizon).x, end)
+#test((left + horizon).x, left)
+#test((right + horizon).x, right)
+#test((center + horizon).x, center)
+#test((top + start).x, start)
+#test((bottom + end).x, end)
+#test((horizon + center).x, center)
+
+--- alignment-fields-y ---
+// Test 2d alignment 'vertical' field.
+#test((start + top).y, top)
+#test((end + top).y, top)
+#test((left + top).y, top)
+#test((right + top).y, top)
+#test((center + top).y, top)
+#test((start + bottom).y, bottom)
+#test((end + bottom).y, bottom)
+#test((left + bottom).y, bottom)
+#test((right + bottom).y, bottom)
+#test((center + bottom).y, bottom)
+#test((start + horizon).y, horizon)
+#test((end + horizon).y, horizon)
+#test((left + horizon).y, horizon)
+#test((right + horizon).y, horizon)
+#test((center + horizon).y, horizon)
+#test((top + start).y, top)
+#test((bottom + end).y, bottom)
+#test((horizon + center).y, horizon)
+
+--- alignment-type ---
+#test(type(center), alignment)
+#test(type(horizon), alignment)
+#test(type(center + horizon), alignment)
+
+--- alignment-axis ---
+// Test alignment methods.
+#test(start.axis(), "horizontal")
+#test(end.axis(), "horizontal")
+#test(left.axis(), "horizontal")
+#test(right.axis(), "horizontal")
+#test(center.axis(), "horizontal")
+#test(top.axis(), "vertical")
+#test(bottom.axis(), "vertical")
+#test(horizon.axis(), "vertical")
+
+--- alignment-inv ---
+#test(start.inv(), end)
+#test(end.inv(), start)
+#test(left.inv(), right)
+#test(right.inv(), left)
+#test(center.inv(), center)
+#test(top.inv(), bottom)
+#test(bottom.inv(), top)
+#test(horizon.inv(), horizon)
+#test((start + top).inv(), (end + bottom))
+#test((end + top).inv(), (start + bottom))
+#test((left + top).inv(), (right + bottom))
+#test((right + top).inv(), (left + bottom))
+#test((center + top).inv(), (center + bottom))
+#test((start + bottom).inv(), (end + top))
+#test((end + bottom).inv(), (start + top))
+#test((left + bottom).inv(), (right + top))
+#test((right + bottom).inv(), (left + top))
+#test((center + bottom).inv(), (center + top))
+#test((start + horizon).inv(), (end + horizon))
+#test((end + horizon).inv(), (start + horizon))
+#test((left + horizon).inv(), (right + horizon))
+#test((right + horizon).inv(), (left + horizon))
+#test((center + horizon).inv(), (center + horizon))
+#test((top + start).inv(), (end + bottom))
+#test((bottom + end).inv(), (start + top))
+#test((horizon + center).inv(), (center + horizon))
+
+--- alignment-add-two-horizontal ---
+// Error: 8-22 cannot add two horizontal alignments
+#align(center + right, [A])
+
+--- alignment-add-two-vertical ---
+// Error: 8-20 cannot add two vertical alignments
+#align(top + bottom, [A])
+
+--- alignment-add-vertical-and-2d ---
+// Error: 8-30 cannot add a vertical and a 2D alignment
+#align(top + (bottom + right), [A])
+
+--- issue-1398-line-align ---
+// Test right-aligning a line and a rectangle.
+#align(right, line(length: 30%))
+#align(right, rect())
diff --git a/tests/suite/layout/angle.typ b/tests/suite/layout/angle.typ
new file mode 100644
index 0000000000..65e80aeb64
--- /dev/null
+++ b/tests/suite/layout/angle.typ
@@ -0,0 +1,8 @@
+--- angle-to-unit ---
+// Test angle methods.
+#test(1rad.rad(), 1.0)
+#test(1.23rad.rad(), 1.23)
+#test(0deg.rad(), 0.0)
+#test(2deg.deg(), 2.0)
+#test(2.94deg.deg(), 2.94)
+#test(0rad.deg(), 0.0)
diff --git a/tests/typ/empty.typ b/tests/suite/layout/clip.typ
similarity index 100%
rename from tests/typ/empty.typ
rename to tests/suite/layout/clip.typ
diff --git a/tests/typ/layout/columns.typ b/tests/suite/layout/columns.typ
similarity index 85%
rename from tests/typ/layout/columns.typ
rename to tests/suite/layout/columns.typ
index ecf636e774..87a9f773aa 100644
--- a/tests/typ/layout/columns.typ
+++ b/tests/suite/layout/columns.typ
@@ -1,6 +1,6 @@
// Test the column layouter.
----
+--- columns-rtl ---
// Test normal operation and RTL directions.
#set page(height: 3.25cm, width: 7.05cm, columns: 2)
#set text(lang: "ar", font: ("Noto Sans Arabic", "Linux Libertine"))
@@ -12,7 +12,7 @@
#box(fill: eastern, height: 8pt, width: 6pt)
الجزيئات الضخمة الأربعة الضرورية للحياة.
----
+--- columns-in-fixed-size-rect ---
// Test the `columns` function.
#set page(width: auto)
@@ -23,7 +23,7 @@
The columns at least were graciously balanced.
]))
----
+--- columns-set-page ---
// Test columns for a sized page.
#set page(height: 5cm, width: 7.05cm, columns: 2)
@@ -36,7 +36,7 @@ so I'm returning to this trusty tool of tangible terror.
Sure, it is not the most creative way of filling up
a page for a test but it does get the job done.
----
+--- columns-in-auto-sized-rect ---
// Test the expansion behaviour.
#set page(height: 2.5cm, width: 7.05cm)
@@ -47,7 +47,7 @@ a page for a test but it does get the job done.
DEF
]))
----
+--- columns-more-with-gutter ---
// Test setting a column gutter and more than two columns.
#set page(height: 3.25cm, width: 7.05cm, columns: 3)
#set columns(gutter: 30pt)
@@ -56,7 +56,7 @@ a page for a test but it does get the job done.
#rect(width: 100%, height: 2cm, fill: eastern) #parbreak()
#circle(fill: eastern)
----
+--- columns-set-page-colbreak-pagebreak ---
// Test the `colbreak` and `pagebreak` functions.
#set page(height: 1cm, width: 7.05cm, columns: 2)
@@ -69,19 +69,19 @@ C
#colbreak()
D
----
+--- columns-empty-second-column ---
// Test an empty second column.
#set page(width: 7.05cm, columns: 2)
#rect(width: 100%, inset: 3pt)[So there isn't anything in the second column?]
----
+--- columns-page-width-auto ---
// Test columns when one of them is empty.
#set page(width: auto, columns: 3)
Arbitrary horizontal growth.
----
+--- columns-page-height-auto ---
// Test columns in an infinitely high frame.
#set page(width: 7.05cm, columns: 2)
@@ -93,20 +93,32 @@ and the document will grow with it.
Only an explicit #colbreak() `#colbreak()` can put content in the
second column.
----
+--- columns-one ---
// Test a page with a single column.
#set page(height: auto, width: 7.05cm, columns: 1)
This is a normal page. Very normal.
----
+--- columns-zero ---
// Test a page with zero columns.
// Error: 49-50 number must be positive
#set page(height: auto, width: 7.05cm, columns: 0)
----
+--- columns-colbreak-after-place ---
// Test colbreak after only out-of-flow elements.
#set page(width: 7.05cm, columns: 2)
#place[OOF]
#colbreak()
In flow.
+
+--- issue-columns-heading ---
+// The well-known columns bug.
+#set page(height: 70pt)
+
+Hallo
+#columns(2)[
+ = A
+ Text
+ = B
+ Text
+]
diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ
new file mode 100644
index 0000000000..aff26bac20
--- /dev/null
+++ b/tests/suite/layout/container.typ
@@ -0,0 +1,179 @@
+// Test the `box` and `block` containers.
+
+--- box ---
+// Test box in paragraph.
+A #box[B \ C] D.
+
+// Test box with height.
+Spaced \
+#box(height: 0.5cm) \
+Apart
+
+--- block-sizing ---
+// Test block sizing.
+#set page(height: 120pt)
+#set block(spacing: 0pt)
+#block(width: 90pt, height: 80pt, fill: red)[
+ #block(width: 60%, height: 60%, fill: green)
+ #block(width: 50%, height: 60%, fill: blue)
+]
+
+--- box-width-fr ---
+// Test fr box.
+Hello #box(width: 1fr, rect(height: 0.7em, width: 100%)) World
+
+--- block-multiple-pages ---
+// Test block over multiple pages.
+#set page(height: 60pt)
+
+First!
+
+#block[
+ But, soft! what light through yonder window breaks? It is the east, and Juliet
+ is the sun.
+]
+
+--- block-box-fill ---
+#set page(height: 100pt)
+#let words = lorem(18).split()
+#block(inset: 8pt, width: 100%, fill: aqua, stroke: aqua.darken(30%))[
+ #words.slice(0, 13).join(" ")
+ #box(fill: teal, outset: 2pt)[tempor]
+ #words.slice(13).join(" ")
+]
+
+--- block-spacing-basic ---
+#set block(spacing: 10pt)
+Hello
+
+There
+
+#block(spacing: 20pt)[Further down]
+
+--- block-spacing-table ---
+// Test that paragraph spacing loses against block spacing.
+// TODO
+#set block(spacing: 100pt)
+#show table: set block(above: 5pt, below: 5pt)
+Hello
+#table(columns: 4, fill: (x, y) => if calc.odd(x + y) { silver })[A][B][C][D]
+
+--- block-spacing-maximum ---
+// While we're at it, test the larger block spacing wins.
+#set block(spacing: 0pt)
+#show raw: set block(spacing: 15pt)
+#show list: set block(spacing: 2.5pt)
+
+```rust
+fn main() {}
+```
+
+- List
+
+Paragraph
+
+--- block-spacing-collapse-text-style ---
+// Test spacing collapsing with different font sizes.
+#grid(columns: 2)[
+ #text(size: 12pt, block(below: 1em)[A])
+ #text(size: 8pt, block(above: 1em)[B])
+][
+ #text(size: 12pt, block(below: 1em)[A])
+ #text(size: 8pt, block(above: 1.25em)[B])
+]
+
+--- block-fixed-height ---
+#set page(height: 100pt)
+#set align(center)
+
+#lorem(10)
+#block(width: 80%, height: 60pt, fill: aqua)
+#lorem(6)
+#block(
+ breakable: false,
+ width: 100%,
+ inset: 4pt,
+ fill: aqua,
+ lorem(8) + colbreak(),
+)
+
+--- box-clip-rect ---
+// Test box clipping with a rectangle
+Hello #box(width: 1em, height: 1em, clip: false)[#rect(width: 3em, height: 3em, fill: red)]
+world 1
+
+Space
+
+Hello #box(width: 1em, height: 1em, clip: true)[#rect(width: 3em, height: 3em, fill: red)]
+world 2
+
+--- block-clip-text ---
+// Test cliping text
+#block(width: 5em, height: 2em, clip: false, stroke: 1pt + black)[
+ But, soft! what light through
+]
+
+#v(2em)
+
+#block(width: 5em, height: 2em, clip: true, stroke: 1pt + black)[
+ But, soft! what light through yonder window breaks? It is the east, and Juliet
+ is the sun.
+]
+
+--- block-clip-svg-glyphs ---
+// Test clipping svg glyphs
+Emoji: #box(height: 0.5em, stroke: 1pt + black)[🐪, 🌋, 🏞]
+
+Emoji: #box(height: 0.5em, clip: true, stroke: 1pt + black)[🐪, 🌋, 🏞]
+
+--- block-clipping-multiple-pages ---
+// Test block clipping over multiple pages.
+#set page(height: 60pt)
+
+First!
+
+#block(height: 4em, clip: true, stroke: 1pt + black)[
+ But, soft! what light through yonder window breaks? It is the east, and Juliet
+ is the sun.
+]
+
+--- box-clip-radius ---
+// Test clipping with `radius`.
+#set page(height: 60pt)
+
+#box(
+ radius: 5pt,
+ stroke: 2pt + black,
+ width: 20pt,
+ height: 20pt,
+ clip: true,
+ image("/assets/images/rhino.png", width: 30pt)
+)
+
+--- box-clip-radius-without-stroke ---
+// Test clipping with `radius`, but without `stroke`.
+#set page(height: 60pt)
+
+#box(
+ radius: 5pt,
+ width: 20pt,
+ height: 20pt,
+ clip: true,
+ image("/assets/images/rhino.png", width: 30pt)
+)
+
+--- container-layoutable-child ---
+// Test box/block sizing with directly layoutable child.
+//
+// Ensure that the output respects the box size.
+#let check(f) = f(
+ width: 40pt, height: 25pt, fill: aqua,
+ grid(rect(width: 5pt, height: 5pt, fill: blue)),
+)
+
+#stack(dir: ltr, spacing: 1fr, check(box), check(block))
+
+--- issue-2128-block-width-box ---
+// Test box in 100% width block.
+#block(width: 100%, fill: red, box("a box"))
+#block(width: 100%, fill: red, [#box("a box") #box()])
diff --git a/tests/suite/layout/dir.typ b/tests/suite/layout/dir.typ
new file mode 100644
index 0000000000..139a2285d2
--- /dev/null
+++ b/tests/suite/layout/dir.typ
@@ -0,0 +1,24 @@
+--- dir-axis ---
+// Test direction methods.
+#test(ltr.axis(), "horizontal")
+#test(rtl.axis(), "horizontal")
+#test(ttb.axis(), "vertical")
+#test(btt.axis(), "vertical")
+
+--- dir-start ---
+#test(ltr.start(), left)
+#test(rtl.start(), right)
+#test(ttb.start(), top)
+#test(btt.start(), bottom)
+
+--- dir-end ---
+#test(ltr.end(), right)
+#test(rtl.end(), left)
+#test(ttb.end(), bottom)
+#test(btt.end(), top)
+
+--- dir-inv ---
+#test(ltr.inv(), rtl)
+#test(rtl.inv(), ltr)
+#test(ttb.inv(), btt)
+#test(btt.inv(), ttb)
diff --git a/tests/suite/layout/flow/flow.typ b/tests/suite/layout/flow/flow.typ
new file mode 100644
index 0000000000..9c48c9acc9
--- /dev/null
+++ b/tests/suite/layout/flow/flow.typ
@@ -0,0 +1,67 @@
+--- flow-fr ---
+#set page(height: 2cm)
+#set text(white)
+#rect(fill: forest)[
+ #v(1fr)
+ #h(1fr) Hi you!
+]
+
+--- issue-flow-overlarge-frames ---
+// In this bug, the first line of the second paragraph was on its page alone an
+// the rest moved down. The reason was that the second block resulted in
+// overlarge frames because the region wasn't finished properly.
+#set page(height: 70pt)
+#block[This file tests a bug where an almost empty page occurs.]
+#block[
+ The text in this second block was torn apart and split up for
+ some reason beyond my knowledge.
+]
+
+--- issue-flow-trailing-leading ---
+// In this bug, the first part of the paragraph moved down to the second page
+// because trailing leading wasn't trimmed, resulting in an overlarge frame.
+#set page(height: 60pt)
+#v(19pt)
+#block[
+ But, soft! what light through yonder window breaks?
+ It is the east, and Juliet is the sun.
+]
+
+--- issue-flow-weak-spacing ---
+// In this bug, there was a bit of space below the heading because weak spacing
+// directly before a layout-induced column or page break wasn't trimmed.
+#set page(height: 60pt)
+#rect(inset: 0pt, columns(2)[
+ Text
+ #v(12pt)
+ Hi
+ #v(10pt, weak: true)
+ At column break.
+])
+
+--- issue-flow-frame-placement ---
+// In this bug, a frame intended for the second region ended up in the first.
+#set page(height: 105pt)
+#block(lorem(20))
+
+--- issue-flow-layout-index-out-of-bounds ---
+// This bug caused an index-out-of-bounds panic when layouting paragraphs needed
+// multiple reorderings.
+#set page(height: 200pt)
+#lorem(30)
+
+#figure(placement: auto, block(height: 100%))
+
+#lorem(10)
+
+#lorem(10)
+
+--- issue-3641-float-loop ---
+// Flow layout should terminate!
+//
+// This is not yet ideal: The heading should not move to the second page, but
+// that's a separate bug and not a regression.
+#set page(height: 40pt)
+
+= Heading
+#lorem(6)
diff --git a/tests/suite/layout/flow/invisibles.typ b/tests/suite/layout/flow/invisibles.typ
new file mode 100644
index 0000000000..7e46037352
--- /dev/null
+++ b/tests/suite/layout/flow/invisibles.typ
@@ -0,0 +1,61 @@
+// Test out-of-flow items (place, counter updates, etc.) at the
+// beginning of a block not creating a frame just for them.
+
+--- flow-first-region-no-item ---
+// No item in the first region.
+#set page(height: 5cm, margin: 1cm)
+No item in the first region.
+#block(breakable: true, stroke: 1pt, inset: 0.5cm)[
+ #rect(height: 2cm, fill: gray)
+]
+
+--- flow-first-region-counter-update ---
+// Counter update in the first region.
+#set page(height: 5cm, margin: 1cm)
+Counter update.
+#block(breakable: true, stroke: 1pt, inset: 0.5cm)[
+ #counter("dummy").step()
+ #rect(height: 2cm, fill: gray)
+]
+
+--- flow-first-region-placed ---
+// Placed item in the first region.
+#set page(height: 5cm, margin: 1cm)
+Placed item in the first region.
+#block(breakable: true, above: 1cm, stroke: 1pt, inset: 0.5cm)[
+ #place(dx: -0.5cm, dy: -0.75cm, box(width: 200%)[OOF])
+ #rect(height: 2cm, fill: gray)
+]
+
+--- flow-first-region-zero-sized-item ---
+// In-flow item with size zero in the first region.
+#set page(height: 5cm, margin: 1cm)
+In-flow, zero-sized item.
+#block(breakable: true, stroke: 1pt, inset: 0.5cm)[
+ #set block(spacing: 0pt)
+ #line(length: 0pt)
+ #rect(height: 2cm, fill: gray)
+ #line(length: 100%)
+]
+
+--- flow-first-region-counter-update-and-placed ---
+// Counter update and placed item in the first region.
+#set page(height: 5cm, margin: 1cm)
+Counter update + place.
+#block(breakable: true, above: 1cm, stroke: 1pt, inset: 0.5cm)[
+ #counter("dummy").step()
+ #place(dx: -0.5cm, dy: -0.75cm, box([OOF]))
+ #rect(height: 2cm, fill: gray)
+]
+
+--- flow-first-region-counter-update-placed-and-line ---
+// Mix-and-match all the previous ones.
+#set page(height: 5cm, margin: 1cm)
+Mix-and-match all the previous tests.
+#block(breakable: true, above: 1cm, stroke: 1pt, inset: 0.5cm)[
+ #counter("dummy").step()
+ #place(dx: -0.5cm, dy: -0.75cm, box(width: 200%)[OOF])
+ #line(length: 100%)
+ #place(dy: -0.8em)[OOF]
+ #rect(height: 2cm, fill: gray)
+]
diff --git a/tests/suite/layout/flow/orphan.typ b/tests/suite/layout/flow/orphan.typ
new file mode 100644
index 0000000000..70eac731b8
--- /dev/null
+++ b/tests/suite/layout/flow/orphan.typ
@@ -0,0 +1,31 @@
+// Test that lines and headings doesn't become orphan.
+
+--- flow-heading-no-orphan ---
+#set page(height: 100pt)
+#lorem(12)
+
+= Introduction
+This is the start and it goes on.
+
+--- flow-par-no-orphan-and-widow-lines ---
+// LARGE
+#set page("a8", height: 140pt)
+#set text(weight: 700)
+
+// Fits fully onto the first page.
+#set text(blue)
+#lorem(27)
+
+// The first line would fit, but is moved to the second page.
+#lorem(20)
+
+// The second-to-last line is moved to the third page so that the last is isn't
+// as lonely.
+#set text(maroon)
+#lorem(11)
+
+#lorem(13)
+
+// All three lines go to the next page.
+#set text(olive)
+#lorem(10)
diff --git a/tests/suite/layout/grid/cell.typ b/tests/suite/layout/grid/cell.typ
new file mode 100644
index 0000000000..3b08c752a6
--- /dev/null
+++ b/tests/suite/layout/grid/cell.typ
@@ -0,0 +1,132 @@
+// Test basic styling using the grid.cell element.
+
+--- grid-cell-override ---
+// Cell override
+#grid(
+ align: left,
+ fill: red,
+ stroke: blue,
+ inset: 5pt,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ grid.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A],
+ grid.cell(align: horizon)[G2], [A\ A\ A],
+ grid.cell(inset: 0pt)[I], [F],
+ [H], grid.cell(fill: blue)[J]
+)
+
+--- grid-cell-show ---
+// Cell show rule
+#show grid.cell: it => [Zz]
+
+#grid(
+ align: left,
+ fill: red,
+ stroke: blue,
+ inset: 5pt,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ grid.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A]
+)
+
+--- grid-cell-show-and-override ---
+#show grid.cell: it => (it.align, it.fill)
+#grid(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ grid.cell(align: right)[B],
+ grid.cell(fill: aqua)[B],
+)
+
+--- grid-cell-set ---
+// Cell set rules
+#set grid.cell(align: center)
+#show grid.cell: it => (it.align, it.fill, it.inset)
+#set grid.cell(inset: 20pt)
+#grid(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ grid.cell(align: right)[B],
+ grid.cell(fill: aqua)[B],
+)
+
+--- grid-cell-folding ---
+// Test folding per-cell properties (align and inset)
+#grid(
+ columns: (1fr, 1fr),
+ rows: (2.5em, auto),
+ align: right,
+ inset: 5pt,
+ fill: (x, y) => (green, aqua).at(calc.rem(x + y, 2)),
+ [Top], grid.cell(align: bottom)[Bot],
+ grid.cell(inset: (bottom: 0pt))[Bot], grid.cell(inset: (bottom: 0pt))[Bot]
+)
+
+--- grid-cell-align-override ---
+// Test overriding outside alignment
+#set align(bottom + right)
+#grid(
+ columns: (1fr, 1fr),
+ rows: 2em,
+ align: auto,
+ fill: green,
+ [BR], [BR],
+ grid.cell(align: left, fill: aqua)[BL], grid.cell(align: top, fill: red.lighten(50%))[TR]
+)
+
+--- grid-cell-various-overrides ---
+#grid(
+ columns: 2,
+ fill: red,
+ align: left,
+ inset: 5pt,
+ [ABC], [ABC],
+ grid.cell(fill: blue)[C], [D],
+ grid.cell(align: center)[E], [F],
+ [G], grid.cell(inset: 0pt)[H]
+)
+
+--- grid-cell-show-emph ---
+#{
+ show grid.cell: emph
+ grid(
+ columns: 2,
+ gutter: 3pt,
+ [Hello], [World],
+ [Sweet], [Italics]
+ )
+}
+
+--- grid-cell-show-based-on-position ---
+// Style based on position
+#{
+ show grid.cell: it => {
+ if it.y == 0 {
+ strong(it)
+ } else if it.x == 1 {
+ emph(it)
+ } else {
+ it
+ }
+ }
+ grid(
+ columns: 3,
+ gutter: 3pt,
+ [Name], [Age], [Info],
+ [John], [52], [Nice],
+ [Mary], [50], [Cool],
+ [Jake], [49], [Epic]
+ )
+}
+
+--- table-cell-in-grid ---
+// Error: 7-19 cannot use `table.cell` as a grid cell; use `grid.cell` instead
+#grid(table.cell[])
diff --git a/tests/suite/layout/grid/colspan.typ b/tests/suite/layout/grid/colspan.typ
new file mode 100644
index 0000000000..707a9456b1
--- /dev/null
+++ b/tests/suite/layout/grid/colspan.typ
@@ -0,0 +1,142 @@
+--- grid-colspan ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-colspan-gutter ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-colspan-thick-stroke ---
+#set page(width: 300pt)
+#table(
+ columns: (2em, 2em, auto, auto),
+ stroke: 5pt,
+ [A], [B], [C], [D],
+ table.cell(colspan: 4, lorem(20)),
+ [A], table.cell(colspan: 2)[BCBCBCBC], [D]
+)
+
+--- grid-colspan-out-of-bounds ---
+// Error: 3:8-3:32 cell's colspan would cause it to exceed the available column(s)
+// Hint: 3:8-3:32 try placing the cell in another position or reducing its colspan
+#grid(
+ columns: 3,
+ [a], grid.cell(colspan: 3)[b]
+)
+
+--- grid-colspan-overlap ---
+// Error: 4:8-4:32 cell would span a previously placed cell at column 2, row 0
+// Hint: 4:8-4:32 try specifying your cells in a different order or reducing the cell's rowspan or colspan
+#grid(
+ columns: 3,
+ grid.cell(x: 2, y: 0)[x],
+ [a], grid.cell(colspan: 2)[b]
+)
+
+--- grid-colspan-over-all-fr-columns ---
+// Colspan over all fractional columns shouldn't expand auto columns on finite pages
+#table(
+ columns: (1fr, 1fr, auto),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+#table(
+ columns: (1fr, 1fr, auto),
+ table.cell(colspan: 3, lorem(8)),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+
+--- grid-colspan-over-some-fr-columns ---
+// Colspan over only some fractional columns will not trigger the heuristic, and
+// the auto column will expand more than it should. The table looks off, as a result.
+#table(
+ columns: (1fr, 1fr, auto),
+ [], table.cell(colspan: 2, lorem(8)),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+
+--- grid-colspan-over-all-fr-columns-page-width-auto ---
+// On infinite pages, colspan over all fractional columns SHOULD expand auto columns
+#set page(width: auto)
+#table(
+ columns: (1fr, 1fr, auto),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+#table(
+ columns: (1fr, 1fr, auto),
+ table.cell(colspan: 3, lorem(8)),
+ [A], [B], [C],
+ [D], [E], [F]
+)
+
+--- grid-colspan-multiple-regions ---
+// Test multiple regions
+#set page(height: 5em)
+#grid(
+ stroke: red,
+ fill: aqua,
+ columns: 4,
+ [a], [b], [c], [d],
+ [a], grid.cell(colspan: 2)[e, f, g, h, i], [f],
+ [e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
+ grid.cell(colspan: 4)[eeee e e e]
+)
diff --git a/tests/suite/layout/grid/footers.typ b/tests/suite/layout/grid/footers.typ
new file mode 100644
index 0000000000..c73bcb3935
--- /dev/null
+++ b/tests/suite/layout/grid/footers.typ
@@ -0,0 +1,404 @@
+--- grid-footer ---
+#set page(width: auto, height: 15em)
+#set text(6pt)
+#set table(inset: 2pt, stroke: 0.5pt)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 5).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten(),
+ table.footer(
+ table.hline(start: 2, end: 3, stroke: yellow),
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.cell(colspan: 5)[*Cool Zone*]
+ )
+)
+
+--- grid-footer-gutter-and-no-repeat ---
+// Gutter & no repetition
+#set page(width: auto, height: 16em)
+#set text(6pt)
+#set table(inset: 2pt, stroke: 0.5pt)
+#table(
+ columns: 5,
+ gutter: 2pt,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 5).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten(),
+ table.footer(
+ repeat: false,
+ table.hline(start: 2, end: 3, stroke: yellow),
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.cell(colspan: 5)[*Cool Zone*]
+ )
+)
+
+--- grid-cell-override-in-header-and-footer ---
+#table(
+ table.header(table.cell(stroke: red)[Hello]),
+ table.footer(table.cell(stroke: aqua)[Bye]),
+)
+
+--- grid-cell-override-in-header-and-footer-with-gutter ---
+#table(
+ gutter: 3pt,
+ table.header(table.cell(stroke: red)[Hello]),
+ table.footer(table.cell(stroke: aqua)[Bye]),
+)
+
+--- grid-footer-top-stroke ---
+// Footer's top stroke should win when repeated, but lose at the last page.
+#set page(height: 10em)
+#table(
+ stroke: green,
+ table.header(table.cell(stroke: red)[Hello]),
+ table.cell(stroke: yellow)[Hi],
+ table.cell(stroke: yellow)[Bye],
+ table.cell(stroke: yellow)[Ok],
+ table.footer[Bye],
+)
+
+--- grid-footer-relative-row-sizes ---
+// Relative lengths
+#set page(height: 10em)
+#table(
+ rows: (30%, 30%, auto),
+ [C],
+ [C],
+ table.footer[*A*][*B*],
+)
+
+--- grid-footer-cell-with-y ---
+#grid(
+ grid.footer(grid.cell(y: 2)[b]),
+ grid.cell(y: 0)[a],
+ grid.cell(y: 1)[c],
+)
+
+--- grid-footer-expand ---
+// Ensure footer properly expands
+#grid(
+ columns: 2,
+ [a], [],
+ [b], [],
+ grid.cell(x: 1, y: 3, rowspan: 4)[b],
+ grid.cell(y: 2, rowspan: 2)[a],
+ grid.footer(),
+ grid.cell(y: 4)[d],
+ grid.cell(y: 5)[e],
+ grid.cell(y: 6)[f],
+)
+
+--- grid-footer-not-at-last-row ---
+// Error: 2:3-2:19 footer must end at the last row
+#grid(
+ grid.footer([a]),
+ [b],
+)
+
+--- grid-footer-not-at-last-row-two-columns ---
+// Error: 3:3-3:19 footer must end at the last row
+#grid(
+ columns: 2,
+ grid.footer([a]),
+ [b],
+)
+
+--- grid-footer-overlap ---
+// Error: 4:3-4:19 footer would conflict with a cell placed before it at column 1 row 0
+// Hint: 4:3-4:19 try reducing that cell's rowspan or moving the footer
+#grid(
+ columns: 2,
+ grid.header(),
+ grid.footer([a]),
+ grid.cell(x: 1, y: 0, rowspan: 2)[a],
+)
+
+--- grid-footer-multiple ---
+// Error: 4:3-4:19 cannot have more than one footer
+#grid(
+ [a],
+ grid.footer([a]),
+ grid.footer([b]),
+)
+
+--- table-footer-in-grid ---
+// Error: 3:3-3:20 cannot use `table.footer` as a grid footer; use `grid.footer` instead
+#grid(
+ [a],
+ table.footer([a]),
+)
+
+--- grid-footer-in-table ---
+// Error: 3:3-3:19 cannot use `grid.footer` as a table footer; use `table.footer` instead
+#table(
+ [a],
+ grid.footer([a]),
+)
+
+--- grid-footer-in-grid-header ---
+// Error: 14-28 cannot place a grid footer within another footer or header
+#grid.header(grid.footer[a])
+
+--- table-footer-in-grid-header ---
+// Error: 14-29 cannot place a table footer within another footer or header
+#grid.header(table.footer[a])
+
+--- grid-footer-in-table-header ---
+// Error: 15-29 cannot place a grid footer within another footer or header
+#table.header(grid.footer[a])
+
+--- table-footer-in-table-header ---
+// Error: 15-30 cannot place a table footer within another footer or header
+#table.header(table.footer[a])
+
+--- grid-footer-in-grid-footer ---
+// Error: 14-28 cannot place a grid footer within another footer or header
+#grid.footer(grid.footer[a])
+
+--- table-footer-in-grid-footer ---
+// Error: 14-29 cannot place a table footer within another footer or header
+#grid.footer(table.footer[a])
+
+--- grid-footer-in-table-footer ---
+// Error: 15-29 cannot place a grid footer within another footer or header
+#table.footer(grid.footer[a])
+
+--- table-footer-in-table-footer ---
+// Error: 15-30 cannot place a table footer within another footer or header
+#table.footer(table.footer[a])
+
+--- grid-header-in-grid-footer ---
+// Error: 14-28 cannot place a grid header within another header or footer
+#grid.footer(grid.header[a])
+
+--- table-header-in-grid-footer ---
+// Error: 14-29 cannot place a table header within another header or footer
+#grid.footer(table.header[a])
+
+--- grid-header-in-table-footer ---
+// Error: 15-29 cannot place a grid header within another header or footer
+#table.footer(grid.header[a])
+
+--- table-header-in-table-footer ---
+// Error: 15-30 cannot place a table header within another header or footer
+#table.footer(table.header[a])
+
+--- grid-header-footer-block-with-fixed-height ---
+#set page(height: 17em)
+#table(
+ rows: (auto, 2.5em, auto),
+ table.header[*Hello*][*World*],
+ block(width: 2em, height: 10em, fill: red),
+ table.footer[*Bye*][*World*],
+)
+
+--- grid-header-footer-and-rowspan-non-contiguous-1 ---
+// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
+// ATM.
+#set page(height: 20em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto, 5em, 2em, 2.5em),
+ table.header[*Hello*][*World*],
+ table.cell(rowspan: 3, lorem(20)),
+ table.footer[*Ok*][*Bye*],
+)
+
+--- grid-header-footer-and-rowspan-non-contiguous-2 ---
+// This should look right
+#set page(height: 20em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto),
+ gutter: 3pt,
+ table.header[*Hello*][*World*],
+ table.cell(rowspan: 3, lorem(20)),
+ table.footer[*Ok*][*Bye*],
+)
+--- grid-header-and-footer-lack-of-space ---
+// Test lack of space for header + text.
+#set page(height: 9em + 2.5em + 1.5em)
+
+#table(
+ rows: (auto, 2.5em, auto, auto, 10em, 2.5em, auto),
+ gutter: 3pt,
+ table.header[*Hello*][*World*],
+ table.cell(rowspan: 3, lorem(30)),
+ table.footer[*Ok*][*Bye*],
+)
+
+--- grid-header-and-footer-orphan-prevention ---
+// Orphan header prevention test
+#set page(height: 13em)
+#v(8em)
+#grid(
+ columns: 3,
+ gutter: 5pt,
+ grid.header(
+ [*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*],
+ [*Header*], [*Header* #v(0.1em)],
+ ),
+ ..([Test], [Test], [Test]) * 7,
+ grid.footer(
+ [*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*],
+ [*Footer*], [*Footer* #v(0.1em)],
+ ),
+)
+
+--- grid-header-and-footer-empty ---
+// Empty footer should just be a repeated blank row
+#set page(height: 8em)
+#table(
+ columns: 4,
+ align: center + horizon,
+ table.header(),
+ ..range(0, 2).map(i => (
+ [John \##i],
+ table.cell(stroke: green)[123],
+ table.cell(stroke: blue)[456],
+ [789]
+ )).flatten(),
+ table.footer(),
+)
+
+--- grid-header-and-footer-containing-rowspan ---
+// When a footer has a rowspan with an empty row, it should be displayed
+// properly
+#set page(height: 14em, width: auto)
+
+#let count = counter("g")
+#table(
+ rows: (auto, 2em, auto, auto),
+ table.header(
+ [eeec],
+ table.cell(rowspan: 2, count.step() + count.display()),
+ ),
+ [d],
+ block(width: 5em, fill: yellow, lorem(7)),
+ [d],
+ table.footer(
+ [eeec],
+ table.cell(rowspan: 2, count.step() + count.display()),
+ )
+)
+#count.display()
+
+--- grid-nested-with-footers ---
+// Nested table with footer should repeat both footers
+#set page(height: 10em, width: auto)
+#table(
+ table(
+ [a\ b\ c\ d],
+ table.footer[b],
+ ),
+ table.footer[a],
+)
+
+--- grid-nested-footers ---
+#set page(height: 12em, width: auto)
+#table(
+ [a\ b\ c\ d],
+ table.footer(table(
+ [c],
+ [d],
+ table.footer[b],
+ ))
+)
+
+--- grid-footer-rowspan ---
+// General footer-only tests
+#set page(height: 9em)
+#table(
+ columns: 2,
+ [a], [],
+ [b], [],
+ [c], [],
+ [d], [],
+ [e], [],
+ table.footer(
+ [*Ok*], table.cell(rowspan: 2)[test],
+ [*Thanks*]
+ )
+)
+
+--- grid-footer-bare-1 ---
+#set page(height: 5em)
+#table(
+ table.footer[a][b][c]
+)
+
+--- grid-footer-bare-2 ---
+#table(table.footer[a][b][c])
+
+#table(
+ gutter: 3pt,
+ table.footer[a][b][c]
+)
+
+--- grid-footer-stroke-edge-cases ---
+// Test footer stroke priority edge case
+#set page(height: 10em)
+#table(
+ columns: 2,
+ stroke: black,
+ ..(table.cell(stroke: aqua)[d],) * 8,
+ table.footer(
+ table.cell(rowspan: 2, colspan: 2)[a],
+ [c], [d]
+ )
+)
+
+--- grid-footer-hline-and-vline-1 ---
+// Footer should appear at the bottom. Red line should be above the footer.
+// Green line should be on the left border.
+#set page(margin: 2pt)
+#set text(6pt)
+#table(
+ columns: 2,
+ inset: 1.5pt,
+ table.cell(y: 0)[a],
+ table.cell(x: 1, y: 1)[a],
+ table.cell(y: 2)[a],
+ table.footer(
+ table.hline(stroke: red),
+ table.vline(stroke: green),
+ [b],
+ ),
+ table.cell(x: 1, y: 3)[c]
+)
+
+--- grid-footer-hline-and-vline-2 ---
+// Table should be just one row. [c] appears at the third column.
+#set page(margin: 2pt)
+#set text(6pt)
+#table(
+ columns: 3,
+ inset: 1.5pt,
+ table.cell(y: 0)[a],
+ table.footer(
+ table.hline(stroke: red),
+ table.hline(y: 1, stroke: aqua),
+ table.cell(y: 0)[b],
+ [c]
+ )
+)
+
+--- grid-footer-below-rowspans ---
+// Footer should go below the rowspans.
+#set page(margin: 2pt)
+#set text(6pt)
+#table(
+ columns: 2,
+ inset: 1.5pt,
+ table.cell(rowspan: 2)[a], table.cell(rowspan: 2)[b],
+ table.footer()
+)
diff --git a/tests/suite/layout/grid/grid.typ b/tests/suite/layout/grid/grid.typ
new file mode 100644
index 0000000000..2d45095d89
--- /dev/null
+++ b/tests/suite/layout/grid/grid.typ
@@ -0,0 +1,285 @@
+// Test grid layouts.
+
+--- grid-columns-sizings-rect ---
+#let cell(width, color) = rect(width: width, height: 2cm, fill: color)
+#set page(width: 100pt, height: 140pt)
+#grid(
+ columns: (auto, 1fr, 3fr, 0.25cm, 3%, 2mm + 10%),
+ cell(0.5cm, rgb("2a631a")),
+ cell(100%, forest),
+ cell(100%, conifer),
+ cell(100%, rgb("ff0000")),
+ cell(100%, rgb("00ff00")),
+ cell(80%, rgb("00faf0")),
+ cell(1cm, rgb("00ff00")),
+ cell(0.5cm, rgb("2a631a")),
+ cell(100%, forest),
+ cell(100%, conifer),
+ cell(100%, rgb("ff0000")),
+ cell(100%, rgb("00ff00")),
+)
+
+--- grid-gutter-fr ---
+#set rect(inset: 0pt)
+#grid(
+ columns: (auto, auto, 40%),
+ column-gutter: 1fr,
+ row-gutter: 1fr,
+ rect(fill: eastern)[dddaa aaa aaa],
+ rect(fill: conifer)[ccc],
+ rect(fill: rgb("dddddd"))[aaa],
+)
+
+--- grid-row-sizing-manual-align ---
+#set page(height: 3cm, margin: 0pt)
+#grid(
+ columns: (1fr,),
+ rows: (1fr, auto, 2fr),
+ [],
+ align(center)[A bit more to the top],
+ [],
+)
+
+--- grid-finance ---
+// Test using the `grid` function to create a finance table.
+#set page(width: 11cm, height: 2.5cm)
+#grid(
+ columns: 5,
+ column-gutter: (2fr, 1fr, 1fr),
+ row-gutter: 6pt,
+ [*Quarter*],
+ [Expenditure],
+ [External Revenue],
+ [Financial ROI],
+ [_total_],
+ [*Q1*],
+ [173,472.57 \$],
+ [472,860.91 \$],
+ [51,286.84 \$],
+ [_350,675.18 \$_],
+ [*Q2*],
+ [93,382.12 \$],
+ [439,382.85 \$],
+ [-1,134.30 \$],
+ [_344,866.43 \$_],
+ [*Q3*],
+ [96,421.49 \$],
+ [238,583.54 \$],
+ [3,497.12 \$],
+ [_145,659.17 \$_],
+)
+// Test grid cells that overflow to the next region.
+
+--- grid-cell-breaking ---
+#set page(width: 5cm, height: 3cm)
+#grid(
+ columns: 2,
+ row-gutter: 8pt,
+ [Lorem ipsum dolor sit amet.
+
+ Aenean commodo ligula eget dolor. Aenean massa. Penatibus et magnis.],
+ [Text that is rather short],
+ [Fireflies],
+ [Critical],
+ [Decorum],
+ [Rampage],
+)
+
+--- grid-consecutive-rows-breaking ---
+// Test a column that starts overflowing right after another row/column did
+// that.
+#set page(width: 5cm, height: 2cm)
+#grid(
+ columns: 4 * (1fr,),
+ row-gutter: 10pt,
+ column-gutter: (0pt, 10%),
+ align(top, image("/assets/images/rhino.png")),
+ align(top, rect(inset: 0pt, fill: eastern, align(right)[LoL])),
+ [rofl],
+ [\ A] * 3,
+ [Ha!\ ] * 3,
+)
+
+--- grid-same-row-multiple-columns-breaking ---
+// Test two columns in the same row overflowing by a different amount.
+#set page(width: 5cm, height: 2cm)
+#grid(
+ columns: 3 * (1fr,),
+ row-gutter: 8pt,
+ column-gutter: (0pt, 10%),
+ [A], [B], [C],
+ [Ha!\ ] * 6,
+ [rofl],
+ [\ A] * 3,
+ [hello],
+ [darkness],
+ [my old]
+)
+
+--- grid-nested-breaking ---
+// Test grid within a grid, overflowing.
+#set page(width: 5cm, height: 2.25cm)
+#grid(
+ columns: 4 * (1fr,),
+ row-gutter: 10pt,
+ column-gutter: (0pt, 10%),
+ [A], [B], [C], [D],
+ grid(columns: 2, [A], [B], [C\ ]*3, [D]),
+ align(top, rect(inset: 0pt, fill: eastern, align(right)[LoL])),
+ [rofl],
+ [E\ ]*4,
+)
+
+--- grid-column-sizing-auto-base ---
+// Test that auto and relative columns use the correct base.
+#grid(
+ columns: (auto, 60%),
+ rows: (auto, auto),
+ rect(width: 50%, height: 0.5cm, fill: conifer),
+ rect(width: 100%, height: 0.5cm, fill: eastern),
+ rect(width: 50%, height: 0.5cm, fill: forest),
+)
+
+--- grid-column-sizing-fr-base ---
+// Test that fr columns use the correct base.
+#grid(
+ columns: (1fr,) * 4,
+ rows: (1cm,),
+ rect(width: 50%, fill: conifer),
+ rect(width: 50%, fill: forest),
+ rect(width: 50%, fill: conifer),
+ rect(width: 50%, fill: forest),
+)
+
+--- grid-column-sizing-mixed-base ---
+// Test that all three kinds of rows use the correct bases.
+#set page(height: 4cm, margin: 0cm)
+#grid(
+ rows: (1cm, 1fr, 1fr, auto),
+ rect(height: 50%, width: 100%, fill: conifer),
+ rect(height: 50%, width: 100%, fill: forest),
+ rect(height: 50%, width: 100%, fill: conifer),
+ rect(height: 25%, width: 100%, fill: forest),
+)
+
+--- grid-trailing-linebreak-region-overflow ---
+// Test that trailing linebreak doesn't overflow the region.
+#set page(height: 2cm)
+#grid[
+ Hello \
+ Hello \
+ Hello \
+
+ World
+]
+
+--- grid-breaking-expand-vertically ---
+// Test that broken cell expands vertically.
+#set page(height: 2.25cm)
+#grid(
+ columns: 2,
+ gutter: 10pt,
+ align(bottom)[A],
+ [
+ Top
+ #align(bottom)[
+ Bottom \
+ Bottom \
+ #v(0pt)
+ Top
+ ]
+ ],
+ align(top)[B],
+)
+
+--- grid-complete-rows ---
+// Ensure grids expand enough for the given rows.
+#grid(
+ columns: (2em, 2em),
+ rows: (2em,) * 4,
+ fill: red,
+ stroke: aqua,
+ [a]
+)
+
+--- grid-auto-shrink ---
+// Test iterative auto column shrinking.
+#set page(width: 210mm - 2 * 2.5cm + 2 * 10pt)
+#set text(11pt)
+#table(
+ columns: 4,
+ [Hello!],
+ [Hello there, my friend!],
+ [Hello there, my friends! Hi!],
+ [Hello there, my friends! Hi! What is going on right now?],
+)
+
+--- issue-grid-base-auto-row ---
+// Test that grid base for auto rows makes sense.
+#set page(height: 150pt)
+#table(
+ columns: (1.5cm, auto),
+ rows: (auto, auto),
+ rect(width: 100%, fill: red),
+ rect(width: 100%, fill: blue),
+ rect(width: 100%, height: 50%, fill: green),
+)
+
+--- issue-grid-base-auto-row-list ---
+#rect(width: 100%, height: 1em)
+- #rect(width: 100%, height: 1em)
+ - #rect(width: 100%, height: 1em)
+
+--- issue-grid-skip ---
+// Grid now skips a remaining region when one of the cells
+// doesn't fit into it at all.
+#set page(height: 100pt)
+#grid(
+ columns: (2cm, auto),
+ rows: (auto, auto),
+ rect(width: 100%, fill: red),
+ rect(width: 100%, fill: blue),
+ rect(width: 100%, height: 80%, fill: green),
+ [hello \ darkness #parbreak() my \ old \ friend \ I],
+ rect(width: 100%, height: 20%, fill: blue),
+ polygon(fill: red, (0%, 0%), (100%, 0%), (100%, 20%))
+)
+
+--- issue-grid-skip-list ---
+#set page(height: 60pt)
+#lorem(5)
+- #lorem(5)
+
+--- issue-grid-double-skip ---
+// Ensure that the list does not jump to the third page.
+#set page(height: 70pt)
+#v(40pt)
+The following:
++ A
++ B
+
+--- issue-grid-gutter-skip ---
+// Ensure gutter rows at the top or bottom of a region are skipped.
+#set page(height: 10em)
+
+#table(
+ row-gutter: 1.5em,
+ inset: 0pt,
+ rows: (1fr, auto),
+ [a],
+ [],
+ [],
+ [f],
+ [e\ e],
+ [],
+ [a]
+)
+
+--- issue-3917-grid-with-infinite-width ---
+// https://github.com/typst/typst/issues/1918
+#set page(width: auto)
+#context layout(available => {
+ let infinite-length = available.width
+ // Error: 3-50 cannot create grid with infinite width
+ grid(gutter: infinite-length, columns: 2)[A][B]
+})
diff --git a/tests/suite/layout/grid/headers.typ b/tests/suite/layout/grid/headers.typ
new file mode 100644
index 0000000000..b9a90461e3
--- /dev/null
+++ b/tests/suite/layout/grid/headers.typ
@@ -0,0 +1,368 @@
+--- grid-headers ---
+#set page(width: auto, height: 12em)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
+
+--- grid-headers-no-repeat ---
+// Disable repetition
+#set page(width: auto, height: 12em)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow),
+ repeat: false
+ ),
+ ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
+
+--- grid-headers-gutter ---
+#set page(width: auto, height: 12em)
+#table(
+ columns: 5,
+ align: center + horizon,
+ gutter: 3pt,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*Name*], table.cell(stroke: aqua)[*Number*], [*Data 1*], [*Data 2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow),
+ ),
+ ..range(0, 6).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
+
+--- grid-header-relative-row-sizes ---
+// Relative lengths
+#set page(height: 10em)
+#table(
+ rows: (30%, 30%, auto),
+ table.header(
+ [*A*],
+ [*B*]
+ ),
+ [C],
+ [C]
+)
+
+--- grid-header-cell-with-y ---
+#grid(
+ grid.cell(y: 1)[a],
+ grid.header(grid.cell(y: 0)[b]),
+ grid.cell(y: 2)[c]
+)
+
+--- grid-header-last-child ---
+// When the header is the last grid child, it shouldn't include the gutter row
+// after it, because there is none.
+#grid(
+ columns: 2,
+ gutter: 3pt,
+ grid.header(
+ [a], [b],
+ [c], [d]
+ )
+)
+
+--- grid-header-nested ---
+#set page(height: 14em)
+#let t(n) = table(
+ columns: 3,
+ align: center + horizon,
+ gutter: 3pt,
+ table.header(
+ table.cell(colspan: 3)[*Cool Zone #n*],
+ [*Name*], [*Num*], [*Data*]
+ ),
+ ..range(0, 5).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456])).flatten()
+)
+#grid(
+ gutter: 3pt,
+ t(0),
+ t(1)
+)
+
+--- grid-header-hline-and-vline ---
+// Test line positioning in header
+#table(
+ columns: 3,
+ stroke: none,
+ table.hline(stroke: red, end: 2),
+ table.vline(stroke: red, end: 3),
+ table.header(
+ table.hline(stroke: aqua, start: 2),
+ table.vline(stroke: aqua, start: 3), [*A*], table.hline(stroke: orange), table.vline(stroke: orange), [*B*],
+ [*C*], [*D*]
+ ),
+ [a], [b],
+ [c], [d],
+ [e], [f]
+)
+
+--- grid-header-not-at-first-row ---
+// Error: 3:3-3:19 header must start at the first row
+// Hint: 3:3-3:19 remove any rows before the header
+#grid(
+ [a],
+ grid.header([b])
+)
+
+--- grid-header-not-at-first-row-two-columns ---
+// Error: 4:3-4:19 header must start at the first row
+// Hint: 4:3-4:19 remove any rows before the header
+#grid(
+ columns: 2,
+ [a],
+ grid.header([b])
+)
+
+--- grow-header-multiple ---
+// Error: 3:3-3:19 cannot have more than one header
+#grid(
+ grid.header([a]),
+ grid.header([b]),
+ [a],
+)
+
+--- table-header-in-grid ---
+// Error: 2:3-2:20 cannot use `table.header` as a grid header; use `grid.header` instead
+#grid(
+ table.header([a]),
+ [a],
+)
+
+--- grid-header-in-table ---
+// Error: 2:3-2:19 cannot use `grid.header` as a table header; use `table.header` instead
+#table(
+ grid.header([a]),
+ [a],
+)
+
+--- grid-header-in-grid-header ---
+// Error: 14-28 cannot place a grid header within another header or footer
+#grid.header(grid.header[a])
+
+--- table-header-in-grid-header ---
+// Error: 14-29 cannot place a table header within another header or footer
+#grid.header(table.header[a])
+
+--- grid-header-in-table-header ---
+// Error: 15-29 cannot place a grid header within another header or footer
+#table.header(grid.header[a])
+
+--- table-header-in-table-header ---
+// Error: 15-30 cannot place a table header within another header or footer
+#table.header(table.header[a])
+
+--- grid-header-block-with-fixed-height ---
+#set page(height: 15em)
+#table(
+ rows: (auto, 2.5em, auto),
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ block(width: 2em, height: 20em, fill: red)
+)
+
+--- grid-header-and-rowspan-non-contiguous-1 ---
+// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
+// ATM.
+#set page(height: 15em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto, 5em),
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(40))
+)
+
+--- grid-header-and-rowspan-non-contiguous-2 ---
+// Rowspan sizing algorithm doesn't do the best job at non-contiguous content
+// ATM.
+#set page(height: 15em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto, 5em),
+ gutter: 3pt,
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(40))
+)
+
+--- grid-header-and-rowspan-non-contiguous-3 ---
+// This should look right
+#set page(height: 15em)
+
+#table(
+ rows: (auto, 2.5em, 2em, auto),
+ gutter: 3pt,
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(40))
+)
+
+--- grid-header-lack-of-space ---
+// Test lack of space for header + text.
+#set page(height: 9em)
+
+#table(
+ rows: (auto, 2.5em, auto, auto, 10em),
+ gutter: 3pt,
+ table.header(
+ [*Hello*],
+ [*World*]
+ ),
+ table.cell(rowspan: 3, lorem(80))
+)
+
+--- grid-header-orphan-prevention ---
+// Orphan header prevention test
+#set page(height: 12em)
+#v(8em)
+#grid(
+ columns: 3,
+ grid.header(
+ [*Mui*], [*A*], grid.cell(rowspan: 2, fill: orange)[*B*],
+ [*Header*], [*Header* #v(0.1em)]
+ ),
+ ..([Test], [Test], [Test]) * 20
+)
+
+--- grid-header-empty ---
+// Empty header should just be a repeated blank row
+#set page(height: 12em)
+#table(
+ columns: 4,
+ align: center + horizon,
+ table.header(),
+ ..range(0, 4).map(i => ([John \##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789])).flatten()
+)
+
+--- grid-header-containing-rowspan ---
+// When a header has a rowspan with an empty row, it should be displayed
+// properly
+#set page(height: 10em)
+
+#let count = counter("g")
+#table(
+ rows: (auto, 2em, auto, auto),
+ table.header(
+ [eeec],
+ table.cell(rowspan: 2, count.step() + count.display()),
+ ),
+ [d],
+ block(width: 5em, fill: yellow, lorem(15)),
+ [d]
+)
+#count.display()
+
+--- grid-header-expand ---
+// Ensure header expands to fit cell placed in it after its declaration
+#set page(height: 10em)
+#table(
+ columns: 2,
+ table.header(
+ [a], [b],
+ [c],
+ ),
+ table.cell(x: 1, y: 1, rowspan: 2, lorem(80))
+)
+
+--- grid-nested-with-headers ---
+// Nested table with header should repeat both headers
+#set page(height: 10em)
+#table(
+ table.header(
+ [a]
+ ),
+ table(
+ table.header(
+ [b]
+ ),
+ [a\ b\ c\ d]
+ )
+)
+
+--- grid-nested-headers ---
+#set page(height: 12em)
+#table(
+ table.header(
+ table(
+ table.header(
+ [b]
+ ),
+ [c],
+ [d]
+ )
+ ),
+ [a\ b]
+)
+
+--- grid-header-stroke-edge-cases ---
+// Test header stroke priority edge case (last header row removed)
+#set page(height: 8em)
+#table(
+ columns: 2,
+ stroke: black,
+ gutter: (auto, 3pt),
+ table.header(
+ [c], [d],
+ ),
+ ..(table.cell(stroke: aqua)[d],) * 8,
+)
+
+--- grid-header-hline-bottom ---
+// Yellow line should be kept here
+#set text(6pt)
+#table(
+ column-gutter: 3pt,
+ inset: 1pt,
+ table.header(
+ [a],
+ table.hline(stroke: yellow),
+ ),
+ table.cell(rowspan: 2)[b]
+)
+
+--- grid-header-hline-bottom-manually ---
+// Red line should be kept here
+#set page(height: 6em)
+#set text(6pt)
+#table(
+ column-gutter: 3pt,
+ inset: 1pt,
+ table.header(
+ table.hline(stroke: red, position: bottom),
+ [a],
+ ),
+ [a],
+ table.cell(stroke: aqua)[b]
+)
+
+--- grid-header-rowspan-base ---
+#set page(height: 7em)
+#set text(6pt)
+#let full-block = block(width: 2em, height: 100%, fill: red)
+#table(
+ columns: 3,
+ inset: 1.5pt,
+ table.header(
+ [a], full-block, table.cell(rowspan: 2, full-block),
+ [b]
+ )
+)
diff --git a/tests/suite/layout/grid/positioning.typ b/tests/suite/layout/grid/positioning.typ
new file mode 100644
index 0000000000..31896d9990
--- /dev/null
+++ b/tests/suite/layout/grid/positioning.typ
@@ -0,0 +1,203 @@
+// Test cell positioning in grids.
+
+--- grid-cell-show-x-y ---
+#{
+ show grid.cell: it => (it.x, it.y)
+ grid(
+ columns: 2,
+ inset: 5pt,
+ fill: aqua,
+ gutter: 3pt,
+ [Hello], [World],
+ [Sweet], [Home]
+ )
+}
+#{
+ show table.cell: it => pad(rest: it.inset)[#(it.x, it.y)]
+ table(
+ columns: 2,
+ gutter: 3pt,
+ [Hello], [World],
+ [Sweet], [Home]
+ )
+}
+
+--- grid-cell-position-out-of-order ---
+// Positioning cells in a different order than they appear
+#grid(
+ columns: 2,
+ [A], [B],
+ grid.cell(x: 1, y: 2)[C], grid.cell(x: 0, y: 2)[D],
+ grid.cell(x: 1, y: 1)[E], grid.cell(x: 0, y: 1)[F],
+)
+
+--- grid-cell-position-extra-rows ---
+// Creating more rows by positioning out of bounds
+#grid(
+ columns: 3,
+ rows: 1.5em,
+ inset: 5pt,
+ fill: (x, y) => if (x, y) == (0, 0) { blue } else if (x, y) == (2, 3) { red } else { green },
+ [A],
+ grid.cell(x: 2, y: 3)[B]
+)
+
+#table(
+ columns: (3em, 1em, 3em),
+ rows: 1.5em,
+ inset: (top: 0pt, bottom: 0pt, rest: 5pt),
+ fill: (x, y) => if (x, y) == (0, 0) { blue } else if (x, y) == (2, 3) { red } else { green },
+ align: (x, y) => (left, center, right).at(x),
+ [A],
+ table.cell(x: 2, y: 3)[B]
+)
+
+--- grid-cell-position-collide ---
+// Error: 3:3-3:42 attempted to place a second cell at column 0, row 0
+// Hint: 3:3-3:42 try specifying your cells in a different order
+#grid(
+ [A],
+ grid.cell(x: 0, y: 0)[This shall error]
+)
+
+--- table-cell-position-collide ---
+// Error: 3:3-3:43 attempted to place a second cell at column 0, row 0
+// Hint: 3:3-3:43 try specifying your cells in a different order
+#table(
+ [A],
+ table.cell(x: 0, y: 0)[This shall error]
+)
+
+--- grid-cell-position-automatic-skip-manual ---
+// Automatic position cell skips custom position cell
+#grid(
+ grid.cell(x: 0, y: 0)[This shall not error],
+ [A]
+)
+
+--- grid-cell-position-x-out-of-bounds ---
+// Error: 4:3-4:36 cell could not be placed at invalid column 2
+#grid(
+ columns: 2,
+ [A],
+ grid.cell(x: 2)[This shall error]
+)
+
+--- grid-cell-position-partial ---
+// Partial positioning
+#grid(
+ columns: 3,
+ rows: 1.5em,
+ inset: 5pt,
+ fill: aqua,
+ [A], grid.cell(y: 1, fill: green)[B], [C], grid.cell(x: auto, y: 1, fill: green)[D], [E],
+ grid.cell(y: 2, fill: green)[F], grid.cell(x: 0, fill: orange)[G], grid.cell(x: 0, y: auto, fill: orange)[H],
+ grid.cell(x: 1, fill: orange)[I]
+)
+
+#table(
+ columns: 3,
+ rows: 1.5em,
+ inset: 5pt,
+ fill: aqua,
+ [A], table.cell(y: 1, fill: green)[B], [C], table.cell(x: auto, y: 1, fill: green)[D], [E],
+ table.cell(y: 2, fill: green)[F], table.cell(x: 0, fill: orange)[G], table.cell(x: 0, y: auto, fill: orange)[H],
+ table.cell(x: 1, fill: orange)[I]
+)
+
+--- grid-cell-position-partial-collide ---
+// Error: 4:3-4:21 cell could not be placed in row 0 because it was full
+// Hint: 4:3-4:21 try specifying your cells in a different order
+#grid(
+ columns: 2,
+ [A], [B],
+ grid.cell(y: 0)[C]
+)
+
+--- table-cell-position-partial-collide ---
+// Error: 4:3-4:22 cell could not be placed in row 0 because it was full
+// Hint: 4:3-4:22 try specifying your cells in a different order
+#table(
+ columns: 2,
+ [A], [B],
+ table.cell(y: 0)[C]
+)
+
+--- grid-calendar ---
+#set page(width: auto)
+#show grid.cell: it => {
+ if it.y == 0 {
+ set text(white)
+ strong(it)
+ } else {
+ // For the second row and beyond, we will write the day number for each
+ // cell.
+
+ // In general, a cell's index is given by cell.x + columns * cell.y.
+ // Days start in the second grid row, so we subtract 1 row.
+ // But the first day is day 1, not day 0, so we add 1.
+ let day = it.x + 7 * (it.y - 1) + 1
+ if day <= 31 {
+ // Place the day's number at the top left of the cell.
+ // Only if the day is valid for this month (not 32 or higher).
+ place(top + left, dx: 2pt, dy: 2pt, text(8pt, red.darken(40%))[#day])
+ }
+ it
+ }
+}
+
+#grid(
+ fill: (x, y) => if y == 0 { gray.darken(50%) },
+ columns: (30pt,) * 7,
+ rows: (auto, 30pt),
+ // Events will be written at the bottom of each day square.
+ align: bottom,
+ inset: 5pt,
+ stroke: (thickness: 0.5pt, dash: "densely-dotted"),
+
+ [Sun], [Mon], [Tue], [Wed], [Thu], [Fri], [Sat],
+
+ // This event will occur on the first Friday (sixth column).
+ grid.cell(x: 5, fill: yellow.darken(10%))[Call],
+
+ // This event will occur every Monday (second column).
+ // We have to repeat it 5 times so it occurs every week.
+ ..(grid.cell(x: 1, fill: red.lighten(50%))[Meet],) * 5,
+
+ // This event will occur at day 19.
+ grid.cell(x: 4, y: 3, fill: orange.lighten(25%))[Talk],
+
+ // These events will occur at the second week, where available.
+ grid.cell(y: 2, fill: aqua)[Chat],
+ grid.cell(y: 2, fill: aqua)[Walk],
+)
+
+--- grid-exam ---
+#set page(width: auto)
+#show table.cell: it => {
+ if it.x == 0 or it.y == 0 {
+ set text(white)
+ strong(it)
+ } else if it.body == [] {
+ // Replace empty cells with 'N/A'
+ pad(rest: it.inset)[_N/A_]
+ } else {
+ it
+ }
+}
+
+#table(
+ fill: (x, y) => if x == 0 or y == 0 { gray.darken(50%) },
+ columns: 4,
+ [], [Exam 1], [Exam 2], [Exam 3],
+ ..([John], [Mary], [Jake], [Robert]).map(table.cell.with(x: 0)),
+
+ // Mary got grade A on Exam 3.
+ table.cell(x: 3, y: 2, fill: green)[A],
+
+ // Everyone got grade A on Exam 2.
+ ..(table.cell(x: 2, fill: green)[A],) * 4,
+
+ // Robert got grade B on other exams.
+ ..(table.cell(y: 4, fill: aqua)[B],) * 2,
+)
diff --git a/tests/suite/layout/grid/rowspan.typ b/tests/suite/layout/grid/rowspan.typ
new file mode 100644
index 0000000000..f7a377b6ed
--- /dev/null
+++ b/tests/suite/layout/grid/rowspan.typ
@@ -0,0 +1,490 @@
+--- grid-rowspan ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ table.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+--- grid-rowspan-gutter ---
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ grid.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+--- grid-rowspan-fixed-size ---
+// Fixed-size rows
+#set page(height: 10em)
+#grid(
+ columns: 2,
+ rows: 1.5em,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ grid.cell(rowspan: 3)[R1], [b],
+ [c],
+ [d],
+ [e], [f],
+ grid.cell(rowspan: 5)[R2], [h],
+ [i],
+ [j],
+ [k],
+ [l],
+ [m], [n]
+)
+
+--- grid-rowspan-cell-coordinates ---
+// Cell coordinate tests
+#set page(height: 10em)
+#show table.cell: it => [(#it.x, #it.y)]
+#table(
+ columns: 3,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c],
+ table.cell(colspan: 2)[d],
+ table.cell(colspan: 3, rowspan: 10)[a],
+ table.cell(colspan: 2)[b],
+)
+#table(
+ columns: 3,
+ gutter: 3pt,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c],
+ table.cell(colspan: 2)[d],
+ table.cell(colspan: 3, rowspan: 9)[a],
+ table.cell(colspan: 2)[b],
+)
+
+--- grid-rowspan-over-auto-row ---
+// Auto row expansion
+#set page(height: 10em)
+#grid(
+ columns: (1em, 1em),
+ rows: (0.5em, 0.5em, auto),
+ fill: orange,
+ gutter: 3pt,
+ grid.cell(rowspan: 4, [x x x x] + place(bottom)[*Bot*]),
+ [a],
+ [b],
+ [c],
+ [d]
+)
+
+--- grid-rowspan-excessive ---
+// Excessive rowspan (no gutter)
+#set page(height: 10em)
+#table(
+ columns: 4,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c], [d],
+ table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f],
+ table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2],
+ [b],
+)
+
+--- grid-rowspan-excessive-gutter ---
+// Excessive rowspan (with gutter)
+#set page(height: 10em)
+#table(
+ columns: 4,
+ gutter: 3pt,
+ fill: red,
+ [a], [b], table.cell(rowspan: 2)[c], [d],
+ table.cell(colspan: 2, stroke: (bottom: aqua + 2pt))[e], table.cell(stroke: (bottom: aqua))[f],
+ table.cell(colspan: 2, rowspan: 10)[R1], table.cell(colspan: 2, rowspan: 10)[R2],
+ [b],
+)
+
+--- grid-rowspan-over-fr-row-at-end ---
+// Fractional rows
+// They cause the auto row to expand more than needed.
+#set page(height: 10em)
+#grid(
+ fill: red,
+ gutter: 3pt,
+ columns: 3,
+ rows: (1em, auto, 1fr),
+ [a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)),
+ [c], [d],
+ [e], [f]
+)
+
+--- grid-rowspan-over-fr-row-at-start ---
+// Fractional rows
+#set page(height: 10em)
+#grid(
+ fill: red,
+ gutter: 3pt,
+ columns: 3,
+ rows: (1fr, auto, 1em),
+ [a], [b], grid.cell(rowspan: 3, block(height: 4em, width: 1em, fill: orange)),
+ [c], [d],
+ [e], [f]
+)
+
+--- grid-rowspan-cell-order ---
+// Cell order
+#let count = counter("count")
+#show grid.cell: it => {
+ count.step()
+ count.display()
+}
+
+#grid(
+ columns: (2em,) * 3,
+ stroke: aqua,
+ rows: 1.2em,
+ fill: (x, y) => if calc.odd(x + y) { red } else { orange },
+ [a], grid.cell(rowspan: 2)[b], grid.cell(rowspan: 2)[c],
+ [d],
+ grid.cell(rowspan: 2)[f], [g], [h],
+ [i], [j],
+ [k], [l], [m],
+ grid.cell(rowspan: 2)[n], [o], [p],
+ [q], [r],
+ [s], [t], [u]
+)
+
+--- grid-rowspan-unbreakable-1 ---
+#table(
+ columns: 3,
+ rows: (auto, auto, auto, 2em),
+ gutter: 3pt,
+ table.cell(rowspan: 4)[a \ b\ c\ d\ e], [c], [d],
+ [e], table.cell(breakable: false, rowspan: 2)[f],
+ [g]
+)
+
+--- grid-rowspan-unbreakable-2 ---
+// Test cell breakability
+#show grid.cell: it => {
+ assert.eq(it.breakable, (it.x, it.y) != (0, 6) and (it.y in (2, 5, 6) or (it.x, it.y) in ((0, 1), (2, 3), (1, 7))))
+ it.breakable
+}
+#grid(
+ columns: 3,
+ rows: (6pt, 1fr, auto, 1%, 1em, auto, auto, 0.2in),
+ row-gutter: (0pt, 0pt, 0pt, auto),
+ [a], [b], [c],
+ grid.cell(rowspan: 3)[d], [e], [f],
+ [g], [h],
+ [i], grid.cell(rowspan: 2)[j],
+ [k],
+ grid.cell(y: 5)[l],
+ grid.cell(y: 6, breakable: false)[m], grid.cell(y: 6, breakable: true)[n],
+ grid.cell(y: 7, breakable: false)[o], grid.cell(y: 7, breakable: true)[p], grid.cell(y: 7, breakable: auto)[q]
+)
+
+--- grid-rowspan-in-all-columns-stroke ---
+#table(
+ columns: 2,
+ table.cell(stroke: (bottom: red))[a], [b],
+ table.hline(stroke: green),
+ table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e],
+ [f],
+ [g]
+)
+
+--- grid-rowspan-in-all-columns-stroke-gutter ---
+#table(
+ columns: 2,
+ gutter: 3pt,
+ table.cell(stroke: (bottom: red))[a], [b],
+ table.hline(stroke: green),
+ table.cell(stroke: (top: yellow, left: green, right: aqua, bottom: blue), colspan: 1, rowspan: 2)[d], table.cell(colspan: 1, rowspan: 2)[e],
+ [f],
+ [g]
+)
+
+--- grid-rowspan-block-full-height ---
+// Block below shouldn't expand to the end of the page, but stay within its
+// rows' boundaries.
+#set page(height: 9em)
+#table(
+ rows: (1em, 1em, 1fr, 1fr, auto),
+ table.cell(rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+ table.cell(rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+ [a]
+)
+
+--- grid-rowspan-block-overflow ---
+#set page(height: 7em)
+#table(
+ columns: 3,
+ [], [], table.cell(breakable: true, rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+ table.cell(breakable: false, block(width: 2em, height: 100%, fill: red)),
+ table.cell(breakable: false, rowspan: 2, block(width: 2em, height: 100%, fill: red)),
+)
+
+// Rowspan split tests
+
+--- grid-rowspan-split-1 ---
+#set page(height: 10em)
+#table(
+ columns: 2,
+ rows: (auto, auto, 3em),
+ fill: red,
+ [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
+ [e],
+ [f]
+)
+
+--- grid-rowspan-split-2 ---
+#set page(height: 10em)
+#table(
+ columns: 2,
+ rows: (auto, auto, 3em),
+ row-gutter: 1em,
+ fill: red,
+ [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
+ [e],
+ [f]
+)
+
+--- grid-rowspan-split-3 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-4 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ gutter: 2pt,
+ table.cell(fill: orange, rowspan: 10, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-5 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-6 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ fill: red,
+ inset: 0pt,
+ gutter: 2pt,
+ table.cell(fill: orange, rowspan: 10, breakable: false, place(bottom)[*Z*] + [x\ ] * 10 + place(bottom)[*ZZ*]),
+ ..([y],) * 10,
+ [a], [b],
+)
+
+--- grid-rowspan-split-7 ---
+#set page(height: 5em)
+#grid(
+ columns: 2,
+ stroke: red,
+ inset: 5pt,
+ grid.cell(rowspan: 5)[a\ b\ c\ d\ e]
+)
+
+--- grid-rowspan-split-8 ---
+#set page(height: 5em)
+#table(
+ columns: 2,
+ gutter: 3pt,
+ stroke: red,
+ inset: 5pt,
+ table.cell(rowspan: 5)[a\ b\ c\ d\ e]
+)
+
+// Rowspan split without ending at the auto row
+
+--- grid-rowspan-split-9 ---
+#set page(height: 6em)
+#table(
+ rows: (4em,) * 7 + (auto,) + (4em,) * 7,
+ columns: 2,
+ column-gutter: 1em,
+ row-gutter: (1em, 2em) * 4,
+ fill: (x, y) => if calc.odd(x + y) { orange.lighten(20%) } else { red },
+ table.cell(rowspan: 15, [a \ ] * 15),
+ [] * 15
+)
+
+--- grid-rowspan-split-10 ---
+#set page(height: 6em)
+#table(
+ rows: (4em,) * 7 + (auto,) + (4em,) * 7,
+ columns: 2,
+ column-gutter: 1em,
+ row-gutter: (1em, 2em) * 4,
+ fill: (x, y) => if calc.odd(x + y) { green } else { green.darken(40%) },
+ table.cell(rowspan: 15, block(fill: blue, width: 2em, height: 4em * 14 + 3em)),
+ [] * 15
+)
+
+--- grid-rowspan-split-11 ---
+#set page(height: 6em)
+#table(
+ rows: (3em,) * 15,
+ columns: 2,
+ column-gutter: 1em,
+ row-gutter: (1em, 2em) * 4,
+ fill: (x, y) => if calc.odd(x + y) { aqua } else { blue },
+ table.cell(breakable: true, rowspan: 15, [a \ ] * 15),
+ [] * 15
+)
+
+// Some splitting corner cases
+
+--- grid-rowspan-split-12 ---
+// Inside the larger rowspan's range, there's an unbreakable rowspan and a
+// breakable rowspan. This should work normally.
+// The auto row will also expand ignoring the last fractional row.
+#set page(height: 10em)
+#table(
+ gutter: 0.5em,
+ columns: 2,
+ rows: (2em,) * 10 + (auto, auto, 2em, 1fr),
+ fill: (_, y) => if calc.even(y) { aqua } else { blue },
+ table.cell(rowspan: 14, block(width: 2em, height: 2em * 10 + 2em + 5em, fill: red)[]),
+ ..([a],) * 5,
+ table.cell(rowspan: 3)[a\ b],
+ table.cell(rowspan: 5, [a\ b\ c\ d\ e\ f\ g\ h]),
+ [z]
+)
+
+--- grid-rowspan-split-13 ---
+// Inset moving to next region bug
+#set page(width: 10cm, height: 2.5cm, margin: 0.5cm)
+#set text(size: 11pt)
+#table(
+ columns: (1fr, 1fr, 1fr),
+ [A],
+ [B],
+ [C],
+ [D],
+ table.cell(rowspan: 2, lorem(4)),
+ [E],
+ [F],
+ [G],
+)
+
+--- grid-rowspan-split-14 ---
+// Second lorem must be sent to the next page, too big
+#set page(width: 10cm, height: 9cm, margin: 1cm)
+#set text(size: 11pt)
+#table(
+ columns: (1fr, 1fr, 1fr),
+ align: center,
+ rows: (4cm, auto),
+ [A], [B], [C],
+ table.cell(rowspan: 4, breakable: false, lorem(10)),
+ [D],
+ table.cell(rowspan: 2, breakable: false, lorem(20)),
+ [E],
+)
+
+--- grid-rowspan-split-15 ---
+// Auto row must expand properly in both cases
+#set text(10pt)
+#show table.cell: it => if it.x == 0 { it } else { layout(size => size.height) }
+#table(
+ columns: 2,
+ rows: (1em, auto, 2em, 3em, 4em),
+ gutter: 3pt,
+ table.cell(rowspan: 5, block(fill: orange, height: 15em)[a]),
+ [b],
+ [c],
+ [d],
+ [e],
+ [f]
+)
+
+#table(
+ columns: 2,
+ rows: (1em, auto, 2em, 3em, 4em),
+ gutter: 3pt,
+ table.cell(rowspan: 5, breakable: false, block(fill: orange, height: 15em)[a]),
+ [b],
+ [c],
+ [d],
+ [e],
+ [f]
+)
+
+--- grid-rowspan-split-16 ---
+// Expanding on unbreakable auto row
+#set page(height: 7em, margin: (bottom: 2em))
+#grid(
+ columns: 2,
+ rows: (1em, 1em, auto, 1em, 1em, 1em),
+ fill: (x, y) => if x == 0 { aqua } else { blue },
+ stroke: black,
+ gutter: 2pt,
+ grid.cell(rowspan: 5, block(height: 10em)[a]),
+ [a],
+ [b],
+ grid.cell(breakable: false, v(3em) + [c]),
+ [d],
+ [e],
+ [f], [g]
+)
+
+--- grid-rowspan-split-17 ---
+#show table.cell.where(x: 0): strong
+#show table.cell.where(y: 0): strong
+#set page(height: 13em)
+#let lets-repeat(thing, n) = ((thing + colbreak(),) * (calc.max(0, n - 1)) + (thing,)).join()
+#table(
+ columns: 4,
+ fill: (x, y) => if x == 0 or y == 0 { gray },
+ [], [Test 1], [Test 2], [Test 3],
+ table.cell(rowspan: 15, align: horizon, lets-repeat((rotate(-90deg, reflow: true)[*All Tests*]), 3)),
+ ..([123], [456], [789]) * 15
+)
diff --git a/tests/suite/layout/grid/rtl.typ b/tests/suite/layout/grid/rtl.typ
new file mode 100644
index 0000000000..7c0e999a28
--- /dev/null
+++ b/tests/suite/layout/grid/rtl.typ
@@ -0,0 +1,195 @@
+// Test RTL grid.
+
+--- list-rtl ---
+#set text(dir: rtl)
+- מימין לשמאל
+
+--- grid-rtl ---
+#set text(dir: rtl)
+#table(columns: 2)[A][B][C][D]
+
+--- grid-rtl-colspan ---
+// Test interaction between RTL and colspans
+#set text(dir: rtl)
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ grid.cell(colspan: 4)[*Full Header*],
+ grid.cell(colspan: 2, fill: orange)[*Half*],
+ grid.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], grid.cell(colspan: 3, fill: orange.darken(10%))[6],
+ grid.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], grid.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-rtl-colspan-stroke ---
+#set text(dir: rtl)
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(colspan: 4)[*Full Header*],
+ table.cell(colspan: 2, fill: orange)[*Half*],
+ table.cell(colspan: 2, fill: orange.darken(10%))[*Half*],
+ [*A*], [*B*], [*C*], [*D*],
+ [1], [2], [3], [4],
+ [5], table.cell(colspan: 3, fill: orange.darken(10%))[6],
+ table.cell(colspan: 2, fill: orange)[7], [8], [9],
+ [10], table.cell(colspan: 2, fill: orange.darken(10%))[11], [12]
+)
+
+--- grid-rtl-multiple-regions ---
+// Test multiple regions
+#set page(height: 5em)
+#set text(dir: rtl)
+#grid(
+ stroke: red,
+ fill: aqua,
+ columns: 4,
+ [a], [b], [c], [d],
+ [a], grid.cell(colspan: 2)[e, f, g, h, i], [f],
+ [e], [g], grid.cell(colspan: 2)[eee\ e\ e\ e],
+ grid.cell(colspan: 4)[eeee e e e]
+)
+
+--- grid-rtl-vline-position ---
+// Test left and right for vlines in RTL
+#set text(dir: rtl)
+#grid(
+ columns: 3,
+ inset: 5pt,
+ grid.vline(stroke: red, position: left), grid.vline(stroke: green, position: right), [a],
+ grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [b],
+ grid.vline(stroke: red, position: left), grid.vline(stroke: 2pt, position: right), [c],
+ grid.vline(stroke: aqua, position: right)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
+ grid.vline(stroke: 2pt, position: right)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
+ grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [b],
+ grid.vline(stroke: 2pt, position: start), grid.vline(stroke: red, position: end), [c],
+ grid.vline(stroke: 2pt, position: start)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ grid.vline(stroke: green, position: start), grid.vline(stroke: red, position: end), [a],
+ grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [b],
+ grid.vline(stroke: blue, position: start), grid.vline(stroke: red, position: end), [c],
+ grid.vline(stroke: 2pt, position: start)
+)
+
+--- grid-rtl-vline-out-of-bounds ---
+// Error: 3:8-3:34 cannot place vertical line at the 'end' position of the end border (x = 1)
+// Hint: 3:8-3:34 set the line's position to 'start' or place it at a smaller 'x' index
+#set text(dir: rtl)
+#grid(
+ [a], grid.vline(position: left)
+)
+
+--- grid-rtl-complex ---
+#set text(dir: rtl)
+
+#grid(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ grid.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], grid.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], grid.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+#table(
+ columns: 4,
+ fill: (x, y) => if calc.odd(x + y) { blue.lighten(50%) } else { blue.lighten(10%) },
+ inset: 5pt,
+ align: center,
+ gutter: 3pt,
+ table.cell(rowspan: 2, fill: orange)[*Left*],
+ [Right A], [Right A], [Right A],
+ [Right B], table.cell(colspan: 2, rowspan: 2, fill: orange.darken(10%))[B Wide],
+ [Left A], [Left A],
+ [Left B], [Left B], table.cell(colspan: 2, rowspan: 3, fill: orange)[Wide and Long]
+)
+
+--- grid-rtl-rowspan ---
+#set page(height: 10em)
+#set text(dir: rtl)
+#table(
+ columns: 2,
+ rows: (auto, auto, 3em),
+ row-gutter: 1em,
+ fill: red,
+ [a], table.cell(rowspan: 3, block(width: 50%, height: 10em, fill: orange) + place(bottom)[*ZD*]),
+ [e],
+ [f]
+)
+
+--- grid-rtl-header ---
+// Headers
+#set page(height: 15em)
+#set text(dir: rtl)
+#table(
+ columns: 5,
+ align: center + horizon,
+ table.header(
+ table.cell(colspan: 5)[*Cool Zone*],
+ table.cell(stroke: red)[*N1*], table.cell(stroke: aqua)[*N2*], [*D1*], [*D2*], [*Etc*],
+ table.hline(start: 2, end: 3, stroke: yellow)
+ ),
+ ..range(0, 10).map(i => ([\##i], table.cell(stroke: green)[123], table.cell(stroke: blue)[456], [789], [?], table.hline(start: 4, end: 5, stroke: red))).flatten()
+)
diff --git a/tests/suite/layout/grid/stroke.typ b/tests/suite/layout/grid/stroke.typ
new file mode 100644
index 0000000000..9c1c3482d4
--- /dev/null
+++ b/tests/suite/layout/grid/stroke.typ
@@ -0,0 +1,435 @@
+--- grid-stroke-pattern ---
+#let double-line = pattern(size: (1.5pt, 1.5pt), {
+ place(line(stroke: .6pt, start: (0%, 50%), end: (100%, 50%)))
+})
+
+#table(
+ stroke: (_, y) => if y != 1 { (bottom: black) },
+ columns: 3,
+ table.cell(colspan: 3, align: center)[*Epic Table*],
+ align(center)[*Name*], align(center)[*Age*], align(center)[*Data*],
+ table.hline(stroke: (paint: double-line, thickness: 2pt)),
+ [John], [30], [None],
+ [Martha], [20], [A],
+ [Joseph], [35], [D]
+)
+
+--- grid-stroke-folding ---
+// Test folding
+#set grid(stroke: red)
+#set grid(stroke: 5pt)
+
+#grid(
+ inset: 10pt,
+ columns: 2,
+ stroke: stroke(dash: "loosely-dotted"),
+ grid.vline(start: 2, end: 3, stroke: (paint: green, dash: none)),
+ [a], [b],
+ grid.hline(end: 1, stroke: blue),
+ [c], [d],
+ [e], grid.cell(stroke: aqua)[f]
+)
+
+--- grid-stroke-set-on-cell-and-line ---
+// Test set rules on cells and folding
+#set table.cell(stroke: 4pt)
+#set table.cell(stroke: blue)
+#set table.hline(stroke: red)
+#set table.hline(stroke: 0.75pt)
+#set table.vline(stroke: 0.75pt)
+#set table.vline(stroke: aqua)
+
+#table(
+ columns: 3,
+ gutter: 3pt,
+ inset: 5pt,
+ [a], [b], table.vline(position: end), [c],
+ [d], [e], [f],
+ table.hline(position: bottom),
+ [g], [h], [i],
+)
+
+--- grid-stroke-field-in-show ---
+// Test stroke field on cell show rules
+#set grid.cell(stroke: (x: 4pt))
+#set grid.cell(stroke: (x: blue))
+#show grid.cell: it => {
+ test(it.stroke, (left: stroke(paint: blue, thickness: 4pt, dash: "loosely-dotted"), right: blue + 4pt, top: stroke(thickness: 1pt), bottom: none))
+ it
+}
+#grid(
+ stroke: (left: (dash: "loosely-dotted")),
+ inset: 5pt,
+ grid.hline(stroke: red),
+ grid.cell(stroke: (top: 1pt))[a], grid.vline(stroke: yellow),
+)
+
+--- grid-stroke-complex ---
+#table(
+ columns: 3,
+ [a], table.cell(colspan: 2)[b c],
+ table.cell(stroke: blue)[d], [e], [f],
+ [g], [h], table.cell(stroke: (left: yellow, top: green, right: aqua, bottom: red))[i],
+ [j], [k], [l],
+ table.cell(stroke: 3pt)[m], [n], table.cell(stroke: (dash: "loosely-dotted"))[o],
+)
+
+--- grid-stroke-array ---
+// Test per-column stroke array
+#let t = table(
+ columns: 3,
+ stroke: (red, blue, green),
+ [a], [b], [c],
+ [d], [e], [f],
+ [h], [i], [j],
+)
+#t
+#set text(dir: rtl)
+#t
+
+--- grid-stroke-func ---
+#grid(
+ columns: 3,
+ inset: 3pt,
+ stroke: (x, _) => (right: (5pt, (dash: "dotted")).at(calc.rem(x, 2)), bottom: (dash: "densely-dotted")),
+ grid.vline(x: 0, stroke: red),
+ grid.vline(x: 1, stroke: red),
+ grid.vline(x: 2, stroke: red),
+ grid.vline(x: 3, stroke: red),
+ grid.hline(y: 0, end: 1, stroke: blue),
+ grid.hline(y: 1, end: 1, stroke: blue),
+ grid.cell[a],
+ [b], [c]
+)
+
+--- grid-stroke-manually-positioned-lines ---
+#set page(height: 5em)
+#table(
+ columns: 3,
+ inset: 3pt,
+ table.hline(y: 0, end: none, stroke: 3pt + blue),
+ table.vline(x: 0, end: none, stroke: 3pt + green),
+ table.hline(y: 5, end: none, stroke: 3pt + red),
+ table.vline(x: 3, end: none, stroke: 3pt + yellow),
+ [a], [b], [c],
+ [a], [b], [c],
+ [a], [b], [c],
+ [a], [b], [c],
+ [a], [b], [c],
+)
+
+--- grid-stroke-automatically-positioned-lines ---
+// Automatically positioned lines
+// Plus stroke thickness ordering
+#table(
+ columns: 3,
+ table.hline(stroke: red + 5pt),
+ table.vline(stroke: blue + 5pt),
+ table.vline(stroke: 2pt),
+ [a],
+ table.vline(x: 1, stroke: aqua + 5pt),
+ [b],
+ table.vline(stroke: aqua + 5pt),
+ [c],
+ table.vline(stroke: yellow + 5.2pt),
+ table.hline(stroke: green + 5pt),
+ [a], [b], [c],
+ [a], table.hline(stroke: green + 2pt), table.vline(stroke: 2pt), [b], [c],
+)
+
+--- grid-stroke-priority-line ---
+// Line specification order priority
+// The last line should be blue, not red.
+// The middle aqua line should be gone due to the 'none' override.
+#grid(
+ columns: 2,
+ inset: 2pt,
+ grid.hline(y: 2, stroke: red + 5pt),
+ grid.vline(),
+ [a], [b],
+ grid.hline(stroke: red),
+ grid.hline(stroke: none),
+ [c], grid.cell(stroke: (top: aqua))[d],
+ grid.hline(stroke: blue),
+)
+
+--- grid-stroke-hline-position-bottom-gutter ---
+// Position: bottom and position: end with gutter should have a visible effect
+// of moving the lines after the next track.
+#table(
+ columns: 3,
+ gutter: 3pt,
+ stroke: blue,
+ table.hline(end: 2, stroke: red),
+ table.hline(end: 2, stroke: aqua, position: bottom),
+ table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), table.vline(end: 2, position: end, stroke: orange), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
+ [d], [e], [f],
+ table.hline(end: 2, stroke: red),
+ [g], [h], [ie],
+ table.hline(end: 2, stroke: green),
+)
+
+--- grid-stroke-hline-position-bottom ---
+// Using position: bottom and position: end without gutter should be the same
+// as placing a line after the next track.
+#table(
+ columns: 3,
+ stroke: blue,
+ table.hline(end: 2, stroke: red),
+ table.hline(end: 2, stroke: aqua, position: bottom),
+ table.vline(end: 2, stroke: green), [a], table.vline(end: 2, stroke: green), [b], table.vline(end: 2, stroke: aqua, position: end), table.vline(end: 2, stroke: green), [c], table.vline(end: 2, stroke: green),
+ table.hline(end: 2, stroke: 5pt),
+ [d], [e], [f],
+ table.hline(end: 2, stroke: red),
+ [g], [h], [i],
+ table.hline(end: 2, stroke: red),
+)
+
+--- grid-stroke-vline-position-left-and-right ---
+// Test left and right for grid vlines.
+#grid(
+ columns: 3,
+ inset: 5pt,
+ grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
+ grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [b],
+ grid.vline(stroke: 2pt, position: left), grid.vline(stroke: red, position: right), [c],
+ grid.vline(stroke: 2pt, position: left)
+)
+
+#grid(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ grid.vline(stroke: green, position: left), grid.vline(stroke: red, position: right), [a],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [b],
+ grid.vline(stroke: blue, position: left), grid.vline(stroke: red, position: right), [c],
+ grid.vline(stroke: 2pt, position: left)
+)
+
+--- table-stroke-vline-position-left-and-right ---
+// Test left and right for table vlines.
+#table(
+ columns: 3,
+ inset: 5pt,
+ table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
+ table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [b],
+ table.vline(stroke: 2pt, position: left), table.vline(stroke: red, position: right), [c],
+ table.vline(stroke: 2pt, position: left)
+)
+
+#table(
+ columns: 3,
+ inset: 5pt,
+ gutter: 3pt,
+ table.vline(stroke: green, position: left), table.vline(stroke: red, position: right), [a],
+ table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [b],
+ table.vline(stroke: blue, position: left), table.vline(stroke: red, position: right), [c],
+ table.vline(stroke: 2pt, position: left)
+)
+
+--- grid-stroke-priority-line-cell ---
+// Hlines and vlines should always appear on top of cell strokes.
+#table(
+ columns: 3,
+ stroke: aqua,
+ table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
+ table.cell(stroke: blue)[d], [e], [f],
+ table.hline(stroke: red),
+ [g], table.cell(stroke: blue)[h], [i],
+)
+
+#table(
+ columns: 3,
+ gutter: 3pt,
+ stroke: aqua,
+ table.vline(stroke: red, position: end), [a], table.vline(stroke: red), [b], [c],
+ table.cell(stroke: blue)[d], [e], [f],
+ table.hline(stroke: red),
+ [g], table.cell(stroke: blue)[h], [i],
+)
+
+--- grid-stroke-priority-cell ---
+// Ensure cell stroke overrides always appear on top.
+#table(
+ columns: 2,
+ stroke: black,
+ table.cell(stroke: red)[a], [b],
+ [c], [d],
+)
+
+#table(
+ columns: 2,
+ table.cell(stroke: red)[a], [b],
+ [c], [d],
+)
+
+--- grid-stroke-hline-position-bad ---
+// Error: 7:3-7:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
+// Hint: 7:3-7:32 set the line's position to 'top' or place it at a smaller 'y' index
+#table(
+ columns: 2,
+ [a], [b],
+ [c], [d],
+ table.hline(stroke: aqua),
+ table.hline(position: top),
+ table.hline(position: bottom)
+)
+
+--- grid-stroke-border-partial ---
+// Test partial border line overrides
+#set page(width: auto, height: 7em, margin: (bottom: 1em))
+#table(
+ columns: 4,
+ stroke: (x, y) => if y == 0 or y == 4 { orange } else { aqua },
+ table.hline(stroke: blue, start: 1, end: 2), table.cell(stroke: red, v(3em)), table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M],
+ [a], [b], [c], [M],
+ [d], [e], [f], [M],
+ [g], [h], [i], [M],
+ table.cell(stroke: red)[a], table.cell(stroke: blue)[b], table.cell(stroke: green)[c], [M],
+ table.hline(stroke: blue, start: 1, end: 2),
+)
+
+--- grid-stroke-vline-colspan ---
+// - Vline should be placed after the colspan.
+// - Hline should be placed under the full-width rowspan.
+#table(
+ columns: 3,
+ rows: 1.25em,
+ inset: 1pt,
+ stroke: none,
+ table.cell(colspan: 2)[a], table.vline(stroke: red), table.hline(stroke: blue), [b],
+ [c], [d], [e],
+ table.cell(colspan: 3, rowspan: 2)[a], table.vline(stroke: blue), table.hline(stroke: red)
+)
+
+--- grid-stroke-hline-rowspan ---
+// Red line should be above [c] (hline skips the shortest rowspan).
+#set text(6pt)
+#table(
+ rows: 1em,
+ columns: 2,
+ inset: 1.5pt,
+ table.cell(rowspan: 3)[a], table.cell(rowspan: 2)[b],
+ table.hline(stroke: red),
+ [c]
+)
+
+--- grid-stroke-hline-position-bottom-out-of-bounds ---
+// Error: 8:3-8:32 cannot place horizontal line at the 'bottom' position of the bottom border (y = 2)
+// Hint: 8:3-8:32 set the line's position to 'top' or place it at a smaller 'y' index
+#table(
+ columns: 2,
+ gutter: 3pt,
+ [a], [b],
+ [c], [d], table.vline(stroke: red),
+ table.hline(stroke: aqua),
+ table.hline(position: top),
+ table.hline(position: bottom)
+)
+
+--- grid-stroke-vline-position-bottom-out-of-bounds ---
+// Error: 6:3-6:28 cannot place vertical line at the 'end' position of the end border (x = 2)
+// Hint: 6:3-6:28 set the line's position to 'start' or place it at a smaller 'x' index
+#grid(
+ columns: 2,
+ [a], [b],
+ grid.vline(stroke: aqua),
+ grid.vline(position: start),
+ grid.vline(position: end)
+)
+
+--- grid-stroke-vline-position-bottom-out-of-bounds-gutter ---
+// Error: 7:3-7:28 cannot place vertical line at the 'end' position of the end border (x = 2)
+// Hint: 7:3-7:28 set the line's position to 'start' or place it at a smaller 'x' index
+#grid(
+ columns: 2,
+ gutter: 3pt,
+ [a], [b],
+ grid.vline(stroke: aqua),
+ grid.vline(position: start),
+ grid.vline(position: end)
+)
+
+--- grid-stroke-hline-out-of-bounds ---
+// Error: 4:3-4:19 cannot place horizontal line at invalid row 3
+#grid(
+ [a],
+ [b],
+ grid.hline(y: 3)
+)
+
+--- grid-stroke-hline-out-of-bounds-gutter ---
+// Error: 5:3-5:19 cannot place horizontal line at invalid row 3
+#grid(
+ gutter: 3pt,
+ [a],
+ [b],
+ grid.hline(y: 3)
+)
+
+--- grid-stroke-vline-out-of-bounds ---
+// Error: 4:3-4:20 cannot place vertical line at invalid column 3
+#table(
+ columns: 2,
+ [a], [b],
+ table.vline(x: 3)
+)
+
+--- grid-stroke-vline-out-of-bounds-gutter ---
+// Error: 5:3-5:20 cannot place vertical line at invalid column 3
+#table(
+ columns: 2,
+ gutter: 3pt,
+ [a], [b],
+ table.vline(x: 3)
+)
+
+--- table-hline-in-grid ---
+// Error: 7-20 cannot use `table.hline` as a grid line; use `grid.hline` instead
+#grid(table.hline())
+
+--- table-vline-in-grid ---
+// Error: 7-20 cannot use `table.vline` as a grid line; use `grid.vline` instead
+#grid(table.vline())
+
+--- grid-hline-in-table ---
+// Error: 8-20 cannot use `grid.hline` as a table line; use `table.hline` instead
+#table(grid.hline())
+
+--- grid-vline-in-table ---
+// Error: 8-20 cannot use `grid.vline` as a table line; use `table.vline` instead
+#table(grid.vline())
+
+--- grid-hline-end-before-start-1 ---
+// Error: 3:3-3:31 line cannot end before it starts
+#grid(
+ columns: 3,
+ grid.hline(start: 2, end: 1),
+ [a], [b], [c],
+)
+
+--- grid-hline-end-before-start-2 ---
+// Error: 3:3-3:32 line cannot end before it starts
+#table(
+ columns: 3,
+ table.vline(start: 2, end: 1),
+ [a], [b], [c],
+ [d], [e], [f],
+ [g], [h], [i],
+)
+
+--- grid-hline-position-horizon ---
+// Error: 24-31 expected `top` or `bottom`, found horizon
+#table.hline(position: horizon)
+
+--- grid-vline-position-center ---
+// Error: 24-30 expected `start`, `left`, `right`, or `end`, found center
+#table.vline(position: center)
+
+--- grid-hline-position-right ---
+// Error: 24-29 expected `top` or `bottom`, found right
+#table.hline(position: right)
+
+--- grid-vline-position-top ---
+// Error: 24-27 expected `start`, `left`, `right`, or `end`, found top
+#table.vline(position: top)
diff --git a/tests/suite/layout/grid/styling.typ b/tests/suite/layout/grid/styling.typ
new file mode 100644
index 0000000000..f7cfb97d48
--- /dev/null
+++ b/tests/suite/layout/grid/styling.typ
@@ -0,0 +1,160 @@
+// Test grid styling options.
+
+--- grid-fill-func ---
+#set page(height: 70pt)
+#set grid(fill: (x, y) => if calc.even(x + y) { rgb("aaa") })
+
+#grid(
+ columns: (1fr,) * 3,
+ stroke: 2pt + rgb("333"),
+ [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
+)
+
+--- grid-stroke-none ---
+#grid(columns: 3, stroke: none, fill: green, [A], [B], [C])
+
+--- grid-align ---
+// Test general alignment.
+#grid(
+ columns: 3,
+ align: left,
+ [Hello], [Hello], [Hello],
+ [A], [B], [C],
+)
+
+// Test alignment with a function.
+#grid(
+ columns: 3,
+ align: (x, y) => (left, center, right).at(x),
+ [Hello], [Hello], [Hello],
+ [A], [B], [C],
+)
+
+// Test alignment with array.
+#grid(
+ columns: (1fr, 1fr, 1fr),
+ align: (left, center, right),
+ [A], [B], [C]
+)
+
+// Test empty array.
+#set align(center)
+#grid(
+ columns: (1fr, 1fr, 1fr),
+ align: (),
+ [A], [B], [C]
+)
+
+a
+
+--- grid-inset ---
+// Test inset.
+#grid(
+ columns: (1fr,) * 3,
+ stroke: 2pt + rgb("333"),
+ inset: 5pt,
+ [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
+)
+
+#grid(
+ columns: 3,
+ inset: 10pt,
+ fill: blue,
+ [A], [B], [C]
+)
+
+#grid(
+ columns: 3,
+ inset: (y: 10pt),
+ [A], [B], [C]
+)
+
+#grid(
+ columns: 3,
+ inset: (left: 20pt, rest: 10pt),
+ stroke: 3pt + red,
+ [A], [B], [C]
+)
+
+#grid(
+ columns: 2,
+ inset: (
+ left: 20pt,
+ right: 5pt,
+ top: 10pt,
+ bottom: 3pt,
+ ),
+ [A],
+ [B],
+)
+
+#grid(
+ columns: 3,
+ fill: (x, y) => (if y == 0 { aqua } else { orange }).darken(x * 15%),
+ inset: (x, y) => (left: if x == 0 { 0pt } else { 5pt }, right: if x == 0 { 5pt } else { 0pt }, y: if y == 0 { 0pt } else { 5pt }),
+ [A], [B], [C],
+ [A], [B], [C],
+)
+
+#grid(
+ columns: 3,
+ inset: (0pt, 5pt, 10pt),
+ fill: (x, _) => aqua.darken(x * 15%),
+ [A], [B], [C],
+)
+
+--- grid-inset-folding ---
+// Test inset folding
+#set grid(inset: 10pt)
+#set grid(inset: (left: 0pt))
+
+#grid(
+ fill: red,
+ inset: (right: 0pt),
+ grid.cell(inset: (top: 0pt))[a]
+)
+
+--- grid-funcs-gutter ---
+// Test interaction with gutters.
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ row-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ column-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#grid(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
diff --git a/tests/suite/layout/hide.typ b/tests/suite/layout/hide.typ
new file mode 100644
index 0000000000..a10090d7ba
--- /dev/null
+++ b/tests/suite/layout/hide.typ
@@ -0,0 +1,104 @@
+// Test the `hide` function.
+
+--- hide-text ---
+AB #h(1fr) CD \
+#hide[A]B #h(1fr) C#hide[D]
+
+--- hide-line ---
+Hidden:
+#hide[#line(length: 100%)]
+#line(length: 100%)
+
+--- hide-table ---
+Hidden:
+#hide(table(rows: 2, columns: 2)[a][b][c][d])
+#table(rows: 2, columns: 2)[a][b][c][d]
+
+--- hide-polygon ---
+Hidden:
+#hide[
+ #polygon((20%, 0pt),
+ (60%, 0pt),
+ (80%, 2cm),
+ (0%, 2cm),)
+]
+#polygon((20%, 0pt),
+ (60%, 0pt),
+ (80%, 2cm),
+ (0%, 2cm),)
+
+--- hide-rect ---
+#set rect(
+ inset: 8pt,
+ fill: rgb("e4e5ea"),
+ width: 100%,
+)
+
+Hidden:
+#hide[
+#grid(
+ columns: (1fr, 1fr, 2fr),
+ rows: (auto, 40pt),
+ gutter: 3pt,
+ rect[A],
+ rect[B],
+ rect[C],
+ rect(height: 100%)[D],
+)
+]
+#grid(
+ columns: (1fr, 1fr, 2fr),
+ rows: (auto, 40pt),
+ gutter: 3pt,
+ rect[A],
+ rect[B],
+ rect[C],
+ rect(height: 100%)[D],
+)
+
+--- hide-list ---
+Hidden:
+#hide[
+- 1
+- 2
+ 1. A
+ 2. B
+- 3
+]
+
+
+- 1
+- 2
+ 1. A
+ 2. B
+- 3
+
+--- hide-image ---
+Hidden:
+#hide(image("/assets/images/tiger.jpg", width: 5cm, height: 1cm,))
+
+#image("/assets/images/tiger.jpg", width: 5cm, height: 1cm,)
+
+--- issue-622-hide-meta-cite ---
+// Test that metadata of hidden stuff stays available.
+#set cite(style: "chicago-notes")
+
+A pirate. @arrgh \
+#set text(2pt)
+#hide[
+ A @arrgh pirate.
+ #bibliography("/assets/bib/works.bib")
+]
+
+--- issue-622-hide-meta-outline ---
+#set text(8pt)
+#outline()
+#set text(2pt)
+#hide(block(grid(
+ [= A],
+ [= B],
+ block(grid(
+ [= C],
+ [= D],
+ ))
+)))
diff --git a/tests/suite/layout/inline/baseline.typ b/tests/suite/layout/inline/baseline.typ
new file mode 100644
index 0000000000..e9f9a64511
--- /dev/null
+++ b/tests/suite/layout/inline/baseline.typ
@@ -0,0 +1,17 @@
+// Test baseline handling.
+
+--- baseline-text ---
+Hi #text(1.5em)[You], #text(0.75em)[how are you?]
+
+Our cockatoo was one of the
+#text(baseline: -0.2em)[#box(circle(radius: 2pt)) first]
+#text(baseline: 0.2em)[birds #box(circle(radius: 2pt))]
+that ever learned to mimic a human voice.
+
+--- baseline-box ---
+Hey #box(baseline: 40%, image("/assets/images/tiger.jpg", width: 1.5cm)) there!
+
+--- issue-2214-baseline-math ---
+// The math content should also be affected by the TextElem baseline.
+hello #text(baseline: -5pt)[123 #sym.WW\orld]\
+hello #text(baseline: -5pt)[$123 WW#text[or]$ld]\
diff --git a/tests/suite/layout/inline/bidi.typ b/tests/suite/layout/inline/bidi.typ
new file mode 100644
index 0000000000..7da23b4176
--- /dev/null
+++ b/tests/suite/layout/inline/bidi.typ
@@ -0,0 +1,77 @@
+// Test bidirectional text and language configuration.
+
+--- bidi-en-he-top-level ---
+// Test reordering with different top-level paragraph directions.
+#let content = par[Text טֶקסט]
+#text(lang: "he", content)
+#text(lang: "de", content)
+
+--- bidi-consecutive-embedded-ltr-runs ---
+// Test that consecutive, embedded LTR runs stay LTR.
+// Here, we have two runs: "A" and italic "B".
+#let content = par[أنت A#emph[B]مطرC]
+#set text(font: ("PT Sans", "Noto Sans Arabic"))
+#text(lang: "ar", content)
+#text(lang: "de", content)
+
+--- bidi-consecutive-embedded-rtl-runs ---
+// Test that consecutive, embedded RTL runs stay RTL.
+// Here, we have three runs: "גֶ", bold "שֶׁ", and "ם".
+#let content = par[Aגֶ#strong[שֶׁ]םB]
+#set text(font: ("Linux Libertine", "Noto Serif Hebrew"))
+#text(lang: "he", content)
+#text(lang: "de", content)
+
+--- bidi-nesting ---
+// Test embedding up to level 4 with isolates.
+#set text(dir: rtl)
+א\u{2066}A\u{2067}Bב\u{2069}?
+
+--- bidi-manual-linebreak ---
+// Test hard line break (leads to two paragraphs in unicode-bidi).
+#set text(lang: "ar", font: ("Noto Sans Arabic", "PT Sans"))
+Life المطر هو الحياة \
+الحياة تمطر is rain.
+
+--- bidi-spacing ---
+// Test spacing.
+L #h(1cm) ריווחR \
+Lריווח #h(1cm) R
+
+--- bidi-obj ---
+// Test inline object.
+#set text(lang: "he")
+קרנפיםRh#box(image("/assets/images/rhino.png", height: 11pt))inoחיים
+
+--- bidi-whitespace-reset ---
+// Test whether L1 whitespace resetting destroys stuff.
+الغالب #h(70pt) ن#" "ة
+
+--- bidi-explicit-dir ---
+// Test explicit dir
+#set text(dir: rtl)
+#text("8:00 - 9:00", dir: ltr) בבוקר
+#linebreak()
+ב #text("12:00 - 13:00", dir: ltr) בצהריים
+
+--- bidi-raw ---
+// Mixing raw
+#set text(lang: "he")
+לדוג. `if a == b:` זה תנאי
+#set raw(lang: "python")
+לדוג. `if a == b:` זה תנאי
+
+#show raw: set text(dir:rtl)
+לתכנת בעברית `אם א == ב:`
+
+--- bidi-vertical ---
+// Test setting a vertical direction.
+// Error: 16-19 text direction must be horizontal
+#set text(dir: ttb)
+
+--- issue-1373-bidi-tofus ---
+// Test that shaping missing characters in both left-to-right and
+// right-to-left directions does not cause a crash.
+#"\u{590}\u{591}\u{592}\u{593}"
+
+#"\u{30000}\u{30001}\u{30002}\u{30003}"
diff --git a/tests/suite/layout/inline/cjk.typ b/tests/suite/layout/inline/cjk.typ
new file mode 100644
index 0000000000..0540cd19fe
--- /dev/null
+++ b/tests/suite/layout/inline/cjk.typ
@@ -0,0 +1,90 @@
+// Test CJK-specific features.
+
+--- text-chinese-basic ---
+// Test basic Chinese text from Wikipedia.
+#set text(font: "Noto Serif CJK SC")
+
+是美国广播公司电视剧《迷失》第3季的第22和23集,也是全剧的第71集和72集
+由执行制作人戴蒙·林道夫和卡尔顿·库斯编剧,导演则是另一名执行制作人杰克·本德
+节目于2007年5月23日在美国和加拿大首播,共计吸引了1400万美国观众收看
+本集加上插播广告一共也持续有两个小时
+
+--- text-cjk-latin-spacing ---
+#set page(width: 50pt + 10pt, margin: (x: 5pt))
+#set text(lang: "zh", font: "Noto Serif CJK SC", cjk-latin-spacing: auto)
+#set par(justify: true)
+
+中文,中12文1中,文12中文
+
+中文,中ab文a中,文ab中文
+
+#set text(cjk-latin-spacing: none)
+
+中文,中12文1中,文12中文
+
+中文,中ab文a中,文ab中文
+
+--- cjk-punctuation-adjustment-1 ---
+#set page(width: 15em)
+
+// In the following example, the space between 》! and ? should be squeezed.
+// because zh-CN follows GB style
+#set text(lang: "zh", region: "CN", font: "Noto Serif CJK SC")
+原来,你也玩《原神》!?
+
+// However, in the following example, the space between 》! and ? should not be squeezed.
+// because zh-TW does not follow GB style
+#set text(lang: "zh", region: "TW", font: "Noto Serif CJK TC")
+原來,你也玩《原神》! ?
+
+#set text(lang: "zh", region: "CN", font: "Noto Serif CJK SC")
+「真的吗?」
+
+#set text(lang: "ja", font: "Noto Serif CJK JP")
+「本当に?」
+
+--- cjk-punctuation-adjustment-2 ---
+#set text(lang: "zh", region: "CN", font: "Noto Serif CJK SC")
+《书名〈章节〉》 // the space between 〉 and 》 should be squeezed
+
+〔茸毛〕:很细的毛 // the space between 〕 and : should be squeezed
+
+--- cjk-punctuation-adjustment-3 ---
+#set page(width: 21em)
+#set text(lang: "zh", region: "CN", font: "Noto Serif CJK SC")
+
+// These examples contain extensive use of Chinese punctuation marks,
+// from 《Which parentheses should be used when applying parentheses?》.
+// link: https://archive.md/2bb1N
+
+
+(〔中〕医、〔中〕药、技)系列评审
+
+(长三角[长江三角洲])(GB/T 16159—2012《汉语拼音正词法基本规则》)
+
+【爱因斯坦(Albert Einstein)】物理学家
+
+〔(2009)民申字第1622号〕
+
+“江南海北长相忆,浅水深山独掩扉。”([唐]刘长卿《会赦后酬主簿所问》)
+
+参看1378页〖象形文字〗。(《现代汉语词典》修订本)
+
+--- issue-2538-cjk-latin-spacing-before-linebreak ---
+// Issue #2538
+#set text(cjk-latin-spacing: auto)
+
+abc字
+
+abc字#linebreak()
+
+abc字#linebreak()
+母
+
+abc字\
+母
+
+--- issue-2650-cjk-latin-spacing-meta ---
+测a试
+
+测#context [a]试
diff --git a/tests/typ/text/hyphenate.typ b/tests/suite/layout/inline/hyphenate.typ
similarity index 90%
rename from tests/typ/text/hyphenate.typ
rename to tests/suite/layout/inline/hyphenate.typ
index 42946a8864..aaabe8160b 100644
--- a/tests/typ/text/hyphenate.typ
+++ b/tests/suite/layout/inline/hyphenate.typ
@@ -1,6 +1,6 @@
// Test hyphenation.
----
+--- hyphenate ---
// Test hyphenating english and greek.
#set text(hyphenate: true)
#set page(width: auto)
@@ -10,7 +10,7 @@
text(lang: "el")[διαμερίσματα. \ λατρευτός],
)
----
+--- hyphenate-off-temporarily ---
// Test disabling hyphenation for short passages.
#set page(width: 110pt)
#set text(hyphenate: true)
@@ -25,13 +25,13 @@ Welcome to wonde#text(hyphenate: false)[rf]ul experiences. \
Welcome to wonderful experiences. \
Welcome to wo#text(hyphenate: true)[nd]erful experiences. \
----
+--- hyphenate-between-shape-runs ---
// Hyphenate between shape runs.
#set page(width: 80pt)
#set text(hyphenate: true)
It's a #emph[Tree]beard.
----
+--- hyphenate-shy ---
// Test shy hyphens.
#set text(lang: "de", hyphenate: true)
#grid(
@@ -41,7 +41,7 @@ It's a #emph[Tree]beard.
[Bar-?ankauf],
)
----
+--- hyphenate-punctuation ---
// This sequence would confuse hypher if we passed trailing / leading
// punctuation instead of just the words. So this tests that we don't
// do that. The test passes if there's just one hyphenation between
diff --git a/tests/suite/layout/inline/justify.typ b/tests/suite/layout/inline/justify.typ
new file mode 100644
index 0000000000..e1e1557878
--- /dev/null
+++ b/tests/suite/layout/inline/justify.typ
@@ -0,0 +1,170 @@
+--- justify ---
+#set page(width: 180pt)
+#set block(spacing: 5pt)
+#set par(justify: true, first-line-indent: 14pt, leading: 5pt)
+
+This text is justified, meaning that spaces are stretched so that the text
+forms a "block" with flush edges at both sides.
+
+First line indents and hyphenation play nicely with justified text.
+
+--- justify-knuth-story ---
+// LARGE
+#set page(width: auto, height: auto)
+#set par(leading: 4pt, justify: true)
+#set text(font: "New Computer Modern")
+
+#let story = [
+ In olden times when wishing still helped one, there lived a king whose
+ daughters were all beautiful; and the youngest was so beautiful that the sun
+ itself, which has seen so much, was astonished whenever it shone in her face.
+ Close by the king’s castle lay a great dark forest, and under an old lime-tree
+ in the forest was a well, and when the day was very warm, the king’s child
+ went out into the forest and sat down by the side of the cool fountain; and
+ when she was bored she took a golden ball, and threw it up on high and caught
+ it; and this ball was her favorite plaything.
+]
+
+#let column(title, linebreaks, hyphenate) = {
+ rect(inset: 0pt, width: 132pt, fill: rgb("eee"))[
+ #set par(linebreaks: linebreaks)
+ #set text(hyphenate: hyphenate)
+ #strong(title) \ #story
+ ]
+}
+
+#grid(
+ columns: 3,
+ gutter: 10pt,
+ column([Simple without hyphens], "simple", false),
+ column([Simple with hyphens], "simple", true),
+ column([Optimized with hyphens], "optimized", true),
+)
+
+--- justify-manual-linebreak ---
+// Test that lines with hard breaks aren't justified.
+#set par(justify: true)
+A B C \
+D
+
+--- justify-justified-linebreak ---
+// Test forced justification with justified break.
+A B C #linebreak(justify: true)
+D E F #linebreak(justify: true)
+
+--- justify-basically-empty ---
+// Test that there are no hick-ups with justification enabled and
+// basically empty paragraph.
+#set par(justify: true)
+#""
+
+--- justify-shrink-last-line ---
+// Test that the last line can be shrunk
+#set page(width: 155pt)
+#set par(justify: true)
+This text can be fitted in one line.
+
+--- justify-avoid-runts ---
+// Test that runts are avoided when it's not too costly to do so.
+#set page(width: 124pt)
+#set par(justify: true)
+#for i in range(0, 20) {
+ "a b c "
+}
+#"d"
+
+--- justify-no-leading-spaces ---
+// Test that justification cannot lead to a leading space
+#set par(justify: true)
+#set text(size: 12pt)
+#set page(width: 45mm, height: auto)
+
+lorem ipsum 1234, lorem ipsum dolor sit amet
+
+#" leading whitespace should still be displayed"
+
+--- justify-code-blocks ---
+// Test that justification doesn't break code blocks
+#set par(justify: true)
+
+```cpp
+int main() {
+ printf("Hello world\n");
+ return 0;
+}
+```
+
+--- justify-chinese ---
+// In Chinese typography, line length should be multiples of the character size
+// and the line ends should be aligned with each other. Most Chinese
+// publications do not use hanging punctuation at line end.
+#set page(width: auto)
+#set par(justify: true)
+#set text(lang: "zh", font: "Noto Serif CJK SC")
+
+#rect(inset: 0pt, width: 80pt, fill: rgb("eee"))[
+ 中文维基百科使用汉字书写,汉字是汉族或华人的共同文字,是中国大陆、新加坡、马来西亚、台湾、香港、澳门的唯一官方文字或官方文字之一。25.9%,而美国和荷兰则分別占13.7%及8.2%。近年來,中国大陆地区的维基百科编辑者正在迅速增加;
+]
+
+--- justify-japanese ---
+// Japanese typography is more complex, make sure it is at least a bit sensible.
+#set page(width: auto)
+#set par(justify: true)
+#set text(lang: "ja", font: ("Linux Libertine", "Noto Serif CJK JP"))
+#rect(inset: 0pt, width: 80pt, fill: rgb("eee"))[
+ ウィキペディア(英: Wikipedia)は、世界中のボランティアの共同作業によって執筆及び作成されるフリーの多言語インターネット百科事典である。主に寄付に依って活動している非営利団体「ウィキメディア財団」が所有・運営している。
+
+ 専門家によるオンライン百科事典プロジェクトNupedia(ヌーペディア)を前身として、2001年1月、ラリー・サンガーとジミー・ウェールズ(英: Jimmy Donal "Jimbo" Wales)により英語でプロジェクトが開始された。
+]
+
+--- justify-whitespace-adjustment ---
+// Test punctuation whitespace adjustment
+#set page(width: auto)
+#set text(lang: "zh", font: "Noto Serif CJK SC")
+#set par(justify: true)
+#rect(inset: 0pt, width: 80pt, fill: rgb("eee"))[
+ “引号测试”,还,
+
+ 《书名》《测试》下一行
+
+ 《书名》《测试》。
+]
+
+「『引号』」。“‘引号’”。
+
+--- justify-variants ---
+// Test Variants of Mainland China, Hong Kong, and Japan.
+
+// 17 characters a line.
+#set page(width: 170pt + 10pt, margin: (x: 5pt))
+#set text(lang: "zh", font: "Noto Serif CJK SC")
+#set par(justify: true)
+
+孔雀最早见于《山海经》中的《海内经》:“有孔雀。”东汉杨孚著《异物志》记载,岭南:“孔雀,其大如大雁而足高,毛皆有斑纹彩,捕而蓄之,拍手即舞。”
+
+#set text(lang: "zh", region: "hk", font: "Noto Serif CJK TC")
+孔雀最早见于《山海经》中的《海内经》:「有孔雀。」东汉杨孚著《异物志》记载,岭南:「孔雀,其大如大雁而足高,毛皆有斑纹彩,捕而蓄之,拍手即舞。」
+
+--- justify-punctuation-adjustment ---
+// Test punctuation marks adjustment in justified paragraph.
+
+// The test case includes the following scenarios:
+// - Compression of punctuation marks at line start or line end
+// - Adjustment of adjacent punctuation marks
+
+#set page(width: 110pt + 10pt, margin: (x: 5pt))
+#set text(lang: "zh", font: "Noto Serif CJK SC")
+#set par(justify: true)
+
+标注在字间的标点符号(乙式括号省略号以外)通常占一个汉字宽度,使其易于识别、适合配置及排版,有些排版风格完全不对标点宽度进行任何调整。但是为了让文字体裁更加紧凑易读,,,以及执行3.1.4 行首行尾禁则时,就需要对标点符号的宽度进行调整。是否调整取决于……
+
+--- justify-without-justifiables ---
+// Test breaking a line without justifiables.
+#set par(justify: true)
+#block(width: 1cm, fill: aqua, lorem(2))
+
+--- issue-2419-justify-hanging-indent ---
+// Test that combination of justification and hanging indent doesn't result in
+// an underfull first line.
+#set par(hanging-indent: 2.5cm, justify: true)
+#lorem(5)
diff --git a/tests/suite/layout/inline/linebreak.typ b/tests/suite/layout/inline/linebreak.typ
new file mode 100644
index 0000000000..2fa29b6cea
--- /dev/null
+++ b/tests/suite/layout/inline/linebreak.typ
@@ -0,0 +1,109 @@
+// Test line breaks.
+
+--- linebreak-overflow ---
+// Test overlong word that is not directly after a hard break.
+This is a spaceexceedinglylongy.
+
+--- linebreak-overflow-double ---
+// Test two overlong words in a row.
+Supercalifragilisticexpialidocious Expialigoricmetrioxidation.
+
+--- linebreak-hyphen-nbsp ---
+// Test for non-breaking space and hyphen.
+There are non\u{2011}breaking~characters.
+
+--- linebreak-narrow-nbsp ---
+// Test for narrow non-breaking space.
+#show "_": sym.space.nobreak.narrow
+0.1_g, 1_g, 10_g, 100_g, 1_000_g, 10_000_g, 100_000_g, 1_000_000_g
+
+--- linebreak-shape-run ---
+// Test that there are no unwanted line break opportunities on run change.
+This is partly emp#emph[has]ized.
+
+--- linebreak-manual ---
+Hard #linebreak() break.
+
+--- linebreak-manual-directly-after-automatic ---
+// Test hard break directly after normal break.
+Hard break directly after \ normal break.
+
+--- linebreak-manual-consecutive ---
+// Test consecutive breaks.
+Two consecutive \ \ breaks and three \ \ more.
+
+--- linebreak-manual-trailing-multiple ---
+// Test forcing an empty trailing line.
+Trailing break \ \
+
+--- linebreak-manual-justified ---
+// Test justified breaks.
+#set par(justify: true)
+With a soft #linebreak(justify: true)
+break you can force a break without #linebreak(justify: true)
+breaking justification. #linebreak(justify: false)
+Nice!
+
+--- linebreak-thai ---
+// Test linebreak for East Asian languages
+ทีวีตรวจทานนอร์ทแฟรีเลคเชอร์โกลด์อัลบัมเชอร์รี่เย้วสโตร์กฤษณ์เคลมเยอบีร่าพ่อค้าบลูเบอร์รี่สหัสวรรษโฮปแคนูโยโย่จูนสตรอว์เบอร์รีซื่อบื้อเยนแบ็กโฮเป็นไงโดนัททอมสเตริโอแคนูวิทย์แดรี่โดนัทวิทย์แอปพริคอทเซอร์ไพรส์ไฮบริดกิฟท์อินเตอร์โซนเซอร์วิสเทียมทานโคโยตี้ม็อบเที่ยงคืนบุญคุณ
+
+--- linebreak-cite-punctuation ---
+// Test punctuation after citations.
+#set page(width: 162pt)
+
+They can look for the details in @netwok,
+which is the authoritative source.
+
+#bibliography("/assets/bib/works.bib")
+
+--- linebreak-math-punctuation ---
+// Test punctuation after math equations.
+#set page(width: 85pt)
+
+We prove $1 < 2$. \
+We prove $1 < 2$! \
+We prove $1 < 2$? \
+We prove $1 < 2$, \
+We prove $1 < 2$; \
+We prove $1 < 2$: \
+We prove $1 < 2$- \
+We prove $1 < 2$– \
+We prove $1 < 2$— \
+
+--- linebreak-link ---
+#link("https://example.com/(ab") \
+#link("https://example.com/(ab)") \
+#link("https://example.com/(paren)") \
+#link("https://example.com/paren)") \
+#link("https://hi.com/%%%%%%%%abcdef") \
+
+--- linebreak-link-justify ---
+#set page(width: 240pt)
+#set par(justify: true)
+
+Here's a link https://url.com/data/extern12840%data_urlenc and then there are more
+links #link("www.url.com/data/extern12840%data_urlenc") in my text of links
+http://mydataurl/hash/12098541029831025981024980124124214/incremental/progress%linkdata_information_setup_my_link_just_never_stops_going/on?query=false
+
+--- linebreak-link-end ---
+// Ensure that there's no unconditional break at the end of a link.
+#set page(width: 180pt, height: auto, margin: auto)
+#set text(11pt)
+
+For info see #link("https://myhost.tld").
+
+--- issue-2105-linebreak-tofu ---
+#linebreak()中文
+
+--- issue-3082-chinese-punctuation ---
+#set text(font: "Noto Serif CJK TC", lang: "zh")
+#set page(width: 230pt)
+
+課有手冬,朱得過已誰卜服見以大您即乙太邊良,因且行肉因和拉幸,念姐遠米巴急(abc0),松黃貫誰。
+
+--- issue-80-emoji-linebreak ---
+// Test that there are no linebreaks in composite emoji (issue #80).
+#set page(width: 50pt, height: auto)
+#h(99%) 🏳️🌈
+🏳️🌈
diff --git a/tests/suite/layout/inline/overhang.typ b/tests/suite/layout/inline/overhang.typ
new file mode 100644
index 0000000000..40b0e7f7a3
--- /dev/null
+++ b/tests/suite/layout/inline/overhang.typ
@@ -0,0 +1,24 @@
+// Test micro-typographical shenanigans.
+
+--- overhang ---
+// Test hanging punctuation.
+// TODO: This test was broken at some point.
+#set page(width: 130pt, margin: 15pt)
+#set par(justify: true, linebreaks: "simple")
+#set text(size: 9pt)
+#rect(inset: 0pt, fill: rgb(0, 0, 0, 0), width: 100%)[
+ This is a little bit of text that builds up to
+ hang-ing hyphens and dash---es and then, you know,
+ some punctuation in the margin.
+]
+
+// Test hanging punctuation with RTL.
+#set text(lang: "he", font: ("PT Sans", "Noto Serif Hebrew"))
+בנייה נכונה של משפטים ארוכים דורשת ידע בשפה. אז בואו נדבר על מזג האוויר.
+
+--- overhang-lone ---
+// Test that lone punctuation doesn't overhang into the margin.
+#set page(margin: 0pt)
+#set align(end)
+#set text(dir: rtl)
+:
diff --git a/tests/suite/layout/inline/shaping.typ b/tests/suite/layout/inline/shaping.typ
new file mode 100644
index 0000000000..ec93eb47b8
--- /dev/null
+++ b/tests/suite/layout/inline/shaping.typ
@@ -0,0 +1,65 @@
+// Test shaping quirks.
+
+--- shaping-script-separation ---
+// Test separation by script.
+#set text(font: ("Linux Libertine", "IBM Plex Sans Devanagari"))
+ABCअपार्टमेंट
+
+// This is how it should look like.
+अपार्टमेंट
+
+// This (without the spaces) is how it would look
+// if we didn't separate by script.
+अ पा र् ट में ट
+
+--- shaping-forced-script-font-feature-inhibited ---
+// A forced `latn` script inhibits Devanagari font features.
+#set text(font: ("Linux Libertine", "IBM Plex Sans Devanagari"), script: "latn")
+ABCअपार्टमेंट
+
+--- shaping-forced-script-font-feature-enabled ---
+// A forced `deva` script enables Devanagari font features.
+#set text(font: ("Linux Libertine", "IBM Plex Sans Devanagari"), script: "deva")
+ABCअपार्टमेंट
+
+--- issue-rtl-safe-to-break-panic ---
+// Test that RTL safe-to-break doesn't panic even though newline
+// doesn't exist in shaping output.
+#set text(dir: rtl, font: "Noto Serif Hebrew")
+\ ט
+
+--- shaping-font-fallback ---
+// Font fallback for emoji.
+A😀B
+
+// Font fallback for entire text.
+دع النص يمطر عليك
+
+// Font fallback in right-to-left text.
+ب🐈😀سم
+
+// Multi-layer font fallback.
+Aب😀🏞سمB
+
+// Font fallback with composed emojis and multiple fonts.
+01️⃣2
+
+// Tofus are rendered with the first font.
+A🐈ዲሞB
+
+--- shaping-emoji-basic ---
+// This should form a three-member family.
+👩👩👦
+
+// This should form a pride flag.
+🏳️🌈
+
+// Skin tone modifier should be applied.
+👍🏿
+
+// This should be a 1 in a box.
+1️⃣
+
+--- shaping-emoji-bad-zwj ---
+// These two shouldn't be affected by a zero-width joiner.
+🏞🌋
diff --git a/tests/suite/layout/inline/text.typ b/tests/suite/layout/inline/text.typ
new file mode 100644
index 0000000000..e2bc84efc4
--- /dev/null
+++ b/tests/suite/layout/inline/text.typ
@@ -0,0 +1,89 @@
+// Test OpenType features.
+
+--- text-kerning ---
+// Test turning kerning off.
+#text(kerning: true)[Tq] \
+#text(kerning: false)[Tq]
+
+--- text-alternates-and-stylistic-sets ---
+// Test alternates and stylistic sets.
+#set text(font: "IBM Plex Serif")
+a vs #text(alternates: true)[a] \
+ß vs #text(stylistic-set: 5)[ß]
+
+--- text-ligatures ---
+// Test ligatures.
+fi vs. #text(ligatures: false)[No fi]
+
+--- text-number-type ---
+// Test number type.
+#set text(number-type: "old-style")
+0123456789 \
+#text(number-type: auto)[0123456789]
+
+--- text-number-width ---
+// Test number width.
+#text(number-width: "proportional")[0123456789] \
+#text(number-width: "tabular")[3456789123] \
+#text(number-width: "tabular")[0123456789]
+
+--- text-slashed-zero-and-fractions ---
+// Test extra number stuff.
+#set text(font: "IBM Plex Serif")
+0 vs. #text(slashed-zero: true)[0] \
+1/2 vs. #text(fractions: true)[1/2]
+
+--- text-features ---
+// Test raw features.
+#text(features: ("smcp",))[Smcp] \
+fi vs. #text(features: (liga: 0))[No fi]
+
+--- text-stylistic-set-bad-type ---
+// Error: 26-31 expected integer or none, found boolean
+#set text(stylistic-set: false)
+
+--- text-stylistic-set-out-of-bounds ---
+// Error: 26-28 stylistic set must be between 1 and 20
+#set text(stylistic-set: 25)
+
+--- text-number-type-bad ---
+// Error: 24-25 expected "lining", "old-style", or auto, found integer
+#set text(number-type: 2)
+
+--- text-features-bad ---
+// Error: 21-26 expected array or dictionary, found boolean
+#set text(features: false)
+
+--- text-features-bad-nested-type ---
+// Error: 21-35 expected string, found boolean
+#set text(features: ("tag", false))
+
+--- text-tracking-negative ---
+// Test tracking.
+#set text(tracking: -0.01em)
+I saw Zoe yӛsterday, on the tram.
+
+--- text-tracking-changed-temporarily ---
+// Test tracking for only part of paragraph.
+I'm in#text(tracking: 0.15em + 1.5pt)[ spaace]!
+
+--- text-tracking-mark-placement ---
+// Test that tracking doesn't disrupt mark placement.
+#set text(font: ("PT Sans", "Noto Serif Hebrew"))
+#set text(tracking: 0.3em)
+טֶקסט
+
+--- text-tracking-arabic ---
+// Test tracking in arabic text (makes no sense whatsoever)
+#set text(tracking: 0.3em)
+النص
+
+--- text-spacing ---
+// Test word spacing.
+#set text(spacing: 1em)
+My text has spaces.
+
+--- text-spacing-relative ---
+// Test word spacing relative to the font's space width.
+#set text(spacing: 50% + 1pt)
+This is tight.
diff --git a/tests/suite/layout/layout.typ b/tests/suite/layout/layout.typ
new file mode 100644
index 0000000000..257e478b49
--- /dev/null
+++ b/tests/suite/layout/layout.typ
@@ -0,0 +1,14 @@
+--- layout-in-fixed-size-block ---
+// Layout inside a block with certain dimensions should provide those dimensions.
+#set page(height: 120pt)
+#block(width: 60pt, height: 80pt, layout(size => [
+ This block has a width of #size.width and height of #size.height
+]))
+
+--- layout-in-page-call ---
+// Layout without any container should provide the page's dimensions, minus its margins.
+#page(width: 100pt, height: 100pt, {
+ layout(size => [This page has a width of #size.width and height of #size.height ])
+ h(1em)
+ place(left, rect(width: 80pt, stroke: blue))
+})
diff --git a/tests/suite/layout/length.typ b/tests/suite/layout/length.typ
new file mode 100644
index 0000000000..6875561971
--- /dev/null
+++ b/tests/suite/layout/length.typ
@@ -0,0 +1,69 @@
+--- length-fields ---
+// Test length fields.
+#test((1pt).em, 0.0)
+#test((1pt).abs, 1pt)
+#test((3em).em, 3.0)
+#test((3em).abs, 0pt)
+#test((2em + 2pt).em, 2.0)
+#test((2em + 2pt).abs, 2pt)
+
+--- length-to-unit ---
+// Test length unit conversions.
+#test((500.934pt).pt(), 500.934)
+#test((3.3453cm).cm(), 3.3453)
+#test((4.3452mm).mm(), 4.3452)
+#test((5.345in).inches(), 5.345)
+#test((500.333666999pt).pt(), 500.333666999)
+#test((3.5234354cm).cm(), 3.5234354)
+#test((4.12345678mm).mm(), 4.12345678)
+#test((5.333666999in).inches(), 5.333666999)
+#test((4.123456789123456mm).mm(), 4.123456789123456)
+#test((254cm).mm(), 2540.0)
+#test(calc.round((254cm).inches(), digits: 2), 100.0)
+#test((2540mm).cm(), 254.0)
+#test(calc.round((2540mm).inches(), digits: 2), 100.0)
+#test((100in).pt(), 7200.0)
+#test(calc.round((100in).cm(), digits: 2), 254.0)
+#test(calc.round((100in).mm(), digits: 2), 2540.0)
+#test(5em.abs.cm(), 0.0)
+#test((5em + 6in).abs.inches(), 6.0)
+
+--- length-to-absolute ---
+// Test length `to-absolute` method.
+#set text(size: 12pt)
+#context {
+ test((6pt).to-absolute(), 6pt)
+ test((6pt + 10em).to-absolute(), 126pt)
+ test((10em).to-absolute(), 120pt)
+}
+
+#set text(size: 64pt)
+#context {
+ test((6pt).to-absolute(), 6pt)
+ test((6pt + 10em).to-absolute(), 646pt)
+ test((10em).to-absolute(), 640pt)
+}
+
+--- length-unit-hint ---
+// Error: 1:17-1:19 expected length, found integer: a length needs a unit - did you mean 12pt?
+#set text(size: 12)
+
+--- length-ignore-em-pt-hint ---
+// Error: 2-21 cannot convert a length with non-zero em units (`-6pt + 10.5em`) to pt
+// Hint: 2-21 use `length.abs.pt()` instead to ignore its em component
+#(10.5em - 6pt).pt()
+
+--- length-ignore-em-cm-hint ---
+// Error: 2-12 cannot convert a length with non-zero em units (`3em`) to cm
+// Hint: 2-12 use `length.abs.cm()` instead to ignore its em component
+#(3em).cm()
+
+--- length-ignore-em-mm-hint ---
+// Error: 2-20 cannot convert a length with non-zero em units (`-226.77pt + 93em`) to mm
+// Hint: 2-20 use `length.abs.mm()` instead to ignore its em component
+#(93em - 80mm).mm()
+
+--- length-ignore-em-inches-hint ---
+// Error: 2-24 cannot convert a length with non-zero em units (`432pt + 4.5em`) to inches
+// Hint: 2-24 use `length.abs.inches()` instead to ignore its em component
+#(4.5em + 6in).inches()
diff --git a/tests/suite/layout/limits.typ b/tests/suite/layout/limits.typ
new file mode 100644
index 0000000000..e1f0ec5fa4
--- /dev/null
+++ b/tests/suite/layout/limits.typ
@@ -0,0 +1,32 @@
+// Test how the layout engine reacts when reaching limits like
+// zero, infinity or when dealing with NaN.
+
+--- issue-1216-clamp-panic ---
+#set page(height: 20pt, margin: 0pt)
+#v(22pt)
+#block(fill: red, width: 100%, height: 10pt, radius: 4pt)
+
+--- issue-1918-layout-infinite-length-grid-columns ---
+// Test that passing infinite lengths to drawing primitives does not crash Typst.
+#set page(width: auto, height: auto)
+
+// Error: 58-59 cannot expand into infinite width
+#layout(size => grid(columns: (size.width, size.height))[a][b][c][d])
+
+--- issue-1918-layout-infinite-length-grid-rows ---
+#set page(width: auto, height: auto)
+
+// Error: 17-66 cannot create grid with infinite height
+#layout(size => grid(rows: (size.width, size.height))[a][b][c][d])
+
+--- issue-1918-layout-infinite-length-line ---
+#set page(width: auto, height: auto)
+
+// Error: 17-41 cannot create line with infinite length
+#layout(size => line(length: size.width))
+
+--- issue-1918-layout-infinite-length-polygon ---
+#set page(width: auto, height: auto)
+
+// Error: 17-54 cannot create polygon with infinite size
+#layout(size => polygon((0pt,0pt), (0pt, size.width)))
diff --git a/tests/suite/layout/measure.typ b/tests/suite/layout/measure.typ
new file mode 100644
index 0000000000..5f82e91503
--- /dev/null
+++ b/tests/suite/layout/measure.typ
@@ -0,0 +1,9 @@
+--- measure ---
+// Test `measure`.
+#let f(lo, hi) = context {
+ let h = measure[Hello].height
+ assert(h > lo)
+ assert(h < hi)
+}
+#text(10pt, f(6pt, 8pt))
+#text(20pt, f(13pt, 14pt))
diff --git a/tests/typ/layout/pad.typ b/tests/suite/layout/pad.typ
similarity index 83%
rename from tests/typ/layout/pad.typ
rename to tests/suite/layout/pad.typ
index 0eff587627..3a7439d00c 100644
--- a/tests/typ/layout/pad.typ
+++ b/tests/suite/layout/pad.typ
@@ -1,6 +1,6 @@
// Test the `pad` function.
----
+--- pad-basic ---
// Use for indentation.
#pad(left: 10pt, [Indented!])
@@ -14,17 +14,17 @@
Hi #box(pad(left: 10pt)[A]) there
----
+--- pad-expanding-contents ---
// Pad can grow.
#pad(left: 10pt, right: 10pt)[PL #h(1fr) PR]
----
+--- pad-followed-by-content ---
// Test that the pad element doesn't consume the whole region.
#set page(height: 6cm)
#align(left)[Before]
#pad(10pt, image("/assets/images/tiger.jpg"))
#align(right)[After]
----
+--- pad-adding-to-100-percent ---
// Test that padding adding up to 100% does not panic.
#pad(50%)[]
diff --git a/tests/suite/layout/page.typ b/tests/suite/layout/page.typ
new file mode 100644
index 0000000000..a529b4297c
--- /dev/null
+++ b/tests/suite/layout/page.typ
@@ -0,0 +1,231 @@
+// Test the page class.
+
+--- page-call-empty ---
+// Just empty page.
+// Should result in auto-sized page, just like nothing.
+#page[]
+
+--- page-call-styled-empty ---
+// Just empty page with styles.
+// Should result in one conifer-colored A11 page.
+#page("a11", flipped: true, fill: conifer)[]
+
+--- page-call-followed-by-pagebreak ---
+// Just page followed by pagebreak.
+// Should result in one forest-colored A11 page and one auto-sized page.
+#page("a11", flipped: true, fill: forest)[]
+#pagebreak()
+
+--- page-set-forces-break ---
+// Set width and height.
+// Should result in one high and one wide page.
+#set page(width: 80pt, height: 80pt)
+#[#set page(width: 40pt);High]
+#[#set page(height: 40pt);Wide]
+
+// Flipped predefined paper.
+#[#set page(paper: "a11", flipped: true);Flipped A11]
+
+--- page-set-in-container ---
+#box[
+ // Error: 4-18 page configuration is not allowed inside of containers
+ #set page("a4")
+]
+
+--- page-set-empty ---
+// Empty with styles
+// Should result in one conifer-colored A11 page.
+#set page("a11", flipped: true, fill: conifer)
+
+--- page-set-only-pagebreak ---
+// Empty with styles and then pagebreak
+// Should result in two forest-colored pages.
+#set page(fill: forest)
+#pagebreak()
+
+--- page-set-override-thrice ---
+// Empty with multiple page styles.
+// Should result in a small white page.
+#set page("a4")
+#set page("a5")
+#set page(width: 1cm, height: 1cm)
+
+--- page-set-override-and-mix ---
+// Empty with multiple page styles.
+// Should result in one eastern-colored A11 page.
+#set page("a4")
+#set page("a5")
+#set page("a11", flipped: true, fill: eastern)
+#set text(font: "Roboto", white)
+#smallcaps[Typst]
+
+--- page-large ---
+#set page("a4")
+
+--- page-fill ---
+// Test page fill.
+#set page(width: 80pt, height: 40pt, fill: eastern)
+#text(15pt, font: "Roboto", fill: white, smallcaps[Typst])
+#page(width: 40pt, fill: none, margin: (top: 10pt, rest: auto))[Hi]
+
+--- page-margin-uniform ---
+// Set all margins at once.
+#[
+ #set page(height: 20pt, margin: 5pt)
+ #place(top + left)[TL]
+ #place(bottom + right)[BR]
+]
+
+--- page-margin-individual ---
+// Set individual margins.
+#set page(height: 40pt)
+#[#set page(margin: (left: 0pt)); #align(left)[Left]]
+#[#set page(margin: (right: 0pt)); #align(right)[Right]]
+#[#set page(margin: (top: 0pt)); #align(top)[Top]]
+#[#set page(margin: (bottom: 0pt)); #align(bottom)[Bottom]]
+
+// Ensure that specific margins override general margins.
+#[#set page(margin: (rest: 0pt, left: 20pt)); Overridden]
+
+--- page-margin-inside-outside-override ---
+#set page(height: 100pt, margin: (inside: 30pt, outside: 20pt))
+#set par(justify: true)
+#set text(size: 8pt)
+
+#page(margin: (x: 20pt), {
+ set align(center + horizon)
+ text(20pt, strong[Title])
+ v(2em, weak: true)
+ text(15pt)[Author]
+})
+
+= Introduction
+#lorem(35)
+
+--- page-margin-inside ---
+#set page(margin: (inside: 30pt))
+#rect(width: 100%)[Bound]
+#pagebreak()
+#rect(width: 100%)[Left]
+
+--- page-margin-inside-with-binding ---
+// Test setting the binding explicitly.
+#set page(binding: right, margin: (inside: 30pt))
+#rect(width: 100%)[Bound]
+#pagebreak()
+#rect(width: 100%)[Right]
+
+--- page-margin-binding-from-text-lang ---
+// Test setting the binding implicitly.
+#set page(margin: (inside: 30pt))
+#set text(lang: "he")
+#rect(width: 100%)[Bound]
+#pagebreak()
+#rect(width: 100%)[Right]
+
+--- page-margin-left-and-outside ---
+// Error: 19-44 `inside` and `outside` are mutually exclusive with `left` and `right`
+#set page(margin: (left: 1cm, outside: 2cm))
+
+--- page-margin-binding-bad ---
+// Error: 20-23 must be `left` or `right`
+#set page(binding: top)
+
+--- page-marginals ---
+#set page(
+ paper: "a8",
+ margin: (x: 15pt, y: 30pt),
+ header: {
+ text(eastern)[*Typst*]
+ h(1fr)
+ text(0.8em)[_Chapter 1_]
+ },
+ footer: context align(center)[\~ #counter(page).display() \~],
+ background: context if counter(page).get().first() <= 2 {
+ place(center + horizon, circle(radius: 1cm, fill: luma(90%)))
+ }
+)
+
+But, soft! what light through yonder window breaks? It is the east, and Juliet
+is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and
+pale with grief, That thou her maid art far more fair than she: Be not her maid,
+since she is envious; Her vestal livery is but sick and green And none but fools
+do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she
+were! She speaks yet she says nothing: what of that? Her eye discourses; I will
+answer it.
+
+#set page(header: none, height: auto, margin: (top: 15pt, bottom: 25pt))
+The END.
+
+--- page-number-align-top-right ---
+#set page(
+ height: 100pt,
+ margin: 30pt,
+ numbering: "(1)",
+ number-align: top + right,
+)
+
+#block(width: 100%, height: 100%, fill: aqua.lighten(50%))
+
+--- page-number-align-bottom-left ---
+#set page(
+ height: 100pt,
+ margin: 30pt,
+ numbering: "[1]",
+ number-align: bottom + left,
+)
+
+#block(width: 100%, height: 100%, fill: aqua.lighten(50%))
+
+--- page-number-align-left-horizon ---
+// Error: 25-39 expected `top` or `bottom`, found horizon
+#set page(number-align: left + horizon)
+
+--- page-numbering-pdf-label ---
+#set page(margin: (bottom: 20pt, rest: 10pt))
+#let filler = lorem(20)
+
+// (i) - (ii). No style opt. because of suffix.
+#set page(numbering: "(i)")
+#filler
+#pagebreak()
+#filler
+
+// 3 - 4. Style opt. Page Label should use /D style.
+#set page(numbering: "1")
+#filler
+#pagebreak()
+#filler
+
+// I - IV. Style opt. Page Label should use /R style and start at 1 again.
+#set page(numbering: "I / I")
+#counter(page).update(1)
+#filler
+#pagebreak()
+#filler
+#pagebreak()
+#filler
+#pagebreak()
+#filler
+
+// Pre: ほ, Pre: ろ, Pre: は, Pre: に. No style opt. Uses prefix field entirely.
+// Counter update without numbering change.
+#set page(numbering: "Pre: い")
+#filler
+#pagebreak()
+#filler
+#counter(page).update(2)
+#filler
+#pagebreak()
+#filler
+#pagebreak()
+#filler
+
+// aa & ba. Style opt only for values <= 26. Page Label uses lower alphabet style.
+// Repeats letter each 26 pages or uses numbering directly as prefix.
+#set page(numbering: "a")
+#counter(page).update(27)
+#filler
+#pagebreak()
+#counter(page).update(53)
+#filler
diff --git a/tests/suite/layout/pagebreak.typ b/tests/suite/layout/pagebreak.typ
new file mode 100644
index 0000000000..a173459680
--- /dev/null
+++ b/tests/suite/layout/pagebreak.typ
@@ -0,0 +1,143 @@
+// Test forced page breaks.
+
+--- pagebreak ---
+// Just a pagebreak.
+// Should result in two pages.
+#pagebreak()
+
+--- pagebreak-around-set-page ---
+// Pagebreak, empty with styles and then pagebreak
+// Should result in one auto-sized page and two conifer-colored 2cm wide pages.
+#pagebreak()
+#set page(width: 2cm, fill: conifer)
+#pagebreak()
+
+--- pagebreak-weak-after-set-page ---
+// Two text bodies separated with and surrounded by weak pagebreaks.
+// Should result in two aqua-colored pages.
+#set page(fill: aqua)
+#pagebreak(weak: true)
+First
+#pagebreak(weak: true)
+Second
+#pagebreak(weak: true)
+
+--- pagebreak-set-page-mixed ---
+// Test a combination of pagebreaks, styled pages and pages with bodies.
+// Should result in three five pages, with the fourth one being forest-colored.
+#set page(width: 80pt, height: 30pt)
+#[#set page(width: 60pt); First]
+#pagebreak()
+#pagebreak()
+Third
+#page(height: 20pt, fill: forest)[]
+Fif#[#set page();th]
+
+--- pagebreak-followed-by-page-call ---
+// Test hard and weak pagebreak followed by page with body.
+// Should result in three navy-colored pages.
+#set page(fill: navy)
+#set text(fill: white)
+First
+#pagebreak()
+#page[Second]
+#pagebreak(weak: true)
+#page[Third]
+
+--- pagebreak-in-container ---
+#box[
+ // Error: 4-15 pagebreaks are not allowed inside of containers
+ #pagebreak()
+]
+
+--- pagebreak-weak-place ---
+// After place
+// Should result in three pages.
+First
+#pagebreak(weak: true)
+#place(right)[placed A]
+#pagebreak(weak: true)
+Third
+
+--- pagebreak-weak-meta ---
+// After only ignorables & invisibles
+// Should result in two pages.
+First
+#pagebreak(weak: true)
+#counter(page).update(1)
+#metadata("Some")
+#pagebreak(weak: true)
+Second
+
+--- pagebreak-meta ---
+// After only ignorables, but regular break
+// Should result in three pages.
+First
+#pagebreak()
+#counter(page).update(1)
+#metadata("Some")
+#pagebreak()
+Third
+
+--- pagebreak-to ---
+#set page(width: 80pt, height: 30pt)
+First
+#pagebreak(to: "odd")
+Third
+#pagebreak(to: "even")
+Fourth
+#pagebreak(to: "even")
+Sixth
+#pagebreak()
+Seventh
+#pagebreak(to: "odd")
+#page[Ninth]
+
+--- pagebreak-to-auto-sized ---
+#set page(width: auto, height: auto)
+
+// Test with auto-sized page.
+First
+#pagebreak(to: "odd")
+Third
+
+--- pagebreak-to-multiple-pages ---
+#set page(height: 30pt, width: 80pt)
+
+// Test when content extends to more than one page
+First
+
+Second
+
+#pagebreak(to: "odd")
+
+Third
+
+--- issue-2134-pagebreak-bibliography ---
+// Test weak pagebreak before bibliography.
+#pagebreak(weak: true)
+#bibliography("/assets/bib/works.bib")
+
+--- issue-2095-pagebreak-numbering ---
+// The empty page 2 should not have a page number
+#set page(numbering: none)
+This and next page should not be numbered
+
+#pagebreak(weak: true, to: "odd")
+
+#set page(numbering: "1")
+#counter(page).update(1)
+
+This page should
+
+--- issue-2162-pagebreak-set-style ---
+// The styles should not be applied to the pagebreak empty page,
+// it should only be applied after that.
+#pagebreak(to: "even") // We should now skip to page 2
+
+Some text on page 2
+
+#pagebreak(to: "even") // We should now skip to page 4
+
+#set page(fill: orange) // This sets the color of the page starting from page 4
+Some text on page 4
diff --git a/tests/suite/layout/place.typ b/tests/suite/layout/place.typ
new file mode 100644
index 0000000000..b8765e937f
--- /dev/null
+++ b/tests/suite/layout/place.typ
@@ -0,0 +1,226 @@
+// Test the `place` function.
+
+--- place-basic ---
+#set page("a8")
+#place(bottom + center)[© Typst]
+
+= Placement
+#place(right, image("/assets/images/tiger.jpg", width: 1.8cm))
+Hi there. This is \
+a placed element. \
+Unfortunately, \
+the line breaks still had to be inserted manually.
+
+#stack(
+ rect(fill: eastern, height: 10pt, width: 100%),
+ place(right, dy: 1.5pt)[ABC],
+ rect(fill: conifer, height: 10pt, width: 80%),
+ rect(fill: forest, height: 10pt, width: 100%),
+ 10pt,
+ block[
+ #place(center, dx: -7pt, dy: -5pt)[Hello]
+ #place(center, dx: 7pt, dy: 5pt)[Hello]
+ Hello #h(1fr) Hello
+ ]
+)
+
+--- place-block-spacing ---
+// Test how the placed element interacts with paragraph spacing around it.
+#set page("a8", height: 60pt)
+
+First
+
+#place(bottom + right)[Placed]
+
+Second
+
+--- place-background ---
+#set page(paper: "a10", flipped: true)
+#set text(fill: white)
+#place(
+ dx: -10pt,
+ dy: -10pt,
+ image(
+ "/assets/images/tiger.jpg",
+ fit: "cover",
+ width: 100% + 20pt,
+ height: 100% + 20pt,
+ )
+)
+#align(bottom + right)[
+ _Welcome to_ #underline[*Tigerland*]
+]
+
+--- place-float ---
+#set page(height: 140pt)
+#set place(clearance: 5pt)
+#lorem(6)
+#place(auto, float: true, rect[A])
+#place(auto, float: true, rect[B])
+#place(auto, float: true, rect[C])
+#place(auto, float: true, rect[D])
+
+--- place-float-missing ---
+// Error: 2-20 automatic positioning is only available for floating placement
+// Hint: 2-20 you can enable floating placement with `place(float: true, ..)`
+#place(auto)[Hello]
+
+--- place-float-center-horizon ---
+// Error: 2-45 floating placement must be `auto`, `top`, or `bottom`
+#place(center + horizon, float: true)[Hello]
+
+--- place-float-horizon ---
+// Error: 2-36 floating placement must be `auto`, `top`, or `bottom`
+#place(horizon, float: true)[Hello]
+
+--- place-float-default ---
+// Error: 2-27 floating placement must be `auto`, `top`, or `bottom`
+#place(float: true)[Hello]
+
+--- place-float-right ---
+// Error: 2-34 floating placement must be `auto`, `top`, or `bottom`
+#place(right, float: true)[Hello]
+
+--- place-float-columns ---
+// LARGE
+#set page(height: 200pt, width: 300pt)
+#show: columns.with(2)
+
+= Introduction
+#figure(
+ placement: bottom,
+ caption: [A glacier],
+ image("/assets/images/glacier.jpg", width: 50%),
+)
+#lorem(45)
+#figure(
+ placement: top,
+ caption: [A rectangle],
+ rect[Hello!],
+)
+#lorem(20)
+
+--- place-float-figure ---
+// LARGE
+#set page(height: 250pt, width: 150pt)
+
+= Introduction
+#lorem(10) #footnote[Lots of Latin]
+
+#figure(
+ placement: bottom,
+ caption: [A glacier #footnote[Lots of Ice]],
+ image("/assets/images/glacier.jpg", width: 80%),
+)
+
+#lorem(40)
+
+#figure(
+ placement: top,
+ caption: [An important],
+ image("/assets/images/diagram.svg", width: 80%),
+)
+
+--- place-bottom-in-box ---
+#box(
+ fill: aqua,
+ width: 30pt,
+ height: 30pt,
+ place(bottom,
+ place(line(start: (0pt, 0pt), end: (20pt, 0pt), stroke: red + 3pt))
+ )
+)
+
+--- place-horizon-in-boxes ---
+#box(
+ fill: aqua,
+ width: 30pt,
+ height: 30pt,
+ {
+ box(fill: yellow, {
+ [Hello]
+ place(horizon, line(start: (0pt, 0pt), end: (20pt, 0pt), stroke: red + 2pt))
+ })
+ place(horizon, line(start: (0pt, 0pt), end: (20pt, 0pt), stroke: green + 3pt))
+ }
+)
+
+--- place-bottom-right-in-box ---
+#box(fill: aqua)[
+ #place(bottom + right)[Hi]
+ Hello World \
+ How are \
+ you?
+]
+
+--- place-top-left-in-box ---
+#box(fill: aqua)[
+ #place(top + left, dx: 50%, dy: 50%)[Hi]
+ #v(30pt)
+ #line(length: 50pt)
+]
+
+--- issue-place-base ---
+// Test that placement is relative to container and not itself.
+#set page(height: 80pt, margin: 0pt)
+#place(right, dx: -70%, dy: 20%, [First])
+#place(left, dx: 20%, dy: 60%, [Second])
+#place(center + horizon, dx: 25%, dy: 25%, [Third])
+
+--- issue-1368-place-pagebreak ---
+// Test placing on an already full page.
+// It shouldn't result in a page break.
+#set page(height: 40pt)
+#block(height: 100%)
+#place(bottom + right)[Hello world]
+
+--- issue-2199-place-spacing-bottom ---
+// Test that placed elements don't add extra block spacing.
+#show figure: set block(spacing: 4em)
+
+Paragraph before float.
+#figure(rect(), placement: bottom)
+Paragraph after float.
+
+--- issue-2199-place-spacing-default ---
+#show place: set block(spacing: 4em)
+
+Paragraph before place.
+#place(rect())
+Paragraph after place.
+
+--- issue-2595-float-overlap ---
+#set page(height: 80pt)
+
+Start.
+
+#place(auto, float: true, [
+ #block(height: 100%, width: 100%, fill: aqua)
+])
+
+#place(auto, float: true, [
+ #block(height: 100%, width: 100%, fill: red)
+])
+
+#lorem(20)
+
+--- issue-2715-float-order ---
+#set page(height: 180pt)
+#set figure(placement: auto)
+
+#figure(
+ rect(height: 60pt),
+ caption: [Rectangle I],
+)
+
+#figure(
+ rect(height: 50pt),
+ caption: [Rectangle II],
+)
+
+#figure(
+ circle(),
+ caption: [Circle],
+)
+
+#lorem(20)
diff --git a/tests/suite/layout/relative.typ b/tests/suite/layout/relative.typ
new file mode 100644
index 0000000000..958aee3d33
--- /dev/null
+++ b/tests/suite/layout/relative.typ
@@ -0,0 +1,7 @@
+--- relative-fields ---
+// Test relative length fields.
+#test((100% + 2em + 2pt).ratio, 100%)
+#test((100% + 2em + 2pt).length, 2em + 2pt)
+#test((100% + 2pt).length, 2pt)
+#test((100% + 2pt - 2pt).length, 0pt)
+#test((56% + 2pt - 56%).ratio, 0%)
diff --git a/tests/typ/layout/repeat.typ b/tests/suite/layout/repeat.typ
similarity index 85%
rename from tests/typ/layout/repeat.typ
rename to tests/suite/layout/repeat.typ
index 173f9d5768..5c82fc1965 100644
--- a/tests/typ/layout/repeat.typ
+++ b/tests/suite/layout/repeat.typ
@@ -1,6 +1,6 @@
// Test the `repeat` function.
----
+--- repeat-basic ---
// Test multiple repeats.
#let sections = (
("Introduction", 1),
@@ -15,20 +15,20 @@
#section.at(0) #box(width: 1fr, repeat[.]) #section.at(1) \
]
----
+--- repeat-dots-rtl ---
// Test dots with RTL.
#set text(lang: "ar")
مقدمة #box(width: 1fr, repeat[.]) 15
----
+--- repeat-empty ---
// Test empty repeat.
A #box(width: 1fr, repeat[]) B
----
+--- repeat-unboxed ---
// Test unboxed repeat.
#repeat(rect(width: 2em, height: 1em))
----
+--- repeat-align-and-dir ---
// Test single repeat in both directions.
A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B
@@ -38,7 +38,7 @@ A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B
#set text(dir: rtl)
ريجين#box(width: 1fr, repeat(rect(width: 4em, height: 0.7em)))سون
----
+--- repeat-unrestricted ---
// Error: 2:2-2:13 repeat with no size restrictions
#set page(width: auto)
#repeat(".")
diff --git a/tests/suite/layout/spacing.typ b/tests/suite/layout/spacing.typ
new file mode 100644
index 0000000000..430e977947
--- /dev/null
+++ b/tests/suite/layout/spacing.typ
@@ -0,0 +1,38 @@
+// Test the `h` and `v` functions.
+
+--- spacing-h-and-v ---
+// Linebreak and leading-sized weak spacing are equivalent.
+#box[A \ B] #box[A #v(0.65em, weak: true) B]
+
+// Eating up soft spacing.
+Inv#h(0pt)isible
+
+// Multiple spacings in a row.
+Add #h(10pt) #h(10pt) up
+
+// Relative to area.
+#let x = 25% - 4pt
+|#h(x)|#h(x)|#h(x)|#h(x)|
+
+// Fractional.
+| #h(1fr) | #h(2fr) | #h(1fr) |
+
+--- spacing-rtl ---
+// Test RTL spacing.
+#set text(dir: rtl)
+A #h(10pt) B \
+A #h(1fr) B
+
+--- spacing-missing-amount ---
+// Missing spacing.
+// Error: 10-13 missing argument: amount
+Totally #h() ignored
+
+--- issue-3624-spacing-behaviour ---
+// Test that metadata after spacing does not force a new paragraph.
+#{
+ h(1em)
+ counter(heading).update(4)
+ [Hello ]
+ counter(heading).display()
+}
diff --git a/tests/suite/layout/stack.typ b/tests/suite/layout/stack.typ
new file mode 100644
index 0000000000..aad273a5ba
--- /dev/null
+++ b/tests/suite/layout/stack.typ
@@ -0,0 +1,91 @@
+// Test stack layouts.
+
+--- stack-basic ---
+// Test stacks with different directions.
+#let widths = (
+ 30pt, 20pt, 40pt, 15pt,
+ 30pt, 50%, 20pt, 100%,
+)
+
+#let shaded(i, w) = {
+ let v = (i + 1) * 10%
+ rect(width: w, height: 10pt, fill: rgb(v, v, v))
+}
+
+#let items = for (i, w) in widths.enumerate() {
+ (align(right, shaded(i, w)),)
+}
+
+#set page(width: 50pt, margin: 0pt)
+#stack(dir: btt, ..items)
+
+--- stack-spacing ---
+// Test spacing.
+#set page(width: 50pt, margin: 0pt)
+
+#let x = square(size: 10pt, fill: eastern)
+#stack(
+ spacing: 5pt,
+ stack(dir: rtl, spacing: 5pt, x, x, x),
+ stack(dir: ltr, x, 20%, x, 20%, x),
+ stack(dir: ltr, spacing: 5pt, x, x, 7pt, 3pt, x),
+)
+
+--- stack-overflow ---
+// Test overflow.
+#set page(width: 50pt, height: 30pt, margin: 0pt)
+#box(stack(
+ rect(width: 40pt, height: 20pt, fill: conifer),
+ rect(width: 30pt, height: 13pt, fill: forest),
+))
+
+--- stack-fr ---
+#set page(height: 3.5cm)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ ..for c in "ABCDEFGHI" {([#c],)}
+)
+
+Hello
+#v(2fr)
+from #h(1fr) the #h(1fr) wonderful
+#v(1fr)
+World! 🌍
+
+--- stack-rtl-align-and-fr ---
+// Test aligning things in RTL stack with align function & fr units.
+#set page(width: 50pt, margin: 5pt)
+#set block(spacing: 5pt)
+#set text(8pt)
+#stack(dir: rtl, 1fr, [A], 1fr, [B], [C])
+#stack(dir: rtl,
+ align(center, [A]),
+ align(left, [B]),
+ [C],
+)
+
+--- issue-1240-stack-h-fr ---
+// This issue is sort of horrible: When you write `h(1fr)` in a `stack` instead
+// of directly `1fr`, things go awry. To fix this, we now transparently detect
+// h/v children.
+#stack(dir: ltr, [a], 1fr, [b], 1fr, [c])
+#stack(dir: ltr, [a], h(1fr), [b], h(1fr), [c])
+
+--- issue-1240-stack-v-fr ---
+#set page(height: 60pt)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ stack([a], 1fr, [b]),
+ stack([a], v(1fr), [b]),
+)
+
+--- issue-1918-stack-with-infinite-spacing ---
+// https://github.com/typst/typst/issues/1918
+#set page(width: auto)
+#context layout(available => {
+ let infinite-length = available.width
+ // Error: 3-40 stack spacing is infinite
+ stack(spacing: infinite-length)[A][B]
+})
diff --git a/tests/suite/layout/table.typ b/tests/suite/layout/table.typ
new file mode 100644
index 0000000000..7eec46a172
--- /dev/null
+++ b/tests/suite/layout/table.typ
@@ -0,0 +1,284 @@
+// Test tables.
+
+--- table-empty ---
+#table()
+
+--- table-newlines ---
+#set page(height: 70pt)
+#set table(fill: (x, y) => if calc.even(x + y) { rgb("aaa") })
+
+#table(
+ columns: (1fr,) * 3,
+ stroke: 2pt + rgb("333"),
+ [A], [B], [C], [], [], [D \ E \ F \ \ \ G], [H],
+)
+
+--- table-fill-basic ---
+#table(columns: 3, stroke: none, fill: green, [A], [B], [C])
+
+--- table-fill-bad ---
+// Error: 14-19 expected color, gradient, pattern, none, array, or function, found string
+#table(fill: "hey")
+
+--- table-align-array ---
+// Test alignment with array.
+#table(
+ columns: (1fr, 1fr, 1fr),
+ align: (left, center, right),
+ [A], [B], [C]
+)
+
+// Test empty array.
+#set align(center)
+#table(
+ columns: (1fr, 1fr, 1fr),
+ align: (),
+ [A], [B], [C]
+)
+
+--- table-inset ---
+// Test inset.
+#table(
+ columns: 3,
+ inset: 10pt,
+ [A], [B], [C]
+)
+
+#table(
+ columns: 3,
+ inset: (y: 10pt),
+ [A], [B], [C]
+)
+
+#table(
+ columns: 3,
+ inset: (left: 20pt, rest: 10pt),
+ [A], [B], [C]
+)
+
+#table(
+ columns: 2,
+ inset: (
+ left: 20pt,
+ right: 5pt,
+ top: 10pt,
+ bottom: 3pt,
+ ),
+ [A],
+ [B],
+)
+
+#table(
+ columns: 3,
+ fill: (x, y) => (if y == 0 { aqua } else { orange }).darken(x * 15%),
+ inset: (x, y) => (left: if x == 0 { 0pt } else { 5pt }, right: if x == 0 { 5pt } else { 0pt }, y: if y == 0 { 0pt } else { 5pt }),
+ [A], [B], [C],
+ [A], [B], [C],
+)
+
+#table(
+ columns: 3,
+ inset: (0pt, 5pt, 10pt),
+ fill: (x, _) => aqua.darken(x * 15%),
+ [A], [B], [C],
+)
+
+--- table-inset-fold ---
+// Test inset folding
+#set table(inset: 10pt)
+#set table(inset: (left: 0pt))
+
+#table(
+ fill: red,
+ inset: (right: 0pt),
+ table.cell(inset: (top: 0pt))[a]
+)
+
+--- table-gutters ---
+// Test interaction with gutters.
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ row-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ column-gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+#table(
+ columns: (3em, 3em),
+ fill: (x, y) => (red, blue).at(calc.rem(x, 2)),
+ align: (x, y) => (left, right).at(calc.rem(y, 2)),
+ gutter: 5pt,
+ [A], [B],
+ [C], [D],
+ [E], [F],
+ [G], [H]
+)
+
+--- table-cell-override ---
+// Cell override
+#table(
+ align: left,
+ fill: red,
+ stroke: blue,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ table.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A],
+ table.cell(align: horizon)[G2], [A\ A\ A],
+ table.cell(inset: 0pt)[I], [F],
+ [H], table.cell(fill: blue)[J]
+)
+
+--- table-cell-show ---
+// Cell show rule
+#show table.cell: it => [Zz]
+
+#table(
+ align: left,
+ fill: red,
+ stroke: blue,
+ columns: 2,
+ [AAAAA], [BBBBB],
+ [A], [B],
+ table.cell(align: right)[C], [D],
+ align(right)[E], [F],
+ align(horizon)[G], [A\ A\ A]
+)
+
+--- table-cell-show-and-override ---
+#show table.cell: it => (it.align, it.fill)
+#table(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ table.cell(align: right)[B],
+ table.cell(fill: aqua)[B],
+)
+
+--- table-cell-set ---
+// Cell set rules
+#set table.cell(align: center)
+#show table.cell: it => (it.align, it.fill, it.inset)
+#set table.cell(inset: 20pt)
+#table(
+ align: left,
+ row-gutter: 5pt,
+ [A],
+ table.cell(align: right)[B],
+ table.cell(fill: aqua)[B],
+)
+
+--- table-cell-folding ---
+// Test folding per-cell properties (align and inset)
+#table(
+ columns: (1fr, 1fr),
+ rows: (2.5em, auto),
+ align: right,
+ fill: (x, y) => (green, aqua).at(calc.rem(x + y, 2)),
+ [Top], table.cell(align: bottom)[Bot],
+ table.cell(inset: (bottom: 0pt))[Bot], table.cell(inset: (bottom: 0pt))[Bot]
+)
+
+--- table-cell-align-override ---
+// Test overriding outside alignment
+#set align(bottom + right)
+#table(
+ columns: (1fr, 1fr),
+ rows: 2em,
+ align: auto,
+ fill: green,
+ [BR], [BR],
+ table.cell(align: left, fill: aqua)[BL], table.cell(align: top, fill: red.lighten(50%))[TR]
+)
+
+--- table-cell-various-overrides ---
+#table(
+ columns: 2,
+ fill: green,
+ align: right,
+ [*Name*], [*Data*],
+ table.cell(fill: blue)[J.], [Organizer],
+ table.cell(align: center)[K.], [Leader],
+ [M.], table.cell(inset: 0pt)[Player]
+)
+
+--- table-cell-show-emph ---
+#{
+ show table.cell: emph
+ table(
+ columns: 2,
+ [Person], [Animal],
+ [John], [Dog]
+ )
+}
+
+--- table-cell-show-based-on-position ---
+// Style based on position
+#{
+ show table.cell: it => {
+ if it.y == 0 {
+ strong(it)
+ } else if it.x == 1 {
+ emph(it)
+ } else {
+ it
+ }
+ }
+ table(
+ columns: 3,
+ gutter: 3pt,
+ [Name], [Age], [Info],
+ [John], [52], [Nice],
+ [Mary], [50], [Cool],
+ [Jake], [49], [Epic]
+ )
+}
+
+--- grid-cell-in-table ---
+// Error: 8-19 cannot use `grid.cell` as a table cell; use `table.cell` instead
+#table(grid.cell[])
+
+--- issue-183-table-lines ---
+// Ensure no empty lines before a table that doesn't fit into the first page.
+#set page(height: 50pt)
+
+Hello
+#table(
+ columns: 4,
+ [1], [2], [3], [4]
+)
+
+--- issue-1388-table-row-missing ---
+// Test that a table row isn't wrongly treated like a gutter row.
+#set page(height: 70pt)
+#table(
+ rows: 16pt,
+ ..range(6).map(str).flatten(),
+)
diff --git a/tests/suite/layout/transform.typ b/tests/suite/layout/transform.typ
new file mode 100644
index 0000000000..50a6d417fe
--- /dev/null
+++ b/tests/suite/layout/transform.typ
@@ -0,0 +1,106 @@
+// Test transformations.
+
+--- transform-tex-logo ---
+// Test creating the TeX and XeTeX logos.
+#let size = 11pt
+#let tex = {
+ [T]
+ h(-0.14 * size)
+ box(move(dy: 0.22 * size)[E])
+ h(-0.12 * size)
+ [X]
+}
+
+#let xetex = {
+ [X]
+ h(-0.14 * size)
+ box(scale(x: -100%, move(dy: 0.26 * size)[E]))
+ h(-0.14 * size)
+ [T]
+ h(-0.14 * size)
+ box(move(dy: 0.26 * size)[E])
+ h(-0.12 * size)
+ [X]
+}
+
+#set text(font: "New Computer Modern", size)
+Neither #tex, \
+nor #xetex!
+
+--- transform-rotate-and-scale ---
+// Test combination of scaling and rotation.
+#set page(height: 80pt)
+#align(center + horizon,
+ rotate(20deg, scale(70%, image("/assets/images/tiger.jpg")))
+)
+
+--- transform-rotate-origin ---
+// Test setting rotation origin.
+#rotate(10deg, origin: top + left,
+ image("/assets/images/tiger.jpg", width: 50%)
+)
+
+--- transform-scale-origin ---
+// Test setting scaling origin.
+#let r = rect(width: 100pt, height: 10pt, fill: forest)
+#set page(height: 65pt)
+#box(scale(r, x: 50%, y: 200%, origin: left + top))
+#box(scale(r, x: 50%, origin: center))
+#box(scale(r, x: 50%, y: 200%, origin: right + bottom))
+
+--- transform-rotate ---
+// Test that rotation impact layout.
+#set page(width: 200pt)
+#set rotate(reflow: true)
+
+#let one(angle) = box(fill: aqua, rotate(angle)[Test Text])
+#for angle in range(0, 360, step: 15) {
+ one(angle * 1deg)
+}
+
+--- transform-rotate-relative-sizing ---
+// Test relative sizing in rotated boxes.
+#set page(width: 200pt, height: 200pt)
+#set text(size: 32pt)
+#let rotated(body) = box(rotate(
+ 90deg,
+ box(stroke: 0.5pt, height: 20%, clip: true, body)
+))
+
+#set rotate(reflow: false)
+Hello #rotated[World]!\
+
+#set rotate(reflow: true)
+Hello #rotated[World]!
+
+--- transform-scale ---
+// Test that scaling impact layout.
+#set page(width: 200pt)
+#set text(size: 32pt)
+#let scaled(body) = box(scale(
+ x: 20%,
+ y: 40%,
+ body
+))
+
+#set scale(reflow: false)
+Hello #scaled[World]!
+
+#set scale(reflow: true)
+Hello #scaled[World]!
+
+--- transform-scale-relative-sizing ---
+// Test relative sizing in scaled boxes.
+#set page(width: 200pt, height: 200pt)
+#set text(size: 32pt)
+#let scaled(body) = box(scale(
+ x: 60%,
+ y: 40%,
+ box(stroke: 0.5pt, width: 30%, clip: true, body)
+))
+
+#set scale(reflow: false)
+Hello #scaled[World]!\
+
+#set scale(reflow: true)
+Hello #scaled[World]!
diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ
new file mode 100644
index 0000000000..415488fcc2
--- /dev/null
+++ b/tests/suite/loading/csv.typ
@@ -0,0 +1,27 @@
+--- csv ---
+// Test reading CSV data.
+#set page(width: auto)
+#let data = csv("/assets/data/zoo.csv")
+#let cells = data.at(0).map(strong) + data.slice(1).flatten()
+#table(columns: data.at(0).len(), ..cells)
+
+--- csv-row-type-dict ---
+// Test reading CSV data with dictionary rows enabled.
+#let data = csv("/assets/data/zoo.csv", row-type: dictionary)
+#test(data.len(), 3)
+#test(data.at(0).Name, "Debby")
+#test(data.at(2).Weight, "150kg")
+#test(data.at(1).Species, "Tiger")
+
+--- csv-file-not-found ---
+// Error: 6-16 file not found (searched at tests/suite/loading/nope.csv)
+#csv("nope.csv")
+
+--- csv-invalid ---
+// Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3)
+#csv("/assets/data/bad.csv")
+
+--- csv-invalid-row-type-dict ---
+// Test error numbering with dictionary rows.
+// Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3)
+#csv("/assets/data/bad.csv", row-type: dictionary)
diff --git a/tests/suite/loading/json.typ b/tests/suite/loading/json.typ
new file mode 100644
index 0000000000..3ebeaf2f70
--- /dev/null
+++ b/tests/suite/loading/json.typ
@@ -0,0 +1,16 @@
+--- json ---
+// Test reading JSON data.
+#let data = json("/assets/data/zoo.json")
+#test(data.len(), 3)
+#test(data.at(0).name, "Debby")
+#test(data.at(2).weight, 150)
+
+--- json-invalid ---
+// Error: 7-30 failed to parse JSON (expected value at line 3 column 14)
+#json("/assets/data/bad.json")
+
+--- issue-3363-json-large-number ---
+// Big numbers (larger than what i64 can store) should just lose some precision
+// but not overflow
+#let bignum = json("/assets/data/big-number.json")
+#bignum
diff --git a/tests/suite/loading/read.typ b/tests/suite/loading/read.typ
new file mode 100644
index 0000000000..b5c9c0892e
--- /dev/null
+++ b/tests/suite/loading/read.typ
@@ -0,0 +1,12 @@
+--- read-text ---
+// Test reading plain text files
+#let data = read("/assets/text/hello.txt")
+#test(data, "Hello, world!\n")
+
+--- read-file-not-found ---
+// Error: 18-44 file not found (searched at assets/text/missing.txt)
+#let data = read("/assets/text/missing.txt")
+
+--- read-invalid-utf-8 ---
+// Error: 18-40 file is not valid utf-8
+#let data = read("/assets/text/bad.txt")
diff --git a/tests/suite/loading/toml.typ b/tests/suite/loading/toml.typ
new file mode 100644
index 0000000000..855ca995dc
--- /dev/null
+++ b/tests/suite/loading/toml.typ
@@ -0,0 +1,41 @@
+--- toml ---
+// Test reading TOML data.
+#let data = toml("/assets/data/toml-types.toml")
+#test(data.string, "wonderful")
+#test(data.integer, 42)
+#test(data.float, 3.14)
+#test(data.boolean, true)
+#test(data.array, (1, "string", 3.0, false))
+#test(data.inline_table, ("first": "amazing", "second": "greater") )
+#test(data.table.element, 5)
+#test(data.table.others, (false, "indeed", 7))
+#test(data.date_time, datetime(
+ year: 2023,
+ month: 2,
+ day: 1,
+ hour: 15,
+ minute: 38,
+ second: 57,
+))
+#test(data.date_time2, datetime(
+ year: 2023,
+ month: 2,
+ day: 1,
+ hour: 15,
+ minute: 38,
+ second: 57,
+))
+#test(data.date, datetime(
+ year: 2023,
+ month: 2,
+ day: 1,
+))
+#test(data.time, datetime(
+ hour: 15,
+ minute: 38,
+ second: 57,
+))
+
+--- toml-invalid ---
+// Error: 7-30 failed to parse TOML (expected `.`, `=` at line 1 column 16)
+#toml("/assets/data/bad.toml")
diff --git a/tests/suite/loading/xml.typ b/tests/suite/loading/xml.typ
new file mode 100644
index 0000000000..41cd20e746
--- /dev/null
+++ b/tests/suite/loading/xml.typ
@@ -0,0 +1,28 @@
+--- xml ---
+// Test reading XML data.
+#let data = xml("/assets/data/hello.xml")
+#test(data, ((
+ tag: "data",
+ attrs: (:),
+ children: (
+ "\n ",
+ (tag: "hello", attrs: (name: "hi"), children: ("1",)),
+ "\n ",
+ (
+ tag: "data",
+ attrs: (:),
+ children: (
+ "\n ",
+ (tag: "hello", attrs: (:), children: ("World",)),
+ "\n ",
+ (tag: "hello", attrs: (:), children: ("World",)),
+ "\n ",
+ ),
+ ),
+ "\n",
+ ),
+),))
+
+--- xml-invalid ---
+// Error: 6-28 failed to parse XML (found closing tag 'data' instead of 'hello' in line 3)
+#xml("/assets/data/bad.xml")
diff --git a/tests/suite/loading/yaml.typ b/tests/suite/loading/yaml.typ
new file mode 100644
index 0000000000..bbfea41cb5
--- /dev/null
+++ b/tests/suite/loading/yaml.typ
@@ -0,0 +1,17 @@
+--- yaml ---
+// Test reading YAML data
+#let data = yaml("/assets/data/yaml-types.yaml")
+#test(data.len(), 9)
+#test(data.null_key, (none, none))
+#test(data.string, "text")
+#test(data.integer, 5)
+#test(data.float, 1.12)
+#test(data.mapping, ("1": "one", "2": "two"))
+#test(data.seq, (1,2,3,4))
+#test(data.bool, false)
+#test(data.keys().contains("true"), true)
+#test(data.at("1"), "ok")
+
+--- yaml-invalid ---
+// Error: 7-30 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18)
+#yaml("/assets/data/bad.yaml")
diff --git a/tests/typ/math/accent.typ b/tests/suite/math/accent.typ
similarity index 75%
rename from tests/typ/math/accent.typ
rename to tests/suite/math/accent.typ
index 315e14b3cb..9f57d69b07 100644
--- a/tests/typ/math/accent.typ
+++ b/tests/suite/math/accent.typ
@@ -1,33 +1,33 @@
// Test math accents.
----
+--- math-accent-sym-call ---
// Test function call.
$grave(a), acute(b), hat(f), tilde(§), macron(ä), diaer(a), ä \
breve(\&), dot(!), circle(a), caron(@), arrow(Z), arrow.l(Z)$
----
+--- math-accent-align ---
$ x &= p \ dot(x) &= v \ dot.double(x) &= a \ dot.triple(x) &= j \ dot.quad(x) &= s $
----
+--- math-accent-func ---
// Test `accent` function.
$accent(ö, .), accent(v, <-), accent(ZZ, \u{0303})$
----
+--- math-accent-bounds ---
// Test accent bounds.
$sqrt(tilde(T)) + hat(f)/hat(g)$
----
+--- math-accent-wide-base ---
// Test wide base.
$arrow("ABC" + d), tilde(sum)$
----
+--- math-accent-superscript ---
// Test effect of accent on superscript.
$A^x != hat(A)^x != hat(hat(A))^x$
----
+--- math-accent-high-base ---
// Test high base.
$ tilde(integral), tilde(integral)_a^b, tilde(integral_a^b) $
----
+--- math-accent-sized ---
// Test accent size.
$tilde(sum), tilde(sum, size: #50%), accent(H, hat, size: #200%)$
diff --git a/tests/suite/math/alignment.typ b/tests/suite/math/alignment.typ
new file mode 100644
index 0000000000..63033ef5c4
--- /dev/null
+++ b/tests/suite/math/alignment.typ
@@ -0,0 +1,51 @@
+// Test implicit alignment math.
+
+--- math-align-weird ---
+// Test alignment step functions.
+#set page(width: 225pt)
+$
+"a" &= c \
+&= c + 1 & "By definition" \
+&= d + 100 + 1000 \
+&= x && "Even longer" \
+$
+
+--- math-align-post-fix ---
+// Test post-fix alignment.
+$
+& "right" \
+"a very long line" \
+"left" \
+$
+
+--- math-align-implicit ---
+// Test no alignment.
+$
+"right" \
+"a very long line" \
+"left" \
+$
+
+--- math-align-toggle ---
+// Test #460 equations.
+$
+a &=b & quad c&=d \
+e &=f & g&=h
+$
+
+--- issue-3973-math-equation-align ---
+// In this bug, the alignment set with "show math.equation: set align(...)"
+// overrides the left-right alternating behavior of alignment points.
+#let equations = [
+$ a + b &= c \
+ e &= f + g + h $
+$ a &= b + c \
+ e + f + g &= h $
+]
+#equations
+
+#show math.equation: set align(start)
+#equations
+
+#show math.equation: set align(end)
+#equations
diff --git a/tests/suite/math/attach.typ b/tests/suite/math/attach.typ
new file mode 100644
index 0000000000..c9510c6a5b
--- /dev/null
+++ b/tests/suite/math/attach.typ
@@ -0,0 +1,130 @@
+// Test t and b attachments, part 1.
+
+--- math-attach-postscripts ---
+// Test basics, postscripts.
+$f_x + t^b + V_1^2 + attach(A, t: alpha, b: beta)$
+
+--- math-attach-prescripts ---
+// Test basics, prescripts. Notably, the upper and lower prescripts' content need to be
+// aligned on the right edge of their bounding boxes, not on the left as in postscripts.
+$
+attach(upright(O), bl: 8, tl: 16, br: 2, tr: 2-),
+attach("Pb", bl: 82, tl: 207) + attach(upright(e), bl: -1, tl: 0) + macron(v)_e \
+$
+
+--- math-attach-mixed ---
+// A mixture of attachment positioning schemes.
+$
+attach(a, tl: u), attach(a, tr: v), attach(a, bl: x),
+attach(a, br: y), limits(a)^t, limits(a)_b \
+
+attach(a, tr: v, t: t),
+attach(a, tr: v, br: y),
+attach(a, br: y, b: b),
+attach(limits(a), b: b, bl: x),
+attach(a, tl: u, bl: x),
+attach(limits(a), t: t, tl: u) \
+
+attach(a, tl: u, tr: v),
+attach(limits(a), t: t, br: y),
+attach(limits(a), b: b, tr: v),
+attach(a, bl: x, br: y),
+attach(limits(a), b: b, tl: u),
+attach(limits(a), t: t, bl: u),
+limits(a)^t_b \
+
+attach(a, tl: u, tr: v, bl: x, br: y),
+attach(limits(a), t: t, bl: x, br: y, b: b),
+attach(limits(a), t: t, tl: u, tr: v, b: b),
+attach(limits(a), tl: u, bl: x, t: t, b: b),
+attach(limits(a), t: t, b: b, tr: v, br: y),
+attach(a, tl: u, t: t, tr: v, bl: x, b: b, br: y)
+$
+
+--- math-attach-followed-by-func-call ---
+// Test function call after subscript.
+$pi_1(Y), a_f(x), a^zeta (x), a^abs(b)_sqrt(c) \
+ a^subset.eq (x), a_(zeta(x)), pi_(1(Y)), a^(abs(b))_(sqrt(c))$
+
+--- math-attach-nested ---
+// Test associativity and scaling.
+$ 1/(V^2^3^4^5),
+ frac(
+ attach(
+ limits(V), br: attach(2, br: 3), b: attach(limits(2), b: 3)),
+ attach(
+ limits(V), tl: attach(2, tl: 3), t: attach(limits(2), t: 3))),
+ attach(Omega,
+ tl: attach(2, tl: attach(3, tl: attach(4, tl: 5))),
+ tr: attach(2, tr: attach(3, tr: attach(4, tr: 5))),
+ bl: attach(2, bl: attach(3, bl: attach(4, bl: 5))),
+ br: attach(2, br: attach(3, br: attach(4, br: 5))),
+ )
+$
+
+--- math-attach-high ---
+// Test high subscript and superscript.
+$ sqrt(a_(1/2)^zeta), sqrt(a_alpha^(1/2)), sqrt(a_(1/2)^(3/4)) \
+ sqrt(attach(a, tl: 1/2, bl: 3/4)),
+ sqrt(attach(a, tl: 1/2, bl: 3/4, tr: 1/2, br: 3/4)) $
+
+--- math-attach-descender-collision ---
+// Test for no collisions between descenders/ascenders and attachments
+
+$ sup_(x in P_i) quad inf_(x in P_i) $
+$ op("fff",limits: #true)^(y) quad op("yyy", limits:#true)_(f) $
+
+--- math-attach-to-group ---
+// Test frame base.
+$ (-1)^n + (1/2 + 3)^(-1/2) $
+
+--- math-attach-horizontal-align ---
+#set text(size: 8pt)
+
+// Test that the attachments are aligned horizontally.
+$ x_1 p_1 frak(p)_1 2_1 dot_1 lg_1 !_1 \\_1 ]_1 "ip"_1 op("iq")_1 \
+ x^1 b^1 frak(b)^1 2^1 dot^1 lg^1 !^1 \\^1 ]^1 "ib"^1 op("id")^1 \
+ x_1 y_1 "_"_1 x^1 l^1 "`"^1 attach(I,tl:1,bl:1,tr:1,br:1)
+ scripts(sum)_1^1 integral_1^1 abs(1/2)_1^1 \
+ x^1_1, "("b y")"^1_1 != (b y)^1_1, "[∫]"_1 [integral]_1 $
+
+--- math-attach-limit ---
+// Test limit.
+$ lim_(n->oo \ n "grows") sum_(k=0 \ k in NN)^n k $
+
+--- math-attach-force-scripts-and-limits ---
+// Test forcing scripts and limits.
+$ limits(A)_1^2 != A_1^2 $
+$ scripts(sum)_1^2 != sum_1^2 $
+$ limits(integral)_a^b != integral_a^b $
+
+--- issue-math-attach-realize-panic ---
+// Error: 25-29 unknown variable: oops
+$ attach(A, t: #context oops) $
+
+--- math-attach-show-limit ---
+// Show and let rules for limits and scripts
+#let eq = $ ∫_a^b iota_a^b $
+#eq
+#show "∫": math.limits
+#show math.iota: math.limits.with(inline: false)
+#eq
+$iota_a^b$
+
+--- math-attach-default-placement ---
+// Test default of limit attachments on relations at all sizes
+#set page(width: auto)
+$ a =^"def" b quad a lt.eq_"really" b quad a arrow.r.long.squiggly^"slowly" b $
+$a =^"def" b quad a lt.eq_"really" b quad a arrow.r.long.squiggly^"slowly" b$
+
+$a scripts(=)^"def" b quad a scripts(lt.eq)_"really" b quad a scripts(arrow.r.long.squiggly)^"slowly" b$
+
+--- math-attach-integral ---
+// Test default of scripts attachments on integrals at display size
+$ integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $
+$integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$
+
+--- math-attach-large-operator ---
+// Test default of limit attachments on large operators at display size only
+$ tack.t.big_0^1 quad \u{02A0A}_0^1 quad join_0^1 $
+$tack.t.big_0^1 quad \u{02A0A}_0^1 quad join_0^1$
diff --git a/tests/suite/math/call.typ b/tests/suite/math/call.typ
new file mode 100644
index 0000000000..ef2851d69f
--- /dev/null
+++ b/tests/suite/math/call.typ
@@ -0,0 +1,97 @@
+// Test math function call edge cases.
+
+// Note: 2d argument calls are tested for matrices in `mat.typ`
+
+--- math-call-non-func ---
+$ pi(a) $
+$ pi(a,) $
+$ pi(a,b) $
+$ pi(a,b,) $
+
+--- math-call-repr ---
+#let args(..body) = body
+#let check(it, r) = test-repr(it.body.text, r)
+#check($args(a)$, "([a])")
+#check($args(a,)$, "([a])")
+#check($args(a,b)$, "([a], [b])")
+#check($args(a,b,)$, "([a], [b])")
+#check($args(,a,b,,,)$, "([], [a], [b], [], [])")
+
+--- math-call-2d-non-func ---
+// Error: 6-7 expected content, found array
+// Error: 8-9 expected content, found array
+$ pi(a;b) $
+
+--- math-call-2d-semicolon-priority ---
+// If the semicolon directlry follows a hash expression, it terminates that
+// instead of indicating 2d arguments.
+$ mat(#"math" ; "wins") $
+$ mat(#"code"; "wins") $
+
+--- math-call-2d-repr ---
+#let args(..body) = body
+#let check(it, r) = test-repr(it.body.text, r)
+#check($args(a;b)$, "(([a],), ([b],))")
+#check($args(a,b;c)$, "(([a], [b]), ([c],))")
+#check($args(a,b;c,d;e,f)$, "(([a], [b]), ([c], [d]), ([e], [f]))")
+
+--- math-call-2d-repr-structure ---
+#let args(..body) = body
+#let check(it, r) = test-repr(it.body.text, r)
+#check($args( a; b; )$, "(([a],), ([b],))")
+#check($args(a; ; c)$, "(([a],), ([],), ([c],))")
+#check($args(a b,/**/; b)$, "(([([a], [ ], [b])], []), ([b],))")
+#check($args(a/**/b, ; b)$, "(([([a], [b])], []), ([b],))")
+#check($args( ;/**/a/**/b/**/; )$, "(([],), ([([a], [b])],))")
+#check($args( ; , ; )$, "(([],), ([], []))")
+#check($args(/**/; // funky whitespace/trivia
+ , /**/ ;/**/)$, "(([],), ([], []))")
+
+--- math-call-empty-args-non-func ---
+// Trailing commas and empty args introduce blank content in math
+$ sin(,x,y,,,) $
+// with whitespace/trivia:
+$ sin( ,/**/x/**/, , /**/y, ,/**/, ) $
+
+--- math-call-empty-args-repr ---
+#let args(..body) = body
+#let check(it, r) = test-repr(it.body.text, r)
+#check($args(,x,,y,,)$, "([], [x], [], [y], [])")
+// with whitespace/trivia:
+#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "([], [x], [], [y], [], [])")
+
+--- math-call-value-non-func ---
+$ sin(1) $
+// Error: 8-9 expected content, found integer
+$ sin(#1) $
+
+--- math-call-pass-to-box ---
+// When passing to a function, we lose the italic styling if we wrap the content
+// in a non-math function unless it's already nested in some math element (lr,
+// attach, etc.)
+//
+// This is not good, so this test should fail and be updated once it is fixed.
+#let id(body) = body
+#let bx(body) = box(body, stroke: blue+0.5pt, inset: (x:2pt, y:3pt))
+#let eq(body) = math.equation(body)
+$
+ x y &&quad x (y z) &quad x y^z \
+ id(x y) &&quad id(x (y z)) &quad id(x y^z) \
+ eq(x y) &&quad eq(x (y z)) &quad eq(x y^z) \
+ bx(x y) &&quad bx(x (y z)) &quad bx(x y^z) \
+$
+
+--- issue-3774-math-call-empty-2d-args ---
+$ mat(;,) $
+// Add some whitespace/trivia:
+$ mat(; ,) $
+$ mat(;/**/,) $
+$ mat(;
+,) $
+$ mat(;// line comment
+,) $
+$ mat(
+ 1, , ;
+ ,1, ;
+ , ,1;
+) $
diff --git a/tests/typ/math/cancel.typ b/tests/suite/math/cancel.typ
similarity index 85%
rename from tests/typ/math/cancel.typ
rename to tests/suite/math/cancel.typ
index ac715154ad..e2fd5efde1 100644
--- a/tests/typ/math/cancel.typ
+++ b/tests/suite/math/cancel.typ
@@ -1,38 +1,38 @@
// Tests the cancel() function.
----
+--- math-cancel-inline ---
// Inline
$a + 5 + cancel(x) + b - cancel(x)$
$c + (a dot.c cancel(b dot.c c))/(cancel(b dot.c c))$
----
+--- math-cancel-display ---
// Display
#set page(width: auto)
$ a + b + cancel(b + c) - cancel(b) - cancel(c) - 5 + cancel(6) - cancel(6) $
$ e + (a dot.c cancel((b + c + d)))/(cancel(b + c + d)) $
----
+--- math-cancel-inverted ---
// Inverted
$a + cancel(x, inverted: #true) - cancel(x, inverted: #true) + 10 + cancel(y) - cancel(y)$
$ x + cancel("abcdefg", inverted: #true) $
----
+--- math-cancel-cross ---
// Cross
$a + cancel(b + c + d, cross: #true, stroke: #red) + e$
$ a + cancel(b + c + d, cross: #true) + e $
----
+--- math-cancel-customized ---
// Resized and styled
#set page(width: 200pt, height: auto)
$a + cancel(x, length: #200%) - cancel(x, length: #50%, stroke: #(red + 1.1pt))$
$ b + cancel(x, length: #150%) - cancel(a + b + c, length: #50%, stroke: #(blue + 1.2pt)) $
----
+--- math-cancel-angle-absolute ---
// Specifying cancel line angle with an absolute angle
$cancel(x, angle: #0deg) + cancel(x, angle: #45deg) + cancel(x, angle: #90deg) + cancel(x, angle: #135deg)$
----
+--- math-cancel-angle-func ---
// Specifying cancel line angle with a function
$x + cancel(y, angle: #{angle => angle + 90deg}) - cancel(z, angle: #(angle => angle + 135deg))$
$ e + cancel((j + e)/(f + e)) - cancel((j + e)/(f + e), angle: #(angle => angle + 30deg)) $
diff --git a/tests/suite/math/cases.typ b/tests/suite/math/cases.typ
new file mode 100644
index 0000000000..e6c4956dcd
--- /dev/null
+++ b/tests/suite/math/cases.typ
@@ -0,0 +1,13 @@
+// Test case distinction.
+
+--- math-cases ---
+$ f(x, y) := cases(
+ 1 quad &"if" (x dot y)/2 <= 0,
+ 2 &"if" x divides 2,
+ 3 &"if" x in NN,
+ 4 &"else",
+) $
+
+--- math-cases-gap ---
+#set math.cases(gap: 1em)
+$ x = cases(1, 2) $
diff --git a/tests/typ/math/class.typ b/tests/suite/math/class.typ
similarity index 88%
rename from tests/typ/math/class.typ
rename to tests/suite/math/class.typ
index a5901b31dd..7aad04465c 100644
--- a/tests/typ/math/class.typ
+++ b/tests/suite/math/class.typ
@@ -1,6 +1,6 @@
// Test math classes.
----
+--- math-class-chars ---
// Test characters.
$ a class("normal", +) b \
a class("binary", .) b \
@@ -12,7 +12,7 @@ $ a class("normal", +) b \
a + class("unary", times) b \
class("vary", :) a class("vary", :) b $
----
+--- math-class-content ---
// Test custom content.
#let dotsq = square(
size: 0.7em,
@@ -26,17 +26,17 @@ $ a dotsq b \
a + class("vary", dotsq) b \
a class("punctuation", dotsq) b $
----
+--- math-class-nested ---
// Test nested.
#let normal = math.class.with("normal")
#let pluseq = $class("binary", normal(+) normal(=))$
$ a pluseq 5 $
----
+--- math-class-exceptions ---
// Test exceptions.
$ sqrt(3)\/2 quad d_0.d_1d_2 dots $
----
+--- math-class-limits ---
// Test if the math class changes the limit configuration.
$ class("normal", ->)_a $
$class("relation", x)_a$
diff --git a/tests/suite/math/delimited.typ b/tests/suite/math/delimited.typ
new file mode 100644
index 0000000000..42a67c4eac
--- /dev/null
+++ b/tests/suite/math/delimited.typ
@@ -0,0 +1,64 @@
+// Test delimiter matching and scaling.
+
+--- math-lr-matching ---
+// Test automatic matching.
+#set page(width:122pt)
+$ (a) + {b/2} + abs(a)/2 + (b) $
+$f(x/2) < zeta(c^2 + abs(a + b/2))$
+
+--- math-lr-unmatched ---
+// Test unmatched.
+$[1,2[ = [1,2) != zeta\(x/2\) $
+
+--- math-lr-call ---
+// Test manual matching.
+$ [|a/b|] != lr(|]a/b|]) != [a/b) $
+$ lr(| ]1,2\[ + 1/2|) $
+
+--- math-lr-fences ---
+// Test fence confusion.
+$ |x + |y| + z/a| \
+ lr(|x + |y| + z/a|) $
+
+--- math-lr-symbol-unmatched ---
+// Test that symbols aren't matched automatically.
+$ bracket.l a/b bracket.r
+ = lr(bracket.l a/b bracket.r) $
+
+--- math-lr-half ---
+// Test half LRs.
+$ lr(a/b\]) = a = lr(\{a/b) $
+
+--- math-lr-size ---
+// Test manual scaling.
+$ lr(]sum_(x=1)^n x], size: #70%)
+ < lr((1, 2), size: #200%) $
+
+--- math-lr-shorthands ---
+// Test predefined delimiter pairings.
+$floor(x/2), ceil(x/2), abs(x), norm(x)$
+
+--- math-lr-color ---
+// Test colored delimiters
+$ lr(
+ text("(", fill: #green) a/b
+ text(")", fill: #blue)
+ ) $
+
+--- math-lr-mid ---
+// Test middle functions
+$ { x mid(|) sum_(i=1)^oo phi_i (x) < 1 } \
+ { integral |dot|
+ mid(bar.v.double)
+ floor(hat(I) mid(slash) { dot mid(|) dot } mid(|) I/n) } $
+
+--- math-lr-unbalanced ---
+// Test unbalanced delimiters.
+$ 1/(2 (x) $
+$ 1_(2 y (x) () $
+$ 1/(2 y (x) (2(3)) $
+
+--- math-lr-weak-spacing ---
+// Test ignoring weak spacing immediately after the opening
+// and immediately before the closing.
+$ [#h(1em, weak: true)A(dif x, f(x) dif x)sum#h(1em, weak: true)] $
diff --git a/tests/suite/math/equation.typ b/tests/suite/math/equation.typ
new file mode 100644
index 0000000000..dddba5018b
--- /dev/null
+++ b/tests/suite/math/equation.typ
@@ -0,0 +1,217 @@
+// Test alignment of block equations.
+// Test show rules on equations.
+
+--- math-equation-numbering ---
+#set page(width: 150pt)
+#set math.equation(numbering: "(I)")
+
+We define $x$ in preparation of @fib:
+$ phi.alt := (1 + sqrt(5)) / 2 $
+
+With @ratio, we get
+$ F_n = round(1 / sqrt(5) phi.alt^n) $
+
+--- math-equation-font ---
+// Test different font.
+#show math.equation: set text(font: "Fira Math")
+$ v := vec(1 + 2, 2 - 4, sqrt(3), arrow(x)) + 1 $
+
+--- math-equation-show-rule ---
+This is small: $sum_(i=0)^n$
+
+#show math.equation: math.display
+This is big: $sum_(i=0)^n$
+
+--- math-equation-align-unnumbered ---
+// Test unnumbered
+#let eq(alignment) = {
+ show math.equation: set align(alignment)
+ $ a + b = c $
+}
+
+#eq(center)
+#eq(left)
+#eq(right)
+
+#set text(dir: rtl)
+#eq(start)
+#eq(end)
+
+--- math-equation-align-numbered ---
+// Test numbered
+#let eq(alignment) = {
+ show math.equation: set align(alignment)
+ $ a + b = c $
+}
+
+#set math.equation(numbering: "(1)")
+
+#eq(center)
+#eq(left)
+#eq(right)
+
+#set text(dir: rtl)
+#eq(start)
+#eq(end)
+
+--- math-equation-number-align ---
+#set math.equation(numbering: "(1)")
+
+$ a + b = c $
+
+#show math.equation: set align(center)
+$ a + b = c $
+#show math.equation: set align(left)
+$ a + b = c $
+#show math.equation: set align(right)
+$ a + b = c $
+
+#set text(dir: rtl)
+#show math.equation: set align(start)
+$ a + b = c $
+#show math.equation: set align(end)
+$ a + b = c $
+
+--- math-equation-number-align-start ---
+#set math.equation(numbering: "(1)", number-align: start)
+
+$ a + b = c $
+
+#show math.equation: set align(center)
+$ a + b = c $
+#show math.equation: set align(left)
+$ a + b = c $
+#show math.equation: set align(right)
+$ a + b = c $
+
+#set text(dir: rtl)
+#show math.equation: set align(start)
+$ a + b = c $
+#show math.equation: set align(end)
+$ a + b = c $
+
+--- math-equation-number-align-end ---
+#set math.equation(numbering: "(1)", number-align: end)
+
+$ a + b = c $
+
+#show math.equation: set align(center)
+$ a + b = c $
+#show math.equation: set align(left)
+$ a + b = c $
+#show math.equation: set align(right)
+$ a + b = c $
+
+#set text(dir: rtl)
+#show math.equation: set align(start)
+$ a + b = c $
+#show math.equation: set align(end)
+$ a + b = c $
+
+--- math-equation-number-align-left ---
+#set math.equation(numbering: "(1)", number-align: left)
+
+$ a + b = c $
+
+#show math.equation: set align(center)
+$ a + b = c $
+#show math.equation: set align(left)
+$ a + b = c $
+#show math.equation: set align(right)
+$ a + b = c $
+
+#set text(dir: rtl)
+#show math.equation: set align(start)
+$ a + b = c $
+#show math.equation: set align(end)
+$ a + b = c $
+
+--- math-equation-number-align-right ---
+#set math.equation(numbering: "(1)", number-align: right)
+
+$ a + b = c $
+
+#show math.equation: set align(center)
+$ a + b = c $
+#show math.equation: set align(left)
+$ a + b = c $
+#show math.equation: set align(right)
+$ a + b = c $
+
+#set text(dir: rtl)
+#show math.equation: set align(start)
+$ a + b = c $
+#show math.equation: set align(end)
+$ a + b = c $
+
+--- math-equation-number-align-center ---
+// Error: 52-58 expected `start`, `left`, `right`, or `end`, found center
+#set math.equation(numbering: "(1)", number-align: center)
+
+--- math-equation-number-align-center-bottom ---
+// Error: 52-67 expected `start`, `left`, `right`, or `end`, found center
+#set math.equation(numbering: "(1)", number-align: center + bottom)
+
+--- math-equation-number-align-multiline ---
+#set math.equation(numbering: "(1)")
+
+$ p &= ln a b \
+ &= ln a + ln b $
+
+--- math-equation-number-align-multiline-top-start ---
+#set math.equation(numbering: "(1)", number-align: top+start)
+
+$ p &= ln a b \
+ &= ln a + ln b $
+
+--- math-equation-number-align-multiline-bottom ---
+#show math.equation: set align(left)
+#set math.equation(numbering: "(1)", number-align: bottom)
+
+$ q &= ln sqrt(a b) \
+ &= 1/2 (ln a + ln b) $
+
+--- math-equation-number-align-multiline-expand ---
+// Tests that if the numbering's layout box vertically exceeds the box of
+// the equation frame's boundary, the latter's frame is resized correctly
+// to encompass the numbering. #box() below delineates the resized frame.
+//
+// A row with "-" only has a height that's smaller than the height of the
+// numbering's layout box. Note we use pattern "1" here, not "(1)", since
+// the parenthesis exceeds the numbering's layout box, due to the default
+// settings of top-edge and bottom-edge of the TextElem that laid it out.
+#set math.equation(numbering: "1", number-align: top)
+#box(
+$ - &- - \
+ a &= b $,
+fill: silver)
+
+#set math.equation(numbering: "1", number-align: horizon)
+#box(
+$ - - - $,
+fill: silver)
+
+#set math.equation(numbering: "1", number-align: bottom)
+#box(
+$ a &= b \
+ - &- - $,
+fill: silver)
+
+--- issue-numbering-hint ---
+// In this bug, the hint and error messages for an equation
+// being reference mentioned that it was a "heading" and was
+// lacking the proper path.
+#set page(height: 70pt)
+
+$
+ Delta = b^2 - 4 a c
+$
+
+// Error: 14-24 cannot reference equation without numbering
+// Hint: 14-24 you can enable equation numbering with `#set math.equation(numbering: "1.")`
+Looks at the @quadratic formula.
+
+--- issue-3696-equation-rtl ---
+#set page(width: 150pt)
+#set text(lang: "he")
+תהא סדרה $a_n$: $[a_n: 1, 1/2, 1/3, dots]$
diff --git a/tests/typ/math/frac.typ b/tests/suite/math/frac.typ
similarity index 77%
rename from tests/typ/math/frac.typ
rename to tests/suite/math/frac.typ
index fc7dd14b70..b3ca8aa0ae 100644
--- a/tests/typ/math/frac.typ
+++ b/tests/suite/math/frac.typ
@@ -1,39 +1,39 @@
// Test fractions.
----
+--- math-frac-baseline ---
// Test that denominator baseline matches in the common case.
$ x = 1/2 = a/(a h) = a/a = a/(1/2) $
----
+--- math-frac-paren-removal ---
// Test parenthesis removal.
$ (|x| + |y|)/2 < [1+2]/3 $
----
+--- math-frac-large ---
// Test large fraction.
$ x = (-b plus.minus sqrt(b^2 - 4a c))/(2a) $
----
+--- math-binom ---
// Test binomial.
$ binom(circle, square) $
----
+--- math-binom-multiple ---
// Test multinomial coefficients.
$ binom(n, k_1, k_2, k_3) $
----
+--- math-binom-missing-lower ---
// Error: 3-13 missing argument: lower
$ binom(x^2) $
----
+--- math-dif ---
// Test dif.
$ (dif y)/(dif x), dif/x, x/dif, dif/dif \
frac(dif y, dif x), frac(dif, x), frac(x, dif), frac(dif, dif) $
----
+--- math-frac-associativity ---
// Test associativity.
$ 1/2/3 = (1/2)/3 = 1/(2/3) $
----
+--- math-frac-precedence ---
// Test precedence.
$ a_1/b_2, 1/f(x), zeta(x)/2, "foo"[|x|]/2 \
1.2/3.7, 2.3^3.4 \
diff --git a/tests/suite/math/interactions.typ b/tests/suite/math/interactions.typ
new file mode 100644
index 0000000000..f5a3218ce1
--- /dev/null
+++ b/tests/suite/math/interactions.typ
@@ -0,0 +1,115 @@
+// Test interactions with styling and normal layout.
+// Hint: They are bad ...
+
+--- math-nested-normal-layout ---
+// Test images and font fallback.
+#let monkey = move(dy: 0.2em, image("/assets/images/monkey.svg", height: 1em))
+$ sum_(i=#emoji.apple)^#emoji.apple.red i + monkey/2 $
+
+--- math-table ---
+// Test tables.
+$ x := #table(columns: 2)[x][y]/mat(1, 2, 3)
+ = #table[A][B][C] $
+
+--- math-equation-auto-wrapping ---
+// Test non-equation math directly in content.
+#math.attach($a$, t: [b])
+
+--- math-font-switch ---
+// Test font switch.
+#let here = text.with(font: "Noto Sans")
+$#here[f] := #here[Hi there]$.
+
+--- math-box-without-baseline ---
+// Test boxes without a baseline act as if the baseline is at the base
+#{
+ box(stroke: 0.2pt, $a #box(stroke: 0.2pt, $a$)$)
+ h(12pt)
+ box(stroke: 0.2pt, $a #box(stroke: 0.2pt, $g$)$)
+ h(12pt)
+ box(stroke: 0.2pt, $g #box(stroke: 0.2pt, $g$)$)
+}
+
+--- math-box-with-baseline ---
+// Test boxes with a baseline are respected
+#box(stroke: 0.2pt, $a #box(baseline:0.5em, stroke: 0.2pt, $a$)$)
+
+--- math-at-par-start ---
+// Test that equation at start of paragraph works fine.
+$x$ is a variable.
+
+--- math-at-par-end ---
+// Test that equation at end of paragraph works fine.
+One number is $1$
+
+--- math-at-line-start ---
+// Test math at the natural end of a line.
+#h(60pt) Number $1$ exists.
+
+--- math-at-line-end ---
+// Test math at the natural end of a line.
+#h(50pt) Number $1$ exists.
+
+--- math-consecutive ---
+// Test immediately consecutive equations.
+$x$$y$
+
+--- issue-2821-missing-fields ---
+// Issue #2821: Setting a figure's supplement to none removes the field
+#show figure.caption: it => {
+ assert(it.has("supplement"))
+ assert(it.supplement == none)
+}
+#figure([], caption: [], supplement: none)
+
+--- math-symbol-show-rule ---
+// Test using rules for symbols
+#show sym.tack: it => $#h(1em) it #h(1em)$
+$ a tack b $
+
+--- issue-math-realize-show ---
+// Test that content in math can be realized without breaking
+// nested equations.
+#let my = $pi$
+#let f1 = box(baseline: 10pt, [f])
+#let f2 = context f1
+#show math.vec: [nope]
+
+$ pi a $
+$ my a $
+$ 1 + sqrt(x/2) + sqrt(#hide($x/2$)) $
+$ a x #link("url", $+ b$) $
+$ f f1 f2 $
+$ vec(1,2) * 2 $
+
+--- issue-math-realize-hide ---
+$ x^2 #hide[$(>= phi.alt) union y^2 0$] z^2 $
+Hello #hide[there $x$]
+and #hide[$ f(x) := x^2 $]
+
+--- issue-math-realize-scripting ---
+// Test equations can embed equation pieces built by functions
+#let foo(v1, v2) = {
+ // Return an equation piece that would've been rendered in
+ // inline style if the piece is not embedded
+ $v1 v2^2$
+}
+#let bar(v1, v2) = {
+ // Return an equation piece that would've been rendered in
+ // block style if the piece is not embedded
+ $ v1 v2^2 $
+}
+#let baz(..sink) = {
+ // Return an equation piece built by joining arrays
+ sink.pos().map(x => $hat(#x)$).join(sym.and)
+}
+
+Inline $2 foo(alpha, (M+foo(a, b)))$.
+
+Inline $2 bar(alpha, (M+foo(a, b)))$.
+
+Inline $2 baz(x,y,baz(u, v))$.
+
+$ 2 foo(alpha, (M+foo(a, b))) $
+$ 2 bar(alpha, (M+foo(a, b))) $
+$ 2 baz(x,y,baz(u, v)) $
diff --git a/tests/suite/math/mat.typ b/tests/suite/math/mat.typ
new file mode 100644
index 0000000000..e6148a348e
--- /dev/null
+++ b/tests/suite/math/mat.typ
@@ -0,0 +1,163 @@
+// Test matrices.
+
+--- math-mat-semicolon ---
+// Test semicolon syntax.
+#set align(center)
+$mat() dot
+ mat(;) dot
+ mat(1, 2) dot
+ mat(1, 2;) \
+ mat(1; 2) dot
+ mat(1, 2; 3, 4) dot
+ mat(1 + &2, 1/2; &3, 4)$
+
+--- math-mat-sparse ---
+// Test sparse matrix.
+$ mat(
+ 1, 2, ..., 10;
+ 2, 2, ..., 10;
+ dots.v, dots.v, dots.down, dots.v;
+ 10, 10, ..., 10;
+) $
+
+--- math-mat-baseline ---
+// Test baseline alignment.
+$ mat(
+ a, b^2;
+ sum_(x \ y) x, a^(1/2);
+ zeta, alpha;
+) $
+
+--- math-mat-delim-set ---
+// Test alternative delimiter with set rule.
+#set math.mat(delim: "[")
+$ mat(1, 2; 3, 4) $
+$ a + mat(delim: #none, 1, 2; 3, 4) + b $
+
+--- math-mat-delim-direct ---
+// Test alternative math delimiter directly in call.
+#set align(center)
+#grid(
+ columns: 3,
+ gutter: 10pt,
+
+ $ mat(1, 2, delim: "[") $,
+ $ mat(1, 2; delim: "[") $,
+ $ mat(delim: "[", 1, 2) $,
+
+ $ mat(1; 2; delim: "[") $,
+ $ mat(1; delim: "[", 2) $,
+ $ mat(delim: "[", 1; 2) $,
+
+ $ mat(1, 2; delim: "[", 3, 4) $,
+ $ mat(delim: "[", 1, 2; 3, 4) $,
+ $ mat(1, 2; 3, 4; delim: "[") $,
+)
+
+--- math-mat-gap ---
+#set math.mat(gap: 1em)
+$ mat(1, 2; 3, 4) $
+
+--- math-mat-gaps ---
+#set math.mat(row-gap: 1em, column-gap: 2em)
+$ mat(1, 2; 3, 4) $
+
+--- math-mat-augment ---
+// Test matrix line drawing (augmentation).
+#grid(
+ columns: 2,
+ gutter: 10pt,
+
+ $ mat(10, 2, 3, 4; 5, 6, 7, 8; augment: #3) $,
+ $ mat(10, 2, 3, 4; 5, 6, 7, 8; augment: #(-1)) $,
+ $ mat(100, 2, 3; 4, 5, 6; 7, 8, 9; augment: #(hline: 2)) $,
+ $ mat(100, 2, 3; 4, 5, 6; 7, 8, 9; augment: #(hline: -1)) $,
+ $ mat(100, 2, 3; 4, 5, 6; 7, 8, 9; augment: #(hline: 1, vline: 1)) $,
+ $ mat(100, 2, 3; 4, 5, 6; 7, 8, 9; augment: #(hline: -2, vline: -2)) $,
+ $ mat(100, 2, 3; 4, 5, 6; 7, 8, 9; augment: #(vline: 2, stroke: 1pt + blue)) $,
+ $ mat(100, 2, 3; 4, 5, 6; 7, 8, 9; augment: #(vline: -1, stroke: 1pt + blue)) $,
+)
+
+--- math-mat-augment-set ---
+// Test using matrix line drawing with a set rule.
+#set math.mat(augment: (hline: 2, vline: 1, stroke: 2pt + green))
+$ mat(1, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 1) $
+
+#set math.mat(augment: 2)
+$ mat(1, 0, 0, 0; 0, 1, 0, 0; 0, 0, 1, 1) $
+
+#set math.mat(augment: none)
+
+--- math-mat-augment-line-out-of-bounds ---
+// Error: 3-37 cannot draw a vertical line after column 3 of a matrix with 3 columns
+$ mat(1, 0, 0; 0, 1, 1; augment: #3) $,
+
+--- math-mat-align-explicit--alternating ---
+// Test alternating explicit alignment in a matrix.
+$ mat(
+ "a" & "a a a" & "a a";
+ "a a" & "a a" & "a";
+ "a a a" & "a" & "a a a";
+) $
+
+--- math-mat-align-implicit ---
+// Test alignment in a matrix.
+$ mat(
+ "a", "a a a", "a a";
+ "a a", "a a", "a";
+ "a a a", "a", "a a a";
+) $
+
+--- math-mat-align-explicit-left ---
+// Test explicit left alignment in a matrix.
+$ mat(
+ &"a", &"a a a", &"a a";
+ &"a a", &"a a", &"a";
+ &"a a a", &"a", &"a a a";
+) $
+
+--- math-mat-align-explicit-right ---
+// Test explicit right alignment in a matrix.
+$ mat(
+ "a"&, "a a a"&, "a a"&;
+ "a a"&, "a a"&, "a"&;
+ "a a a"&, "a"&, "a a a"&;
+) $
+
+--- math-mat-align-complex ---
+// Test #460 equations.
+#let stop = {
+ math.class("punctuation",$.$)
+}
+$ mat(&a+b,c;&d, e) $
+$ mat(&a+b&,c;&d&, e) $
+$ mat(&&&a+b,c;&&&d, e) $
+$ mat(stop &a+b&stop,c;...stop stop&d&...stop stop, e) $
+
+--- math-mat-align-signed-numbers ---
+// Test #454 equations.
+$ mat(-1, 1, 1; 1, -1, 1; 1, 1, -1) $
+$ mat(-1&, 1&, 1&; 1&, -1&, 1&; 1&, 1&, -1&) $
+$ mat(-1&, 1&, 1&; 1, -1, 1; 1, 1, -1) $
+$ mat(&-1, &1, &1; 1, -1, 1; 1, 1, -1) $
+
+--- math-mat-bad-comma ---
+// This error message is bad.
+// Error: 13-14 expected array, found content
+$ mat(1, 2; 3, 4, delim: "[") $,
+
+--- issue-852-mat-type ---
+$ mat(B, A B) $
+$ mat(B, A B, dots) $
+$ mat(B, A B, dots;) $
+$ mat(#1, #(foo: "bar")) $
+
+--- issue-2268-mat-augment-color ---
+// The augment line should be of the same color as the text
+#set text(
+ font: "New Computer Modern",
+ lang: "en",
+ fill: yellow,
+)
+
+$mat(augment: #1, M, v) arrow.r.squiggly mat(augment: #1, R, b)$
diff --git a/tests/suite/math/multiline.typ b/tests/suite/math/multiline.typ
new file mode 100644
index 0000000000..85433627fb
--- /dev/null
+++ b/tests/suite/math/multiline.typ
@@ -0,0 +1,109 @@
+// Test multiline math.
+
+--- math-align-basic ---
+// Test basic alignment.
+$ x &= x + y \
+ &= x + 2z \
+ &= sum x dot 2z $
+
+--- math-align-wider-first-column ---
+// Test text before first alignment point.
+$ x + 1 &= a^2 + b^2 \
+ y &= a + b^2 \
+ z &= alpha dot beta $
+
+--- math-align-aligned-in-source ---
+// Test space between inner alignment points.
+$ a + b &= 2 + 3 &= 5 \
+ b &= c &= 3 $
+
+--- math-align-cases ---
+// Test in case distinction.
+$ f := cases(
+ 1 + 2 &"iff" &x,
+ 3 &"if" &y,
+) $
+
+--- math-align-lines-mixed ---
+// Test mixing lines with and some without alignment points.
+$ "abc" &= c \
+ &= d + 1 \
+ = x $
+
+--- math-attach-subscript-multiline ---
+// Test multiline subscript.
+$ sum_(n in NN \ n <= 5) n = (5(5+1))/2 = 15 $
+
+--- math-multiline-no-trailing-linebreak ---
+// Test no trailing line break.
+$
+"abc" &= c
+$
+No trailing line break.
+
+--- math-multiline-trailing-linebreak ---
+// Test single trailing line break.
+$
+"abc" &= c \
+$
+One trailing line break.
+
+--- math-multiline-multiple-trailing-linebreaks ---
+// Test multiple trailing line breaks.
+$
+"abc" &= c \ \ \
+$
+Multiple trailing line breaks.
+
+--- math-linebreaking-after-binop-and-rel ---
+// Basic breaking after binop, rel
+#let hrule(x) = box(line(length: x))
+#hrule(45pt)$e^(pi i)+1 = 0$\
+#hrule(55pt)$e^(pi i)+1 = 0$\
+#hrule(70pt)$e^(pi i)+1 = 0$
+
+--- math-linebreaking-lr ---
+// LR groups prevent linbreaking.
+#let hrule(x) = box(line(length: x))
+#hrule(76pt)$a+b$\
+#hrule(74pt)$(a+b)$\
+#hrule(74pt)$paren.l a+b paren.r$
+
+--- math-linebreaking-multiline ---
+// Multiline yet inline does not linebreak
+#let hrule(x) = box(line(length: x))
+#hrule(80pt)$a + b \ c + d$\
+
+--- math-linebreaking-trailing-linebreak ---
+// A single linebreak at the end still counts as one line.
+#let hrule(x) = box(line(length: x))
+#hrule(60pt)$e^(pi i)+1 = 0\ $
+
+--- math-linebreaking-in-box ---
+// Inline, in a box, doesn't linebreak.
+#let hrule(x) = box(line(length: x))
+#hrule(80pt)#box($a+b$)
+
+--- math-linebreaking-between-consecutive-relations ---
+// A relation followed by a relation doesn't linebreak
+#let hrule(x) = box(line(length: x))
+#hrule(70pt)$a < = b$\
+#hrule(74pt)$a < = b$
+
+--- math-linebreaking-after-relation-without-space ---
+// Line breaks can happen after a relation even if there is no
+// explicit space.
+#let hrule(x) = box(line(length: x))
+#hrule(90pt)$<;$\
+#hrule(95pt)$<;$\
+#hrule(90pt)$<)$\
+#hrule(95pt)$<)$
+
+--- math-linebreaking-empty ---
+// Verify empty rows are handled ok.
+$ $\
+Nothing: $ $, just empty.
+
+--- issue-1948-math-text-break ---
+// Test text with linebreaks in math.
+$ x := "a\nb\nc\nd\ne" $
diff --git a/tests/typ/math/op.typ b/tests/suite/math/op.typ
similarity index 76%
rename from tests/typ/math/op.typ
rename to tests/suite/math/op.typ
index 14e1c6e616..4139a08be7 100644
--- a/tests/typ/math/op.typ
+++ b/tests/suite/math/op.typ
@@ -1,30 +1,30 @@
// Test text operators.
----
+--- math-op-predefined ---
// Test predefined.
$ max_(1<=n<=m) n $
----
+--- math-op-call ---
// With or without parens.
$ &sin x + log_2 x \
= &sin(x) + log_2(x) $
----
+--- math-op-scripts-vs-limits ---
// Test scripts vs limits.
#set page(width: auto)
#set text(font: "New Computer Modern")
Discuss $lim_(n->oo) 1/n$ now.
$ lim_(n->infinity) 1/n = 0 $
----
+--- math-op-custom ---
// Test custom operator.
$ op("myop", limits: #false)_(x:=1) x \
op("myop", limits: #true)_(x:=1) x $
----
+--- math-op-styled ---
// Test styled operator.
$ bold(op("bold", limits: #true))_x y $
----
+--- math-non-math-content ---
// With non-text content
$ op(#underline[ul]) a $
diff --git a/tests/suite/math/primes.typ b/tests/suite/math/primes.typ
new file mode 100644
index 0000000000..e10f8876ce
--- /dev/null
+++ b/tests/suite/math/primes.typ
@@ -0,0 +1,50 @@
+--- math-primes ---
+// Test dedicated syntax for primes
+$a'$, $a'''_b$, $'$, $'''''''$
+
+--- math-primes-spaces ---
+// Test spaces between
+$a' ' '$, $' ' '$, $a' '/b$
+
+--- math-primes-complex ---
+// Test complex prime combinations
+$a'_b^c$, $a_b'^c$, $a_b^c'$, $a_b'^c'^d'$
+
+$(a'_b')^(c'_d')$, $a'/b'$, $a_b'/c_d'$
+
+$∫'$, $∑'$, $ ∑'_S' $
+
+--- math-primes-attach ---
+// Test attaching primes only
+$a' = a^', a_', a_'''^''^'$
+
+--- math-primes-scripts ---
+// Test primes always attaching as scripts
+$ x' $
+$ x^' $
+$ attach(x, t: ') $
+$ <' $
+$ attach(<, br: ') $
+$ op(<, limits: #true)' $
+$ limits(<)' $
+
+--- math-primes-limits ---
+// Test forcefully attaching primes as limits
+$ attach(<, t: ') $
+$ <^' $
+$ attach(<, b: ') $
+$ <_' $
+
+$ limits(x)^' $
+$ attach(limits(x), t: ') $
+
+--- math-primes-after-code-expr ---
+// Test prime symbols after code mode.
+#let g = $f$
+#let gg = $f$
+
+$
+ #(g)' #g' #g ' \
+ #g''''''''''''''''' \
+ gg'
+$
diff --git a/tests/typ/math/root.typ b/tests/suite/math/root.typ
similarity index 81%
rename from tests/typ/math/root.typ
rename to tests/suite/math/root.typ
index 6eba1275b4..a690802e11 100644
--- a/tests/typ/math/root.typ
+++ b/tests/suite/math/root.typ
@@ -1,10 +1,10 @@
// Test roots.
----
+--- math-root-basic ---
// Test root with more than one character.
$A = sqrt(x + y) = c$
----
+--- math-root-radical-attachment ---
// Test root size with radicals containing attachments.
$ sqrt(a) quad
sqrt(f) quad
@@ -15,7 +15,7 @@ $ sqrt(a) quad
sqrt(b^2) quad
sqrt(q_1^2) $
----
+--- math-root-precomposed ---
// Test precomposed vs constructed roots.
// 3 and 4 are precomposed.
$sqrt(x)$
@@ -24,21 +24,21 @@ $root(3, x)$
$root(4, x)$
$root(5, x)$
----
+--- math-root-large-body ---
// Test large bodies
$ sqrt([|x|]^2 + [|y|]^2) < [|z|] $
$ v = sqrt((1/2) / (4/5))
= root(3, (1/2/3) / (4/5/6))
= root(4, ((1/2) / (3/4)) / ((1/2) / (3/4))) $
----
+--- math-root-large-index ---
// Test large index.
$ root(2, x) quad
root(3/(2/1), x) quad
root(1/11, x) quad
root(1/2/3, 1) $
----
+--- math-root-syntax ---
// Test shorthand.
$ √2^3 = sqrt(2^3) $
$ √(x+y) quad ∛x quad ∜x $
diff --git a/tests/suite/math/size.typ b/tests/suite/math/size.typ
new file mode 100644
index 0000000000..d0d41dc9ee
--- /dev/null
+++ b/tests/suite/math/size.typ
@@ -0,0 +1,9 @@
+--- math-size ---
+// Test forcing math size
+$a/b, display(a/b), display(a)/display(b), inline(a/b), script(a/b), sscript(a/b) \
+ mono(script(a/b)), script(mono(a/b))\
+ script(a^b, cramped: #true), script(a^b, cramped: #false)$
+
+--- issue-3658-math-size ---
+$ #rect[$1/2$] $
+$#rect[$1/2$]$
diff --git a/tests/suite/math/spacing.typ b/tests/suite/math/spacing.typ
new file mode 100644
index 0000000000..2a387f9296
--- /dev/null
+++ b/tests/suite/math/spacing.typ
@@ -0,0 +1,59 @@
+// Test spacing in math formulas.
+
+--- math-spacing-basic ---
+// Test spacing cases.
+$ä, +, c, (, )$ \
+$=), (+), {times}$ \
+$⟧<⟦, abs(-), [=$ \
+$a=b, a==b$ \
+$-a, +a$ \
+$a not b$ \
+$a+b, a*b$ \
+$sum x, sum(x)$ \
+$sum product x$ \
+$f(x), zeta(x), "frac"(x)$ \
+$a+dots.c+b$
+$f(x) sin(y)$
+
+--- math-spacing-kept-spaces ---
+// Test ignored vs non-ignored spaces.
+$f (x), f(x)$ \
+$[a|b], [a | b]$ \
+$a"is"b, a "is" b$
+
+--- math-spacing-predefined ---
+// Test predefined spacings.
+$a thin b, a med b, a thick b, a quad b$ \
+$a = thin b$ \
+$a - b equiv c quad (mod 2)$
+
+--- math-spacing-set-comprehension ---
+// Test spacing for set comprehension.
+#set page(width: auto)
+$ { x in RR | x "is natural" and x < 10 } $
+
+--- math-spacing-decorated ---
+// Test spacing for operators with decorations and modifiers on them
+#set page(width: auto)
+$a equiv b + c - d => e log 5 op("ln") 6$ \
+$a cancel(equiv) b overline(+) c arrow(-) d hat(=>) e cancel(log) 5 dot(op("ln")) 6$ \
+$a overbrace(equiv) b underline(+) c grave(-) d underbracket(=>) e circle(log) 5 caron(op("ln")) 6$ \
+\
+$a attach(equiv, tl: a, tr: b) b attach(limits(+), t: a, b: b) c tilde(-) d breve(=>) e attach(limits(log), t: a, b: b) 5 attach(op("ln"), tr: a, bl: b) 6$
+
+--- math-spacing-weak ---
+// Test weak spacing
+$integral f(x) dif x$,
+// Not weak
+$integral f(x) thin dif x$,
+// Both are weak, collide
+$integral f(x) #h(0.166em, weak: true)dif x$
+
+--- issue-1052-math-number-spacing ---
+// Test spacing after numbers in math.
+$
+10degree \
+10 degree \
+10.1degree \
+10.1 degree
+$
diff --git a/tests/suite/math/style.typ b/tests/suite/math/style.typ
new file mode 100644
index 0000000000..09ddd3c159
--- /dev/null
+++ b/tests/suite/math/style.typ
@@ -0,0 +1,34 @@
+// Test text styling in math.
+
+--- math-style-italic-default ---
+// Test italic defaults.
+$a, A, delta, ϵ, diff, Delta, ϴ$
+
+--- math-style ---
+// Test forcing a specific style.
+$A, italic(A), upright(A), bold(A), bold(upright(A)), \
+ serif(A), sans(A), cal(A), frak(A), mono(A), bb(A), \
+ italic(diff), upright(diff), \
+ bb("hello") + bold(cal("world")), \
+ mono("SQRT")(x) wreath mono(123 + 456)$
+
+--- math-style-exceptions ---
+// Test a few style exceptions.
+$h, bb(N), cal(R), Theta, italic(Theta), sans(Theta), sans(italic(Theta)) \
+ bb(d), bb(italic(d)), italic(bb(d)), bb(e), bb(italic(e)), italic(bb(e)) \
+ bb(i), bb(italic(i)), italic(bb(i)), bb(j), bb(italic(j)), italic(bb(j)) \
+ bb(D), bb(italic(D)), italic(bb(D))$
+
+--- math-style-greek-exceptions ---
+// Test a few greek exceptions.
+$bb(Gamma) , bb(gamma), bb(Pi), bb(pi), bb(sum)$
+
+--- math-style-hebrew-exceptions ---
+// Test hebrew exceptions.
+$aleph, beth, gimel, daleth$
+
+--- issue-3650-italic-equation ---
+_abc $sin(x) "abc"$_ \
+$italic(sin(x) "abc" #box[abc])$ \
+*abc $sin(x) "abc"$* \
+$bold(sin(x) "abc" #box[abc])$ \
diff --git a/tests/suite/math/syntax.typ b/tests/suite/math/syntax.typ
new file mode 100644
index 0000000000..cd1124c37d
--- /dev/null
+++ b/tests/suite/math/syntax.typ
@@ -0,0 +1,28 @@
+// Test math syntax.
+
+--- math-unicode ---
+// Test Unicode math.
+$ ∑_(i=0)^ℕ a ∘ b = \u{2211}_(i=0)^NN a compose b $
+
+--- math-shorthands ---
+// Test a few shorthands.
+$ underline(f' : NN -> RR) \
+ n |-> cases(
+ [|1|] &"if" n >>> 10,
+ 2 * 3 &"if" n != 5,
+ 1 - 0 thick &...,
+ ) $
+
+--- math-common-symbols ---
+// Test common symbols.
+$ dot \ dots \ ast \ tilde \ star $
+
+--- issue-2044-invalid-parsed-ident ---
+// In this bug, the dot at the end was causing the right parenthesis to be
+// parsed as an identifier instead of the closing right parenthesis.
+$floor(phi.alt.)$
+$floor(phi.alt. )$
+
+--- math-unclosed ---
+// Error: 1-2 unclosed delimiter
+$a
diff --git a/tests/suite/math/text.typ b/tests/suite/math/text.typ
new file mode 100644
index 0000000000..760910f4d6
--- /dev/null
+++ b/tests/suite/math/text.typ
@@ -0,0 +1,45 @@
+// Test that setting font features in math.equation has an effect.
+
+--- math-font-fallback ---
+// Test font fallback.
+$ よ and 🏳️🌈 $
+
+--- math-text-color ---
+// Test text properties.
+$text(#red, "time"^2) + sqrt("place")$
+
+--- math-font-features ---
+$ nothing $
+$ "hi ∅ hey" $
+$ sum_(i in NN) 1 + i $
+#show math.equation: set text(features: ("cv01",), fallback: false)
+$ nothing $
+$ "hi ∅ hey" $
+$ sum_(i in NN) 1 + i $
+
+--- math-optical-size-nested-scripts ---
+// Test transition from script to scriptscript.
+#[
+#set text(size:20pt)
+$ e^(e^(e^(e))) $
+]
+A large number: $e^(e^(e^(e)))$.
+
+--- math-optical-size-primes ---
+// Test prime/double prime via scriptsize
+#let prime = [ \u{2032} ]
+#let dprime = [ \u{2033} ]
+#let tprime = [ \u{2034} ]
+$ y^dprime-2y^prime + y = 0 $
+$y^dprime-2y^prime + y = 0$
+$ y^tprime_3 + g^(prime 2) $
+
+--- math-optical-size-prime-large-operator ---
+// Test prime superscript on large symbol
+$ scripts(sum_(k in NN))^prime 1/k^2 $
+$sum_(k in NN)^prime 1/k^2$
+
+--- math-optical-size-frac-script-script ---
+// Test script-script in a fraction.
+$ 1/(x^A) $
+#[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$]
diff --git a/tests/typ/math/underover.typ b/tests/suite/math/underover.typ
similarity index 78%
rename from tests/typ/math/underover.typ
rename to tests/suite/math/underover.typ
index f0f6730838..0768bf737d 100644
--- a/tests/typ/math/underover.typ
+++ b/tests/suite/math/underover.typ
@@ -1,20 +1,20 @@
// Test under/over things.
----
+--- math-underover-brace ---
// Test braces.
$ x = underbrace(
1 + 2 + ... + 5,
underbrace("numbers", x + y)
) $
----
+--- math-underover-line-bracket ---
// Test lines and brackets.
$ x = overbracket(
overline(underline(x + y)),
1 + 2 + ... + 5,
) $
----
+--- math-underover-brackets ---
// Test brackets.
$ underbracket([1, 2/3], "relevant stuff")
arrow.l.r.double.long
diff --git a/tests/suite/math/vec.typ b/tests/suite/math/vec.typ
new file mode 100644
index 0000000000..312c0ee45b
--- /dev/null
+++ b/tests/suite/math/vec.typ
@@ -0,0 +1,27 @@
+// Test vectors.
+
+--- math-vec-gap ---
+#set math.vec(gap: 1em)
+$ vec(1, 2) $
+
+
+--- math-vec-align-explicit-alternating ---
+// Test alternating alignment in a vector.
+$ vec(
+ "a" & "a a a" & "a a",
+ "a a" & "a a" & "a",
+ "a a a" & "a" & "a a a",
+) $
+
+--- math-vec-wide ---
+// Test wide cell.
+$ v = vec(1, 2+3, 4) $
+
+--- math-vec-delim-set ---
+// Test alternative delimiter.
+#set math.vec(delim: "[")
+$ vec(1, 2) $
+
+--- math-vec-delim-invalid ---
+// Error: 22-25 expected "(", "[", "{", "|", "||", or none
+#set math.vec(delim: "%")
diff --git a/tests/suite/model/bibliography.typ b/tests/suite/model/bibliography.typ
new file mode 100644
index 0000000000..7197082fbe
--- /dev/null
+++ b/tests/suite/model/bibliography.typ
@@ -0,0 +1,55 @@
+// Test citations and bibliographies.
+
+--- bibliography-basic ---
+#set page(width: 200pt)
+
+= Details
+See also @arrgh #cite(, supplement: [p.~22]), @arrgh[p.~4], and @distress[p.~5].
+#bibliography("/assets/bib/works.bib")
+
+--- bibliography-before-content ---
+// Test unconventional order.
+#set page(width: 200pt)
+#bibliography(
+ "/assets/bib/works.bib",
+ title: [Works to be cited],
+ style: "chicago-author-date",
+)
+#line(length: 100%)
+
+As described by #cite(, form: "prose"),
+the net-work is a creature of its own.
+This is close to piratery! @arrgh
+And quark! @quark
+
+--- bibliography-multiple-files ---
+#set page(width: 200pt)
+#set heading(numbering: "1.")
+#show bibliography: set heading(numbering: "1.")
+
+= Multiple Bibs
+Now we have multiple bibliographies containing @glacier-melt @keshav2007read
+#bibliography(("/assets/bib/works.bib", "/assets/bib/works_too.bib"))
+
+--- bibliography-duplicate-key ---
+// Error: 15-65 duplicate bibliography keys: netwok, issue201, arrgh, quark, distress, glacier-melt, tolkien54, DBLP:books/lib/Knuth86a, sharing, restful, mcintosh_anxiety, psychology25
+#bibliography(("/assets/bib/works.bib", "/assets/bib/works.bib"))
+
+--- bibliography-ordering ---
+#set page(width: 300pt)
+
+@mcintosh_anxiety
+@psychology25
+
+#bibliography("/assets/bib/works.bib")
+
+--- bibliography-full ---
+// LARGE
+#set page(paper: "a6", height: 170mm)
+#bibliography("/assets/bib/works.bib", full: true)
+
+--- bibliography-math ---
+#set page(width: 200pt)
+
+@Zee04
+#bibliography("/assets/bib/works_too.bib", style: "mla")
diff --git a/tests/suite/model/cite.typ b/tests/suite/model/cite.typ
new file mode 100644
index 0000000000..24d6ad0478
--- /dev/null
+++ b/tests/suite/model/cite.typ
@@ -0,0 +1,116 @@
+--- cite-footnote ---
+Hello @netwok
+And again: @netwok
+
+#pagebreak()
+#bibliography("/assets/bib/works.bib", style: "chicago-notes")
+
+--- cite-form ---
+#set page(width: 200pt)
+
+Nothing: #cite(, form: none)
+
+#cite(, form: "prose") say stuff.
+
+#bibliography("/assets/bib/works.bib", style: "apa")
+
+--- cite-group ---
+A#[@netwok@arrgh]B \
+A@netwok@arrgh B \
+A@netwok @arrgh B \
+A@netwok @arrgh. B \
+
+A @netwok#[@arrgh]B \
+A @netwok@arrgh, B \
+A @netwok @arrgh, B \
+A @netwok @arrgh. B \
+
+A#[@netwok @arrgh @quark]B. \
+A @netwok @arrgh @quark B. \
+A @netwok @arrgh @quark, B.
+
+#set text(0pt)
+#bibliography("/assets/bib/works.bib")
+
+--- cite-grouping-and-ordering ---
+@mcintosh_anxiety
+@psychology25
+@netwok
+@issue201
+@arrgh
+@quark
+@distress,
+@glacier-melt
+@issue201
+@tolkien54
+@sharing
+@restful
+
+#show bibliography: none
+#bibliography("/assets/bib/works.bib")
+
+--- issue-785-cite-locate ---
+// Test citation in other introspection.
+#set page(width: 180pt)
+#set heading(numbering: "1")
+
+#outline(
+ title: [List of Figures],
+ target: figure.where(kind: image),
+)
+
+#pagebreak()
+
+= Introduction
+#figure(
+ rect[-- PIRATE --],
+ caption: [A pirate @arrgh in @intro],
+)
+
+#context [Citation @distress on page #here().page()]
+
+#pagebreak()
+#bibliography("/assets/bib/works.bib", style: "chicago-notes")
+
+--- issue-1597-cite-footnote ---
+// Tests that when a citation footnote is pushed to next page, things still
+// work as expected.
+#set page(height: 60pt)
+#lorem(4)
+
+#footnote[@netwok]
+#show bibliography: none
+#bibliography("/assets/bib/works.bib")
+
+--- issue-2531-cite-show-set ---
+// Test show set rules on citations.
+#show cite: set text(red)
+A @netwok @arrgh.
+B #cite() #cite().
+
+#show bibliography: none
+#bibliography("/assets/bib/works.bib")
+
+--- issue-3481-cite-location ---
+// The locator was cloned in the wrong location, leading to inconsistent
+// citation group locations in the second footnote attempt.
+#set page(height: 60pt)
+
+// First page shouldn't be empty because otherwise we won't skip the first
+// region which causes the bug in the first place.
+#v(10pt)
+
+// Everything moves to the second page because we want to keep the line and
+// its footnotes together.
+#footnote[@netwok]
+#footnote[A]
+
+#show bibliography: none
+#bibliography("/assets/bib/works.bib")
+
+--- issue-3699-cite-twice-et-al ---
+// Citing a second time showed all authors instead of "et al".
+@mcintosh_anxiety \
+@mcintosh_anxiety
+#show bibliography: none
+#bibliography("/assets/bib/works.bib", style: "chicago-author-date")
diff --git a/tests/suite/model/document.typ b/tests/suite/model/document.typ
new file mode 100644
index 0000000000..6f8e713154
--- /dev/null
+++ b/tests/suite/model/document.typ
@@ -0,0 +1,36 @@
+// Test document and page-level styles.
+
+--- document-set-title ---
+// This is okay.
+#set document(title: [Hello])
+What's up?
+
+--- document-set-author-date ---
+// This, too.
+#set document(author: ("A", "B"), date: datetime.today())
+
+--- document-date-bad ---
+// Error: 21-28 expected datetime, none, or auto, found string
+#set document(date: "today")
+
+--- document-author-bad ---
+// This, too.
+// Error: 23-29 expected string, found integer
+#set document(author: (123,))
+What's up?
+
+--- document-set-after-content ---
+Hello
+
+// Error: 2-30 document set rules must appear before any content
+#set document(title: [Hello])
+
+--- document-constructor ---
+// Error: 2-12 can only be used in set rules
+#document()
+
+--- document-set-in-container ---
+#box[
+ // Error: 4-32 document set rules are not allowed inside of containers
+ #set document(title: [Hello])
+]
diff --git a/tests/suite/model/emph-strong.typ b/tests/suite/model/emph-strong.typ
new file mode 100644
index 0000000000..2af8bb1609
--- /dev/null
+++ b/tests/suite/model/emph-strong.typ
@@ -0,0 +1,74 @@
+// Test emph and strong.
+
+--- emph-syntax ---
+// Basic.
+_Emphasized and *strong* words!_
+
+// Inside of a word it's a normal underscore or star.
+hello_world Nutzer*innen
+
+// CJK characters will not need spaces.
+中文一般使用*粗体*或者_楷体_来表示强调。
+
+日本語では、*太字*や_斜体_を使って強調します。
+
+中文中混有*Strong*和_Empasis_。
+
+// Can contain paragraph in nested content block.
+_Still #[
+
+] emphasized._
+
+--- emph-and-strong-call-in-word ---
+// Inside of words can still use the functions.
+P#strong[art]ly em#emph[phas]ized.
+
+--- emph-empty-hint ---
+// Warning: 1-3 no text within underscores
+// Hint: 1-3 using multiple consecutive underscores (e.g. __) has no additional effect
+__
+
+--- emph-double-underscore-empty-hint ---
+// Warning: 1-3 no text within underscores
+// Hint: 1-3 using multiple consecutive underscores (e.g. __) has no additional effect
+// Warning: 13-15 no text within underscores
+// Hint: 13-15 using multiple consecutive underscores (e.g. __) has no additional effect
+__not italic__
+
+--- emph-unclosed ---
+// Error: 6-7 unclosed delimiter
+#box[_Scoped] to body.
+
+--- emph-ends-at-parbreak ---
+// Ends at paragraph break.
+// Error: 1-2 unclosed delimiter
+_Hello
+
+World
+
+--- emph-strong-unclosed-nested ---
+// Error: 11-12 unclosed delimiter
+// Error: 3-4 unclosed delimiter
+#[_Cannot *be interleaved]
+
+--- strong-delta ---
+// Adjusting the delta that strong applies on the weight.
+Normal
+
+#set strong(delta: 300)
+*Bold*
+
+#set strong(delta: 150)
+*Medium* and *#[*Bold*]*
+
+--- strong-empty-hint ---
+// Warning: 1-3 no text within stars
+// Hint: 1-3 using multiple consecutive stars (e.g. **) has no additional effect
+**
+
+--- strong-double-star-empty-hint ---
+// Warning: 1-3 no text within stars
+// Hint: 1-3 using multiple consecutive stars (e.g. **) has no additional effect
+// Warning: 11-13 no text within stars
+// Hint: 11-13 using multiple consecutive stars (e.g. **) has no additional effect
+**not bold**
diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ
new file mode 100644
index 0000000000..57a4d7a68a
--- /dev/null
+++ b/tests/suite/model/enum.typ
@@ -0,0 +1,156 @@
+// Test enumerations.
+
+--- enum-function-call ---
+#enum[Embrace][Extend][Extinguish]
+
+--- enum-number-override-nested ---
+0. Before first!
+1. First.
+ 2. Indented
+
++ Second
+
+--- enum-built-in-loop ---
+// Test automatic numbering in summed content.
+#for i in range(5) {
+ [+ #numbering("I", 1 + i)]
+}
+
+--- list-mix ---
+// Mix of different lists
+- Bullet List
++ Numbered List
+/ Term: List
+
+--- enum-syntax-at-start ---
+// In the line.
+1.2 \
+This is 0. \
+See 0.3. \
+
+--- enum-syntax-edge-cases ---
+// Edge cases.
++
+Empty \
++Nope \
+a + 0.
+
+--- enum-number-override ---
+// Test item number overriding.
+1. first
++ second
+5. fifth
+
+#enum(
+ enum.item(1)[First],
+ [Second],
+ enum.item(5)[Fifth]
+)
+
+--- enum-numbering-pattern ---
+// Test numbering pattern.
+#set enum(numbering: "(1.a.*)")
++ First
++ Second
+ 2. Nested
+ + Deep
++ Normal
+
+--- enum-numbering-full ---
+// Test full numbering.
+#set enum(numbering: "1.a.", full: true)
++ First
+ + Nested
+
+--- enum-numbering-closure ---
+// Test numbering with closure.
+#enum(
+ start: 3,
+ spacing: 0.65em - 3pt,
+ tight: false,
+ numbering: n => text(
+ fill: (red, green, blue).at(calc.rem(n, 3)),
+ numbering("A", n),
+ ),
+ [Red], [Green], [Blue], [Red],
+)
+
+--- enum-numbering-closure-nested ---
+// Test numbering with closure and nested lists.
+#set enum(numbering: n => super[#n])
++ A
+ + B
++ C
+
+--- enum-numbering-closure-nested-complex ---
+// Test numbering with closure and nested lists.
+#set text(font: "New Computer Modern")
+#set enum(numbering: (..args) => math.mat(args.pos()), full: true)
++ A
+ + B
+ + C
+ + D
++ E
++ F
+
+--- enum-numbering-pattern-empty ---
+// Error: 22-24 invalid numbering pattern
+#set enum(numbering: "")
+
+--- enum-numbering-pattern-invalid ---
+// Error: 22-28 invalid numbering pattern
+#set enum(numbering: "(())")
+
+--- enum-number-align-unaffected ---
+// Alignment shouldn't affect number
+#set align(horizon)
+
++ ABCDEF\ GHIJKL\ MNOPQR
+ + INNER\ INNER\ INNER
++ BACK\ HERE
+
+--- enum-number-align-default ---
+// Enum number alignment should be 'end' by default
+1. a
+10. b
+100. c
+
+--- enum-number-align-specified ---
+#set enum(number-align: start)
+1. a
+8. b
+16. c
+
+--- enum-number-align-2d ---
+#set enum(number-align: center + horizon)
+1. #box(fill: teal, inset: 10pt )[a]
+8. #box(fill: teal, inset: 10pt )[b]
+16. #box(fill: teal,inset: 10pt )[c]
+
+--- enum-number-align-unfolded ---
+// Number align option should not be affected by the context.
+#set align(center)
+#set enum(number-align: start)
+
+4. c
+8. d
+16. e\ f
+ 2. f\ g
+ 32. g
+ 64. h
+
+--- enum-number-align-values ---
+// Test valid number align values (horizontal and vertical)
+#set enum(number-align: start)
+#set enum(number-align: end)
+#set enum(number-align: left)
+#set enum(number-align: center)
+#set enum(number-align: right)
+#set enum(number-align: top)
+#set enum(number-align: horizon)
+#set enum(number-align: bottom)
+
+--- issue-2530-enum-item-panic ---
+// Enum item (pre-emptive)
+#enum.item(none)[Hello]
+#enum.item(17)[Hello]
diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ
new file mode 100644
index 0000000000..6846760f1e
--- /dev/null
+++ b/tests/suite/model/figure.typ
@@ -0,0 +1,220 @@
+// Test figures.
+
+--- figure-basic ---
+#set page(width: 150pt)
+#set figure(numbering: "I")
+
+We can clearly see that @fig-cylinder and
+@tab-complex are relevant in this context.
+
+#figure(
+ table(columns: 2)[a][b],
+ caption: [The basic table.],
+)
+
+#figure(
+ pad(y: -6pt, image("/assets/images/cylinder.svg", height: 2cm)),
+ caption: [The basic shapes.],
+ numbering: "I",
+)
+
+#figure(
+ table(columns: 3)[a][b][c][d][e][f],
+ caption: [The complex table.],
+)
+
+--- figure-table ---
+// Testing figures with tables.
+#figure(
+ table(
+ columns: 2,
+ [Second cylinder],
+ image("/assets/images/cylinder.svg"),
+ ),
+ caption: "A table containing images."
+)
+
+--- figure-theorem ---
+// Testing show rules with figures with a simple theorem display
+#show figure.where(kind: "theorem"): it => {
+ let name = none
+ if not it.caption == none {
+ name = [ #emph(it.caption.body)]
+ } else {
+ name = []
+ }
+
+ let title = none
+ if not it.numbering == none {
+ title = it.supplement
+ if not it.numbering == none {
+ title += " " + it.counter.display(it.numbering)
+ }
+ }
+ title = strong(title)
+ pad(
+ top: 0em, bottom: 0em,
+ block(
+ fill: green.lighten(90%),
+ stroke: 1pt + green,
+ inset: 10pt,
+ width: 100%,
+ radius: 5pt,
+ breakable: false,
+ [#title#name#h(0.1em):#h(0.2em)#it.body#v(0.5em)]
+ )
+ )
+}
+
+#set page(width: 150pt)
+#figure(
+ $a^2 + b^2 = c^2$,
+ supplement: "Theorem",
+ kind: "theorem",
+ caption: "Pythagoras' theorem.",
+ numbering: "1",
+)
+
+#figure(
+ $a^2 + b^2 = c^2$,
+ supplement: "Theorem",
+ kind: "theorem",
+ caption: "Another Pythagoras' theorem.",
+ numbering: none,
+)
+
+#figure(
+ ```rust
+ fn main() {
+ println!("Hello!");
+ }
+ ```,
+ caption: [Hello world in _rust_],
+)
+
+--- figure-breakable ---
+// Test breakable figures
+#set page(height: 6em)
+#show figure: set block(breakable: true)
+
+#figure(table[a][b][c][d][e], caption: [A table])
+
+--- figure-caption-separator ---
+// Test custom separator for figure caption
+#set figure.caption(separator: [ --- ])
+
+#figure(
+ table(columns: 2)[a][b],
+ caption: [The table with custom separator.],
+)
+
+--- figure-caption-show ---
+// Test figure.caption element
+#show figure.caption: emph
+
+#figure(
+ [Not italicized],
+ caption: [Italicized],
+)
+
+--- figure-caption-where-selector ---
+// Test figure.caption element for specific figure kinds
+#show figure.caption.where(kind: table): underline
+
+#figure(
+ [Not a table],
+ caption: [Not underlined],
+)
+
+#figure(
+ table[A table],
+ caption: [Underlined],
+)
+
+--- figure-and-caption-show ---
+// Test creating custom figure and custom caption
+
+#let gap = 0.7em
+#show figure.where(kind: "custom"): it => rect(inset: gap, {
+ align(center, it.body)
+ v(gap, weak: true)
+ line(length: 100%)
+ v(gap, weak: true)
+ align(center, it.caption)
+})
+
+#figure(
+ [A figure],
+ kind: "custom",
+ caption: [Hi],
+ supplement: [A],
+)
+
+#show figure.caption: it => emph[
+ #it.body
+ (#it.supplement
+ #context it.counter.display(it.numbering))
+]
+
+#figure(
+ [Another figure],
+ kind: "custom",
+ caption: [Hi],
+ supplement: [B],
+)
+
+--- figure-caption-position ---
+#set figure.caption(position: top)
+
+--- figure-caption-position-bad ---
+// Error: 31-38 expected `top` or `bottom`, found horizon
+#set figure.caption(position: horizon)
+
+--- figure-localization-fr ---
+// Test French
+#set text(lang: "fr")
+#figure(
+ circle(),
+ caption: [Un cercle.],
+)
+
+--- figure-localization-zh ---
+// Test Chinese
+#set text(lang: "zh")
+#figure(
+ rect(),
+ caption: [一个矩形],
+)
+
+--- figure-localization-ru ---
+// Test Russian
+#set text(lang: "ru")
+
+#figure(
+ polygon.regular(size: 1cm, vertices: 8),
+ caption: [Пятиугольник],
+)
+
+--- figure-localization-gr ---
+// Test Greek
+#set text(lang: "gr")
+#figure(
+ circle(),
+ caption: [Ένας κύκλος.],
+)
+
+--- issue-2165-figure-caption-panic ---
+#figure.caption[]
+
+--- issue-2328-figure-entry-panic ---
+// Error: 4-43 footnote entry must have a location
+// Hint: 4-43 try using a query or a show rule to customize the footnote instead
+HI#footnote.entry(clearance: 2.5em)[There]
+
+--- issue-2530-figure-caption-panic ---
+#figure(caption: [test])[].caption
+
+--- issue-3586-figure-caption-separator ---
+// Test that figure caption separator is synthesized correctly.
+#show figure.caption: c => test(c.separator, [#": "])
+#figure(table[], caption: [This is a test caption])
diff --git a/tests/suite/model/footnote.typ b/tests/suite/model/footnote.typ
new file mode 100644
index 0000000000..34450ca4c7
--- /dev/null
+++ b/tests/suite/model/footnote.typ
@@ -0,0 +1,189 @@
+// Test footnotes.
+
+--- footnote-basic ---
+#footnote[Hi]
+
+--- footnote-space-collapsing ---
+// Test space collapsing before footnote.
+A#footnote[A] \
+A #footnote[A]
+
+--- footnote-nested ---
+// Test nested footnotes.
+First \
+Second #footnote[A, #footnote[B, #footnote[C]]] \
+Third #footnote[D, #footnote[E]] \
+Fourth
+
+--- footnote-nested-same-frame ---
+// Currently, numbers a bit out of order if a nested footnote ends up in the
+// same frame as another one. :(
+#footnote[A, #footnote[B]], #footnote[C]
+
+--- footnote-entry ---
+// Test customization.
+#show footnote: set text(red)
+#show footnote.entry: set text(8pt, style: "italic")
+#set footnote.entry(
+ indent: 0pt,
+ gap: 0.6em,
+ clearance: 0.3em,
+ separator: repeat[.],
+)
+
+Beautiful footnotes. #footnote[Wonderful, aren't they?]
+
+--- footnote-break-across-pages ---
+// LARGE
+#set page(height: 200pt)
+
+#lorem(5)
+#footnote[ // 1
+ A simple footnote.
+ #footnote[Well, not that simple ...] // 2
+]
+#lorem(15)
+#footnote[Another footnote: #lorem(30)] // 3
+#lorem(15)
+#footnote[My fourth footnote: #lorem(50)] // 4
+#lorem(15)
+#footnote[And a final footnote.] // 5
+
+--- footnote-in-columns ---
+// Test footnotes in columns, even those that are not enabled via `set page`.
+#set page(height: 120pt)
+#align(center, strong[Title])
+#show: columns.with(2)
+#lorem(3) #footnote(lorem(6))
+Hello there #footnote(lorem(2))
+
+--- footnote-in-caption ---
+// Test footnote in caption.
+Read the docs #footnote[https://typst.app/docs]!
+#figure(
+ image("/assets/images/graph.png", width: 70%),
+ caption: [
+ A graph #footnote[A _graph_ is a structure with nodes and edges.]
+ ]
+)
+More #footnote[just for ...] footnotes #footnote[... testing. :)]
+
+--- footnote-duplicate ---
+// Test duplicate footnotes.
+#let lang = footnote[Languages.]
+#let nums = footnote[Numbers.]
+
+/ "Hello": A word #lang
+/ "123": A number #nums
+
+- "Hello" #lang
+- "123" #nums
+
++ "Hello" #lang
++ "123" #nums
+
+#table(
+ columns: 2,
+ [Hello], [A word #lang],
+ [123], [A number #nums],
+)
+
+--- footnote-invariant ---
+// Ensure that a footnote and the first line of its entry
+// always end up on the same page.
+#set page(height: 120pt)
+
+#lorem(13)
+
+There #footnote(lorem(20))
+
+--- footnote-ref ---
+// Test references to footnotes.
+A footnote #footnote[Hi] \
+A reference to it @fn
+
+--- footnote-ref-multiple ---
+// Multiple footnotes are refs
+First #footnote[A] \
+Second #footnote[B] \
+First ref @fn1 \
+Third #footnote[C] \
+Fourth #footnote[D] \
+Fourth ref @fn4 \
+Second ref @fn2 \
+Second ref again @fn2
+
+--- footnote-ref-forward ---
+// Forward reference
+Usage @fn \
+Definition #footnote[Hi]
+
+--- footnote-ref-in-footnote ---
+// Footnote ref in footnote
+#footnote[Reference to next @fn]
+#footnote[Reference to myself @fn]
+#footnote[Reference to previous @fn]
+
+--- footnote-styling ---
+// Styling
+#show footnote: text.with(fill: red)
+Real #footnote[...] \
+Ref @fn
+
+--- footnote-ref-call ---
+// Footnote call with label
+#footnote()
+#footnote[Hi]
+#ref()
+#footnote()
+
+--- footnote-in-table ---
+// Test footnotes in tables. When the table spans multiple pages, the footnotes
+// will all be after the table, but it shouldn't create any empty pages.
+#set page(height: 100pt)
+
+= Tables
+#table(
+ columns: 2,
+ [Hello footnote #footnote[This is a footnote.]],
+ [This is more text],
+ [This cell
+ #footnote[This footnote is not on the same page]
+ breaks over multiple pages.],
+ image("/assets/images/tiger.jpg"),
+)
+
+#table(
+ columns: 3,
+ ..range(1, 10)
+ .map(numbering.with("a"))
+ .map(v => upper(v) + footnote(v))
+)
+
+--- issue-multiple-footnote-in-one-line ---
+// Test that the logic that keeps footnote entry together with
+// their markers also works for multiple footnotes in a single
+// line or frame (here, there are two lines, but they are one
+// unit due to orphan prevention).
+#set page(height: 100pt)
+#v(40pt)
+A #footnote[a] \
+B #footnote[b]
+
+--- issue-1433-footnote-in-list ---
+// Test that footnotes in lists do not produce extraneous page breaks. The list
+// layout itself does not currently react to the footnotes layout, weakening the
+// "footnote and its entry are on the same page" invariant somewhat, but at
+// least there shouldn't be extra page breaks.
+#set page(height: 100pt)
+#block(height: 50pt, width: 100%, fill: aqua)
+
+- #footnote[1]
+- #footnote[2]
+
+--- issue-footnotes-skip-first-page ---
+// In this issue, we would get an empty page at the beginning because footnote
+// layout didn't properly check for in_last.
+#set page(height: 50pt)
+#footnote[A]
+#footnote[B]
diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ
new file mode 100644
index 0000000000..5d50eeeec8
--- /dev/null
+++ b/tests/suite/model/heading.typ
@@ -0,0 +1,80 @@
+// Test headings.
+
+--- heading-basic ---
+// Different number of equals signs.
+
+= Level 1
+== Level 2
+=== Level 3
+
+// After three, it stops shrinking.
+=========== Level 11
+
+--- heading-syntax-at-start ---
+// Heading vs. no heading.
+
+// Parsed as headings if at start of the context.
+/**/ = Level 1
+#[== Level 2]
+#box[=== Level 3]
+
+// Not at the start of the context.
+No = heading
+
+// Escaped.
+\= No heading
+
+--- heading-block ---
+// Blocks can continue the heading.
+
+= #[This
+is
+multiline.
+]
+
+= This
+ is not.
+
+--- heading-show-where ---
+// Test styling.
+#show heading.where(level: 5): it => block(
+ text(font: "Roboto", fill: eastern, it.body + [!])
+)
+
+= Heading
+===== Heading 🌍
+#heading(level: 5)[Heading]
+
+--- heading-offset ---
+// Test setting the starting offset.
+#set heading(numbering: "1.1")
+#show heading.where(level: 2): set text(blue)
+= Level 1
+
+#heading(depth: 1)[We're twins]
+#heading(level: 1)[We're twins]
+
+== Real level 2
+
+#set heading(offset: 1)
+= Fake level 2
+== Fake level 3
+
+--- heading-offset-and-level ---
+// Passing level directly still overrides all other set values
+#set heading(numbering: "1.1", offset: 1)
+#heading(level: 1)[Still level 1]
+
+--- heading-syntax-edge-cases ---
+// Edge cases.
+#set heading(numbering: "1.")
+=
+Not in heading
+=Nope
+
+--- heading-numbering-hint ---
+= Heading
+
+// Error: 1:20-1:26 cannot reference heading without numbering
+// Hint: 1:20-1:26 you can enable heading numbering with `#set heading(numbering: "1.")`
+Can not be used as @intro
diff --git a/tests/typ/meta/link.typ b/tests/suite/model/link.typ
similarity index 84%
rename from tests/typ/meta/link.typ
rename to tests/suite/model/link.typ
index dd5bffa889..27afd53c1f 100644
--- a/tests/typ/meta/link.typ
+++ b/tests/suite/model/link.typ
@@ -1,6 +1,6 @@
// Test hyperlinking.
----
+--- link-basic ---
// Link syntax.
https://example.com/
@@ -14,7 +14,7 @@ This link appears #link("https://google.com/")[in the middle of] a paragraph.
Contact #link("mailto:hi@typst.app") or
call #link("tel:123") for more information.
----
+--- link-trailing-period ---
// Test that the period is trimmed.
#show link: underline
https://a.b.?q=%10#. \
@@ -22,55 +22,55 @@ Wahttp://link \
Nohttps:\//link \
Nohttp\://comment
----
+--- link-bracket-balanced ---
// Verify that brackets are included in links.
https://[::1]:8080/ \
https://example.com/(paren) \
https://example.com/#(((nested))) \
----
+--- link-bracket-unbalanced-closing ---
// Check that unbalanced brackets are not included in links.
#[https://example.com/] \
https://example.com/)
----
+--- link-bracket-unbalanced-opening ---
// Verify that opening brackets without closing brackets throw an error.
// Error: 1-22 automatic links cannot contain unbalanced brackets, use the `link` function instead
https://exam(ple.com/
----
+--- link-show ---
// Styled with underline and color.
#show link: it => underline(text(fill: rgb("283663"), it))
You could also make the
#link("https://html5zombo.com/")[link look way more typical.]
----
+--- link-transformed ---
// Transformed link.
#set page(height: 60pt)
#let mylink = link("https://typst.org/")[LINK]
My cool #box(move(dx: 0.7cm, dy: 0.7cm, rotate(10deg, scale(200%, mylink))))
----
+--- link-on-block ---
// Link containing a block.
#link("https://example.com/", block[
My cool rhino
#box(move(dx: 10pt, image("/assets/images/rhino.png", width: 1cm)))
])
----
+--- link-to-page ---
// Link to page one.
#link((page: 1, x: 10pt, y: 20pt))[Back to the start]
----
+--- link-to-label ---
// Test link to label.
Text
#link()[Go to text.]
----
+--- link-to-label-missing ---
// Error: 2-20 label `` does not exist in the document
#link()[Nope.]
----
+--- link-to-label-duplicate ---
Text
Text
// Error: 2-20 label `` occurs multiple times in the document
diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ
new file mode 100644
index 0000000000..e37fa65dbe
--- /dev/null
+++ b/tests/suite/model/list.typ
@@ -0,0 +1,147 @@
+// Test bullet lists.
+
+--- list-basic ---
+_Shopping list_
+#list[Apples][Potatoes][Juice]
+
+--- list-nested ---
+- First level.
+
+ - Second level.
+ There are multiple paragraphs.
+
+ - Third level.
+
+ Still the same bullet point.
+
+ - Still level 2.
+
+- At the top.
+
+--- list-content-block ---
+- Level 1
+ - Level #[
+2 through content block
+]
+
+--- list-top-level-indent ---
+ - Top-level indent
+- is fine.
+
+--- list-indent-specifics ---
+ - A
+ - B
+ - C
+- D
+
+--- list-tabs ---
+// This works because tabs are used consistently.
+ - A with 1 tab
+ - B with 2 tabs
+
+--- list-mixed-tabs-and-spaces ---
+// This doesn't work because of mixed tabs and spaces.
+ - A with 2 spaces
+ - B with 2 tabs
+
+--- list-syntax-edge-cases ---
+// Edge cases.
+-
+Not in list
+-Nope
+
+--- list-marker-align-unaffected ---
+// Alignment shouldn't affect marker
+#set align(horizon)
+
+- ABCDEF\ GHIJKL\ MNOPQR
+
+--- list-marker-dash ---
+// Test en-dash.
+#set list(marker: [--])
+- A
+- B
+
+--- list-marker-cycle ---
+// Test that items are cycled.
+#set list(marker: ([--], [•]))
+- A
+ - B
+ - C
+
+--- list-marker-closure ---
+// Test function.
+#set list(marker: n => if n == 1 [--] else [•])
+- A
+- B
+ - C
+ - D
+ - E
+- F
+
+--- list-marker-bare-hyphen ---
+// Test that bare hyphen doesn't lead to cycles and crashes.
+#set list(marker: [-])
+- Bare hyphen is
+- a bad marker
+
+--- list-marker-array-empty ---
+// Error: 19-21 array must contain at least one marker
+#set list(marker: ())
+
+--- list-attached ---
+// Test basic attached list.
+Attached to:
+- the bottom
+- of the paragraph
+
+Next paragraph.
+
+--- list-attached-above-spacing ---
+// Test that attached list isn't affected by block spacing.
+#show list: set block(above: 100pt)
+Hello
+- A
+World
+- B
+
+--- list-non-attached-followed-by-attached ---
+// Test non-attached list followed by attached list,
+// separated by only word.
+Hello
+
+- A
+
+World
+- B
+
+--- list-tight-non-attached-tight ---
+// Test non-attached tight list.
+#set block(spacing: 15pt)
+Hello
+- A
+World
+
+- B
+- C
+
+More.
+
+--- list-wide-cannot-attach ---
+// Test that wide lists cannot be ...
+#set block(spacing: 15pt)
+Hello
+- A
+
+- B
+World
+
+--- list-wide-really-cannot-attach ---
+// ... even if forced to.
+Hello
+#list(tight: false)[A][B]
+World
+
+--- issue-2530-list-item-panic ---
+// List item (pre-emptive)
+#list.item[Hello]
diff --git a/tests/suite/model/numbering.typ b/tests/suite/model/numbering.typ
new file mode 100644
index 0000000000..c2de1e05ed
--- /dev/null
+++ b/tests/suite/model/numbering.typ
@@ -0,0 +1,103 @@
+// Test integrated numbering patterns.
+
+--- numbering-symbol-and-roman ---
+#for i in range(0, 9) {
+ numbering("*", i)
+ [ and ]
+ numbering("I.a", i, i)
+ [ for #i \ ]
+}
+
+--- numbering-latin ---
+#for i in range(0, 4) {
+ numbering("A", i)
+ [ for #i \ ]
+}
+... \
+#for i in range(26, 30) {
+ numbering("A", i)
+ [ for #i \ ]
+}
+... \
+#for i in range(702, 706) {
+ numbering("A", i)
+ [ for #i \ ]
+}
+
+--- numbering-hebrew ---
+#set text(lang: "he")
+#for i in range(9, 21, step: 2) {
+ numbering("א.", i)
+ [ עבור #i \ ]
+}
+
+--- numbering-chinese ---
+#set text(lang: "zh", font: ("Linux Libertine", "Noto Serif CJK SC"))
+#for i in range(9, 21, step: 2){
+ numbering("一", i)
+ [ and ]
+ numbering("壹", i)
+ [ for #i \ ]
+}
+
+--- numbering-japanese-iroha ---
+#set text(lang: "ja", font: ("Linux Libertine", "Noto Serif CJK JP"))
+#for i in range(0, 4) {
+ numbering("イ", i)
+ [ (or ]
+ numbering("い", i)
+ [) for #i \ ]
+}
+... \
+#for i in range(47, 51) {
+ numbering("イ", i)
+ [ (or ]
+ numbering("い", i)
+ [) for #i \ ]
+}
+... \
+#for i in range(2256, 2260) {
+ numbering("イ", i)
+ [ for #i \ ]
+}
+
+--- numbering-korean ---
+#set text(lang: "ko", font: ("Linux Libertine", "Noto Serif CJK KR"))
+#for i in range(0, 4) {
+ numbering("가", i)
+ [ (or ]
+ numbering("ㄱ", i)
+ [) for #i \ ]
+}
+... \
+#for i in range(47, 51) {
+ numbering("가", i)
+ [ (or ]
+ numbering("ㄱ", i)
+ [) for #i \ ]
+}
+... \
+#for i in range(2256, 2260) {
+ numbering("ㄱ", i)
+ [ for #i \ ]
+}
+
+--- numbering-japanese-aiueo ---
+#set text(lang: "jp", font: ("Linux Libertine", "Noto Serif CJK JP"))
+#for i in range(0, 9) {
+ numbering("あ", i)
+ [ and ]
+ numbering("I.あ", i, i)
+ [ for #i \ ]
+}
+
+#for i in range(0, 9) {
+ numbering("ア", i)
+ [ and ]
+ numbering("I.ア", i, i)
+ [ for #i \ ]
+}
+
+--- numbering-negative ---
+// Error: 17-19 number must be at least zero
+#numbering("1", -1)
diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ
new file mode 100644
index 0000000000..d8fc1a43bf
--- /dev/null
+++ b/tests/suite/model/outline.typ
@@ -0,0 +1,176 @@
+--- outline ---
+// LARGE
+#set page("a7", margin: 20pt, numbering: "1")
+#set heading(numbering: "(1/a)")
+#show heading.where(level: 1): set text(12pt)
+#show heading.where(level: 2): set text(10pt)
+#set math.equation(numbering: "1")
+
+#outline()
+#outline(title: [Figures], target: figure)
+#outline(title: [Equations], target: math.equation)
+
+= Introduction
+#lorem(12)
+
+= Analysis
+#lorem(10)
+
+#[
+ #set heading(outlined: false)
+ == Methodology
+ #lorem(6)
+]
+
+== Math
+$x$ is a very useful constant. See it in action:
+$ x = x $
+
+== Interesting figures
+#figure(rect[CENSORED], kind: image, caption: [A picture showing a programmer at work.])
+#figure(table[1x1], caption: [A very small table.])
+
+== Programming
+```rust
+fn main() {
+ panic!("in the disco");
+}
+```
+
+==== Deep Stuff
+Ok ...
+
+// Ensure 'bookmarked' option doesn't affect the outline
+#set heading(numbering: "(I)", bookmarked: false)
+
+= #text(blue)[Sum]mary
+#lorem(10)
+
+--- outline-indent-numbering ---
+// LARGE
+// With heading numbering
+#set page(width: 200pt)
+#set heading(numbering: "1.a.")
+#show heading: none
+#set outline(fill: none)
+
+#context test(outline.indent, none)
+#outline(indent: false)
+#outline(indent: true)
+#outline(indent: none)
+#outline(indent: auto)
+#outline(indent: 2em)
+#outline(indent: n => ([-], [], [==], [====]).at(n))
+
+= About ACME Corp.
+== History
+== Products
+=== Categories
+==== General
+
+--- outline-indent-no-numbering ---
+// Without heading numbering
+#set page(width: 200pt)
+#show heading: none
+#set outline(fill: none)
+
+#outline(indent: false)
+#outline(indent: true)
+#outline(indent: none)
+#outline(indent: auto)
+#outline(indent: n => 2em * n)
+
+= About
+== History
+
+--- outline-indent-bad-type ---
+// Error: 2-35 expected relative length or content, found dictionary
+#outline(indent: n => (a: "dict"))
+
+= Heading
+
+--- outline-first-line-indent ---
+#set par(first-line-indent: 1.5em)
+#set heading(numbering: "1.1.a.")
+#show outline.entry.where(level: 1): it => {
+ v(0.5em, weak: true)
+ strong(it)
+}
+
+#outline()
+
+= Introduction
+= Background
+== History
+== State of the Art
+= Analysis
+== Setup
+
+--- outline-entry ---
+#set page(width: 150pt)
+#set heading(numbering: "1.")
+
+#show outline.entry.where(
+ level: 1
+): it => {
+ v(12pt, weak: true)
+ strong(it)
+}
+
+#outline(indent: auto)
+
+#set text(8pt)
+#show heading: set block(spacing: 0.65em)
+
+= Introduction
+= Background
+== History
+== State of the Art
+= Analysis
+== Setup
+
+--- outline-entry-complex ---
+#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt))
+#set heading(numbering: "1.")
+#show outline.entry.where(level: 1): it => [
+ #let loc = it.element.location()
+ #let num = numbering(loc.page-numbering(), ..counter(page).at(loc))
+ #emph(link(loc, it.body))
+ #text(luma(100), box(width: 1fr, repeat[#it.fill.body;·]))
+ #link(loc, num)
+]
+
+#counter(page).update(3)
+#outline(indent: auto, fill: repeat[--])
+
+#set text(8pt)
+#show heading: set block(spacing: 0.65em)
+
+= Top heading
+== Not top heading
+=== Lower heading
+=== Lower too
+== Also not top
+
+#pagebreak()
+#set page(numbering: "1")
+
+= Another top heading
+== Middle heading
+=== Lower heading
+
+--- outline-bad-element ---
+// Error: 2-27 cannot outline metadata
+#outline(target: metadata)
+#metadata("hello")
+
+--- issue-2530-outline-entry-panic-text ---
+// Outline entry (pre-emptive)
+// Error: 2-48 cannot outline text
+#outline.entry(1, [Hello], [World!], none, [1])
+
+--- issue-2530-outline-entry-panic-heading ---
+// Outline entry (pre-emptive, improved error)
+// Error: 2-55 heading must have a location
+// Hint: 2-55 try using a query or a show rule to customize the outline.entry instead
+#outline.entry(1, heading[Hello], [World!], none, [1])
diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ
new file mode 100644
index 0000000000..65779f6a99
--- /dev/null
+++ b/tests/suite/model/par.typ
@@ -0,0 +1,78 @@
+// Test configuring paragraph properties.
+
+--- par-basic ---
+#set page(width: 250pt, height: 120pt)
+
+But, soft! what light through yonder window breaks? It is the east, and Juliet
+is the sun. Arise, fair sun, and kill the envious moon, Who is already sick and
+pale with grief, That thou her maid art far more fair than she: Be not her maid,
+since she is envious; Her vestal livery is but sick and green And none but fools
+do wear it; cast it off. It is my lady, O, it is my love! O, that she knew she
+were! She speaks yet she says nothing: what of that? Her eye discourses; I will
+answer it.
+
+I am too bold, 'tis not to me she speaks: Two of the fairest stars in all the
+heaven, Having some business, do entreat her eyes To twinkle in their spheres
+till they return. What if her eyes were there, they in her head? The brightness
+of her cheek would shame those stars, As daylight doth a lamp; her eyes in
+heaven Would through the airy region stream so bright That birds would sing and
+think it were not night. See, how she leans her cheek upon her hand! O, that I
+were a glove upon that hand, That I might touch that cheek!
+
+--- par-leading-and-block-spacing ---
+// Test changing leading and spacing.
+#set block(spacing: 1em)
+#set par(leading: 2pt)
+But, soft! what light through yonder window breaks?
+
+It is the east, and Juliet is the sun.
+
+--- par-first-line-indent ---
+#set par(first-line-indent: 12pt, leading: 5pt)
+#set block(spacing: 5pt)
+#show heading: set text(size: 10pt)
+
+The first paragraph has no indent.
+
+But the second one does.
+
+#box(image("/assets/images/tiger.jpg", height: 6pt))
+starts a paragraph, also with indent.
+
+#align(center, image("/assets/images/rhino.png", width: 1cm))
+
+= Headings
+- And lists.
+- Have no indent.
+
+ Except if you have another paragraph in them.
+
+#set text(8pt, lang: "ar", font: ("Noto Sans Arabic", "Linux Libertine"))
+#set par(leading: 8pt)
+
+= Arabic
+دع النص يمطر عليك
+
+ثم يصبح النص رطبًا وقابل للطرق ويبدو المستند رائعًا.
+
+--- par-spacing-and-first-line-indent ---
+// This is madness.
+#set par(first-line-indent: 12pt)
+Why would anybody ever ...
+
+... want spacing and indent?
+
+--- par-hanging-indent ---
+// Test hanging indent.
+#set par(hanging-indent: 15pt, justify: true)
+#lorem(10)
+
+--- par-hanging-indent-manual-linebreak ---
+#set par(hanging-indent: 1em)
+Welcome \ here. Does this work well?
+
+--- par-hanging-indent-rtl ---
+#set par(hanging-indent: 2em)
+#set text(dir: rtl)
+لآن وقد أظلم الليل وبدأت النجوم
+تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار
diff --git a/tests/suite/model/quote.typ b/tests/suite/model/quote.typ
new file mode 100644
index 0000000000..446784ee0a
--- /dev/null
+++ b/tests/suite/model/quote.typ
@@ -0,0 +1,86 @@
+// Test the quote element.
+
+--- quote-dir-author-pos ---
+// Text direction affects author positioning
+And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum].
+
+#set text(lang: "ar")
+#quote(attribution: [عالم])[مرحبًا]
+
+--- quote-dir-align ---
+// Text direction affects block alignment
+#set quote(block: true)
+#quote(attribution: [René Descartes])[cogito, ergo sum]
+
+#set text(lang: "ar")
+#quote(attribution: [عالم])[مرحبًا]
+
+--- quote-block-spacing ---
+// Spacing with other blocks
+#set quote(block: true)
+#set text(8pt)
+
+#lorem(10)
+#quote(lorem(10))
+#lorem(10)
+
+--- quote-inline ---
+// Inline citation
+#set text(8pt)
+#quote(attribution: )[In a hole in the ground there lived a hobbit.]
+
+#set text(0pt)
+#bibliography("/assets/bib/works.bib")
+
+--- quote-cite-format-label-or-numeric ---
+// Citation-format: label or numeric
+#set text(8pt)
+#set quote(block: true)
+#quote(attribution: )[In a hole in the ground there lived a hobbit.]
+
+#set text(0pt)
+#bibliography("/assets/bib/works.bib", style: "ieee")
+
+--- quote-cite-format-note ---
+// Citation-format: note
+#set text(8pt)
+#set quote(block: true)
+#quote(attribution: )[In a hole in the ground there lived a hobbit.]
+
+#set text(0pt)
+#bibliography("/assets/bib/works.bib", style: "chicago-notes")
+
+--- quote-cite-format-author-date ---
+// Citation-format: author-date or author
+#set text(8pt)
+#set quote(block: true)
+#quote(attribution: )[In a hole in the ground there lived a hobbit.]
+
+#set text(0pt)
+#bibliography("/assets/bib/works.bib", style: "apa")
+
+--- quote-nesting ---
+// Test quote selection.
+#set page(width: auto)
+#set text(lang: "en")
+=== EN
+#quote[An apostroph'] \
+#quote[A #quote[nested] quote] \
+#quote[A #quote[very #quote[nested]] quote]
+
+#set text(lang: "de")
+=== DE
+#quote[Satz mit Apostroph'] \
+#quote[Satz mit #quote[Zitat]] \
+#quote[A #quote[very #quote[nested]] quote]
+
+#set smartquote(alternative: true)
+=== DE Alternative
+#quote[Satz mit Apostroph'] \
+#quote[Satz mit #quote[Zitat]] \
+#quote[A #quote[very #quote[nested]] quote]
+
+--- quote-nesting-custom ---
+// With custom quotes.
+#set smartquote(quotes: (single: ("<", ">"), double: ("(", ")")))
+#quote[A #quote[nested] quote]
diff --git a/tests/suite/model/ref.typ b/tests/suite/model/ref.typ
new file mode 100644
index 0000000000..200f40aaa3
--- /dev/null
+++ b/tests/suite/model/ref.typ
@@ -0,0 +1,56 @@
+// Test references.
+
+--- ref-basic ---
+#set heading(numbering: "1.")
+
+= Introduction
+See @setup.
+
+== Setup
+As seen in @intro, we proceed.
+
+--- ref-label-missing ---
+// Error: 1-5 label `` does not exist in the document
+@foo
+
+--- ref-label-duplicate ---
+= First
+= Second
+
+// Error: 1-5 label `` occurs multiple times in the document
+@foo
+
+--- ref-supplements ---
+#set heading(numbering: "1.", supplement: [Chapter])
+#set math.equation(numbering: "(1)", supplement: [Eq.])
+
+= Intro
+#figure(
+ image("/assets/images/cylinder.svg", height: 1cm),
+ caption: [A cylinder.],
+ supplement: "Fig",
+)
+
+#figure(
+ image("/assets/images/tiger.jpg", height: 1cm),
+ caption: [A tiger.],
+ supplement: "Tig",
+)
+
+$ A = 1 $
+
+#set math.equation(supplement: none)
+$ A = 1 $
+
+@fig1, @fig2, @eq1, (@eq2)
+
+#set ref(supplement: none)
+@fig1, @fig2, @eq1, @eq2
+
+--- ref-ambigious ---
+// Test ambiguous reference.
+= Introduction
+
+// Error: 1-7 label occurs in the document and its bibliography
+@arrgh
+#bibliography("/assets/bib/works.bib")
diff --git a/tests/suite/model/terms.typ b/tests/suite/model/terms.typ
new file mode 100644
index 0000000000..6a08b92363
--- /dev/null
+++ b/tests/suite/model/terms.typ
@@ -0,0 +1,77 @@
+// Test term list.
+
+--- terms-constructor ---
+// Test with constructor.
+#terms(
+ ([One], [First]),
+ ([Two], [Second]),
+)
+
+--- terms-built-in-loop ---
+// Test joining.
+#for word in lorem(4).split().map(s => s.trim(".")) [
+ / #word: Latin stuff.
+]
+
+--- terms-multiline ---
+// Test multiline.
+#set text(8pt)
+
+/ Fruit: A tasty, edible thing.
+/ Veggie:
+ An important energy source
+ for vegetarians.
+
+ And healthy!
+
+--- terms-style-change-interrupted ---
+// Test style change.
+#set text(8pt)
+/ First list: #lorem(6)
+
+#set terms(hanging-indent: 30pt)
+/ Second list: #lorem(5)
+
+--- terms-rtl ---
+// Test RTL.
+#set text(8pt, dir: rtl)
+
+/ פרי: דבר טעים, אכיל. ומקור אנרגיה חשוב לצמחונים.
+
+--- terms-grid ---
+// Test grid like show rule.
+#show terms: it => table(
+ columns: 2,
+ inset: 3pt,
+ ..it.children.map(v => (emph(v.term), v.description)).flatten(),
+)
+
+/ A: One letter
+/ BB: Two letters
+/ CCC: Three letters
+
+--- terms-syntax-edge-cases ---
+/ Term:
+Not in list
+/Nope
+
+--- terms-missing-colon ---
+// Error: 8 expected colon
+/ Hello
+
+--- issue-1050-terms-indent ---
+#set page(width: 200pt)
+#set par(first-line-indent: 0.5cm)
+
+- #lorem(10)
+- #lorem(10)
+
++ #lorem(10)
++ #lorem(10)
+
+/ Term 1: #lorem(10)
+/ Term 2: #lorem(10)
+
+--- issue-2530-term-item-panic ---
+// Term item (pre-emptive)
+#terms.item[Hello][World!]
diff --git a/tests/suite/playground.typ b/tests/suite/playground.typ
new file mode 100644
index 0000000000..1af70bea0f
--- /dev/null
+++ b/tests/suite/playground.typ
@@ -0,0 +1 @@
+--- playground ---
diff --git a/tests/suite/scripting/blocks.typ b/tests/suite/scripting/blocks.typ
new file mode 100644
index 0000000000..f139b8c62f
--- /dev/null
+++ b/tests/suite/scripting/blocks.typ
@@ -0,0 +1,143 @@
+// Test code blocks.
+
+--- code-block-basic-syntax ---
+
+// Evaluates to join of none, [My ] and the two loop bodies.
+#{
+ let parts = ("my fri", "end.")
+ [Hello, ]
+ for s in parts [#s]
+}
+
+// Evaluates to join of the content and strings.
+#{
+ [How]
+ if true {
+ " are"
+ }
+ [ ]
+ if false [Nope]
+ [you] + "?"
+}
+
+--- code-block-empty ---
+// Nothing evaluates to none.
+#test({}, none)
+
+--- code-block-let ---
+// Let evaluates to none.
+#test({ let v = 0 }, none)
+
+--- code-block-single-expression ---
+// Evaluates to single expression.
+#test({ "hello" }, "hello")
+
+--- code-block-multiple-expressions-single-line ---
+// Evaluates to string.
+#test({ let x = "m"; x + "y" }, "my")
+
+--- code-block-join-let-with-expression ---
+// Evaluated to int.
+#test({
+ let x = 1
+ let y = 2
+ x + y
+}, 3)
+
+--- code-block-join-expression-with-none ---
+// String is joined with trailing none, evaluates to string.
+#test({
+ type("")
+ none
+}, str)
+
+--- code-block-join-int-with-content ---
+// Some things can't be joined.
+#{
+ [A]
+ // Error: 3-4 cannot join content with integer
+ 1
+ [B]
+}
+
+--- code-block-scope-in-markup ---
+// Block directly in markup also creates a scope.
+#{ let x = 1 }
+
+// Error: 7-8 unknown variable: x
+#test(x, 1)
+
+--- code-block-scope-in-let ---
+// Block in expression does create a scope.
+#let a = {
+ let b = 1
+ b
+}
+
+#test(a, 1)
+
+// Error: 3-4 unknown variable: b
+#{b}
+
+--- code-block-double-scope ---
+// Double block creates a scope.
+#{{
+ import "module.typ": b
+ test(b, 1)
+}}
+
+// Error: 2-3 unknown variable: b
+#b
+
+--- code-block-nested-scopes ---
+// Multiple nested scopes.
+#{
+ let a = "a1"
+ {
+ let a = "a2"
+ {
+ test(a, "a2")
+ let a = "a3"
+ test(a, "a3")
+ }
+ test(a, "a2")
+ }
+ test(a, "a1")
+}
+
+--- code-block-multiple-literals-without-semicolon ---
+// Multiple unseparated expressions in one line.
+// Error: 4 expected semicolon or line break
+#{1 2}
+
+--- code-block-multiple-expressions-without-semicolon ---
+// Error: 13 expected semicolon or line break
+// Error: 23 expected semicolon or line break
+#{let x = -1 let y = 3 x + y}
+
+--- code-block-incomplete-expressions ---
+#{
+ // Error: 7-10 expected pattern, found string
+ for "v"
+
+ // Error: 8 expected keyword `in`
+ // Error: 22 expected block
+ for v let z = 1 + 2
+
+ z
+}
+
+--- code-block-unclosed ---
+// Error: 2-3 unclosed delimiter
+#{
+
+--- code-block-unopened ---
+// Error: 2-3 unexpected closing brace
+#}
+
+--- content-block-in-markup-scope ---
+// Content blocks also create a scope.
+#[#let x = 1]
+
+// Error: 2-3 unknown variable: x
+#x
diff --git a/tests/suite/scripting/call.typ b/tests/suite/scripting/call.typ
new file mode 100644
index 0000000000..e79fc949e5
--- /dev/null
+++ b/tests/suite/scripting/call.typ
@@ -0,0 +1,200 @@
+// Test function calls.
+
+--- call-basic ---
+
+// Omitted space.
+#let f() = {}
+#[#f()*Bold*]
+
+// Call return value of function with body.
+#let f(x, body) = (y) => [#x] + body + [#y]
+#f(1)[2](3)
+
+// Don't parse this as a function.
+#test (it)
+
+#let f(body) = body
+#f[A]
+#f()[A]
+#f([A])
+
+#let g(a, b) = a + b
+#g[A][B]
+#g([A], [B])
+#g()[A][B]
+
+--- call-aliased-function ---
+// Call function assigned to variable.
+#let alias = type
+#test(alias(alias), type)
+
+--- call-complex-callee-expression ---
+// Callee expressions.
+#{
+ // Wrapped in parens.
+ test((type)("hi"), str)
+
+ // Call the return value of a function.
+ let adder(dx) = x => x + dx
+ test(adder(2)(5), 7)
+}
+
+--- call-bad-type-bool-literal ---
+// Error: 2-6 expected function, found boolean
+#true()
+
+--- call-bad-type-string-var ---
+#let x = "x"
+
+// Error: 2-3 expected function, found string
+#x()
+
+--- call-bad-type-int-expr ---
+#let f(x) = x
+
+// Error: 2-6 expected function, found integer
+#f(1)(2)
+
+--- call-bad-type-content-expr ---
+#let f(x) = x
+
+// Error: 2-6 expected function, found content
+#f[1](2)
+
+--- call-args-trailing-comma ---
+// Trailing comma.
+#test(1 + 1, 2,)
+
+--- call-args-duplicate ---
+// Error: 26-30 duplicate argument: font
+#set text(font: "Arial", font: "Helvetica")
+
+--- call-args-bad-positional-as-named ---
+// Error: 4-15 the argument `amount` is positional
+// Hint: 4-15 try removing `amount:`
+#h(amount: 0.5)
+
+--- call-args-bad-colon ---
+// Error: 7-8 unexpected colon
+#func(:)
+
+--- call-args-bad-token ---
+// Error: 10-12 unexpected end of block comment
+#func(a:1*/)
+
+--- call-args-missing-comma ---
+// Error: 8 expected comma
+#func(1 2)
+
+--- call-args-bad-name-and-incomplete-pair ---
+// Error: 7-8 expected identifier, found integer
+// Error: 9 expected expression
+#func(1:)
+
+--- call-args-bad-name-int ---
+// Error: 7-8 expected identifier, found integer
+#func(1:2)
+
+--- call-args-bad-name-string ---
+// Error: 7-12 expected identifier, found string
+#func("abc": 2)
+
+--- call-args-bad-name-group ---
+// Error: 7-10 expected identifier, found group
+#func((x):1)
+
+--- call-args-lone-underscore ---
+// Test that lone underscore works.
+#test((1, 2, 3).map(_ => {}).len(), 3)
+
+--- call-args-spread-override ---
+// Test standard argument overriding.
+#{
+ let f(style: "normal", weight: "regular") = {
+ "(style: " + style + ", weight: " + weight + ")"
+ }
+
+ let myf(..args) = f(weight: "bold", ..args)
+ test(myf(), "(style: normal, weight: bold)")
+ test(myf(weight: "black"), "(style: normal, weight: black)")
+ test(myf(style: "italic"), "(style: italic, weight: bold)")
+}
+
+--- call-args-spread-forward ---
+// Test multiple calls.
+#{
+ let f(b, c: "!") = b + c
+ let g(a, ..sink) = a + f(..sink)
+ test(g("a", "b", c: "c"), "abc")
+}
+
+--- call-args-spread-type-repr ---
+// Test doing things with arguments.
+#{
+ let save(..args) = {
+ test(type(args), arguments)
+ test(repr(args), "(three: true, 1, 2)")
+ }
+
+ save(1, 2, three: true)
+}
+
+--- call-args-spread-array-and-dict ---
+// Test spreading array and dictionary.
+#{
+ let more = (3, -3, 6, 10)
+ test(calc.min(1, 2, ..more), -3)
+ test(calc.max(..more, 9), 10)
+ test(calc.max(..more, 11), 11)
+}
+
+#{
+ let more = (c: 3, d: 4)
+ let tostr(..args) = repr(args)
+ test(tostr(a: 1, ..more, b: 2), "(a: 1, c: 3, d: 4, b: 2)")
+}
+
+--- call-args-spread-none ---
+// None is spreadable.
+#let f() = none
+#f(..none)
+#f(..if false {})
+#f(..for x in () [])
+
+--- call-args-spread-string-invalid ---
+// Error: 11-19 cannot spread string
+#calc.min(.."nope")
+
+--- call-args-content-block-unclosed ---
+// Error: 6-7 unclosed delimiter
+#func[`a]`
+
+--- issue-886-args-sink ---
+// Test bugs with argument sinks.
+#let foo(..body) = repr(body.pos())
+#foo(a: "1", b: "2", 1, 2, 3, 4, 5, 6)
+
+--- issue-3144-unexpected-arrow ---
+#let f(a: 10) = a(1) + 1
+#test(f(a: _ => 5), 6)
+
+--- issue-3502-space-and-comments-around-destructuring-colon ---
+#let ( key : /* hi */ binding ) = ( key: "ok" )
+#test(binding, "ok")
+
+--- issue-3502-space-around-dict-colon ---
+#test(( key : "value" ).key, "value")
+
+--- issue-3502-space-around-param-colon ---
+// Test that a space after a named parameter is permissible.
+#let f( param : v ) = param
+#test(f( param /* ok */ : 2 ), 2)
+
+--- call-args-unclosed ---
+// Error: 7-8 unclosed delimiter
+#{func(}
+
+--- call-args-unclosed-string ---
+// Error: 6-7 unclosed delimiter
+// Error: 1:7-2:1 unclosed string
+#func("]
diff --git a/tests/typ/compiler/closure.typ b/tests/suite/scripting/closure.typ
similarity index 78%
rename from tests/typ/compiler/closure.typ
rename to tests/suite/scripting/closure.typ
index 29c092b7c7..e3677d331b 100644
--- a/tests/typ/compiler/closure.typ
+++ b/tests/suite/scripting/closure.typ
@@ -1,23 +1,21 @@
// Test closures.
-// Ref: false
----
+--- closure-without-params-non-atomic ---
// Don't parse closure directly in content.
-// Ref: true
#let x = "x"
// Should output `x => y`.
#x => y
----
+--- closure-without-captures ---
// Basic closure without captures.
#{
let adder = (x, y) => x + y
test(adder(2, 3), 5)
}
----
+--- closure-as-arg ---
// Pass closure as argument and return closure.
// Also uses shorthand syntax for a single argument.
#{
@@ -28,7 +26,7 @@
test(h(2), 5)
}
----
+--- closure-capture-from-popped-stack-frame ---
// Capture environment.
#{
let mark = "!"
@@ -46,7 +44,7 @@
test(greet("Typst"), "Hi, Typst!")
}
----
+--- closure-shadows-outer-var ---
// Redefined variable.
#{
let x = 1
@@ -57,7 +55,7 @@
test(f(), 3)
}
----
+--- closure-shadows-outer-var-import ---
// Import bindings.
#{
let b = "module.typ"
@@ -68,7 +66,7 @@
test(f(), 1)
}
----
+--- closure-shadows-outer-var-for-loop ---
// For loop bindings.
#{
let v = (1, 2, 3)
@@ -80,7 +78,7 @@
test(f(), 6)
}
----
+--- closure-let-basic ---
// Let + closure bindings.
#{
let g = "hi"
@@ -91,7 +89,7 @@
test(f(), "bye")
}
----
+--- closure-let-args ---
// Parameter bindings.
#{
let x = 5
@@ -103,7 +101,7 @@
test(g()(8), 13)
}
----
+--- closure-bad-capture ---
// Don't leak environment.
#{
// Error: 16-17 unknown variable: x
@@ -112,7 +110,7 @@
func()
}
----
+--- closure-missing-arg-positional ---
// Too few arguments.
#{
let types(x, y) = "[" + str(type(x)) + ", " + str(type(y)) + "]"
@@ -122,7 +120,7 @@
test(types("nope"), "[string, none]")
}
----
+--- closure-too-many-args-positional ---
// Too many arguments.
#{
let f(x) = x + 1
@@ -131,7 +129,7 @@
f(1, "two", () => x)
}
----
+--- closure-capture-in-lvalue ---
// Mutable method with capture in argument.
#let x = "b"
#let f() = {
@@ -141,7 +139,7 @@
}
#f()
----
+--- closure-capture-mutate ---
#let x = ()
#let f() = {
// Error: 3-4 variables from outside the function are read-only and cannot be modified
@@ -149,7 +147,7 @@
}
#f()
----
+--- closure-named-args-basic ---
// Named arguments.
#{
let greet(name, birthday: false) = {
@@ -163,7 +161,7 @@
test(greet("Typst", whatever: 10))
}
----
+--- closure-args-sink ---
// Parameter unpacking.
#let f((a, b), ..c) = (a, b, c)
#test(f((1, 2), 3, 4), (1, 2, (3, 4)))
@@ -180,40 +178,46 @@
// Error: 10-16 expected pattern, found array
#let f(..(a, b)) = none
----
+--- closure-param-duplicate-positional ---
// Error: 11-12 duplicate parameter: x
#let f(x, x) = none
----
+--- closure-body-multiple-expressions ---
// Error: 21 expected comma
// Error: 22-23 expected pattern, found integer
// Error: 24-25 unexpected plus
// Error: 26-27 expected pattern, found integer
#let f = (x: () => 1 2 + 3) => 4
----
+--- closure-param-duplicate-mixed ---
// Error: 14-15 duplicate parameter: a
// Error: 23-24 duplicate parameter: b
// Error: 35-36 duplicate parameter: b
#let f(a, b, a: none, b: none, c, b) = none
----
+--- closure-param-duplicate-spread ---
// Error: 13-14 duplicate parameter: a
#let f(a, ..a) = none
----
+--- closure-pattern-bad-string ---
// Error: 7-14 expected pattern, found string
#((a, "named": b) => none)
----
+--- closure-let-pattern-bad-string ---
// Error: 10-15 expected pattern, found string
#let foo("key": b) = key
----
+--- closure-param-keyword ---
// Error: 10-14 expected pattern, found `none`
// Hint: 10-14 keyword `none` is not allowed as an identifier; try `none_` instead
#let foo(none: b) = key
----
+--- closure-param-named-underscore ---
// Error: 10-11 expected identifier, found underscore
#let foo(_: 3) = none
+
+--- issue-non-atomic-closure ---
+// Ensure that we can't have non-atomic closures.
+#let x = 1
+#let c = [#(x) => (1, 2)]
+#test(c.children.last(), [(1, 2)]))
diff --git a/tests/suite/scripting/destructuring.typ b/tests/suite/scripting/destructuring.typ
new file mode 100644
index 0000000000..0a3c1c5486
--- /dev/null
+++ b/tests/suite/scripting/destructuring.typ
@@ -0,0 +1,357 @@
+--- destructuring-group-1 ---
+// This wasn't allowed.
+#let ((x)) = 1
+#test(x, 1)
+
+--- destructuring-group-2 ---
+// This also wasn't allowed.
+#let ((a, b)) = (1, 2)
+#test(a, 1)
+#test(b, 2)
+
+--- destructuring-dict-underscore ---
+// Here, `best` was accessed as a variable, where it shouldn't have.
+#{
+ (best: _) = (best: "brr")
+}
+
+--- destructuring-dict-array-at ---
+// Same here.
+#{
+ let array = (1, 2, 3, 4)
+ (test: array.at(1), best: _) = (test: "baz", best: "brr")
+ test(array, (1, "baz", 3, 4))
+}
+
+--- destructuring-dict-bad ---
+// Error: 7-10 expected identifier, found group
+// Error: 12-14 expected pattern, found integer
+#let ((a): 10) = "world"
+
+--- destructuring-bad-duplicate ---
+// Here, `a` is not duplicate, where it was previously identified as one.
+#let f((a: b), (c,), a) = (a, b, c)
+#test(f((a: 1), (2,), 3), (3, 1, 2))
+
+--- destructuring-non-atomic ---
+// Ensure that we can't have non-atomic destructuring.
+#let x = 1
+#let c = [#() = ()]
+#test(c.children.last(), [()])
+
+--- destructuring-let-array ---
+// Simple destructuring.
+#let (a, b) = (1, 2)
+#test(a, 1)
+#test(b, 2)
+
+--- destructuring-let-array-single-item ---
+#let (a,) = (1,)
+#test(a, 1)
+
+--- destructuring-let-array-placeholders ---
+// Destructuring with multiple placeholders.
+#let (a, _, c, _) = (1, 2, 3, 4)
+#test(a, 1)
+#test(c, 3)
+
+--- destructuring-let-array-with-sink-at-end ---
+// Destructuring with a sink.
+#let (a, b, ..c) = (1, 2, 3, 4, 5, 6)
+#test(a, 1)
+#test(b, 2)
+#test(c, (3, 4, 5, 6))
+
+--- destructuring-let-array-with-sink-in-middle ---
+// Destructuring with a sink in the middle.
+#let (a, ..b, c) = (1, 2, 3, 4, 5, 6)
+#test(a, 1)
+#test(b, (2, 3, 4, 5))
+#test(c, 6)
+
+--- destructuring-let-array-with-sink-at-start-empty ---
+// Destructuring with an empty sink.
+#let (..a, b, c) = (1, 2)
+#test(a, ())
+#test(b, 1)
+#test(c, 2)
+
+--- destructuring-let-array-with-sink-in-middle-empty ---
+// Destructuring with an empty sink.
+#let (a, ..b, c) = (1, 2)
+#test(a, 1)
+#test(b, ())
+#test(c, 2)
+
+--- destructuring-let-array-with-sink-at-end-empty ---
+// Destructuring with an empty sink.
+#let (a, b, ..c) = (1, 2)
+#test(a, 1)
+#test(b, 2)
+#test(c, ())
+
+--- destructuring-let-array-with-sink-empty ---
+// Destructuring with an empty sink and empty array.
+#let (..a) = ()
+#test(a, ())
+
+--- destructuring-let-array-with-unnamed-sink ---
+// Destructuring with unnamed sink.
+#let (a, .., b) = (1, 2, 3, 4)
+#test(a, 1)
+#test(b, 4)
+
+// Error: 10-11 duplicate binding: a
+#let (a, a) = (1, 2)
+
+// Error: 12-15 only one destructuring sink is allowed
+#let (..a, ..a) = (1, 2)
+
+// Error: 12-13 duplicate binding: a
+#let (a, ..a) = (1, 2)
+
+// Error: 13-14 duplicate binding: a
+#let (a: a, a) = (a: 1, b: 2)
+
+// Error: 13-20 expected pattern, found function call
+#let (a, b: b.at(0)) = (a: 1, b: 2)
+
+// Error: 7-14 expected pattern, found function call
+#let (a.at(0),) = (1,)
+
+--- destructuring-let-array-too-few-elements ---
+// Error: 13-14 not enough elements to destructure
+#let (a, b, c) = (1, 2)
+
+--- destructuring-let-array-too-few-elements-with-sink ---
+// Error: 7-10 not enough elements to destructure
+#let (..a, b, c, d) = (1, 2)
+
+--- destructuring-let-array-bool-invalid ---
+// Error: 6-12 cannot destructure boolean
+#let (a, b) = true
+
+--- destructuring-let-dict ---
+// Simple destructuring.
+#let (a: a, b, x: c) = (a: 1, b: 2, x: 3)
+#test(a, 1)
+#test(b, 2)
+#test(c, 3)
+
+--- destructuring-let-dict-with-sink-at-end ---
+// Destructuring with a sink.
+#let (a: _, ..b) = (a: 1, b: 2, c: 3)
+#test(b, (b: 2, c: 3))
+
+--- destructuring-let-dict-with-sink-in-middle ---
+// Destructuring with a sink in the middle.
+#let (a: _, ..b, c: _) = (a: 1, b: 2, c: 3)
+#test(b, (b: 2))
+
+--- destructuring-let-dict-with-sink-at-end-empty ---
+// Destructuring with an empty sink.
+#let (a: _, ..b) = (a: 1)
+#test(b, (:))
+
+--- destructuring-let-dict-with-sink-empty ---
+// Destructuring with an empty sink and empty dict.
+#let (..a) = (:)
+#test(a, (:))
+
+--- destructuring-let-dict-with-unnamed-sink ---
+// Destructuring with unnamed sink.
+#let (a, ..) = (a: 1, b: 2)
+#test(a, 1)
+
+--- destructuring-let-nested ---
+// Nested destructuring.
+#let ((a, b), (key: c)) = ((1, 2), (key: 3))
+#test((a, b, c), (1, 2, 3))
+
+--- destructuring-let-dict-key-string-invalid ---
+// Keyed destructuring is not currently supported.
+// Error: 7-18 expected pattern, found string
+#let ("spacy key": val) = ("spacy key": 123)
+#val
+
+--- destructuring-let-dict-key-expr-invalid ---
+// Keyed destructuring is not currently supported.
+#let x = "spacy key"
+// Error: 7-10 expected identifier, found group
+#let ((x): v) = ("spacy key": 123)
+
+--- destructuring-let-array-trailing-placeholders ---
+// Trailing placeholders.
+// Error: 10-11 not enough elements to destructure
+#let (a, _, _, _, _) = (1,)
+#test(a, 1)
+
+--- destructuring-let-dict-patterns-invalid ---
+// Error: 10-13 expected pattern, found string
+// Error: 18-19 expected pattern, found integer
+#let (a: "a", b: 2) = (a: 1, b: 2)
+
+--- destructuring-let-dict-shorthand-missing-key ---
+// Error: 10-11 dictionary does not contain key "b"
+#let (a, b) = (a: 1)
+
+--- destructuring-let-dict-missing-key ---
+// Error: 10-11 dictionary does not contain key "b"
+#let (a, b: b) = (a: 1)
+
+--- destructuring-let-dict-from-array ---
+// Error: 7-11 cannot destructure named pattern from an array
+#let (a: a, b) = (1, 2, 3)
+
+--- destructuring-during-loop-continue ---
+// Test continue while destructuring.
+// Should output "one = I \ two = II \ one = I".
+#for num in (1, 2, 3, 1) {
+ let (word, roman) = if num == 1 {
+ ("one", "I")
+ } else if num == 2 {
+ ("two", "II")
+ } else {
+ continue
+ }
+ [#word = #roman \ ]
+}
+
+--- destructuring-assign ---
+// Test destructuring assignments.
+
+#let a = none
+#let b = none
+#let c = none
+#((a,) = (1,))
+#test(a, 1)
+
+#((_, a, b, _) = (1, 2, 3, 4))
+#test(a, 2)
+#test(b, 3)
+
+#((a, b, ..c) = (1, 2, 3, 4, 5, 6))
+#test(a, 1)
+#test(b, 2)
+#test(c, (3, 4, 5, 6))
+
+#((a: a, b, x: c) = (a: 1, b: 2, x: 3))
+#test(a, 1)
+#test(b, 2)
+#test(c, 3)
+
+#let a = (1, 2)
+#((a: a.at(0), b) = (a: 3, b: 4))
+#test(a, (3, 2))
+#test(b, 4)
+
+#let a = (1, 2)
+#((a.at(0), b) = (3, 4))
+#test(a, (3, 2))
+#test(b, 4)
+
+#((a, ..b) = (1, 2, 3, 4))
+#test(a, 1)
+#test(b, (2, 3, 4))
+
+#let a = (1, 2)
+#((b, ..a.at(0)) = (1, 2, 3, 4))
+#test(a, ((2, 3, 4), 2))
+#test(b, 1)
+
+--- destructuring-assign-commas ---
+// Test comma placement in destructuring assignment.
+#let array = (1, 2, 3)
+#((key: array.at(1)) = (key: "hi"))
+#test(array, (1, "hi", 3))
+
+#let array = (1, 2, 3)
+#((array.at(1)) = ("hi"))
+#test(array, (1, "hi", 3))
+
+#let array = (1, 2, 3)
+#((array.at(1),) = ("hi",))
+#test(array, (1, "hi", 3))
+
+#let array = (1, 2, 3)
+#((array.at(1)) = ("hi",))
+#test(array, (1, ("hi",), 3))
+
+--- destructuring-assign-nested ---
+// Test nested destructuring assignment.
+#let a
+#let b
+#let c
+#(((a, b), (key: c)) = ((1, 2), (key: 3)))
+#test((a, b, c), (1, 2, 3))
+
+--- destructuring-assign-nested-invalid ---
+#let array = (1, 2, 3)
+// Error: 3-17 cannot destructure string
+#((array.at(1),) = ("hi"))
+#test(array, (1, ("hi",), 3))
+
+--- issue-3275-normal-variable ---
+// Normal variable.
+#for x in (1, 2) {}
+#for x in (a: 1, b: 2) {}
+#for x in "foo" {}
+#for x in bytes("😊") {}
+
+--- issue-3275-placeholder ---
+// Placeholder.
+#for _ in (1, 2) {}
+#for _ in (a: 1, b: 2) {}
+#for _ in "foo" {}
+#for _ in bytes("😊") {}
+
+--- issue-3275-destructuring ---
+// Destructuring.
+#for (a,b,c) in (("a", 1, bytes(())), ("b", 2, bytes(""))) {}
+#for (a, ..) in (("a", 1, bytes(())), ("b", 2, bytes(""))) {}
+#for (k, v) in (a: 1, b: 2, c: 3) {}
+#for (.., v) in (a: 1, b: 2, c: 3) {}
+
+--- issue-3275-loop-over-content ---
+// Error: 11-17 cannot loop over content
+#for x in [1, 2] {}
+
+--- issue-3275-loop-over-arguments ---
+// Error: 11-25 cannot loop over arguments
+#for _ in arguments("a") {}
+
+--- issue-3275-loop-over-integer ---
+// Error: 16-21 cannot loop over integer
+#for (x, y) in 12306 {}
+
+--- issue-3275-destructuring-loop-over-content ---
+// Error: 16-22 cannot loop over content
+#for (x, y) in [1, 2] {}
+
+--- issue-3275-destructuring-loop-over-string ---
+// Error: 6-12 cannot destructure values of string
+#for (x, y) in "foo" {}
+
+--- issue-3275-destructuring-loop-over-string-array ---
+// Error: 6-12 cannot destructure string
+#for (x, y) in ("foo", "bar") {}
+
+--- issue-3275-destructuring-loop-over-bytes ---
+// Error: 6-12 cannot destructure values of bytes
+#for (x, y) in bytes("😊") {}
+
+--- issue-3275-destructuring-loop-over-bytes-array ---
+// Error: 6-12 cannot destructure bytes
+#for (x, y) in (bytes((1,2)), bytes((1,2))) {}
+
+--- issue-3275-destructuring-loop-over-int-array ---
+// Error: 6-12 cannot destructure integer
+#for (x, y) in (1, 2) {}
+
+--- issue-3275-destructuring-loop-over-2d-array-1 ---
+// Error: 10-11 not enough elements to destructure
+#for (x, y) in ((1,), (2,)) {}
+
+--- issue-3275-destructuring-loop-over-2d-array-2 ---
+// Error: 6-12 too many elements to destructure
+#for (x, y) in ((1,2,3), (4,5,6)) {}
diff --git a/tests/suite/scripting/field.typ b/tests/suite/scripting/field.typ
new file mode 100644
index 0000000000..7b2427e380
--- /dev/null
+++ b/tests/suite/scripting/field.typ
@@ -0,0 +1,76 @@
+// Test field access.
+
+--- field-function ---
+// Test fields on function scopes.
+#enum.item
+#assert.eq
+#assert.ne
+
+--- field-normal-function-invalid ---
+// Error: 9-16 function `assert` does not contain field `invalid`
+#assert.invalid
+
+--- field-elem-function-invalid ---
+// Error: 7-14 function `enum` does not contain field `invalid`
+#enum.invalid
+
+--- field-elem-function-invalid-call ---
+// Error: 7-14 function `enum` does not contain field `invalid`
+#enum.invalid()
+
+--- field-closure-invalid ---
+// Closures cannot have fields.
+#let f(x) = x
+// Error: 4-11 cannot access fields on user-defined functions
+#f.invalid
+
+--- field-bool-invalid ---
+// Error: 8-10 cannot access fields on type boolean
+#false.ok
+
+--- field-bool-keyword-invalid ---
+// Error: 9-13 cannot access fields on type boolean
+#{false.true}
+
+--- field-invalid-none ---
+#{
+ let object = none
+ // Error: 3-9 none does not have accessible fields
+ object.property = "value"
+}
+
+--- field-invalid-int ---
+#{
+ let object = 10
+ // Error: 3-9 integer does not have accessible fields
+ object.property = "value"
+}
+
+--- field-mutable-invalid-symbol ---
+#{
+ let object = sym.eq.not
+ // Error: 3-9 cannot mutate fields on symbol
+ object.property = "value"
+}
+
+--- field-mutable-invalid-module ---
+#{
+ let object = calc
+ // Error: 3-9 cannot mutate fields on module
+ object.property = "value"
+}
+
+--- field-mutable-invalid-function ---
+#{
+ let object = calc.sin
+ // Error: 3-9 cannot mutate fields on function
+ object.property = "value"
+}
+
+--- field-mutable-invalid-stroke ---
+#{
+ let s = 1pt + red
+ // Error: 3-4 fields on stroke are not yet mutable
+ // Hint: 3-4 try creating a new stroke with the updated field value instead
+ s.thickness = 5pt
+}
diff --git a/tests/typ/compiler/for.typ b/tests/suite/scripting/for.typ
similarity index 91%
rename from tests/typ/compiler/for.typ
rename to tests/suite/scripting/for.typ
index 392dd6764e..e98b3c7298 100644
--- a/tests/typ/compiler/for.typ
+++ b/tests/suite/scripting/for.typ
@@ -1,8 +1,6 @@
// Test for loops.
-// Ref: false
----
-// Ref: true
+--- for-loop-basic ---
// Empty array.
#for x in () [Nope]
@@ -38,7 +36,7 @@
#let f(..args) = (f1(..args) + f2(..args)).join(", ")
#f(1, a: 2)
----
+--- for-loop-integrated ---
#let out = ()
// Values of array.
@@ -78,19 +76,19 @@
#test(for v in "" [], none)
#test(type(for v in "1" []), content)
----
+--- for-loop-over-bool ---
// Uniterable expression.
// Error: 11-15 cannot loop over boolean
#for v in true {}
----
+--- for-loop-over-string ---
// Keys and values of strings.
// Error: 6-12 cannot destructure values of string
#for (k, v) in "hi" {
dont-care
}
----
+--- for-loop-destructuring-without-parentheses ---
// Destructuring without parentheses.
// Error: 7-8 unexpected comma
// Hint: 7-8 destructuring patterns must be wrapped in parentheses
@@ -98,11 +96,12 @@
dont-care
}
+--- for-loop-destructuring-half ---
// Error: 7-8 unexpected comma
// Hint: 7-8 destructuring patterns must be wrapped in parentheses
#for k, in () {}
----
+--- for-loop-incomplete ---
// Error: 5 expected pattern
#for
diff --git a/tests/suite/scripting/get-rule.typ b/tests/suite/scripting/get-rule.typ
new file mode 100644
index 0000000000..24d4e5dbb3
--- /dev/null
+++ b/tests/suite/scripting/get-rule.typ
@@ -0,0 +1,67 @@
+--- get-rule-basic ---
+// Test basic get rule.
+#context test(text.lang, "en")
+#set text(lang: "de")
+#context test(text.lang, "de")
+#text(lang: "es", context test(text.lang, "es"))
+
+--- get-rule-in-function ---
+// Test whether context is retained in nested function.
+#let translate(..args) = args.named().at(text.lang)
+#set text(lang: "de")
+#context test(translate(de: "Inhalt", en: "Contents"), "Inhalt")
+
+--- get-rule-in-array-callback ---
+// Test whether context is retained in built-in callback.
+#set text(lang: "de")
+#context test(
+ ("en", "de", "fr").sorted(key: v => v != text.lang),
+ ("de", "en", "fr"),
+)
+
+--- get-rule-folding ---
+// Test folding.
+#set rect(stroke: red)
+#context {
+ test(type(rect.stroke), stroke)
+ test(rect.stroke.paint, red)
+}
+#[
+ #set rect(stroke: 4pt)
+ #context test(rect.stroke, 4pt + red)
+]
+#context test(rect.stroke, stroke(red))
+
+--- get-rule-figure-caption-collision ---
+// We have one collision: `figure.caption` could be both the element and a get
+// rule for the `caption` field, which is settable. We always prefer the
+// element. It's unfortunate, but probably nobody writes
+// `set figure(caption: ..)` anyway.
+#test(type(figure.caption), function)
+#context test(type(figure.caption), function)
+
+--- get-rule-assertion-failure ---
+// Error: 10-31 Assertion failed: "en" != "de"
+#context test(text.lang, "de")
+
+--- get-rule-unknown-field ---
+// Error: 15-20 function `text` does not contain field `langs`
+#context text.langs
+
+--- get-rule-inherent-field ---
+// Error: 18-22 function `heading` does not contain field `body`
+#context heading.body
+
+--- get-rule-missing-context-no-context ---
+// Error: 7-11 can only be used when context is known
+// Hint: 7-11 try wrapping this in a `context` expression
+// Hint: 7-11 the `context` expression should wrap everything that depends on this function
+#text.lang
+
+--- get-rule-unknown-field-no-context ---
+// Error: 7-12 function `text` does not contain field `langs`
+#text.langs
+
+--- get-rule-inherent-field-no-context ---
+// Error: 10-14 function `heading` does not contain field `body`
+#heading.body
diff --git a/tests/typ/compiler/if.typ b/tests/suite/scripting/if.typ
similarity index 90%
rename from tests/typ/compiler/if.typ
rename to tests/suite/scripting/if.typ
index 1d2ed88be7..cc88925fb2 100644
--- a/tests/typ/compiler/if.typ
+++ b/tests/suite/scripting/if.typ
@@ -1,6 +1,6 @@
// Test if-else expressions.
----
+--- if-markup ---
// Test condition evaluation.
#if 1 < 2 [
One.
@@ -10,7 +10,7 @@
{Bad}, but we {dont-care}!
]
----
+--- if-condition-complex ---
// Braced condition.
#if {true} [
One.
@@ -54,9 +54,8 @@
Seven.
]
----
+--- if-else-if-else ---
// Test else if.
-// Ref: false
#let nth(n) = {
str(n)
@@ -72,9 +71,8 @@
#test(nth(4), "4th")
#test(nth(5), "5th")
----
+--- if-expression ---
// Value of if expressions.
-// Ref: false
#{
let x = 1
@@ -94,18 +92,18 @@
test(z, none)
}
----
+--- if-condition-string-invalid ---
// Condition must be boolean.
// If it isn't, neither branch is evaluated.
// Error: 5-14 expected boolean, found string
#if "a" + "b" { nope } else { nope }
----
+--- if-condition-invalid-and-wrong-type ---
// Make sure that we don't complain twice.
// Error: 5-12 cannot add integer and string
#if 1 + "2" {}
----
+--- if-incomplete ---
// Error: 4 expected expression
#if
diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ
new file mode 100644
index 0000000000..820f81d6c8
--- /dev/null
+++ b/tests/suite/scripting/import.typ
@@ -0,0 +1,334 @@
+// Test function and module imports.
+
+--- import-basic ---
+// Test basic syntax and semantics.
+
+// Test that this will be overwritten.
+#let value = [foo]
+
+// Import multiple things.
+#import "module.typ": fn, value
+#fn[Like and Subscribe!]
+#value
+
+// Should output `bye`.
+// Stop at semicolon.
+#import "module.typ": a, c;bye
+
+--- import-item-markup ---
+// An item import.
+#import "module.typ": item
+#test(item(1, 2), 3)
+
+--- import-item-in-code ---
+// Code mode
+#{
+ import "module.typ": b
+ test(b, 1)
+}
+
+--- import-wildcard-in-markup ---
+// A wildcard import.
+#import "module.typ": *
+
+// It exists now!
+#test(d, 3)
+
+--- import-item-renamed ---
+// A renamed item import.
+#import "module.typ": item as something
+#test(something(1, 2), 3)
+
+--- import-items-renamed-mixed ---
+// Mixing renamed and not renamed items.
+#import "module.typ": fn, b as val, item as other
+#test(val, 1)
+#test(other(1, 2), 3)
+
+--- import-from-function-scope ---
+// Test importing from function scopes.
+
+#import enum: item
+#import assert.with(true): *
+
+#enum(
+ item(1)[First],
+ item(5)[Fifth]
+)
+#eq(10, 10)
+#ne(5, 6)
+
+--- import-from-function-scope-item-renamed ---
+// Test renaming items imported from function scopes.
+#import assert: eq as aseq
+#aseq(10, 10)
+
+--- import-from-file-bare ---
+// A module import without items.
+#import "module.typ"
+#test(module.b, 1)
+#test(module.item(1, 2), 3)
+#test(module.push(2), 3)
+
+--- import-from-file-renamed ---
+// A renamed module import without items.
+#import "module.typ" as other
+#test(other.b, 1)
+#test(other.item(1, 2), 3)
+#test(other.push(2), 3)
+
+--- import-from-file-items-renamed-mixed ---
+// Mixing renamed module and items.
+#import "module.typ" as newname: b as newval, item
+#test(newname.b, 1)
+#test(newval, 1)
+#test(item(1, 2), 3)
+#test(newname.item(1, 2), 3)
+
+--- import-from-function-scope-renamed ---
+// Renamed module import with function scopes.
+#import enum as othernum
+#test(enum, othernum)
+
+--- import-from-function-scope-renamed-twice ---
+// Mixing renamed module import from function with renamed item import.
+#import assert as asrt
+#import asrt: ne as asne
+#asne(1, 2)
+
+--- import-module-item-name-mutating ---
+// Edge case for module access that isn't fixed.
+#import "module.typ"
+
+// Works because the method name isn't categorized as mutating.
+#test((module,).at(0).item(1, 2), 3)
+
+// Doesn't work because of mutating name.
+// Error: 2-11 cannot mutate a temporary value
+#(module,).at(0).push()
+
+--- import-no-whitespace ---
+// Who needs whitespace anyways?
+#import"module.typ":*
+
+--- import-trailing-comma ---
+// Allow the trailing comma.
+#import "module.typ": a, c,
+
+--- import-source-field-access ---
+// Usual importing syntax also works for function scopes
+#let d = (e: enum)
+#import d.e
+#import d.e as renamed
+#import d.e: item
+#item(2)[a]
+
+--- import-item-rename-unnecessary ---
+// Warning: 23-27 unnecessary import rename to same name
+#import enum: item as item
+
+--- import-rename-unnecessary ---
+// Warning: 17-21 unnecessary import rename to same name
+#import enum as enum
+
+--- import-rename-unnecessary-mixed ---
+// Warning: 17-21 unnecessary import rename to same name
+#import enum as enum: item
+
+// Warning: 17-21 unnecessary import rename to same name
+// Warning: 31-35 unnecessary import rename to same name
+#import enum as enum: item as item
+
+--- import-item-rename-unnecessary-but-ok ---
+// No warning on a case that isn't obviously pathological
+#import "module.typ" as module
+
+--- import-from-closure-invalid ---
+// Can't import from closures.
+#let f(x) = x
+// Error: 9-10 cannot import from user-defined functions
+#import f: x
+
+--- import-from-closure-renamed-invalid ---
+// Can't import from closures, despite renaming.
+#let f(x) = x
+// Error: 9-10 cannot import from user-defined functions
+#import f as g
+
+--- import-from-with-closure-invalid ---
+// Can't import from closures, despite modifiers.
+#let f(x) = x
+// Error: 9-18 cannot import from user-defined functions
+#import f.with(5): x
+
+--- import-from-with-closure-literal-invalid ---
+// Error: 9-18 cannot import from user-defined functions
+#import () => {5}: x
+
+--- import-from-int-invalid ---
+// Error: 9-10 expected path, module, function, or type, found integer
+#import 5: something
+
+--- import-from-int-renamed-invalid ---
+// Error: 9-10 expected path, module, function, or type, found integer
+#import 5 as x
+
+--- import-from-string-invalid ---
+// Error: 9-11 failed to load file (is a directory)
+#import "": name
+
+--- import-from-string-renamed-invalid ---
+// Error: 9-11 failed to load file (is a directory)
+#import "" as x
+
+--- import-file-not-found-invalid ---
+// Error: 9-20 file not found (searched at tests/suite/scripting/lib/0.2.1)
+#import "lib/0.2.1"
+
+--- import-file-not-found-renamed-invalid ---
+// Error: 9-20 file not found (searched at tests/suite/scripting/lib/0.2.1)
+#import "lib/0.2.1" as x
+
+--- import-file-not-valid-utf-8 ---
+// Some non-text stuff.
+// Error: 9-35 file is not valid utf-8
+#import "/assets/images/rhino.png"
+
+--- import-item-not-found ---
+// Unresolved import.
+// Error: 23-35 unresolved import
+#import "module.typ": non_existing
+
+--- import-cyclic ---
+// Cyclic import of this very file.
+// Error: 9-23 cyclic import
+#import "./import.typ"
+
+--- import-cyclic-in-other-file ---
+// Cyclic import in other file.
+#import "./modules/cycle1.typ": *
+
+This is never reached.
+
+--- import-renamed-old-name ---
+// Renaming does not import the old name (without items).
+#import "./modules/chap1.typ" as something
+#test(something.name, "Klaus")
+// Error: 7-12 unknown variable: chap1
+#test(chap1.name, "Klaus")
+
+--- import-items-renamed-old-name ---
+// Renaming does not import the old name (with items).
+#import "./modules/chap1.typ" as something: name as other
+#test(other, "Klaus")
+#test(something.name, "Klaus")
+// Error: 7-12 unknown variable: chap1
+#test(chap1.b, "Klaus")
+
+--- import-incomplete ---
+// Error: 8 expected expression
+#import
+
+--- import-item-string-invalid ---
+// Error: 26-29 unexpected string
+#import "module.typ": a, "b", c
+
+--- import-bad-token ---
+// Error: 23-24 unexpected equals sign
+#import "module.typ": =
+
+--- import-duplicate-comma ---
+// An additional trailing comma.
+// Error: 31-32 unexpected comma
+#import "module.typ": a, b, c,,
+
+--- import-no-colon ---
+// Error: 2:2 expected semicolon or line break
+#import "module.typ
+"stuff
+
+--- import-bad-token-star ---
+// A star in the list.
+// Error: 26-27 unexpected star
+#import "module.typ": a, *, b
+
+--- import-item-after-star ---
+// An item after a star.
+// Error: 24 expected semicolon or line break
+#import "module.typ": *, a
+
+--- import-bad-colon-in-items ---
+// Error: 14-15 unexpected colon
+// Error: 16-17 unexpected integer
+#import "": a: 1
+
+--- import-missing-comma ---
+// Error: 14 expected comma
+#import "": a b
+
+--- import-from-package-bare ---
+// Test import without items.
+#import "@test/adder:0.1.0"
+#test(adder.add(2, 8), 10)
+
+--- import-from-package-items ---
+// Test import with items.
+#import "@test/adder:0.1.0": add
+#test(add(2, 8), 10)
+
+--- import-from-package-required-compiler-version ---
+// Test too high required compiler version.
+// Error: 9-29 package requires typst 1.0.0 or newer (current version is VERSION)
+#import "@test/future:0.1.0": future
+
+--- import-from-package-namespace-invalid-1 ---
+// Error: 9-13 `@` is not a valid package namespace
+#import "@@": *
+
+--- import-from-package-name-missing-1 ---
+// Error: 9-16 package specification is missing name
+#import "@heya": *
+
+--- import-from-package-namespace-invalid-2 ---
+// Error: 9-15 `123` is not a valid package namespace
+#import "@123": *
+
+--- import-from-package-name-missing-2 ---
+// Error: 9-17 package specification is missing name
+#import "@test/": *
+
+--- import-from-package-version-missing-1 ---
+// Error: 9-22 package specification is missing version
+#import "@test/mypkg": *
+
+--- import-from-package-name-invalid ---
+// Error: 9-20 `$$$` is not a valid package name
+#import "@test/$$$": *
+
+--- import-from-package-version-missing-2 ---
+// Error: 9-23 package specification is missing version
+#import "@test/mypkg:": *
+
+--- import-from-package-version-missing-minor ---
+// Error: 9-24 version number is missing minor version
+#import "@test/mypkg:0": *
+
+--- import-from-package-version-major-invalid-1 ---
+// Error: 9-29 `latest` is not a valid major version
+#import "@test/mypkg:latest": *
+
+--- import-from-package-version-major-invalid-2 ---
+// Error: 9-29 `-3` is not a valid major version
+#import "@test/mypkg:-3.0.0": *
+
+--- import-from-package-version-missing-patch-1 ---
+// Error: 9-26 version number is missing patch version
+#import "@test/mypkg:0.3": *
+
+--- import-from-package-version-missing-patch-2 ---
+// Error: 9-27 version number is missing patch version
+#import "@test/mypkg:0.3.": *
+
+--- import-from-file-package-lookalike ---
+// Error: 9-28 file not found (searched at tests/suite/scripting/#test/mypkg:1.0.0)
+#import "#test/mypkg:1.0.0": *
diff --git a/tests/suite/scripting/include.typ b/tests/suite/scripting/include.typ
new file mode 100644
index 0000000000..e4da6d1935
--- /dev/null
+++ b/tests/suite/scripting/include.typ
@@ -0,0 +1,32 @@
+// Test module includes.
+
+--- include-file ---
+#set page(width: 200pt)
+
+= Document
+
+// Include a file
+#include "modules/chap1.typ"
+
+// Expression as a file name.
+#let chap2 = include "modu" + "les/chap" + "2.typ"
+
+-- _Intermission_ --
+#chap2
+
+--- include-file-not-found ---
+#{
+ // Error: 19-38 file not found (searched at tests/suite/scripting/modules/chap3.typ)
+ let x = include "modules/chap3.typ"
+}
+
+--- include-no-bindings ---
+#include "modules/chap1.typ"
+
+// The variables of the file should not appear in this scope.
+// Error: 2-6 unknown variable: name
+#name
+
+--- include-semicolon-or-linebreak ---
+// Error: 18 expected semicolon or line break
+#include "hi.typ" Hi
diff --git a/tests/suite/scripting/let.typ b/tests/suite/scripting/let.typ
new file mode 100644
index 0000000000..2604c4ea2b
--- /dev/null
+++ b/tests/suite/scripting/let.typ
@@ -0,0 +1,143 @@
+// Test let bindings.
+
+--- let-basic ---
+// Automatically initialized with none.
+#let x
+#test(x, none)
+
+// Manually initialized with one.
+#let z = 1
+#test(z, 1)
+
+// Syntax sugar for function definitions.
+#let fill = conifer
+#let f(body) = rect(width: 2cm, fill: fill, inset: 5pt, body)
+#f[Hi!]
+
+--- let-termination ---
+// Termination.
+
+// Terminated by line break.
+#let v1 = 1
+One
+
+// Terminated by semicolon.
+#let v2 = 2; Two
+
+// Terminated by semicolon and line break.
+#let v3 = 3;
+Three
+
+#test(v1, 1)
+#test(v2, 2)
+#test(v3, 3)
+
+--- let-valid-idents ---
+// Test what constitutes a valid Typst identifier.
+#let name = 1
+#test(name, 1)
+#let name_ = 1
+#test(name_, 1)
+#let name-2 = 1
+#test(name-2, 1)
+#let name_2 = 1
+#test(name_2, 1)
+#let __name = 1
+#test(__name, 1)
+#let ůñıćóðė = 1
+#test(ůñıćóðė, 1)
+
+--- let-binding-keyword-in-markup ---
+// Error: 6-8 expected pattern, found keyword `as`
+// Hint: 6-8 keyword `as` is not allowed as an identifier; try `as_` instead
+#let as = 1 + 2
+
+--- let-binding-keyword-in-code ---
+#{
+ // Error: 7-9 expected pattern, found keyword `as`
+ // Hint: 7-9 keyword `as` is not allowed as an identifier; try `as_` instead
+ let as = 10
+}
+
+--- let-ident-parenthesized ---
+// Test parenthesised assignments.
+#let (a) = (1, 2)
+
+--- let-incomplete ---
+// Error: 5 expected pattern
+#let
+
+// Error: 6 expected pattern
+#{let}
+
+// Error: 6-9 expected pattern, found string
+#let "v"
+
+// Error: 7 expected semicolon or line break
+#let v 1
+
+// Error: 9 expected expression
+#let v =
+
+// Error: 6-9 expected pattern, found string
+#let "v" = 1
+
+// Terminated because expression ends.
+// Error: 12 expected semicolon or line break
+#let v4 = 4 Four
+
+// Terminated by semicolon even though we are in a paren group.
+// Error: 18 expected expression
+// Error: 11-12 unclosed delimiter
+#let v5 = (1, 2 + ; Five
+
+// Error: 9-13 expected pattern, found boolean
+#let (..true) = false
+
+--- underscore-invalid ---
+#let _ = 4
+
+#for _ in range(2) []
+
+// Error: 2-3 unexpected underscore
+#_
+
+// Error: 8-9 expected expression, found underscore
+#lorem(_)
+
+// Error: 3-4 expected expression, found underscore
+#(_,)
+
+// Error: 3-4 expected expression, found underscore
+#{_}
+
+// Error: 8-9 expected expression, found underscore
+#{ 1 + _ }
+
+--- let-function-incomplete ---
+// Error: 13 expected equals sign
+#let func(x)
+
+// Error: 15 expected expression
+#let func(x) =
+
+--- let-function-parenthesized ---
+// This is not yet parsed in the ideal way.
+// Error: 12 expected equals sign
+#let (func)(x)
+
+--- let-function-parenthesized-with-init ---
+// These errors aren't great.
+// Error: 12 expected equals sign
+// Error: 15-15 expected semicolon or line break
+#let (func)(x) = 3
+
+--- let-with-no-init-group ---
+// This was unintentionally allowed ...
+// Error: 9 expected equals sign
+#let (a)
+
+--- let-with-no-init-destructuring ---
+// ... where this wasn't.
+// Error: 12 expected equals sign
+#let (a, b)
diff --git a/tests/suite/scripting/loop.typ b/tests/suite/scripting/loop.typ
new file mode 100644
index 0000000000..689c1c9338
--- /dev/null
+++ b/tests/suite/scripting/loop.typ
@@ -0,0 +1,142 @@
+// Test break and continue in loops.
+
+--- loop-break-basic ---
+// Test break.
+
+#let var = 0
+#let error = false
+
+#for i in range(10) {
+ var += i
+ if i > 5 {
+ break
+ error = true
+ }
+}
+
+#test(var, 21)
+#test(error, false)
+
+--- loop-break-join-basic ---
+// Test joining with break.
+
+#let i = 0
+#let x = while true {
+ i += 1
+ str(i)
+ if i >= 5 {
+ "."
+ break
+ }
+}
+
+#test(x, "12345.")
+
+--- loop-continue-basic ---
+// Test continue.
+
+#let i = 0
+#let x = 0
+
+#while x < 8 {
+ i += 1
+ if calc.rem(i, 3) == 0 {
+ continue
+ }
+ x += i
+}
+
+// If continue did not work, this would equal 10.
+#test(x, 12)
+
+--- loop-continue-join ---
+// Test joining with continue.
+
+#let x = for i in range(5) {
+ "a"
+ if calc.rem(i, 3) == 0 {
+ "_"
+ continue
+ }
+ str(i)
+}
+
+#test(x, "a_a1a2a_a4")
+
+--- loop-break-outside-of-loop ---
+// Test break outside of loop.
+#let f() = {
+ // Error: 3-8 cannot break outside of loop
+ break
+}
+
+#for i in range(1) {
+ f()
+}
+
+--- loop-break-join-in-last-arg ---
+// Test break in function call.
+#let identity(x) = x
+#let out = for i in range(5) {
+ "A"
+ identity({
+ "B"
+ break
+ })
+ "C"
+}
+
+#test(out, "AB")
+
+--- loop-continue-outside-of-loop-in-block ---
+// Test continue outside of loop.
+
+// Error: 12-20 cannot continue outside of loop
+#let x = { continue }
+
+--- loop-continue-outside-of-loop-in-markup ---
+// Error: 2-10 cannot continue outside of loop
+#continue
+
+--- loop-break-join-in-nested-blocks ---
+// Should output `Hello World 🌎`.
+#for _ in range(10) {
+ [Hello ]
+ [World #{
+ [🌎]
+ break
+ }]
+}
+
+--- loop-break-join-set-and-show ---
+// Should output `Some` in red, `Some` in blue and `Last` in green.
+// Everything should be in smallcaps.
+#for color in (red, blue, green, yellow) [
+ #set text(font: "Roboto")
+ #show: it => text(fill: color, it)
+ #smallcaps(if color != green [
+ Some
+ ] else [
+ Last
+ #break
+ ])
+]
+
+--- loop-break-join-in-set-rule-args ---
+// Test break in set rule.
+// Should output `Hi` in blue.
+#for i in range(10) {
+ [Hello]
+ set text(blue, ..break)
+ [Not happening]
+}
+
+--- loop-break-join-in-first-arg ---
+// Test second block during break flow.
+
+#for i in range(10) {
+ table(
+ { [A]; break },
+ for _ in range(3) [B]
+ )
+}
diff --git a/tests/suite/scripting/methods.typ b/tests/suite/scripting/methods.typ
new file mode 100644
index 0000000000..5deea2cfad
--- /dev/null
+++ b/tests/suite/scripting/methods.typ
@@ -0,0 +1,51 @@
+// Test method calls.
+
+--- method-whitespace ---
+// Test whitespace around dot.
+#test( "Hi there" . split() , ("Hi", "there"))
+
+--- method-mutating ---
+// Test mutating indexed value.
+#{
+ let matrix = (((1,), (2,)), ((3,), (4,)))
+ matrix.at(1).at(0).push(5)
+ test(matrix, (((1,), (2,)), ((3, 5), (4,))))
+}
+
+--- method-multiline ---
+// Test multiline chain in code block.
+#{
+ let rewritten = "Hello. This is a sentence. And one more."
+ .split(".")
+ .map(s => s.trim())
+ .filter(s => s != "")
+ .map(s => s + "!")
+ .join("\n ")
+
+ test(rewritten, "Hello!\n This is a sentence!\n And one more!")
+}
+
+--- method-unknown ---
+// Error: 2:10-2:13 type array has no method `fun`
+#let numbers = ()
+#numbers.fun()
+
+--- method-unknown-but-field-exists ---
+// Error: 2:4-2:10 type content has no method `stroke`
+// Hint: 2:4-2:10 did you mean to access the field `stroke`?
+#let l = line(stroke: red)
+#l.stroke()
+
+--- method-mutate-on-temporary ---
+// Error: 2:2-2:43 cannot mutate a temporary value
+#let numbers = (1, 2, 3)
+#numbers.map(v => v / 2).sorted().map(str).remove(4)
+
+--- assign-to-method-invalid ---
+// Error: 2:3-2:19 cannot mutate a temporary value
+#let numbers = (1, 2, 3)
+#(numbers.sorted() = 1)
+
+--- method-mutate-on-std-constant ---
+// Error: 2-5 cannot mutate a constant: box
+#box.push(1)
diff --git a/tests/typ/compiler/module.typ b/tests/suite/scripting/module.typ
similarity index 94%
rename from tests/typ/compiler/module.typ
rename to tests/suite/scripting/module.typ
index f06526779e..8a67d22552 100644
--- a/tests/typ/compiler/module.typ
+++ b/tests/suite/scripting/module.typ
@@ -1,5 +1,5 @@
+// SKIP
// A file to import in import / include tests.
-// Ref: false
#let a
#let b = 1
diff --git a/tests/typ/compiler/modules/chap1.typ b/tests/suite/scripting/modules/chap1.typ
similarity index 95%
rename from tests/typ/compiler/modules/chap1.typ
rename to tests/suite/scripting/modules/chap1.typ
index 06a4c1a1a3..13d0acf8a3 100644
--- a/tests/typ/compiler/modules/chap1.typ
+++ b/tests/suite/scripting/modules/chap1.typ
@@ -1,5 +1,4 @@
-// Ref: false
-
+// SKIP
#let name = "Klaus"
== Chapter 1
diff --git a/tests/typ/compiler/modules/chap2.typ b/tests/suite/scripting/modules/chap2.typ
similarity index 96%
rename from tests/typ/compiler/modules/chap2.typ
rename to tests/suite/scripting/modules/chap2.typ
index d4aedc60d8..9c9d12d7f0 100644
--- a/tests/typ/compiler/modules/chap2.typ
+++ b/tests/suite/scripting/modules/chap2.typ
@@ -1,5 +1,4 @@
-// Ref: false
-
+// SKIP
#let name = "Klaus"
== Chapter 2
diff --git a/tests/typ/compiler/modules/cycle1.typ b/tests/suite/scripting/modules/cycle1.typ
similarity index 86%
rename from tests/typ/compiler/modules/cycle1.typ
rename to tests/suite/scripting/modules/cycle1.typ
index 02067b7153..0f924ac731 100644
--- a/tests/typ/compiler/modules/cycle1.typ
+++ b/tests/suite/scripting/modules/cycle1.typ
@@ -1,5 +1,4 @@
-// Ref: false
-
+// SKIP
#import "cycle2.typ": *
#let inaccessible = "wow"
diff --git a/tests/typ/compiler/modules/cycle2.typ b/tests/suite/scripting/modules/cycle2.typ
similarity index 86%
rename from tests/typ/compiler/modules/cycle2.typ
rename to tests/suite/scripting/modules/cycle2.typ
index 191647db26..69eb20337e 100644
--- a/tests/typ/compiler/modules/cycle2.typ
+++ b/tests/suite/scripting/modules/cycle2.typ
@@ -1,5 +1,4 @@
-// Ref: false
-
+// SKIP
#import "cycle1.typ": *
#let val = "much cycle"
diff --git a/tests/suite/scripting/ops.typ b/tests/suite/scripting/ops.typ
new file mode 100644
index 0000000000..0f13d21260
--- /dev/null
+++ b/tests/suite/scripting/ops.typ
@@ -0,0 +1,465 @@
+// Test binary expressions.
+
+--- ops-add-content ---
+// Test adding content.
+#([*Hello* ] + [world!])
+
+--- ops-unary-basic ---
+// Test math operators.
+
+// Test plus and minus.
+#for v in (1, 3.14, 12pt, 45deg, 90%, 13% + 10pt, 6.3fr) {
+ // Test plus.
+ test(+v, v)
+
+ // Test minus.
+ test(-v, -1 * v)
+ test(--v, v)
+
+ // Test combination.
+ test(-++ --v, -v)
+}
+
+#test(-(4 + 2), 6-12)
+
+// Addition.
+#test(2 + 4, 6)
+#test("a" + "b", "ab")
+#test("a" + if false { "b" }, "a")
+#test("a" + if true { "b" }, "ab")
+#test(13 * "a" + "bbbbbb", "aaaaaaaaaaaaabbbbbb")
+#test((1, 2) + (3, 4), (1, 2, 3, 4))
+#test((a: 1) + (b: 2, c: 3), (a: 1, b: 2, c: 3))
+
+--- ops-add-too-large ---
+// Error: 3-26 value is too large
+#(9223372036854775807 + 1)
+
+--- ops-binary-basic ---
+// Subtraction.
+#test(1-4, 3*-1)
+#test(4cm - 2cm, 2cm)
+#test(1e+2-1e-2, 99.99)
+
+// Multiplication.
+#test(2 * 4, 8)
+
+// Division.
+#test(12pt/.4, 30pt)
+#test(7 / 2, 3.5)
+
+// Combination.
+#test(3-4 * 5 < -10, true)
+#test({ let x; x = 1 + 4*5 >= 21 and { x = "a"; x + "b" == "ab" }; x }, true)
+
+// With block.
+#test(if true {
+ 1
+} + 2, 3)
+
+// Mathematical identities.
+#let nums = (
+ 1, 3.14,
+ 12pt, 3em, 12pt + 3em,
+ 45deg,
+ 90%,
+ 13% + 10pt, 5% + 1em + 3pt,
+ 2.3fr,
+)
+
+#for v in nums {
+ // Test plus and minus.
+ test(v + v - v, v)
+ test(v - v - v, -v)
+
+ // Test plus/minus and multiplication.
+ test(v - v, 0 * v)
+ test(v + v, 2 * v)
+
+ // Integer addition does not give a float.
+ if type(v) != int {
+ test(v + v, 2.0 * v)
+ }
+
+ if type(v) != relative and ("pt" not in repr(v) or "em" not in repr(v)) {
+ test(v / v, 1.0)
+ }
+}
+
+// Make sure length, ratio and relative length
+// - can all be added to / subtracted from each other,
+// - multiplied with integers and floats,
+// - divided by integers and floats.
+#let dims = (10pt, 1em, 10pt + 1em, 30%, 50% + 3cm, 40% + 2em + 1cm)
+#for a in dims {
+ for b in dims {
+ test(type(a + b), type(a - b))
+ }
+
+ for b in (7, 3.14) {
+ test(type(a * b), type(a))
+ test(type(b * a), type(a))
+ test(type(a / b), type(a))
+ }
+}
+
+// Test division of different numeric types with zero components.
+#for a in (0pt, 0em, 0%) {
+ for b in (10pt, 10em, 10%) {
+ test((2 * b) / b, 2)
+ test((a + b * 2) / b, 2)
+ test(b / (b * 2 + a), 0.5)
+ }
+}
+
+--- ops-multiply-inf-with-length ---
+// Test that multiplying infinite numbers by certain units does not crash.
+#(float("inf") * 1pt)
+#(float("inf") * 1em)
+#(float("inf") * (1pt + 1em))
+
+--- ops-attempt-nan-length ---
+// Test that trying to produce a NaN scalar (such as in lengths) does not crash.
+#let infpt = float("inf") * 1pt
+#test(infpt - infpt, 0pt)
+#test(infpt + (-infpt), 0pt)
+// TODO: this result is surprising
+#test(infpt / float("inf"), 0pt)
+
+--- ops-unary-bool ---
+// Test boolean operators.
+
+// Test not.
+#test(not true, false)
+#test(not false, true)
+
+// And.
+#test(false and false, false)
+#test(false and true, false)
+#test(true and false, false)
+#test(true and true, true)
+
+// Or.
+#test(false or false, false)
+#test(false or true, true)
+#test(true or false, true)
+#test(true or true, true)
+
+// Short-circuiting.
+#test(false and dont-care, false)
+#test(true or dont-care, true)
+
+--- ops-equality ---
+// Test equality operators.
+
+// Most things compare by value.
+#test(1 == "hi", false)
+#test(1 == 1.0, true)
+#test(30% == 30% + 0cm, true)
+#test(1in == 0% + 72pt, true)
+#test(30% == 30% + 1cm, false)
+#test("ab" == "a" + "b", true)
+#test(() == (1,), false)
+#test((1, 2, 3) == (1, 2.0) + (3,), true)
+#test((:) == (a: 1), false)
+#test((a: 2 - 1.0, b: 2) == (b: 2, a: 1), true)
+#test("a" != "a", false)
+
+// Functions compare by identity.
+#test(test == test, true)
+#test((() => {}) == (() => {}), false)
+
+// Content compares field by field.
+#let t = [a]
+#test(t == t, true)
+#test([] == [], true)
+#test([a] == [a], true)
+#test(grid[a] == grid[a], true)
+#test(grid[a] == grid[b], false)
+
+--- ops-compare ---
+// Test comparison operators.
+
+#test(13 * 3 < 14 * 4, true)
+#test(5 < 10, true)
+#test(5 > 5, false)
+#test(5 <= 5, true)
+#test(5 <= 4, false)
+#test(45deg < 1rad, true)
+#test(10% < 20%, true)
+#test(50% < 40% + 0pt, false)
+#test(40% + 0pt < 50% + 0pt, true)
+#test(1em < 2em, true)
+#test((0, 1, 2, 4) < (0, 1, 2, 5), true)
+#test((0, 1, 2, 4) < (0, 1, 2, 3), false)
+#test((0, 1, 2, 3.3) > (0, 1, 2, 4), false)
+#test((0, 1, 2) < (0, 1, 2, 3), true)
+#test((0, 1, "b") > (0, 1, "a", 3), true)
+#test((0, 1.1, 3) >= (0, 1.1, 3), true)
+#test((0, 1, datetime(day: 1, month: 12, year: 2023)) <= (0, 1, datetime(day: 1, month: 12, year: 2023), 3), true)
+#test(("a", 23, 40, "b") > ("a", 23, 40), true)
+#test(() <= (), true)
+#test(() >= (), true)
+#test(() <= (1,), true)
+#test((1,) <= (), false)
+
+--- ops-in ---
+// Test `in` operator.
+#test("hi" in "worship", true)
+#test("hi" in ("we", "hi", "bye"), true)
+#test("Hey" in "abHeyCd", true)
+#test("Hey" in "abheyCd", false)
+#test(5 in range(10), true)
+#test(12 in range(10), false)
+#test("" in (), false)
+#test("key" in (key: "value"), true)
+#test("value" in (key: "value"), false)
+#test("Hey" not in "abheyCd", true)
+#test("a" not
+/* fun comment? */ in "abc", false)
+
+--- ops-not-trailing ---
+// Error: 10 expected keyword `in`
+#("a" not)
+
+--- func-with ---
+// Test `with` method.
+
+// Apply positional arguments.
+#let add(x, y) = x + y
+#test(add.with(2)(3), 5)
+#test(add.with(2, 3)(), 5)
+#test(add.with(2).with(3)(), 5)
+#test((add.with(2))(4), 6)
+#test((add.with(2).with(3))(), 5)
+
+// Make sure that named arguments are overridable.
+#let inc(x, y: 1) = x + y
+#test(inc(1), 2)
+
+#let inc2 = inc.with(y: 2)
+#test(inc2(2), 4)
+#test(inc2(2, y: 4), 6)
+
+// Apply arguments to an argument sink.
+#let times(..sink) = {
+ let res = sink.pos().product()
+ if sink.named().at("negate", default: false) { res *= -1 }
+ res
+}
+#test((times.with(2, negate: true).with(5))(), -10)
+#test((times.with(2).with(5).with(negate: true))(), -10)
+#test((times.with(2).with(5, negate: true))(), -10)
+#test((times.with(2).with(negate: true))(5), -10)
+
+--- ops-precedence-basic ---
+// Multiplication binds stronger than addition.
+#test(1+2*-3, -5)
+
+// Subtraction binds stronger than comparison.
+#test(3 == 5 - 2, true)
+
+// Boolean operations bind stronger than '=='.
+#test("a" == "a" and 2 < 3, true)
+#test(not "b" == "b", false)
+
+--- ops-precedence-boolean-ops ---
+// Assignment binds stronger than boolean operations.
+// Error: 2:3-2:8 cannot mutate a temporary value
+#let x = false
+#(not x = "a")
+
+--- ops-precedence-unary ---
+// Precedence doesn't matter for chained unary operators.
+// Error: 3-12 cannot apply '-' to boolean
+#(-not true)
+
+--- ops-precedence-not-in ---
+// Not in handles precedence.
+#test(-1 not in (1, 2, 3), true)
+
+--- ops-precedence-parentheses ---
+// Parentheses override precedence.
+#test((1), 1)
+#test((1+2)*-3, -9)
+
+// Error: 8-9 unclosed delimiter
+#test({(1 + 1}, 2)
+
+--- ops-associativity-left ---
+// Math operators are left-associative.
+#test(10 / 2 / 2 == (10 / 2) / 2, true)
+#test(10 / 2 / 2 == 10 / (2 / 2), false)
+#test(1 / 2 * 3, 1.5)
+
+--- ops-associativity-right ---
+// Assignment is right-associative.
+#{
+ let x = 1
+ let y = 2
+ x = y = "ok"
+ test(x, none)
+ test(y, "ok")
+}
+
+--- ops-unary-minus-missing-expr ---
+// Error: 4 expected expression
+#(-)
+
+--- ops-add-missing-rhs ---
+// Error: 10 expected expression
+#test({1+}, 1)
+
+--- ops-mul-missing-rhs ---
+// Error: 10 expected expression
+#test({2*}, 2)
+
+--- ops-unary-plus-on-content ---
+// Error: 3-13 cannot apply unary '+' to content
+#(+([] + []))
+
+--- ops-unary-plus-on-string ---
+// Error: 3-6 cannot apply '-' to string
+#(-"")
+
+--- ops-not-on-array ---
+// Error: 3-9 cannot apply 'not' to array
+#(not ())
+
+--- ops-compare-relative-length-and-ratio ---
+// Error: 3-19 cannot compare relative length and ratio
+#(30% + 1pt <= 40%)
+
+--- ops-compare-em-with-abs ---
+// Error: 3-14 cannot compare 1em with 10pt
+#(1em <= 10pt)
+
+--- ops-compare-normal-float-with-nan ---
+// Error: 3-22 cannot compare 2.2 with NaN
+#(2.2 <= float("nan"))
+
+--- ops-compare-int-and-str ---
+// Error: 3-26 cannot compare integer and string
+#((0, 1, 3) > (0, 1, "a"))
+
+--- ops-compare-array-nested-failure ---
+// Error: 3-42 cannot compare 3.5 with NaN
+#((0, "a", 3.5) <= (0, "a", float("nan")))
+
+--- ops-divide-by-zero-float ---
+// Error: 3-12 cannot divide by zero
+#(1.2 / 0.0)
+
+--- ops-divide-by-zero-int ---
+// Error: 3-8 cannot divide by zero
+#(1 / 0)
+
+--- ops-divide-by-zero-angle ---
+// Error: 3-15 cannot divide by zero
+#(15deg / 0deg)
+
+--- ops-binary-arithmetic-error-message ---
+// Special messages for +, -, * and /.
+// Error: 3-10 cannot add integer and string
+#(1 + "2", 40% - 1)
+
+--- add-assign-int-and-str ---
+// Error: 15-23 cannot add integer and string
+#{ let x = 1; x += "2" }
+
+--- ops-divide-ratio-by-length ---
+// Error: 4-13 cannot divide ratio by length
+#( 10% / 5pt )
+
+--- ops-divide-em-by-abs ---
+// Error: 3-12 cannot divide these two lengths
+#(1em / 5pt)
+
+--- ops-divide-relative-length-by-ratio ---
+// Error: 3-19 cannot divide relative length by ratio
+#((10% + 1pt) / 5%)
+
+--- ops-divide-relative-lengths ---
+// Error: 3-28 cannot divide these two relative lengths
+#((10% + 1pt) / (20% + 1pt))
+
+--- ops-subtract-int-from-ratio ---
+// Error: 13-20 cannot subtract integer from ratio
+#((1234567, 40% - 1))
+
+--- ops-multiply-int-with-bool ---
+// Error: 3-11 cannot multiply integer with boolean
+#(2 * true)
+
+--- ops-divide-int-by-length ---
+// Error: 3-11 cannot divide integer by length
+#(3 / 12pt)
+
+--- multiply-negative-int-with-str ---
+// Error: 3-10 number must be at least zero
+#(-1 * "")
+
+--- ops-assign ---
+// Test assignment operators.
+
+#let x = 0
+#(x = 10) #test(x, 10)
+#(x -= 5) #test(x, 5)
+#(x += 1) #test(x, 6)
+#(x *= x) #test(x, 36)
+#(x /= 2.0) #test(x, 18.0)
+#(x = "some") #test(x, "some")
+#(x += "thing") #test(x, "something")
+
+--- ops-assign-unknown-var-lhs ---
+#{
+ // Error: 3-6 unknown variable: a-1
+ // Hint: 3-6 if you meant to use subtraction, try adding spaces around the minus sign
+ a-1 = 2
+}
+
+--- ops-assign-unknown-var-rhs ---
+#{
+ let a = 2
+ a = 1-a
+ a = a -1
+
+ // Error: 7-10 unknown variable: a-1
+ // Hint: 7-10 if you meant to use subtraction, try adding spaces around the minus sign
+ a = a-1
+}
+
+--- ops-assign-unknown-parenthesized-variable ---
+// Error: 4-5 unknown variable: x
+#((x) = "")
+
+--- ops-assign-destructuring-unknown-variable ---
+// Error: 4-5 unknown variable: x
+#((x,) = (1,))
+
+--- ops-assign-to-temporary ---
+// Error: 3-8 cannot mutate a temporary value
+#(1 + 2 += 3)
+
+--- ops-assign-to-invalid-unary-op ---
+// Error: 2:3-2:8 cannot apply 'not' to string
+#let x = "Hey"
+#(not x = "a")
+
+--- ops-assign-to-invalid-binary-op ---
+// Error: 7-8 unknown variable: x
+#(1 + x += 3)
+
+--- ops-assign-unknown-variable ---
+// Error: 3-4 unknown variable: z
+#(z = 1)
+
+--- ops-assign-to-std-constant ---
+// Error: 3-7 cannot mutate a constant: rect
+#(rect = "hi")
+
+--- ops-assign-to-shadowed-std-constant ---
+// Works if we define rect beforehand
+// (since then it doesn't resolve to the standard library version anymore).
+#let rect = ""
+#(rect = "hi")
diff --git a/tests/suite/scripting/params.typ b/tests/suite/scripting/params.typ
new file mode 100644
index 0000000000..688124f206
--- /dev/null
+++ b/tests/suite/scripting/params.typ
@@ -0,0 +1,69 @@
+--- param-underscore-missing-argument ---
+// Error: 17-20 missing argument: pattern parameter
+#let f(a: 10) = a() + 1
+#f(a: _ => 5)
+
+--- params-sink-named ---
+// ... but this was.
+#let f(..x) = {}
+#f(arg: 1)
+
+--- params-sink-unnamed ---
+// unnamed spread
+#let f(.., a) = a
+#test(f(1, 2, 3), 3)
+
+// This wasn't allowed before the bug fix ...
+#let f(..) = 2
+#test(f(arg: 1), 2)
+
+--- params-sink-bool-invalid ---
+// Error: 10-14 expected pattern, found boolean
+#let f(..true) = none
+
+--- params-sink-multiple-invalid ---
+// Error: 13-16 only one argument sink is allowed
+#let f(..a, ..b) = none
+
+--- params-sink-at-start ---
+// Spread at beginning.
+#{
+ let f(..a, b) = (a, b)
+ test(repr(f(1)), "((), 1)")
+ test(repr(f(1, 2, 3)), "((1, 2), 3)")
+ test(repr(f(1, 2, 3, 4, 5)), "((1, 2, 3, 4), 5)")
+}
+
+--- params-sink-in-middle ---
+// Spread in the middle.
+#{
+ let f(a, ..b, c) = (a, b, c)
+ test(repr(f(1, 2)), "(1, (), 2)")
+ test(repr(f(1, 2, 3, 4, 5)), "(1, (2, 3, 4), 5)")
+}
+
+--- params-sink-unnamed-empty ---
+// Unnamed sink should just ignore any extra arguments.
+#{
+ let f(a, b: 5, ..) = (a, b)
+ test(f(4), (4, 5))
+ test(f(10, b: 11), (10, 11))
+ test(f(13, 20, b: 12), (13, 12))
+ test(f(15, b: 16, c: 13), (15, 16))
+}
+
+--- params-sink-missing-arguments ---
+#{
+ let f(..a, b, c, d) = none
+
+ // Error: 3-10 missing argument: d
+ f(1, 2)
+}
+
+--- issue-1029-parameter-destructuring ---
+// Test that underscore works in parameter patterns.
+#test((1, 2, 3).zip((1, 2, 3)).map(((_, x)) => x), (1, 2, 3))
+
+--- issue-1351-parameter-dictionary ---
+// Error: 17-22 expected pattern, found string
+#let foo((test: "bar")) = {}
diff --git a/tests/typ/compiler/recursion.typ b/tests/suite/scripting/recursion.typ
similarity index 78%
rename from tests/typ/compiler/recursion.typ
rename to tests/suite/scripting/recursion.typ
index 421b638b0a..43fe848e08 100644
--- a/tests/typ/compiler/recursion.typ
+++ b/tests/suite/scripting/recursion.typ
@@ -1,7 +1,6 @@
// Test recursive function calls.
-// Ref: false
----
+--- recursion-named ---
// Test with named function.
#let fib(n) = {
if n <= 2 {
@@ -13,42 +12,42 @@
#test(fib(10), 55)
----
+--- recursion-unnamed-invalid ---
// Test with unnamed function.
// Error: 17-18 unknown variable: f
#let f = (n) => f(n - 1)
#f(10)
----
+--- recursion-named-returns-itself ---
// Test capturing with named function.
#let f = 10
#let f() = f
#test(type(f()), function)
----
+--- recursion-unnamed-does-not-return-itself ---
// Test capturing with unnamed function.
#let f = 10
#let f = () => f
#test(type(f()), int)
----
+--- recursion-shadowing ---
// Test redefinition.
#let f(x) = "hello"
#let f(x) = if x != none { f(none) } else { "world" }
#test(f(1), "world")
----
+--- recursion-maximum-depth ---
// Error: 15-21 maximum function call depth exceeded
#let rec(n) = rec(n) + 1
#rec(1)
----
+--- recursion-via-include-in-layout ---
// Test cyclic imports during layout.
// Error: 2-38 maximum show rule depth exceeded
// Hint: 2-38 check whether the show rule matches its own output
#layout(_ => include "recursion.typ")
----
+--- recursion-show-math ---
// Test recursive show rules.
// Error: 22-25 maximum show rule depth exceeded
// Hint: 22-25 check whether the show rule matches its own output
diff --git a/tests/typ/compiler/return.typ b/tests/suite/scripting/return.typ
similarity index 83%
rename from tests/typ/compiler/return.typ
rename to tests/suite/scripting/return.typ
index e709d6a7cf..63e1c0b991 100644
--- a/tests/typ/compiler/return.typ
+++ b/tests/suite/scripting/return.typ
@@ -1,7 +1,6 @@
// Test return out of functions.
-// Ref: false
----
+--- return-with-value ---
// Test return with value.
#let f(x) = {
return x + 1
@@ -9,7 +8,7 @@
#test(f(1), 2)
----
+--- return-join ---
// Test return with joining.
#let f(x) = {
@@ -29,9 +28,8 @@
#test(f(1), "ac")
#test(f(2), "ad")
----
+--- return-in-nested-content-block ---
// Test return with joining and content.
-// Ref: true
#let f(text, caption: none) = {
text
@@ -45,7 +43,7 @@
#f[My other figure]
----
+--- return-outside-of-function ---
// Test return outside of function.
#for x in range(5) {
@@ -53,7 +51,7 @@
return
}
----
+--- return-in-first-arg ---
// Test that the expression is evaluated to the end.
#let sum(..args) = {
let s = 0
@@ -70,7 +68,7 @@
#test(f(), 6)
----
+--- return-in-content-block ---
// Test value return from content.
#let x = 3
#let f() = [
@@ -81,7 +79,7 @@
#test(f(), "nope")
----
+--- return-semicolon-or-linebreak ---
// Test rejection of extra value
#let f() = [
// Error: 16-16 expected semicolon or line break
diff --git a/tests/typ/compiler/while.typ b/tests/suite/scripting/while.typ
similarity index 80%
rename from tests/typ/compiler/while.typ
rename to tests/suite/scripting/while.typ
index 56409c6e29..5e452a89de 100644
--- a/tests/typ/compiler/while.typ
+++ b/tests/suite/scripting/while.typ
@@ -1,6 +1,6 @@
// Test while expressions.
----
+--- while-loop-basic ---
// Should output `2 4 6 8 10`.
#let i = 0
#while i < 10 [
@@ -19,30 +19,29 @@
dont-care
}
----
+--- while-loop-expr ---
// Value of while loops.
-// Ref: false
#test(while false {}, none)
#let i = 0
#test(type(while i < 1 [#(i += 1)]), content)
----
+--- while-loop-condition-content-invalid ---
// Condition must be boolean.
// Error: 8-14 expected boolean, found content
#while [nope] [nope]
----
+--- while-loop-condition-always-true ---
// Error: 8-25 condition is always true
#while 2 < "hello".len() {}
----
+--- while-loop-limit ---
// Error: 2:2-2:24 loop seems to be infinite
#let i = 1
#while i > 0 { i += 1 }
----
+--- while-loop-incomplete ---
// Error: 7 expected expression
#while
diff --git a/tests/suite/styling/fold.typ b/tests/suite/styling/fold.typ
new file mode 100644
index 0000000000..26fe991b2a
--- /dev/null
+++ b/tests/suite/styling/fold.typ
@@ -0,0 +1,19 @@
+--- fold-vec-order-text-features ---
+// Test fold order of vectors.
+#set text(features: (liga: 1))
+#set text(features: (liga: 0))
+fi
+
+--- fold-vec-order-text-decos ---
+#underline(stroke: aqua + 4pt)[
+ #underline[Hello]
+]
+
+--- fold-vec-order-meta ---
+#let c = counter("mycounter")
+#c.update(1)
+#locate(loc => [
+ #c.update(2)
+ #c.at(loc) \
+ Second: #locate(loc => c.at(loc))
+])
diff --git a/tests/suite/styling/set.typ b/tests/suite/styling/set.typ
new file mode 100644
index 0000000000..a31cd1650e
--- /dev/null
+++ b/tests/suite/styling/set.typ
@@ -0,0 +1,96 @@
+// General tests for set.
+
+--- set-instantiation-site ---
+// Test that text is affected by instantiation-site bold.
+#let x = [World]
+Hello *#x*
+
+--- set-instantiation-site-markup ---
+// Test that lists are affected by correct indents.
+#let fruit = [
+ - Apple
+ - Orange
+ #list(body-indent: 20pt)[Pear]
+]
+
+- Fruit
+#[#set list(indent: 10pt)
+ #fruit]
+- No more fruit
+
+--- set-text-override ---
+// Test that that block spacing and text style are respected from
+// the outside, but the more specific fill is respected.
+#set block(spacing: 4pt)
+#set text(style: "italic", fill: eastern)
+#let x = [And the forest #parbreak() lay silent!]
+#text(fill: forest, x)
+
+--- set-scoped-in-code-block ---
+// Test that scoping works as expected.
+#{
+ if true {
+ set text(blue)
+ [Blue ]
+ }
+ [Not blue]
+}
+
+--- closure-path-resolve-in-layout-phase ---
+// Test relative path resolving in layout phase.
+#let choice = ("monkey.svg", "rhino.png", "tiger.jpg")
+#set enum(numbering: n => {
+ let path = "/assets/images/" + choice.at(n - 1)
+ move(dy: -0.15em, image(path, width: 1em, height: 1em))
+})
+
++ Monkey
++ Rhino
++ Tiger
+
+--- set-if ---
+// Test conditional set.
+#show ref: it => {
+ set text(red) if it.target ==
+ "@" + str(it.target)
+}
+
+@hello from the @unknown
+
+--- set-if-bad-type ---
+// Error: 19-24 expected boolean, found integer
+#set text(red) if 1 + 2
+
+--- set-in-expr ---
+// Error: 12-26 set is only allowed directly in code and content blocks
+#{ let x = set text(blue) }
+
+--- set-vs-construct-1 ---
+// Ensure that constructor styles aren't passed down the tree.
+// The inner list should have no extra indent.
+#set par(leading: 2pt)
+#list(body-indent: 20pt, [First], list[A][B])
+
+--- set-vs-construct-2 ---
+// Ensure that constructor styles win, but not over outer styles.
+// The outer paragraph should be right-aligned,
+// but the B should be center-aligned.
+#set list(marker: [>])
+#list(marker: [--])[
+ #rect(width: 2cm, fill: conifer, inset: 4pt, list[A])
+]
+
+--- set-vs-construct-3 ---
+// The inner rectangle should also be yellow here.
+// (and therefore invisible)
+#[#set rect(fill: yellow);#text(1em, rect(inset: 5pt, rect()))]
+
+--- set-vs-construct-4 ---
+// The inner rectangle should not be yellow here.
+A #box(rect(fill: yellow, inset: 5pt, rect())) B
+
+--- show-set-vs-construct ---
+// The constructor property should still work
+// when there are recursive show rules.
+#show enum: set text(blue)
+#enum(numbering: "(a)", [A], enum[B])
diff --git a/tests/suite/styling/show-set.typ b/tests/suite/styling/show-set.typ
new file mode 100644
index 0000000000..ea788c9385
--- /dev/null
+++ b/tests/suite/styling/show-set.typ
@@ -0,0 +1,70 @@
+// Test show-set rules.
+
+--- show-set-override ---
+// Test overriding show-set rules.
+#show strong: set text(red)
+Hello *World*
+
+#show strong: set text(blue)
+Hello *World*
+
+--- show-set-on-same-element ---
+// Test show-set rule on the same element.
+#set figure(supplement: [Default])
+#show figure.where(kind: table): set figure(supplement: [Tableau])
+#figure(
+ table(columns: 2)[A][B][C][D],
+ caption: [Four letters],
+)
+
+--- show-set-same-element-and-order ---
+// Test both things at once.
+#show heading: set text(red)
+= Level 1
+== Level 2
+
+#show heading.where(level: 1): set text(blue)
+#show heading.where(level: 1): set text(green)
+#show heading.where(level: 1): set heading(numbering: "(I)")
+= Level 1
+== Level 2
+
+--- show-set-same-element-matched-field ---
+// Test setting the thing we just matched on.
+// This is quite cursed, but it works.
+#set heading(numbering: "(I)")
+#show heading.where(numbering: "(I)"): set heading(numbering: "1.")
+= Heading
+
+--- show-set-same-element-synthesized-matched-field ---
+// Same thing, but even more cursed, because `kind` is synthesized.
+#show figure.where(kind: table): set figure(kind: raw)
+#figure(table[A], caption: [Code])
+
+--- show-set-same-element-matching-interaction ---
+// Test that show-set rules on the same element don't affect each other. This
+// could be implemented, but isn't as of yet.
+#show heading.where(level: 1): set heading(numbering: "(I)")
+#show heading.where(numbering: "(I)"): set text(red)
+= Heading
+
+--- show-set-on-layoutable-element ---
+// Test show-set rules on layoutable element to ensure it is realized
+// even though it implements `LayoutMultiple`.
+#show table: set text(red)
+#pad(table(columns: 4)[A][B][C][D])
+
+--- show-function-order-with-set ---
+// These are both red because in the expanded form, `set text(red)` ends up
+// closer to the content than `set text(blue)`.
+#show strong: it => { set text(red); it }
+Hello *World*
+
+#show strong: it => { set text(blue); it }
+Hello *World*
+
+--- show-function-set-on-it ---
+// This doesn't have an effect. An element is materialized before any show
+// rules run.
+#show heading: it => { set heading(numbering: "(I)"); it }
+= Heading
diff --git a/tests/suite/styling/show-text.typ b/tests/suite/styling/show-text.typ
new file mode 100644
index 0000000000..56b659b243
--- /dev/null
+++ b/tests/suite/styling/show-text.typ
@@ -0,0 +1,133 @@
+// Test text replacement show rules.
+
+--- show-text-basic ---
+// Test classic example.
+#set text(font: "Roboto")
+#show "Der Spiegel": smallcaps
+Die Zeitung Der Spiegel existiert.
+
+--- show-text-regex ---
+// Another classic example.
+#show "TeX": [T#h(-0.145em)#box(move(dy: 0.233em)[E])#h(-0.135em)X]
+#show regex("(Lua)?(La)?TeX"): name => box(text(font: "New Computer Modern")[#name])
+
+TeX, LaTeX, LuaTeX and LuaLaTeX!
+
+--- show-text-cyclic ---
+// Test direct cycle.
+#show "Hello": text(red)[Hello]
+Hello World!
+
+--- show-text-cyclic-raw ---
+// Test replacing text with raw text.
+#show "rax": `rax`
+The register rax.
+
+--- show-text-indirectly-cyclic ---
+// Test indirect cycle.
+#show "Good": [Typst!]
+#show "Typst": [Fun!]
+#show "Fun": [Good!]
+
+#set text(ligatures: false)
+Good \
+Fun \
+Typst \
+
+--- show-text-exactly-once ---
+// Test that replacements happen exactly once.
+#show "A": [BB]
+#show "B": [CC]
+AA (8)
+
+--- show-text-regex-word-boundary ---
+// Test caseless match and word boundaries.
+#show regex("(?i)\bworld\b"): [🌍]
+
+Treeworld, the World of worlds, is a world.
+
+--- show-text-empty ---
+// Test there is no crashing on empty strings
+// Error: 1:7-1:9 text selector is empty
+#show "": []
+
+--- show-text-regex-empty ---
+// Error: 1:7-1:16 regex selector is empty
+#show regex(""): [AA]
+
+--- show-text-regex-matches-empty ---
+// Error: 1:7-1:42 regex matches empty text
+#show regex("(VAR_GLOBAL|END_VAR||BOOL)") : []
+
+--- show-text-regex-character-class ---
+// This is a fun one.
+#set par(justify: true)
+#show regex("\S"): letter => box(stroke: 1pt, inset: 2pt, upper(letter))
+#lorem(5)
+
+--- show-text-regex-case-insensitive ---
+// See also: https://github.com/mTvare6/hello-world.rs
+#show regex("(?i)rust"): it => [#it (🚀)]
+Rust is memory-safe and blazingly fast. Let's rewrite everything in rust.
+
+--- show-text-get-text-on-it ---
+// Test accessing the string itself.
+#show "hello": it => it.text.split("").map(upper).join("|")
+Oh, hello there!
+
+--- show-text-in-other-show ---
+// Replace worlds but only in lists.
+#show list: it => [
+ #show "World": [🌎]
+ #it
+]
+
+World
+- World
+
+--- show-text-path-resolving ---
+// Test absolute path in layout phase.
+
+#show "GRAPH": image("/assets/images/graph.png")
+
+The GRAPH has nodes.
+
+--- show-set-text-order-adjacent-1 ---
+#show "He": set text(red)
+#show "ya": set text(blue)
+Heya
+
+--- show-set-text-order-contained-1 ---
+#show "Heya": set text(red)
+#show "ya": set text(blue)
+Heya
+
+--- show-set-text-order-contained-3 ---
+#show "He": set text(red)
+#show "Heya": set text(blue)
+Heya
+
+--- show-set-text-order-overlapping-1 ---
+#show "Heya": set text(red)
+#show "yaho": set text(blue)
+Heyaho
+
+--- show-set-text-order-adjacent-2 ---
+#show "He": set text(red)
+#show "ya": set text(weight: "bold")
+Heya
+
+--- show-set-text-order-contained-2 ---
+#show "Heya": set text(red)
+#show "ya": set text(weight: "bold")
+Heya
+
+--- show-set-text-order-contained-4 ---
+#show "He": set text(red)
+#show "Heya": set text(weight: "bold")
+Heya
+
+--- show-set-text-order-overlapping-2 ---
+#show "Heya": set text(red)
+#show "yaho": set text(weight: "bold")
+Heyaho
diff --git a/tests/suite/styling/show-where.typ b/tests/suite/styling/show-where.typ
new file mode 100644
index 0000000000..72dbae69c8
--- /dev/null
+++ b/tests/suite/styling/show-where.typ
@@ -0,0 +1,89 @@
+--- show-where-optional-field-raw ---
+// Test that where selectors also trigger on set rule fields.
+#show raw.where(block: false): box.with(
+ fill: luma(220),
+ inset: (x: 3pt, y: 0pt),
+ outset: (y: 3pt),
+ radius: 2pt,
+)
+
+This is #raw("fn main() {}") some text.
+
+--- show-where-optional-field-text ---
+// Note: This show rule is horribly inefficient because it triggers for
+// every individual text element. But it should still work.
+#show text.where(lang: "de"): set text(red)
+
+#set text(lang: "es")
+Hola, mundo!
+
+#set text(lang: "de")
+Hallo Welt!
+
+#set text(lang: "en")
+Hello World!
+
+--- show-where-folding-text-size ---
+// Test that folding is taken into account.
+#set text(5pt)
+#set text(2em)
+
+#[
+ #show text.where(size: 2em): set text(blue)
+ 2em not blue
+]
+
+#[
+ #show text.where(size: 10pt): set text(blue)
+ 10pt blue
+]
+
+--- show-where-folding-stroke ---
+// Test again that folding is taken into account.
+#set rect(width: 40pt, height: 10pt)
+#set rect(stroke: blue)
+#set rect(stroke: 2pt)
+
+#{
+ show rect.where(stroke: blue): "Not Triggered"
+ rect()
+}
+#{
+ show rect.where(stroke: 2pt): "Not Triggered"
+ rect()
+}
+#{
+ show rect.where(stroke: 2pt + blue): "Triggered"
+ rect()
+}
+
+--- show-where-resolving-length ---
+// Test that resolving is *not* taken into account.
+#set line(start: (1em, 1em + 2pt))
+
+#{
+ show line.where(start: (1em, 1em + 2pt)): "Triggered"
+ line()
+}
+#{
+ show line.where(start: (10pt, 12pt)): "Not Triggered"
+ line()
+}
+
+
+--- show-where-resolving-hyphenate ---
+// Test again that resolving is *not* taken into account.
+#set text(hyphenate: auto)
+
+#[
+ #show text.where(hyphenate: auto): underline
+ Auto
+]
+#[
+ #show text.where(hyphenate: true): underline
+ True
+]
+#[
+ #show text.where(hyphenate: false): underline
+ False
+]
diff --git a/tests/suite/styling/show.typ b/tests/suite/styling/show.typ
new file mode 100644
index 0000000000..aa121bffba
--- /dev/null
+++ b/tests/suite/styling/show.typ
@@ -0,0 +1,262 @@
+// Test show rules.
+
+--- show-selector-basic ---
+// Override lists.
+#show list: it => "(" + it.children.map(v => v.body).join(", ") + ")"
+
+- A
+ - B
+ - C
+- D
+- E
+
+--- show-selector-replace-and-show-set ---
+// Test full reset.
+#show heading: [B]
+#show heading: set text(size: 10pt, weight: 400)
+A #[= Heading] C
+
+--- show-selector-discard ---
+// Test full removal.
+#show heading: none
+
+Where is
+= There are no headings around here!
+my heading?
+
+--- show-selector-realistic ---
+// Test integrated example.
+#show heading: it => block({
+ set text(10pt)
+ box(move(dy: -1pt)[📖])
+ h(5pt)
+ if it.level == 1 {
+ underline(text(1.25em, blue, it.body))
+ } else {
+ text(red, it.body)
+ }
+})
+
+= Task 1
+Some text.
+
+== Subtask
+Some more text.
+
+= Task 2
+Another text.
+
+--- show-in-show ---
+// Test set and show in code blocks.
+#show heading: it => {
+ set text(red)
+ show "ding": [🛎]
+ it.body
+}
+
+= Heading
+
+--- show-nested-scopes ---
+// Test that scoping works as expected.
+#{
+ let world = [ World ]
+ show "W": strong
+ world
+ {
+ set text(blue)
+ show: it => {
+ show "o": "Ø"
+ it
+ }
+ world
+ }
+ world
+}
+
+--- show-selector-replace ---
+#show heading: [1234]
+= Heading
+
+--- show-unknown-field ---
+// Error: 25-29 content does not contain field "page"
+#show heading: it => it.page
+= Heading
+
+--- show-text-element-discard ---
+#show text: none
+Hey
+
+--- show-selector-not-an-element-function ---
+// Error: 7-12 only element functions can be used as selectors
+#show upper: it => {}
+
+--- show-bad-replacement-type ---
+// Error: 16-20 expected content or function, found integer
+#show heading: 1234
+= Heading
+
+--- show-bad-selector-type ---
+// Error: 7-10 expected symbol, string, label, function, regex, or selector, found color
+#show red: []
+
+--- show-selector-in-expression ---
+// Error: 7-25 show is only allowed directly in code and content blocks
+#(1 + show heading: none)
+
+--- show-bare-basic ---
+#set page(height: 130pt)
+#set text(0.7em)
+
+#align(center)[
+ #text(1.3em)[*Essay on typography*] \
+ T. Ypst
+]
+
+#show: columns.with(2)
+Great typography is at the essence of great storytelling. It is the medium that
+transports meaning from parchment to reader, the wave that sparks a flame
+in booklovers and the great fulfiller of human need.
+
+--- show-bare-content-block ---
+// Test bare show in content block.
+A #[_B #show: c => [*#c*]; C_] D
+
+--- show-bare-vs-set-text ---
+// Test style precedence.
+#set text(fill: eastern, size: 1.5em)
+#show: text.with(fill: forest)
+Forest
+
+--- show-bare-replace-with-content ---
+#show: [Shown]
+Ignored
+
+--- show-bare-in-expression ---
+// Error: 4-19 show is only allowed directly in code and content blocks
+#((show: body => 2) * body)
+
+--- show-bare-missing-colon-closure ---
+// Error: 6 expected colon
+#show it => {}
+
+--- show-bare-missing-colon ---
+// Error: 6 expected colon
+#show it
+
+--- show-recursive-identity ---
+// Test basic identity.
+#show heading: it => it
+= Heading
+
+--- show-multiple-rules ---
+// Test more recipes down the chain.
+#show list: scale.with(origin: left, x: 80%)
+#show heading: []
+#show enum: []
+- Actual
+- Tight
+- List
+= Nope
+
+--- show-rule-in-function ---
+// Test show rule in function.
+#let starwars(body) = {
+ show list: it => block({
+ stack(dir: ltr,
+ text(red, it),
+ 1fr,
+ scale(x: -100%, text(blue, it)),
+ )
+ })
+ body
+}
+
+- Normal list
+
+#starwars[
+ - Star
+ - Wars
+ - List
+]
+
+- Normal list
+
+--- show-recursive-multiple ---
+// Test multi-recursion with nested lists.
+#set rect(inset: 3pt)
+#show list: rect.with(stroke: blue)
+#show list: rect.with(stroke: red)
+#show list: block
+
+- List
+ - Nested
+ - List
+- Recursive!
+
+--- show-selector-where ---
+// Inline code.
+#show raw.where(block: false): box.with(
+ radius: 2pt,
+ outset: (y: 2.5pt),
+ inset: (x: 3pt, y: 0pt),
+ fill: luma(230),
+)
+
+// Code blocks.
+#show raw.where(block: true): block.with(
+ outset: -3pt,
+ inset: 11pt,
+ fill: luma(230),
+ stroke: (left: 1.5pt + luma(180)),
+)
+
+#set page(margin: (top: 12pt))
+#set par(justify: true)
+
+This code tests `code`
+with selectors and justification.
+
+```rs
+code!("it");
+```
+
+You can use the ```rs *const T``` pointer or
+the ```rs &mut T``` reference.
+
+--- show-set-where-override ---
+#show heading: set text(green)
+#show heading.where(level: 1): set text(red)
+#show heading.where(level: 2): set text(blue)
+= Red
+== Blue
+=== Green
+
+--- show-selector-or-elements-with-set ---
+// Looking forward to `heading.where(level: 1 | 2)` :)
+#show heading.where(level: 1).or(heading.where(level: 2)): set text(red)
+= L1
+== L2
+=== L3
+==== L4
+
+--- show-selector-element-or-label ---
+// Test element selector combined with label selector.
+#show selector(strong).or(): highlight
+I am *strong*, I am _emphasized_, and I am #[special].
+
+--- show-selector-element-or-text ---
+// Ensure that text selector cannot be nested in and/or. That's too complicated,
+// at least for now.
+
+// Error: 7-41 this selector cannot be used with show
+#show heading.where(level: 1).or("more"): set text(red)
+
+--- show-delayed-error ---
+// Error: 21-34 panicked with: "hey1"
+#show heading: _ => panic("hey1")
+
+// Error: 20-33 panicked with: "hey2"
+#show strong: _ => panic("hey2")
+
+= Hello
+*strong*
diff --git a/tests/suite/symbols/symbol.typ b/tests/suite/symbols/symbol.typ
new file mode 100644
index 0000000000..8f9a49ca2e
--- /dev/null
+++ b/tests/suite/symbols/symbol.typ
@@ -0,0 +1,38 @@
+// Test symbols.
+
+--- symbol ---
+#emoji.face
+#emoji.woman.old
+#emoji.turtle
+
+#set text(font: "New Computer Modern Math")
+#sym.arrow
+#sym.arrow.l
+#sym.arrow.r.squiggly
+#sym.arrow.tr.hook
+
+#sym.arrow.r;this and this#sym.arrow.l;
+
+--- symbol-constructor ---
+#let envelope = symbol(
+ "🖂",
+ ("stamped", "🖃"),
+ ("stamped.pen", "🖆"),
+ ("lightning", "🖄"),
+ ("fly", "🖅"),
+)
+
+#envelope
+#envelope.stamped
+#envelope.pen
+#envelope.stamped.pen
+#envelope.lightning
+#envelope.fly
+
+--- symbol-constructor-empty ---
+// Error: 2-10 expected at least one variant
+#symbol()
+
+--- symbol-unknown-modifier ---
+// Error: 13-20 unknown symbol modifier
+#emoji.face.garbage
diff --git a/tests/typ/compiler/backtracking.typ b/tests/suite/syntax/backtracking.typ
similarity index 82%
rename from tests/typ/compiler/backtracking.typ
rename to tests/suite/syntax/backtracking.typ
index 9c3ab8ec46..33f0577087 100644
--- a/tests/typ/compiler/backtracking.typ
+++ b/tests/suite/syntax/backtracking.typ
@@ -2,9 +2,8 @@
// If this regresses, the test suite will not terminate, which is a bit
// unfortunate compared to a good error, but at least we know something is up.
//
-// Ref: false
----
+--- parser-backtracking-param-default-value ---
#{
let s = "(x: 1) => x"
let pat = "(x: {}) => 1 + x()"
@@ -14,7 +13,7 @@
test(eval(s)(), 51)
}
----
+--- parser-backtracking-destructuring-assignment ---
#{
let s = "(x) = 1"
let pat = "(x: {_}) = 1"
@@ -25,7 +24,7 @@
eval(s)
}
----
+--- parser-backtracking-destructuring-whitespace ---
// Test whitespace after memoized part.
#( (x: () => 1 ) => 1 )
// -------
diff --git a/tests/suite/syntax/comment.typ b/tests/suite/syntax/comment.typ
new file mode 100644
index 0000000000..ac3e1943f0
--- /dev/null
+++ b/tests/suite/syntax/comment.typ
@@ -0,0 +1,43 @@
+// Test line and block comments.
+
+--- comments ---
+// Line comment acts as spacing.
+A// you
+B
+
+// Block comment does not act as spacing, nested block comments.
+C/*
+ /* */
+*/D
+
+// Works in code.
+#test(type(/*1*/ 1) //
+, int)
+
+// End of block comment in line comment.
+// Hello */
+
+// Nested "//" doesn't count as line comment.
+/* // */
+E
+
+/*//*/
+This is a comment.
+*/*/
+
+--- comment-end-of-line ---
+// Test comments at the end of a line
+First part//
+Second part
+
+// Test comments at the end of a line with pre-spacing
+First part //
+Second part
+
+--- comment-block-unclosed ---
+// End should not appear without start.
+// Error: 7-9 unexpected end of block comment
+/* */ */
+
+// Unterminated is okay.
+/*
diff --git a/tests/suite/syntax/embedded.typ b/tests/suite/syntax/embedded.typ
new file mode 100644
index 0000000000..74ce4a0327
--- /dev/null
+++ b/tests/suite/syntax/embedded.typ
@@ -0,0 +1,9 @@
+// Test embedded expressions.
+
+--- markup-expr-incomplete ---
+// Error: 2-2 expected expression
+#
+
+--- markup-expr-incomplete-followed-by-text ---
+// Error: 2-2 expected expression
+# hello
diff --git a/tests/typ/text/escape.typ b/tests/suite/syntax/escape.typ
similarity index 88%
rename from tests/typ/text/escape.typ
rename to tests/suite/syntax/escape.typ
index 901632ba50..ff05aa9901 100644
--- a/tests/typ/text/escape.typ
+++ b/tests/suite/syntax/escape.typ
@@ -1,6 +1,6 @@
// Test escape sequences.
----
+--- escape ---
// Escapable symbols.
\\ \/ \[ \] \{ \} \# \* \_ \+ \= \~ \
\` \$ \" \' \< \> \@ \( \) \A
@@ -25,12 +25,12 @@ let f() , ; : | + - /= == 12 "string"
// Escaped dot.
10\. May
----
+--- escape-invalid-codepoint ---
// Unicode codepoint does not exist.
// Error: 1-11 invalid Unicode codepoint: FFFFFF
\u{FFFFFF}
----
+--- escape-unclosed ---
// Unterminated.
// Error: 1-6 unclosed Unicode escape sequence
\u{41[*Bold*]
diff --git a/tests/suite/syntax/newlines.typ b/tests/suite/syntax/newlines.typ
new file mode 100644
index 0000000000..eef4561920
--- /dev/null
+++ b/tests/suite/syntax/newlines.typ
@@ -0,0 +1,77 @@
+// Test newline continuations.
+
+--- newline-continuation-code ---
+#{
+ "hello"
+ .clusters()
+ if false {
+
+ }
+ else {
+ ("1", "2")
+ }
+}
+
+--- newline-continuation-markup ---
+#"hello"
+ .codepoints()
+
+#if false {
+
+}
+else {
+ ("1", "2")
+}
+
+--- newline-continuation-method-blank ---
+#test({
+ "hi 1"
+
+ .clusters()
+}, ("h", "i", " ", "1"))
+
+--- newline-continuation-method-line-comment-after ---
+#test({
+ "hi 2"// comment
+ .clusters()
+}, ("h", "i", " ", "2"))
+
+--- newline-continuation-method-block-comment-after ---
+#test({
+ "hi 3"/* comment */
+ .clusters()
+}, ("h", "i", " ", "3"))
+
+--- newline-continuation-method-line-comment-between ---
+#test({
+ "hi 4"
+ // comment
+ .clusters()
+}, ("h", "i", " ", "4"))
+
+--- newline-continuation-method-block-comment-between ---
+#test({
+ "hi 5"
+ /*comment*/.clusters()
+}, ("h", "i", " ", "5"))
+
+--- newline-continuation-method-comments-and-blanks ---
+#test({
+ "hi 6"
+ // comment
+
+
+ /* comment */
+ .clusters()
+}, ("h", "i", " ", "6"))
+
+--- newline-continuation-if-else-comment ---
+#test({
+ let foo(x) = {
+ if x < 0 { "negative" }
+ // comment
+ else { "non-negative" }
+ }
+
+ foo(1)
+}, "non-negative")
diff --git a/tests/suite/syntax/numbers.typ b/tests/suite/syntax/numbers.typ
new file mode 100644
index 0000000000..1f15ac720c
--- /dev/null
+++ b/tests/suite/syntax/numbers.typ
@@ -0,0 +1,32 @@
+// Test how numbers are displayed.
+
+--- numbers ---
+// Test numbers in text mode.
+12 \
+12.0 \
+3.14 \
+1234567890 \
+0123456789 \
+0 \
+0.0 \
++0 \
++0.0 \
+-0 \
+-0.0 \
+-1 \
+-3.14 \
+-9876543210 \
+-0987654321 \
+٣٫١٤ \
+-٣٫١٤ \
+-¾ \
+#text(fractions: true)[-3/2] \
+2022 - 2023 \
+2022 -- 2023 \
+2022--2023 \
+2022-2023 \
+٢٠٢٢ - ٢٠٢٣ \
+٢٠٢٢ -- ٢٠٢٣ \
+٢٠٢٢--٢٠٢٣ \
+٢٠٢٢-٢٠٢٣ \
+-500 -- -400
diff --git a/tests/typ/compiler/shorthand.typ b/tests/suite/syntax/shorthand.typ
similarity index 81%
rename from tests/typ/compiler/shorthand.typ
rename to tests/suite/syntax/shorthand.typ
index 54ae747384..81aa6b7bd1 100644
--- a/tests/typ/compiler/shorthand.typ
+++ b/tests/suite/syntax/shorthand.typ
@@ -1,9 +1,9 @@
// Test shorthands for unicode codepoints.
----
+--- shorthand-nbsp-and-shy-hyphen ---
The non-breaking space~does work, soft-?hyphen also does.
----
+--- shorthand-nbsp-width ---
// Make sure non-breaking and normal space always
// have the same width. Even if the font decided
// differently.
@@ -11,15 +11,15 @@ The non-breaking space~does work, soft-?hyphen also does.
a b \
a~b
----
+--- shorthand-dashes ---
- En dash: --
- Em dash: ---
----
+--- shorthand-ellipsis ---
#set text(font: "Roboto")
A... vs #"A..."
----
+--- shorthands-math ---
// Check all math shorthands
$...$\
$-$\
diff --git a/tests/typ/text/case.typ b/tests/suite/text/case.typ
similarity index 85%
rename from tests/typ/text/case.typ
rename to tests/suite/text/case.typ
index 75574f2150..2bf68bc336 100644
--- a/tests/typ/text/case.typ
+++ b/tests/suite/text/case.typ
@@ -1,12 +1,11 @@
// Test the `upper` and `lower` functions.
-// Ref: false
----
+--- lower-and-upper ---
#let memes = "ArE mEmEs gReAt?";
#test(lower(memes), "are memes great?")
#test(upper(memes), "ARE MEMES GREAT?")
#test(upper("Ελλάδα"), "ΕΛΛΆΔΑ")
----
+--- upper-bad-type ---
// Error: 8-9 expected string or content, found integer
#upper(1)
diff --git a/tests/typ/coma.typ b/tests/suite/text/coma.typ
similarity index 97%
rename from tests/typ/coma.typ
rename to tests/suite/text/coma.typ
index 8ca08ddb9b..df75763399 100644
--- a/tests/typ/coma.typ
+++ b/tests/suite/text/coma.typ
@@ -1,3 +1,5 @@
+--- coma ---
+// LARGE
#set page(width: 450pt, margin: 1cm)
*Technische Universität Berlin* #h(1fr) *WiSe 2019/2020* \
diff --git a/tests/typ/text/copy-paste.typ b/tests/suite/text/copy-paste.typ
similarity index 84%
rename from tests/typ/text/copy-paste.typ
rename to tests/suite/text/copy-paste.typ
index 5d8264825c..ff6da893ee 100644
--- a/tests/typ/text/copy-paste.typ
+++ b/tests/suite/text/copy-paste.typ
@@ -1,7 +1,7 @@
// Test copy-paste and search in PDF with ligatures
// and Arabic test. Must be tested manually!
----
+--- text-copy-paste-ligatures ---
The after fira 🏳️🌈!
#set text(lang: "ar", font: "Noto Sans Arabic")
diff --git a/tests/typ/text/deco.typ b/tests/suite/text/deco.typ
similarity index 87%
rename from tests/typ/text/deco.typ
rename to tests/suite/text/deco.typ
index cc9b9b3ae8..07fdb6c192 100644
--- a/tests/typ/text/deco.typ
+++ b/tests/suite/text/deco.typ
@@ -1,6 +1,6 @@
// Test text decorations.
----
+--- underline-overline-strike ---
#let red = rgb("fc0030")
// Basic strikethrough.
@@ -18,7 +18,7 @@
// Both over- and underline.
#overline(underline[Running amongst the wolves.])
----
+--- strike-with ---
#let redact = strike.with(stroke: 10pt, extent: 0.05em)
#let highlight-custom = strike.with(stroke: 10pt + rgb("abcdef88"), extent: 0.05em)
@@ -26,25 +26,40 @@
Sometimes, we work #redact[in secret].
There might be #highlight-custom[redacted] things.
----
+--- underline-stroke-folding ---
// Test stroke folding.
#set underline(stroke: 2pt, offset: 2pt)
#underline(text(red, [DANGER!]))
----
+--- underline-background ---
+// Test underline background
+#set underline(background: true, stroke: (thickness: 0.5em, paint: red, cap: "round"))
+#underline[This is in the background]
+
+--- overline-background ---
+// Test overline background
+#set overline(background: true, stroke: (thickness: 0.5em, paint: red, cap: "round"))
+#overline[This is in the background]
+
+--- strike-background ---
+// Test strike background
+#set strike(background: true, stroke: 5pt + red)
+#strike[This is in the background]
+
+--- highlight ---
// Test highlight.
This is the built-in #highlight[highlight with default color].
We can also specify a customized value
#highlight(fill: green.lighten(80%))[to highlight].
----
+--- highlight-bounds ---
// Test default highlight bounds.
#highlight[ace],
#highlight[base],
#highlight[super],
#highlight[phone #sym.integral]
----
+--- highlight-edges ---
// Test a tighter highlight.
#set highlight(top-edge: "x-height", bottom-edge: "baseline")
#highlight[ace],
@@ -52,34 +67,19 @@ We can also specify a customized value
#highlight[super],
#highlight[phone #sym.integral]
----
+--- highlight-edges-bounds ---
// Test a bounds highlight.
#set highlight(top-edge: "bounds", bottom-edge: "bounds")
#highlight[abc]
#highlight[abc #sym.integral]
----
+--- highlight-radius ---
// Test highlight radius
#highlight(radius: 3pt)[abc],
#highlight(radius: 1em)[#lorem(5)]
----
+--- highlight-stroke ---
// Test highlight stroke
#highlight(stroke: 2pt + blue)[abc]
#highlight(stroke: (top: blue, left: red, bottom: green, right: orange))[abc]
#highlight(stroke: 1pt, radius: 3pt)[#lorem(5)]
-
----
-// Test underline background
-#set underline(background: true, stroke: (thickness: 0.5em, paint: red, cap: "round"))
-#underline[This is in the background]
-
----
-// Test overline background
-#set overline(background: true, stroke: (thickness: 0.5em, paint: red, cap: "round"))
-#overline[This is in the background]
-
----
-// Test strike background
-#set strike(background: true, stroke: 5pt + red)
-#strike[This is in the background]
diff --git a/tests/typ/text/edge.typ b/tests/suite/text/edge.typ
similarity index 91%
rename from tests/typ/text/edge.typ
rename to tests/suite/text/edge.typ
index 053576e844..57732156ca 100644
--- a/tests/typ/text/edge.typ
+++ b/tests/suite/text/edge.typ
@@ -1,6 +1,6 @@
// Test top and bottom text edge.
----
+--- text-edge ---
#set page(width: 160pt)
#set text(size: 8pt)
@@ -26,14 +26,14 @@
#try(4pt, -2pt)
#try(1pt + 0.3em, -0.15em)
----
+--- text-edge-bad-type ---
// Error: 21-23 expected "ascender", "cap-height", "x-height", "baseline", "bounds", or length, found array
#set text(top-edge: ())
----
+--- text-edge-bad-value ---
// Error: 24-26 expected "baseline", "descender", "bounds", or length
#set text(bottom-edge: "")
----
+--- text-edge-wrong-edge ---
// Error: 24-36 expected "baseline", "descender", "bounds", or length
#set text(bottom-edge: "cap-height")
diff --git a/tests/typ/text/em.typ b/tests/suite/text/em.typ
similarity index 90%
rename from tests/typ/text/em.typ
rename to tests/suite/text/em.typ
index bf191c1fdf..be7e34284a 100644
--- a/tests/typ/text/em.typ
+++ b/tests/suite/text/em.typ
@@ -1,6 +1,6 @@
// Test font-relative sizing.
----
+--- text-size-em-nesting ---
#set text(size: 5pt)
A // 5pt
#[
@@ -16,7 +16,7 @@ A // 5pt
]
G // 5pt
----
+--- text-size-em ---
// Test using ems in arbitrary places.
#set text(size: 5pt)
#set text(size: 2em)
diff --git a/tests/typ/text/font.typ b/tests/suite/text/font.typ
similarity index 89%
rename from tests/typ/text/font.typ
rename to tests/suite/text/font.typ
index 736ded41be..47ec641918 100644
--- a/tests/typ/text/font.typ
+++ b/tests/suite/text/font.typ
@@ -1,6 +1,6 @@
// Test configuring font properties.
----
+--- text-font-properties ---
// Set same font size in three different ways.
#text(20pt)[A]
#text(2em)[A]
@@ -41,7 +41,7 @@ Emoji: 🐪, 🌋, 🏞
#set text(font: ("PT Sans", "Twitter Color Emoji"), fallback: false)
2π = 𝛼 + 𝛽. ✅
----
+--- text-call-body ---
// Test string body.
#text("Text") \
#text(red, "Text") \
@@ -49,18 +49,18 @@ Emoji: 🐪, 🌋, 🏞
#text([Text], teal, font: "IBM Plex Serif") \
#text(forest, font: "New Computer Modern", [Text]) \
----
+--- text-bad-argument ---
// Error: 11-16 unexpected argument
#set text(false)
----
+--- text-style-bad ---
// Error: 18-24 expected "normal", "italic", or "oblique"
#set text(style: "bold", weight: "thin")
----
+--- text-bad-extra-argument ---
// Error: 23-27 unexpected argument
#set text(size: 10pt, 12pt)
----
+--- text-bad-named-argument ---
// Error: 11-31 unexpected argument: something
#set text(something: "invalid")
diff --git a/tests/suite/text/lang.typ b/tests/suite/text/lang.typ
new file mode 100644
index 0000000000..74f7014078
--- /dev/null
+++ b/tests/suite/text/lang.typ
@@ -0,0 +1,74 @@
+// Test setting the document language.
+
+--- text-lang ---
+// without any region
+#set text(font: "Noto Serif CJK TC", lang: "zh")
+#outline()
+
+--- text-lang-unknown-region ---
+// with unknown region configured
+#set text(font: "Noto Serif CJK TC", lang: "zh", region: "XX")
+#outline()
+
+--- text-lang-region ---
+// with region configured
+#set text(font: "Noto Serif CJK TC", lang: "zh", region: "TW")
+#outline()
+
+--- text-lang-hyphenate ---
+// Ensure that setting the language does have effects.
+#set text(hyphenate: true)
+#grid(
+ columns: 2 * (20pt,),
+ gutter: 1fr,
+ text(lang: "en")["Eingabeaufforderung"],
+ text(lang: "de")["Eingabeaufforderung"],
+)
+
+--- text-lang-shaping ---
+// Test that the language passed to the shaper has an effect.
+#set text(font: "Ubuntu")
+
+// Some lowercase letters are different in Serbian Cyrillic compared to other
+// Cyrillic languages. Since there is only one set of Unicode codepoints for
+// Cyrillic, these can only be seen when setting the language to Serbian and
+// selecting one of the few fonts that support these letterforms.
+Бб
+#text(lang: "uk")[Бб]
+#text(lang: "sr")[Бб]
+
+--- text-lang-script-shaping ---
+// Verify that writing script/language combination has an effect
+#{
+ set text(size:20pt)
+ set text(script: "latn", lang: "en")
+ [Ş ]
+ set text(script: "latn", lang: "ro")
+ [Ş ]
+ set text(script: "grek", lang: "ro")
+ [Ş ]
+}
+
+--- text-script-bad-type ---
+// Error: 19-23 expected string or auto, found none
+#set text(script: none)
+
+--- text-script-bad-value ---
+// Error: 19-23 expected three or four letter script code (ISO 15924 or 'math')
+#set text(script: "ab")
+
+--- text-lang-bad-type ---
+// Error: 17-21 expected string, found none
+#set text(lang: none)
+
+--- text-lang-bad-value ---
+// Error: 17-20 expected two or three letter language code (ISO 639-1/2/3)
+#set text(lang: "ӛ")
+
+--- text-lang-bad-value-emoji ---
+// Error: 17-20 expected two or three letter language code (ISO 639-1/2/3)
+#set text(lang: "😃")
+
+--- text-region-bad-value ---
+// Error: 19-24 expected two letter region code (ISO 3166-1 alpha-2)
+#set text(region: "hey")
diff --git a/tests/typ/text/lorem.typ b/tests/suite/text/lorem.typ
similarity index 86%
rename from tests/typ/text/lorem.typ
rename to tests/suite/text/lorem.typ
index 804f804df8..1524e2a397 100644
--- a/tests/typ/text/lorem.typ
+++ b/tests/suite/text/lorem.typ
@@ -1,10 +1,10 @@
// Test blind text.
----
+--- lorem ---
// Test basic call.
#lorem(19)
----
+--- lorem-pars ---
// Test custom paragraphs with user code.
#set text(8pt)
@@ -27,6 +27,6 @@
}
}
----
+--- lorem-missing-words ---
// Error: 2-9 missing argument: words
#lorem()
diff --git a/tests/suite/text/raw.typ b/tests/suite/text/raw.typ
new file mode 100644
index 0000000000..6a3ea6bd71
--- /dev/null
+++ b/tests/suite/text/raw.typ
@@ -0,0 +1,643 @@
+// Test raw blocks.
+
+--- raw-empty ---
+// Empty raw block.
+Empty raw block:``.
+
+--- raw-consecutive-single-backticks ---
+// No extra space.
+`A``B`
+
+--- raw-typst-lang ---
+// Typst syntax inside.
+```typ #let x = 1``` \
+```typ #f(1)```
+
+--- raw-block-no-parbreaks ---
+// Multiline block splits paragraphs.
+
+Text
+```rust
+fn code() {}
+```
+Text
+
+--- raw-more-backticks ---
+// Lots of backticks inside.
+````
+```backticks```
+````
+
+--- raw-trimming ---
+// Trimming.
+
+// Space between "rust" and "let" is trimmed.
+The keyword ```rust let```.
+
+// Trimming depends on number backticks.
+(``) \
+(` untrimmed `) \
+(``` trimmed` ```) \
+(``` trimmed ```) \
+(``` trimmed```) \
+
+--- raw-single-backtick-lang ---
+// Single ticks should not have a language.
+`rust let`
+
+--- raw-dedent-first-line ---
+// First line is not dedented and leading space is still possible.
+ ``` A
+ B
+ C
+ ```
+
+--- raw-dedent-empty-line ---
+// Do not take empty lines into account when computing dedent.
+```
+ A
+
+ B
+```
+
+--- raw-dedent-last-line ---
+// Take last line into account when computing dedent.
+```
+ A
+
+ B
+ ```
+
+--- raw-tab-size ---
+#set raw(tab-size: 8)
+
+```tsv
+Year Month Day
+2000 2 3
+2001 2 1
+2002 3 10
+```
+
+--- raw-syntaxes ---
+#set page(width: 180pt)
+#set text(6pt)
+#set raw(syntaxes: "/assets/syntaxes/SExpressions.sublime-syntax")
+
+```sexp
+(defun factorial (x)
+ (if (zerop x)
+ ; with a comment
+ 1
+ (* x (factorial (- x 1)))))
+```
+
+
+--- raw-theme ---
+// Test code highlighting with custom theme.
+#set page(width: 180pt)
+#set text(6pt)
+#set raw(theme: "/assets/themes/halcyon.tmTheme")
+#show raw: it => {
+ set text(fill: rgb("a2aabc"))
+ rect(
+ width: 100%,
+ inset: (x: 4pt, y: 5pt),
+ radius: 4pt,
+ fill: rgb("1d2433"),
+ place(right, text(luma(240), it.lang)) + it,
+ )
+}
+
+```typ
+= Chapter 1
+#lorem(100)
+
+#let hi = "Hello World"
+#show heading: emph
+```
+
+--- raw-show-set ---
+// Text show rule
+#show raw: set text(font: "Roboto")
+`Roboto`
+
+--- raw-align-default ---
+// Text inside raw block should be unaffected by outer alignment by default.
+#set align(center)
+#set page(width: 180pt)
+#set text(6pt)
+
+#lorem(20)
+
+```py
+def something(x):
+ return x
+
+a = 342395823859823958329
+b = 324923
+```
+
+#lorem(20)
+
+--- raw-align-specified ---
+// Text inside raw block should follow the specified alignment.
+#set page(width: 180pt)
+#set text(6pt)
+
+#lorem(20)
+#align(center, raw(
+ lang: "typ",
+ block: true,
+ align: right,
+ "#let f(x) = x\n#align(center, line(length: 1em))",
+))
+#lorem(20)
+
+--- raw-align-invalid ---
+// Error: 17-20 expected `start`, `left`, `center`, `right`, or `end`, found top
+#set raw(align: top)
+
+--- raw-highlight-typ ---
+// LARGE
+#set page(width: auto)
+
+```typ
+#set hello()
+#set hello()
+#set hello.world()
+#set hello.my.world()
+#let foo(x) = x * 2
+#show heading: func
+#show module.func: func
+#show module.func: it => {}
+#foo(ident: ident)
+#hello
+#hello()
+#box[]
+#hello.world
+#hello.world()
+#hello().world()
+#hello.my.world
+#hello.my.world()
+#hello.my().world
+#hello.my().world()
+#{ hello }
+#{ hello() }
+#{ hello.world() }
+$ hello $
+$ hello() $
+$ box[] $
+$ hello.world $
+$ hello.world() $
+$ hello.my.world() $
+$ f_zeta(x), f_zeta(x)/1 $
+$ emph(hello.my.world()) $
+$ emph(hello.my().world) $
+$ emph(hello.my().world()) $
+$ #hello $
+$ #hello() $
+$ #hello.world $
+$ #hello.world() $
+$ #box[] $
+#if foo []
+```
+
+--- raw-highlight ---
+#set page(width: 180pt)
+#set text(6pt)
+#show raw: it => rect(
+ width: 100%,
+ inset: (x: 4pt, y: 5pt),
+ radius: 4pt,
+ fill: rgb(239, 241, 243),
+ place(right, text(luma(110), it.lang)) + it,
+)
+
+```typ
+= Chapter 1
+#lorem(100)
+
+#let hi = "Hello World"
+#show heading: emph
+```
+
+```rust
+/// A carefully designed state machine.
+#[derive(Debug)]
+enum State<'a> { A(u8), B(&'a str) }
+
+fn advance(state: State<'_>) -> State<'_> {
+ unimplemented!("state machine")
+}
+```
+
+```py
+import this
+
+def hi():
+ print("Hi!")
+```
+
+```cpp
+#include
+
+int main() {
+ std::cout << "Hello, world!";
+}
+```
+
+```julia
+# Add two numbers
+function add(x, y)
+ return x * y
+end
+```
+
+ // Try with some indent.
+ ```html
+
+
+
+
+
+
+ Topic
+ The Hypertext Markup Language.
+
+
+
+ ```
+
+--- raw-inline-multiline ---
+#set page(width: 180pt)
+#set text(6pt)
+#set raw(lang:"python")
+
+Inline raws, multiline e.g. `for i in range(10):
+ # Only this line is a comment.
+ print(i)` or otherwise e.g. `print(j)`, are colored properly.
+
+Inline raws, multiline e.g. `
+# Appears blocky due to linebreaks at the boundary.
+for i in range(10):
+ print(i)
+` or otherwise e.g. `print(j)`, are colored properly.
+
+--- raw-blocky ---
+// Test various raw parsing edge cases.
+#let empty = (
+ name: "empty",
+ input: ``,
+ text: "",
+)
+
+#let backtick = (
+ name: "backtick",
+ input: ``` ` ```,
+ text: "`",
+ block: false,
+)
+
+#let lang-backtick = (
+ name: "lang-backtick",
+ input: ```js ` ```,
+ lang: "js",
+ text: "`",
+ block: false,
+)
+
+// The language tag stops on space
+#let lang-space = (
+ name: "lang-space",
+ input: ```js test ```,
+ lang: "js",
+ text: "test ",
+ block: false,
+)
+
+// The language tag stops on newline
+#let lang-newline = (
+ name: "lang-newline",
+ input: ```js
+test
+```,
+ lang: "js",
+ text: "test",
+ block: true,
+)
+
+// The first line and the last line are ignored
+#let blocky = (
+ name: "blocky",
+ input: {
+```
+test
+```
+},
+ text: "test",
+ block: true,
+)
+
+// A blocky raw should handle dedents
+#let blocky-dedent = (
+ name: "blocky-dedent",
+ input: {
+```
+ test
+ ```
+ },
+ text: "test",
+ block: true,
+)
+
+// When there is content in the first line, it should exactly eat a whitespace char.
+#let blocky-dedent-firstline = (
+ name: "blocky-dedent-firstline",
+ input: ``` test
+ ```,
+ text: "test",
+ block: true,
+)
+
+// When there is content in the first line, it should exactly eat a whitespace char.
+#let blocky-dedent-firstline2 = (
+ name: "blocky-dedent-firstline2",
+ input: ``` test
+```,
+ text: "test",
+ block: true,
+)
+
+// The first line is not affected by dedent, and the middle lines don't consider the whitespace prefix of the first line.
+#let blocky-dedent-firstline3 = (
+ name: "blocky-dedent-firstline3",
+ input: ``` test
+ test2
+ ```,
+ text: "test\n test2",
+ block: true,
+)
+
+// The first line is not affected by dedent, and the middle lines don't consider the whitespace prefix of the first line.
+#let blocky-dedent-firstline4 = (
+ name: "blocky-dedent-firstline4",
+ input: ``` test
+ test2
+ ```,
+ text: " test\ntest2",
+ block: true,
+)
+
+#let blocky-dedent-lastline = (
+ name: "blocky-dedent-lastline",
+ input: ```
+ test
+ ```,
+ text: " test",
+ block: true,
+)
+
+#let blocky-dedent-lastline2 = (
+ name: "blocky-dedent-lastline2",
+ input: ```
+ test
+ ```,
+ text: "test",
+ block: true,
+)
+
+#let blocky-tab = (
+ name: "blocky-tab",
+ input: {
+```
+ test
+```
+},
+ text: "\ttest",
+ block: true,
+)
+
+// This one is a bit problematic because there is a trailing tab below "test"
+// which the editor constantly wants to remove.
+#let blocky-tab-dedent = (
+ name: "blocky-tab-dedent",
+ input: eval("```\n\ttest\n \n ```"),
+ text: "test\n ",
+ block: true,
+)
+
+#let cases = (
+ empty,
+ backtick,
+ lang-backtick,
+ lang-space,
+ lang-newline,
+ blocky,
+ blocky-dedent,
+ blocky-dedent-firstline,
+ blocky-dedent-firstline2,
+ blocky-dedent-firstline3,
+ blocky-dedent-lastline,
+ blocky-dedent-lastline2,
+ blocky-tab,
+ blocky-tab-dedent,
+)
+
+#for c in cases {
+ assert.eq(c.text, c.input.text, message: "in point " + c.name + ", expect " + repr(c.text) + ", got " + repr(c.input.text) + "")
+ let block = c.at("block", default: false)
+ assert.eq(block, c.input.block, message: "in point " + c.name + ", expect " + repr(block) + ", got " + repr(c.input.block) + "")
+}
+
+--- raw-line ---
+#set page(width: 200pt)
+
+```rs
+fn main() {
+ println!("Hello, world!");
+}
+```
+
+#show raw.line: it => {
+ box(stack(
+ dir: ltr,
+ box(width: 15pt)[#it.number],
+ it.body,
+ ))
+ linebreak()
+}
+
+```rs
+fn main() {
+ println!("Hello, world!");
+}
+```
+
+--- raw-line-alternating-fill ---
+#set page(width: 200pt)
+#show raw: it => stack(dir: ttb, ..it.lines)
+#show raw.line: it => {
+ box(
+ width: 100%,
+ height: 1.75em,
+ inset: 0.25em,
+ fill: if calc.rem(it.number, 2) == 0 {
+ luma(90%)
+ } else {
+ white
+ },
+ align(horizon, stack(
+ dir: ltr,
+ box(width: 15pt)[#it.number],
+ it.body,
+ ))
+ )
+}
+
+```typ
+#show raw.line: block.with(
+ fill: luma(60%)
+);
+
+Hello, world!
+
+= A heading for good measure
+```
+
+--- raw-line-text-fill ---
+#set page(width: 200pt)
+#show raw.line: set text(fill: red)
+
+```py
+import numpy as np
+
+def f(x):
+ return x**2
+
+x = np.linspace(0, 10, 100)
+y = f(x)
+
+print(x)
+print(y)
+```
+
+--- raw-line-scripting ---
+
+// Test line extraction works.
+
+#show raw: code => {
+ for i in code.lines {
+ test(i.count, 10)
+ }
+
+ test(code.lines.at(0).text, "import numpy as np")
+ test(code.lines.at(1).text, "")
+ test(code.lines.at(2).text, "def f(x):")
+ test(code.lines.at(3).text, " return x**2")
+ test(code.lines.at(4).text, "")
+ test(code.lines.at(5).text, "x = np.linspace(0, 10, 100)")
+ test(code.lines.at(6).text, "y = f(x)")
+ test(code.lines.at(7).text, "")
+ test(code.lines.at(8).text, "print(x)")
+ test(code.lines.at(9).text, "print(y)")
+ test(code.lines.at(10, default: none), none)
+}
+
+```py
+import numpy as np
+
+def f(x):
+ return x**2
+
+x = np.linspace(0, 10, 100)
+y = f(x)
+
+print(x)
+print(y)
+```
+
+--- issue-3601-empty-raw ---
+// Test that empty raw block with `typ` language doesn't cause a crash.
+```typ
+```
+
+--- issue-3841-tabs-in-raw-type-code ---
+// Tab chars were not rendered in raw blocks with lang: "typ(c)"
+#raw("#if true {\n\tf()\t// typ\n}", lang: "typ")
+
+#raw("if true {\n\tf()\t// typc\n}", lang: "typc")
+
+```typ
+#if true {
+ // tabs around f()
+ f() // typ
+}
+```
+
+```typc
+if true {
+ // tabs around f()
+ f() // typc
+}
+```
+
+--- issue-2259-raw-color-overwrite ---
+// Test that the color of a raw block is not overwritten
+#show raw: set text(fill: blue)
+
+`Hello, World!`
+
+```rs
+fn main() {
+ println!("Hello, World!");
+}
+```
+
+--- issue-3191-raw-indent-shrink ---
+// Spaces in raw blocks should not be shrunk as it would mess up the indentation
+// of code.
+#set par(justify: true)
+
+#show raw.where(block: true): block.with(
+ fill: luma(240),
+ inset: 10pt,
+)
+
+#block(
+ width: 60%,
+ ```py
+ for x in xs:
+ print("x=",x)
+ ```
+)
+
+--- issue-3191-raw-normal-paragraphs-still-shrink ---
+// In normal paragraphs, spaces should still be shrunk.
+// The first line here serves as a reference, while the second
+// uses non-breaking spaces to create an overflowing line
+// (which should shrink).
+~~~~No shrinking here
+
+~~~~The~spaces~on~this~line~shrink
+
+--- issue-3820-raw-space-when-end-with-backtick ---
+```typ
+`code`
+```
+
+ ```typ
+ `code`
+ ```
+
+--- raw-unclosed ---
+// Test unterminated raw text.
+//
+// Note: This test should be the final one in the file because it messes up
+// syntax highlighting.
+//
+// Error: 1-2:1 unclosed raw text
+`endless
diff --git a/tests/typ/text/shift.typ b/tests/suite/text/shift.typ
similarity index 87%
rename from tests/typ/text/shift.typ
rename to tests/suite/text/shift.typ
index 2b1b898456..090f6ee8fb 100644
--- a/tests/typ/text/shift.typ
+++ b/tests/suite/text/shift.typ
@@ -1,6 +1,6 @@
// Test sub- and superscipt shifts.
----
+--- sub-super ---
#table(
columns: 3,
[Typo.], [Fallb.], [Synth],
@@ -8,11 +8,11 @@
[x#sub[1]], [x#sub[5n]], [x#sub[2 #box(square(size: 6pt))]],
)
----
+--- sub-super-non-typographic ---
#set super(typographic: false, baseline: -0.25em, size: 0.7em)
n#super[1], n#sub[2], ... n#super[N]
----
+--- super-underline ---
#set underline(stroke: 0.5pt, offset: 0.15em)
#underline[The claim#super[\[4\]]] has been disputed. \
The claim#super[#underline[\[4\]]] has been disputed. \
diff --git a/tests/suite/text/smallcaps.typ b/tests/suite/text/smallcaps.typ
new file mode 100644
index 0000000000..6f977244e1
--- /dev/null
+++ b/tests/suite/text/smallcaps.typ
@@ -0,0 +1,3 @@
+--- smallcaps ---
+// Test smallcaps.
+#smallcaps[Smallcaps]
diff --git a/tests/suite/text/smartquote.typ b/tests/suite/text/smartquote.typ
new file mode 100644
index 0000000000..28fcba5b7e
--- /dev/null
+++ b/tests/suite/text/smartquote.typ
@@ -0,0 +1,122 @@
+--- smartquote ---
+// LARGE
+#set page(width: 250pt)
+
+// Test simple quotations in various languages.
+#set text(lang: "en")
+"The horse eats no cucumber salad" was the first sentence ever uttered on the 'telephone.'
+
+#set text(lang: "de")
+"Das Pferd frisst keinen Gurkensalat" war der erste jemals am 'Fernsprecher' gesagte Satz.
+
+#set text(lang: "de", region: "CH")
+"Das Pferd frisst keinen Gurkensalat" war der erste jemals am 'Fernsprecher' gesagte Satz.
+
+#set text(lang: "es", region: none)
+"El caballo no come ensalada de pepino" fue la primera frase pronunciada por 'teléfono'.
+
+#set text(lang: "es", region: "MX")
+"El caballo no come ensalada de pepino" fue la primera frase pronunciada por 'teléfono'.
+
+#set text(lang: "fr", region: none)
+"Le cheval ne mange pas de salade de concombres" est la première phrase jamais prononcée au 'téléphone'.
+
+#set text(lang: "fi")
+"Hevonen ei syö kurkkusalaattia" oli ensimmäinen koskaan 'puhelimessa' lausuttu lause.
+
+#set text(lang: "gr")
+"Το άλογο δεν τρώει αγγουροσαλάτα" ήταν η πρώτη πρόταση που ειπώθηκε στο 'τηλέφωνο'.
+
+#set text(lang: "he")
+"הסוס לא אוכל סלט מלפפונים" היה המשפט ההראשון שנאמר ב 'טלפון'.
+
+#set text(lang: "ro")
+"Calul nu mănâncă salată de castraveți" a fost prima propoziție rostită vreodată la 'telefon'.
+
+#set text(lang: "ru")
+"Лошадь не ест салат из огурцов" - это была первая фраза, сказанная по 'телефону'.
+
+--- smartquote-empty ---
+// Test single pair of quotes.
+""
+
+--- smartquote-apostrophe ---
+// Test sentences with numbers and apostrophes.
+The 5'11" 'quick' brown fox jumps over the "lazy" dog's ear.
+
+He said "I'm a big fella."
+
+--- smartquote-escape ---
+// Test escape sequences.
+The 5\'11\" 'quick\' brown fox jumps over the \"lazy" dog\'s ear.
+
+--- smartquote-disable ---
+// Test turning smart quotes off.
+He's told some books contain questionable "example text".
+
+#set smartquote(enabled: false)
+He's told some books contain questionable "example text".
+
+--- smartquote-disabled-temporarily ---
+// Test changing properties within text.
+"She suddenly started speaking french: #text(lang: "fr")['Je suis une banane.']" Roman told me.
+
+Some people's thought on this would be #[#set smartquote(enabled: false); "strange."]
+
+--- smartquote-nesting ---
+// Test nested double and single quotes.
+"'test statement'" \
+"'test' statement" \
+"statement 'test'"
+
+--- smartquote-custom ---
+// Use language quotes for missing keys, allow partial reset
+#set smartquote(quotes: "«»")
+"Double and 'Single' Quotes"
+
+#set smartquote(quotes: (double: auto, single: "«»"))
+"Double and 'Single' Quotes"
+
+--- smartquote-custom-complex ---
+// Allow 2 graphemes
+#set smartquote(quotes: "a\u{0301}a\u{0301}")
+"Double and 'Single' Quotes"
+
+#set smartquote(quotes: (single: "a\u{0301}a\u{0301}"))
+"Double and 'Single' Quotes"
+
+--- smartquote-custom-bad-string ---
+// Error: 25-28 expected 2 characters, found 1 character
+#set smartquote(quotes: "'")
+
+--- smartquote-custom-bad-array ---
+// Error: 25-35 expected 2 quotes, found 4 quotes
+#set smartquote(quotes: ("'",) * 4)
+
+--- smartquote-custom-bad-dict ---
+// Error: 25-45 expected 2 quotes, found 4 quotes
+#set smartquote(quotes: (single: ("'",) * 4))
+
+--- issue-3662-pdf-smartquotes ---
+// Smart quotes were not appearing in the PDF outline, because they didn't
+// implement `PlainText`.
+= It's "Unnormal Heading"
+= It’s “Normal Heading”
+
+#set smartquote(enabled: false)
+= It's "Unnormal Heading"
+= It's 'single quotes'
+= It’s “Normal Heading”
+
+--- issue-1041-smartquotes-in-outline ---
+#set page(width: 15em)
+#outline()
+
+= "This" "is" "a" "test"
+
+--- issue-1540-smartquotes-across-newlines ---
+// Test that smart quotes are inferred correctly across newlines.
+"test"#linebreak()"test"
+
+"test"\
+"test"
diff --git a/tests/suite/text/space.typ b/tests/suite/text/space.typ
new file mode 100644
index 0000000000..97541e3857
--- /dev/null
+++ b/tests/suite/text/space.typ
@@ -0,0 +1,60 @@
+// Test whitespace handling.
+
+--- space-collapsing ---
+// Spacing around code constructs.
+A#let x = 1;B #test(x, 1) \
+C #let x = 2;D #test(x, 2) \
+E#if true [F]G \
+H #if true{"I"} J \
+K #if true [L] else []M \
+#let c = true; N#while c [#(c = false)O] P \
+#let c = true; Q #while c { c = false; "R" } S \
+T#for _ in (none,) {"U"}V
+#let foo = "A" ; \
+#foo;B \
+#foo; B \
+#foo ;B
+
+--- space-collapsing-comments ---
+// Test spacing with comments.
+A/**/B/**/C \
+A /**/ B/**/C \
+A /**/B/**/ C
+
+--- space-collapsing-with-h ---
+// Test spacing collapsing before spacing.
+#set align(right)
+A #h(0pt) B #h(0pt) \
+A B \
+A #h(-1fr) B
+
+--- text-font-just-a-space ---
+// Test that a run consisting only of whitespace isn't trimmed.
+A#text(font: "IBM Plex Serif")[ ]B
+
+--- text-font-change-after-space ---
+// Test font change after space.
+Left #text(font: "IBM Plex Serif")[Right].
+
+--- space-collapsing-linebreaks ---
+// Test that linebreak consumed surrounding spaces.
+#align(center)[A \ B \ C]
+
+--- space-collapsing-stringy-linebreak ---
+// Test that space at start of non-backslash-linebreak line isn't trimmed.
+A#"\n" B
+
+--- space-trailing-linebreak ---
+// Test that trailing space does not force a line break.
+LLLLLLLLLLLLLLLLLL R _L_
+
+--- space-ideographic-kept ---
+// Test that ideographic spaces are preserved.
+#set text(lang: "ja", font: "Noto Serif CJK JP")
+
+だろうか? 何のために! 私は、
+
+--- space-thin-kept ---
+// Test that thin spaces are preserved.
+| | U+0020 regular space \
+| | U+2009 thin space
diff --git a/tests/suite/visualize/circle.typ b/tests/suite/visualize/circle.typ
new file mode 100644
index 0000000000..43459eb5e2
--- /dev/null
+++ b/tests/suite/visualize/circle.typ
@@ -0,0 +1,69 @@
+// Test the `circle` function.
+
+--- circle ---
+// Default circle.
+#box(circle())
+#box(circle[Hey])
+
+--- circle-auto-sizing ---
+// Test auto sizing.
+#set circle(inset: 0pt)
+
+Auto-sized circle.
+#circle(fill: rgb("eb5278"), stroke: 2pt + black,
+ align(center + horizon)[But, soft!]
+)
+
+Center-aligned rect in auto-sized circle.
+#circle(fill: forest, stroke: conifer,
+ align(center + horizon,
+ rect(fill: conifer, inset: 5pt)[But, soft!]
+ )
+)
+
+Rect in auto-sized circle.
+#circle(fill: forest,
+ rect(fill: conifer, stroke: white, inset: 4pt)[
+ #set text(8pt)
+ But, soft! what light through yonder window breaks?
+ ]
+)
+
+Expanded by height.
+#circle(stroke: black, align(center)[A \ B \ C])
+
+--- circle-directly-in-rect ---
+// Ensure circle directly in rect works.
+#rect(width: 40pt, height: 30pt, fill: forest,
+ circle(fill: conifer))
+
+--- circle-relative-sizing ---
+// Test relative sizing.
+#set text(fill: white)
+#show: rect.with(width: 100pt, height: 50pt, inset: 0pt, fill: rgb("aaa"))
+#set align(center + horizon)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ 1fr,
+ circle(radius: 10pt, fill: eastern, [A]), // D=20pt
+ circle(height: 60%, fill: eastern, [B]), // D=30pt
+ circle(width: 20% + 20pt, fill: eastern, [C]), // D=40pt
+ 1fr,
+)
+
+--- circle-radius-width-and-height ---
+// Radius wins over width and height.
+// Error: 23-34 unexpected argument: width
+#circle(radius: 10pt, width: 50pt, height: 100pt, fill: eastern)
+
+--- circle-sizing-options ---
+// Test different ways of sizing.
+#set page(width: 120pt, height: 40pt)
+#stack(
+ dir: ltr,
+ spacing: 2pt,
+ circle(radius: 5pt),
+ circle(width: 10%),
+ circle(height: 50%),
+)
diff --git a/tests/suite/visualize/color.typ b/tests/suite/visualize/color.typ
new file mode 100644
index 0000000000..6cf887a42e
--- /dev/null
+++ b/tests/suite/visualize/color.typ
@@ -0,0 +1,331 @@
+// Test color modification methods.
+
+--- color-mix ---
+// Compare both ways.
+#test-repr(rgb(0%, 30.2%, 70.2%), rgb("004db3"))
+
+// Alpha channel.
+#test(rgb(255, 0, 0, 50%), rgb("ff000080"))
+
+// Test color modification methods.
+#test(rgb(25, 35, 45).lighten(10%), rgb(48, 57, 66))
+#test(rgb(40, 30, 20).darken(10%), rgb(36, 27, 18))
+#test(rgb("#133337").negate(space: rgb), rgb(236, 204, 200))
+#test(white.lighten(100%), white)
+
+// Color mixing, in Oklab space by default.
+#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"))), rgb("#d0a800"))
+#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: oklab)), rgb("#d0a800"))
+#test(rgb(color.mix(rgb("#ff0000"), rgb("#00ff00"), space: rgb)), rgb("#808000"))
+
+#test(rgb(color.mix(red, green, blue)), rgb("#909282"))
+#test(rgb(color.mix(red, blue, green)), rgb("#909282"))
+#test(rgb(color.mix(blue, red, green)), rgb("#909282"))
+
+// Mix with weights.
+#test(rgb(color.mix((red, 50%), (green, 50%))), rgb("#c0983b"))
+#test(rgb(color.mix((red, 0.5), (green, 0.5))), rgb("#c0983b"))
+#test(rgb(color.mix((red, 5), (green, 5))), rgb("#c0983b"))
+#test(rgb(color.mix((green, 5), (white, 0), (red, 5))), rgb("#c0983b"))
+#test(color.mix((rgb("#aaff00"), 25%), (rgb("#aa00ff"), 75%), space: rgb), rgb("#aa40bf"))
+#test(color.mix((rgb("#aaff00"), 50%), (rgb("#aa00ff"), 50%), space: rgb), rgb("#aa8080"))
+#test(color.mix((rgb("#aaff00"), 75%), (rgb("#aa00ff"), 25%), space: rgb), rgb("#aabf40"))
+
+// Mix in hue-based space.
+#test(rgb(color.mix(red, blue, space: color.hsl)), rgb("#c408ff"))
+#test(rgb(color.mix((red, 50%), (blue, 100%), space: color.hsl)), rgb("#5100f8"))
+// Error: 6-51 cannot mix more than two colors in a hue-based space
+#rgb(color.mix(red, blue, white, space: color.hsl))
+
+--- color-conversion ---
+// Test color conversion method kinds
+#test(rgb(rgb(10, 20, 30)).space(), rgb)
+#test(color.linear-rgb(rgb(10, 20, 30)).space(), color.linear-rgb)
+#test(oklab(rgb(10, 20, 30)).space(), oklab)
+#test(oklch(rgb(10, 20, 30)).space(), oklch)
+#test(color.hsl(rgb(10, 20, 30)).space(), color.hsl)
+#test(color.hsv(rgb(10, 20, 30)).space(), color.hsv)
+#test(cmyk(rgb(10, 20, 30)).space(), cmyk)
+#test(luma(rgb(10, 20, 30)).space(), luma)
+
+#test(rgb(color.linear-rgb(10, 20, 30)).space(), rgb)
+#test(color.linear-rgb(color.linear-rgb(10, 20, 30)).space(), color.linear-rgb)
+#test(oklab(color.linear-rgb(10, 20, 30)).space(), oklab)
+#test(oklch(color.linear-rgb(10, 20, 30)).space(), oklch)
+#test(color.hsl(color.linear-rgb(10, 20, 30)).space(), color.hsl)
+#test(color.hsv(color.linear-rgb(10, 20, 30)).space(), color.hsv)
+#test(cmyk(color.linear-rgb(10, 20, 30)).space(), cmyk)
+#test(luma(color.linear-rgb(10, 20, 30)).space(), luma)
+
+#test(rgb(oklab(10%, 20%, 30%)).space(), rgb)
+#test(color.linear-rgb(oklab(10%, 20%, 30%)).space(), color.linear-rgb)
+#test(oklab(oklab(10%, 20%, 30%)).space(), oklab)
+#test(oklch(oklab(10%, 20%, 30%)).space(), oklch)
+#test(color.hsl(oklab(10%, 20%, 30%)).space(), color.hsl)
+#test(color.hsv(oklab(10%, 20%, 30%)).space(), color.hsv)
+#test(cmyk(oklab(10%, 20%, 30%)).space(), cmyk)
+#test(luma(oklab(10%, 20%, 30%)).space(), luma)
+
+#test(rgb(oklch(60%, 40%, 0deg)).space(), rgb)
+#test(color.linear-rgb(oklch(60%, 40%, 0deg)).space(), color.linear-rgb)
+#test(oklab(oklch(60%, 40%, 0deg)).space(), oklab)
+#test(oklch(oklch(60%, 40%, 0deg)).space(), oklch)
+#test(color.hsl(oklch(60%, 40%, 0deg)).space(), color.hsl)
+#test(color.hsv(oklch(60%, 40%, 0deg)).space(), color.hsv)
+#test(cmyk(oklch(60%, 40%, 0deg)).space(), cmyk)
+#test(luma(oklch(60%, 40%, 0deg)).space(), luma)
+
+#test(rgb(color.hsl(10deg, 20%, 30%)).space(), rgb)
+#test(color.linear-rgb(color.hsl(10deg, 20%, 30%)).space(), color.linear-rgb)
+#test(oklab(color.hsl(10deg, 20%, 30%)).space(), oklab)
+#test(oklch(color.hsl(10deg, 20%, 30%)).space(), oklch)
+#test(color.hsl(color.hsl(10deg, 20%, 30%)).space(), color.hsl)
+#test(color.hsv(color.hsl(10deg, 20%, 30%)).space(), color.hsv)
+#test(cmyk(color.hsl(10deg, 20%, 30%)).space(), cmyk)
+#test(luma(color.hsl(10deg, 20%, 30%)).space(), luma)
+
+#test(rgb(color.hsv(10deg, 20%, 30%)).space(), rgb)
+#test(color.linear-rgb(color.hsv(10deg, 20%, 30%)).space(), color.linear-rgb)
+#test(oklab(color.hsv(10deg, 20%, 30%)).space(), oklab)
+#test(oklch(color.hsv(10deg, 20%, 30%)).space(), oklch)
+#test(color.hsl(color.hsv(10deg, 20%, 30%)).space(), color.hsl)
+#test(color.hsv(color.hsv(10deg, 20%, 30%)).space(), color.hsv)
+#test(cmyk(color.hsv(10deg, 20%, 30%)).space(), cmyk)
+#test(luma(color.hsv(10deg, 20%, 30%)).space(), luma)
+
+#test(rgb(cmyk(10%, 20%, 30%, 40%)).space(), rgb)
+#test(color.linear-rgb(cmyk(10%, 20%, 30%, 40%)).space(), color.linear-rgb)
+#test(oklab(cmyk(10%, 20%, 30%, 40%)).space(), oklab)
+#test(oklch(cmyk(10%, 20%, 30%, 40%)).space(), oklch)
+#test(color.hsl(cmyk(10%, 20%, 30%, 40%)).space(), color.hsl)
+#test(color.hsv(cmyk(10%, 20%, 30%, 40%)).space(), color.hsv)
+#test(cmyk(cmyk(10%, 20%, 30%, 40%)).space(), cmyk)
+#test(luma(cmyk(10%, 20%, 30%, 40%)).space(), luma)
+
+#test(rgb(luma(10%)).space(), rgb)
+#test(color.linear-rgb(luma(10%)).space(), color.linear-rgb)
+#test(oklab(luma(10%)).space(), oklab)
+#test(oklch(luma(10%)).space(), oklch)
+#test(color.hsl(luma(10%)).space(), color.hsl)
+#test(color.hsv(luma(10%)).space(), color.hsv)
+#test(cmyk(luma(10%)).space(), cmyk)
+#test(luma(luma(10%)).space(), luma)
+
+#test(rgb(1, 2, 3).to-hex(), "#010203")
+#test(rgb(1, 2, 3, 4).to-hex(), "#01020304")
+#test(luma(40).to-hex(), "#282828")
+#test-repr(cmyk(4%, 5%, 6%, 7%).to-hex(), "#e0dcda")
+#test-repr(rgb(cmyk(4%, 5%, 6%, 7%)), rgb(87.84%, 86.27%, 85.49%, 100%))
+#test-repr(rgb(luma(40%)), rgb(40%, 40%, 40%))
+#test-repr(cmyk(luma(40)), cmyk(11.76%, 10.67%, 10.51%, 14.12%))
+#test-repr(cmyk(rgb(1, 2, 3)), cmyk(66.67%, 33.33%, 0%, 98.82%))
+#test-repr(luma(rgb(1, 2, 3)), luma(0.73%))
+#test-repr(color.hsl(luma(40)), color.hsl(0deg, 0%, 15.69%))
+#test-repr(color.hsv(luma(40)), color.hsv(0deg, 0%, 15.69%))
+#test-repr(color.linear-rgb(luma(40)), color.linear-rgb(2.12%, 2.12%, 2.12%))
+#test-repr(color.linear-rgb(rgb(1, 2, 3)), color.linear-rgb(0.03%, 0.06%, 0.09%))
+#test-repr(color.hsl(rgb(1, 2, 3)), color.hsl(-150deg, 50%, 0.78%))
+#test-repr(color.hsv(rgb(1, 2, 3)), color.hsv(-150deg, 66.67%, 1.18%))
+#test-repr(oklab(luma(40)), oklab(27.68%, 0.0, 0.0, 100%))
+#test-repr(oklab(rgb(1, 2, 3)), oklab(8.23%, -0.004, -0.007, 100%))
+#test-repr(oklch(oklab(40%, 0.2, 0.2)), oklch(40%, 0.283, 45deg, 100%))
+#test-repr(oklch(luma(40)), oklch(27.68%, 0.0, 72.49deg, 100%))
+#test-repr(oklch(rgb(1, 2, 3)), oklch(8.23%, 0.008, 240.75deg, 100%))
+
+--- color-spaces ---
+// The the different color spaces
+#let col = rgb(50%, 64%, 16%)
+#box(square(size: 9pt, fill: col))
+#box(square(size: 9pt, fill: rgb(col)))
+#box(square(size: 9pt, fill: oklab(col)))
+#box(square(size: 9pt, fill: oklch(col)))
+#box(square(size: 9pt, fill: luma(col)))
+#box(square(size: 9pt, fill: cmyk(col)))
+#box(square(size: 9pt, fill: color.linear-rgb(col)))
+#box(square(size: 9pt, fill: color.hsl(col)))
+#box(square(size: 9pt, fill: color.hsv(col)))
+
+--- color-space ---
+// Test color kind method.
+#test(rgb(1, 2, 3, 4).space(), rgb)
+#test(cmyk(4%, 5%, 6%, 7%).space(), cmyk)
+#test(luma(40).space(), luma)
+#test(rgb(1, 2, 3, 4).space() != luma, true)
+
+--- color-components ---
+// Test color '.components()' without conversions
+
+#let test-components(col, ref, has-alpha: true) = {
+ // Perform an approximate scalar comparison.
+ let are-equal((a, b)) = {
+ let to-float(x) = if type(x) == angle { x.rad() } else { float(x) }
+ let epsilon = 1e-4 // The maximum error between both numbers
+ assert.eq(type(a), type(b))
+ calc.abs(to-float(a) - to-float(b)) < epsilon
+ }
+
+ let ref-without-alpha = if has-alpha { ref.slice(0, -1) } else { ref }
+ assert.eq(col.components().len(), ref.len())
+ assert(col.components().zip(ref).all(are-equal))
+ assert(col.components(alpha: false).zip(ref-without-alpha).all(are-equal))
+}
+#test-components(rgb(1, 2, 3, 4), (0.39%, 0.78%, 1.18%, 1.57%))
+#test-components(luma(40), (15.69%, 100%))
+#test-components(luma(40, 50%), (15.69%, 50%))
+#test-components(cmyk(4%, 5%, 6%, 7%), (4%, 5%, 6%, 7%), has-alpha: false)
+#test-components(oklab(10%, 0.2, 0.4), (10%, 0.2, 0.4, 100%))
+#test-components(oklch(10%, 0.2, 90deg), (10%, 0.2, 90deg, 100%))
+#test-components(oklab(10%, 50%, 200%), (10%, 0.2, 0.8, 100%))
+#test-components(oklch(10%, 50%, 90deg), (10%, 0.2, 90deg, 100%))
+#test-components(color.linear-rgb(10%, 20%, 30%), (10%, 20%, 30%, 100%))
+#test-components(color.hsv(10deg, 20%, 30%), (10deg, 20%, 30%, 100%))
+#test-components(color.hsl(10deg, 20%, 30%), (10deg, 20%, 30%, 100%))
+
+--- color-luma ---
+// Test gray color conversion.
+#stack(dir: ltr, rect(fill: luma(0)), rect(fill: luma(80%)))
+
+--- color-rgb-out-of-range ---
+// Error for values that are out of range.
+// Error: 11-14 number must be between 0 and 255
+#test(rgb(-30, 15, 50))
+
+--- color-rgb-bad-string ---
+// Error: 6-11 color string contains non-hexadecimal letters
+#rgb("lol")
+
+--- color-rgb-missing-argument-red ---
+// Error: 2-7 missing argument: red component
+#rgb()
+
+--- color-rgb-missing-argument-blue ---
+// Error: 2-11 missing argument: blue component
+#rgb(0, 1)
+
+--- color-rgb-bad-type ---
+// Error: 21-26 expected integer or ratio, found boolean
+#rgb(10%, 20%, 30%, false)
+
+--- color-luma-unexpected-argument ---
+// Error: 10-20 unexpected argument: key
+#luma(1, key: "val")
+
+--- color-mix-bad-amount-type ---
+// Error: 12-24 expected float or ratio, found string
+// Error: 26-39 expected float or ratio, found string
+#color.mix((red, "yes"), (green, "no"), (green, 10%))
+
+--- color-mix-bad-value ---
+// Error: 12-23 expected a color or color-weight pair
+#color.mix((red, 1, 2))
+
+--- color-mix-bad-space-type ---
+// Error: 31-38 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`, found string
+#color.mix(red, green, space: "cyber")
+
+--- color-mix-bad-space-value-1 ---
+// Error: 31-36 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`
+#color.mix(red, green, space: image)
+
+--- color-mix-bad-space-value-2 ---
+// Error: 31-41 expected `rgb`, `luma`, `cmyk`, `oklab`, `oklch`, `color.linear-rgb`, `color.hsl`, or `color.hsv`
+#color.mix(red, green, space: calc.round)
+
+--- color-cmyk-ops ---
+// Test CMYK color conversion.
+#let c = cmyk(50%, 64%, 16%, 17%)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)),
+ rect(width: 1cm, fill: c),
+ rect(width: 1cm, fill: c.negate(space: cmyk)),
+)
+
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: c.lighten(x * 10%)))
+}
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: c.darken(x * 10%)))
+}
+
+--- color-outside-srgb-gamut ---
+// Colors outside the sRGB gamut.
+#box(square(size: 9pt, fill: oklab(90%, -0.2, -0.1)))
+#box(square(size: 9pt, fill: oklch(50%, 0.5, 0deg)))
+
+--- color-rotate-hue ---
+// Test hue rotation
+#let col = rgb(50%, 64%, 16%)
+
+// Oklch
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg)))
+}
+
+// HSL
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsl)))
+}
+
+// HSV
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsv)))
+}
+
+--- color-saturation ---
+// Test saturation
+#let col = color.hsl(180deg, 0%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.saturate(x * 10%)))
+}
+
+#let col = color.hsl(180deg, 100%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.desaturate(x * 10%)))
+}
+
+#let col = color.hsv(180deg, 0%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.saturate(x * 10%)))
+}
+
+#let col = color.hsv(180deg, 100%, 50%)
+#for x in range(0, 11) {
+ box(square(size: 9pt, fill: col.desaturate(x * 10%)))
+}
+
+--- color-luma-ops ---
+// Test gray color modification.
+#test-repr(luma(20%).lighten(50%), luma(60%))
+#test-repr(luma(80%).darken(20%), luma(64%))
+#test-repr(luma(80%).negate(space: luma), luma(20%))
+
+--- color-transparentize ---
+// Test alpha modification.
+#test-repr(luma(100%, 100%).transparentize(50%), luma(100%, 50%))
+#test-repr(luma(100%, 100%).transparentize(75%), luma(100%, 25%))
+#test-repr(luma(100%, 50%).transparentize(50%), luma(100%, 25%))
+#test-repr(luma(100%, 10%).transparentize(250%), luma(100%, 0%))
+#test-repr(luma(100%, 40%).transparentize(-50%), luma(100%, 70%))
+#test-repr(luma(100%, 0%).transparentize(-100%), luma(100%, 100%))
+
+--- color-opacify ---
+#test-repr(luma(100%, 50%).opacify(50%), luma(100%, 75%))
+#test-repr(luma(100%, 20%).opacify(100%), luma(100%, 100%))
+#test-repr(luma(100%, 100%).opacify(250%), luma(100%, 100%))
+#test-repr(luma(100%, 50%).opacify(-50%), luma(100%, 25%))
+#test-repr(luma(100%, 0%).opacify(0%), luma(100%, 0%))
+
+--- repr-color ---
+// Colors
+#set page(width: 400pt)
+#set text(0.8em)
+#blue \
+#color.linear-rgb(blue) \
+#oklab(blue) \
+#oklch(blue) \
+#cmyk(blue) \
+#color.hsl(blue) \
+#color.hsv(blue) \
+#luma(blue)
diff --git a/tests/suite/visualize/ellipse.typ b/tests/suite/visualize/ellipse.typ
new file mode 100644
index 0000000000..970a795e80
--- /dev/null
+++ b/tests/suite/visualize/ellipse.typ
@@ -0,0 +1,31 @@
+// Test the `ellipse` function.
+
+--- ellipse ---
+// Default ellipse.
+#ellipse()
+
+--- ellipse-auto-sizing ---
+#set rect(inset: 0pt)
+#set ellipse(inset: 0pt)
+
+Rect in ellipse in fixed rect.
+#rect(width: 3cm, height: 2cm, fill: rgb("2a631a"),
+ ellipse(fill: forest, width: 100%, height: 100%,
+ rect(fill: conifer, width: 100%, height: 100%,
+ align(center + horizon)[
+ Stuff inside an ellipse!
+ ]
+ )
+ )
+)
+
+Auto-sized ellipse.
+#ellipse(fill: conifer, stroke: 3pt + forest, inset: 3pt)[
+ #set text(8pt)
+ But, soft! what light through yonder window breaks?
+]
+
+
+An inline
+#box(ellipse(width: 8pt, height: 6pt, outset: (top: 3pt, rest: 5.5pt)))
+ellipse.
diff --git a/tests/suite/visualize/gradient.typ b/tests/suite/visualize/gradient.typ
new file mode 100644
index 0000000000..1ee5489a0f
--- /dev/null
+++ b/tests/suite/visualize/gradient.typ
@@ -0,0 +1,631 @@
+--- gradient-linear-angled ---
+// Test gradients with direction.
+#set page(width: 90pt)
+#grid(
+ gutter: 3pt,
+ columns: 4,
+ ..range(0, 360, step: 15).map(i => box(
+ height: 15pt,
+ width: 15pt,
+ fill: gradient.linear(angle: i * 1deg, (red, 0%), (blue, 100%)),
+ ))
+)
+
+
+--- gradient-linear-oklab ---
+// The tests below test whether hue rotation works correctly.
+// Here we test in Oklab space for reference.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: oklab)
+)
+
+--- gradient-linear-oklch ---
+// Test in OkLCH space.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: oklch)
+)
+
+--- gradient-linear-hsv ---
+// Test in HSV space.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: color.hsv)
+)
+
+--- gradient-linear-hsl ---
+// Test in HSL space.
+#set page(
+ width: 100pt,
+ height: 30pt,
+ fill: gradient.linear(red, purple, space: color.hsl)
+)
+
+
+--- gradient-linear-relative-parent ---
+// The image should look as if there is a single gradient that is being used for
+// both the page and the rectangles.
+#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-linear-relative-self ---
+// The image should look as if there are multiple gradients, one for each
+// rectangle.
+#let grad = gradient.linear(red, blue, green, purple, relative: "self");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-linear-repeat-and-mirror-1 ---
+// Test repeated gradients.
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.inferno).repeat(2, mirror: true)
+)
+
+--- gradient-linear-repeat-and-mirror-2 ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).repeat(2, mirror: true),
+)
+
+--- gradient-linear-repeat-and-mirror-3 ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).repeat(5, mirror: true)
+)
+
+--- gradient-linear-sharp-and-repeat ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).sharp(10).repeat(5, mirror: false)
+)
+
+--- gradient-linear-sharp-repeat-and-mirror ---
+#rect(
+ height: 40pt,
+ width: 100%,
+ fill: gradient.linear(..color.map.rainbow).sharp(10).repeat(5, mirror: true)
+)
+
+--- gradient-linear-sharp ---
+#square(
+ size: 100pt,
+ fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10),
+)
+#square(
+ size: 100pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10),
+)
+#square(
+ size: 100pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsl).sharp(10),
+)
+
+--- gradient-linear-sharp-and-smooth ---
+#square(
+ size: 100pt,
+ fill: gradient.linear(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
+)
+#square(
+ size: 100pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
+)
+#square(
+ size: 100pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsl).sharp(10, smoothness: 40%),
+)
+
+--- gradient-linear-stroke ---
+#align(center + top, square(size: 50pt, fill: black, stroke: 5pt + gradient.linear(red, blue)))
+
+--- gradient-fill-and-stroke ---
+#align(
+ center + bottom,
+ square(
+ size: 50pt,
+ fill: gradient.radial(red, blue, radius: 70.7%, focal-center: (10%, 10%)),
+ stroke: 10pt + gradient.radial(red, blue, radius: 70.7%, focal-center: (10%, 10%))
+ )
+)
+
+--- gradient-linear-line ---
+// Test gradient on lines
+#set page(width: 100pt, height: 100pt)
+#line(length: 100%, stroke: 1pt + gradient.linear(red, blue))
+#line(length: 100%, angle: 10deg, stroke: 1pt + gradient.linear(red, blue))
+#line(length: 100%, angle: 10deg, stroke: 1pt + gradient.linear(red, blue, relative: "parent"))
+
+--- gradient-radial-hsl ---
+#square(
+ size: 100pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl),
+)
+
+--- gradient-radial-center ---
+#grid(
+ columns: 2,
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (0%, 0%)),
+ ),
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (0%, 100%)),
+ ),
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (100%, 0%)),
+ ),
+ square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, center: (100%, 100%)),
+ ),
+)
+
+--- gradient-radial-radius ---
+#square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, radius: 10%),
+)
+#square(
+ size: 50pt,
+ fill: gradient.radial(..color.map.rainbow, space: color.hsl, radius: 72%),
+)
+
+--- gradient-radial-focal-center-and-radius ---
+#circle(
+ radius: 25pt,
+ fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (35%, 35%), focal-radius: 5%),
+)
+#circle(
+ radius: 25pt,
+ fill: gradient.radial(white, rgb("#8fbc8f"), focal-center: (75%, 35%), focal-radius: 5%),
+)
+
+--- gradient-radial-relative-parent ---
+// The image should look as if there is a single gradient that is being used for
+// both the page and the rectangles.
+#let grad = gradient.radial(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-radial-relative-self ---
+// The image should look as if there are multiple gradients, one for each
+// rectangle.
+#let grad = gradient.radial(red, blue, green, purple, relative: "self");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-radial-text ---
+// Test that gradient fills on text.
+// The solid bar gradients are used to make sure that all transforms are
+// correct: if you can see the text through the bar, then the gradient is
+// misaligned to its reference container.
+#set page(width: 200pt, height: auto, margin: 10pt)
+#set par(justify: true)
+#set text(fill: gradient.radial(red, blue))
+#lorem(30)
+
+--- gradient-conic ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv),
+)
+
+--- gradient-conic-center-shifted-1 ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv, center: (10%, 10%)),
+)
+
+--- gradient-conic-center-shifted-2 ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv, center: (90%, 90%)),
+)
+
+--- gradient-conic-angled ---
+#square(
+ size: 50pt,
+ fill: gradient.conic(..color.map.rainbow, space: color.hsv, angle: 90deg),
+)
+
+--- gradient-conic-oklab ---
+// Test in Oklab space for reference.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: oklab)
+)
+
+--- gradient-conic-oklch ---
+// Test in OkLCH space.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: oklch)
+)
+
+--- gradient-conic-hsv ---
+// Test in HSV space.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: color.hsv)
+)
+
+--- gradient-conic-hsl ---
+// Test in HSL space.
+#set page(
+ width: 100pt,
+ height: 100pt,
+ fill: gradient.conic(red, purple, space: color.hsl)
+)
+
+--- gradient-conic-relative-parent ---
+// The image should look as if there is a single gradient that is being used for
+// both the page and the rectangles.
+#let grad = gradient.conic(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-conic-relative-self ---
+// The image should look as if there are multiple gradients, one for each
+// rectangle.
+#let grad = gradient.conic(red, blue, green, purple, relative: "self");
+#let my-rect = rect(width: 50%, height: 50%, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+ fill: grad,
+ background: place(top + left, my-rect),
+)
+#place(top + right, my-rect)
+#place(bottom + center, rotate(45deg, my-rect))
+
+--- gradient-conic-stroke ---
+#align(
+ center + bottom,
+ square(
+ size: 50pt,
+ fill: black,
+ stroke: 10pt + gradient.conic(red, blue)
+ )
+)
+
+--- gradient-conic-text ---
+#set page(width: 200pt, height: auto, margin: 10pt)
+#set par(justify: true)
+#set text(fill: gradient.conic(red, blue, angle: 45deg))
+#lorem(30)
+
+--- gradient-text-bad-relative ---
+// Make sure they don't work when `relative: "self"`.
+// Hint: 17-61 make sure to set `relative: auto` on your text fill
+// Error: 17-61 gradients and patterns on text must be relative to the parent
+#set text(fill: gradient.linear(red, blue, relative: "self"))
+
+--- gradient-text-global ---
+// Test that gradient fills on text work for globally defined gradients.
+#set page(width: 200pt, height: auto, margin: 10pt, background: {
+ rect(width: 100%, height: 30pt, fill: gradient.linear(red, blue))
+})
+#set par(justify: true)
+#set text(fill: gradient.linear(red, blue))
+#lorem(30)
+
+--- gradient-text-dir ---
+// Sanity check that the direction works on text.
+#set page(width: 200pt, height: auto, margin: 10pt, background: {
+ rect(height: 100%, width: 30pt, fill: gradient.linear(dir: btt, red, blue))
+})
+#set par(justify: true)
+#set text(fill: gradient.linear(dir: btt, red, blue))
+#lorem(30)
+
+--- gradient-text-in-container ---
+// Test that gradient fills on text work for locally defined gradients.
+#set page(width: auto, height: auto, margin: 10pt)
+#show box: set text(fill: gradient.linear(..color.map.rainbow))
+Hello, #box[World]!
+
+--- gradient-text-rotate ---
+// Test that gradients fills on text work with transforms.
+#set page(width: auto, height: auto, margin: 10pt)
+#show box: set text(fill: gradient.linear(..color.map.rainbow))
+#rotate(45deg, box[World])
+
+--- gradient-text-decoration ---
+#set text(fill: gradient.linear(red, blue))
+
+Hello #underline[World]! \
+Hello #overline[World]! \
+Hello #strike[World]! \
+
+--- gradient-transformed ---
+// Test whether gradients work well when they are contained within a transform.
+#let grad = gradient.linear(red, blue, green, purple, relative: "parent");
+#let my-rect = rect(width: 50pt, height: 50pt, fill: grad)
+#set page(
+ height: 50pt,
+ width: 50pt,
+ margin: 2.5pt,
+)
+#place(top + right, scale(x: 200%, y: 130%, my-rect))
+#place(bottom + center, rotate(45deg, my-rect))
+#place(horizon + center, scale(x: 200%, y: 130%, rotate(45deg, my-rect)))
+
+--- gradient-presets ---
+// Test all gradient presets.
+#set page(width: 100pt, height: auto, margin: 0pt)
+#set text(fill: white, size: 18pt)
+#set text(top-edge: "bounds", bottom-edge: "bounds")
+
+#let presets = (
+ ("turbo", color.map.turbo),
+ ("cividis", color.map.cividis),
+ ("rainbow", color.map.rainbow),
+ ("spectral", color.map.spectral),
+ ("viridis", color.map.viridis),
+ ("inferno", color.map.inferno),
+ ("magma", color.map.magma),
+ ("plasma", color.map.plasma),
+ ("rocket", color.map.rocket),
+ ("mako", color.map.mako),
+ ("vlag", color.map.vlag),
+ ("icefire", color.map.icefire),
+ ("flare", color.map.flare),
+ ("crest", color.map.crest),
+)
+
+#stack(
+ spacing: 3pt,
+ ..presets.map(((name, preset)) => block(
+ width: 100%,
+ height: 20pt,
+ fill: gradient.linear(..preset),
+ align(center + horizon, smallcaps(name)),
+ ))
+)
+
+// Test that gradients are applied correctly on equations.
+
+--- gradient-math-cancel ---
+// Test on cancel
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ a dot cancel(5) = cancel(25) 5 x + cancel(5) 1 $
+
+--- gradient-math-frac ---
+// Test on frac
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ nabla dot bold(E) = frac(rho, epsilon_0) $
+
+--- gradient-math-root ---
+// Test on root
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ x_"1,2" = frac(-b +- sqrt(b^2 - 4 a c), 2 a) $
+
+--- gradient-math-mat ---
+// Test on matrix
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+--- gradient-math-underover ---
+// Test on underover
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ underline(X^2) $
+$ overline("hello, world!") $
+
+--- gradient-math-dir ---
+// Test a different direction
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow, dir: ttb))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+$ x_"1,2" = frac(-b +- sqrt(b^2 - 4 a c), 2 a) $
+
+--- gradient-math-misc ---
+// Test miscellaneous
+#show math.equation: set text(fill: gradient.linear(..color.map.rainbow))
+#show math.equation: box
+
+$ hat(x) = bar x bar = vec(x, y, z) = tilde(x) = dot(x) $
+$ x prime = vec(1, 2, delim: "[") $
+$ sum_(i in NN) 1 + i $
+$ attach(
+ Pi, t: alpha, b: beta,
+ tl: 1, tr: 2+3, bl: 4+5, br: 6,
+) $
+
+--- gradient-math-radial ---
+// Test radial gradient
+#show math.equation: set text(fill: gradient.radial(..color.map.rainbow, center: (30%, 30%)))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+--- gradient-math-conic ---
+// Test conic gradient
+#show math.equation: set text(fill: gradient.conic(red, blue, angle: 45deg))
+#show math.equation: box
+
+$ A = mat(
+ 1, 2, 3;
+ 4, 5, 6;
+ 7, 8, 9
+) $
+
+
+--- gradient-kind ---
+// Test gradient functions.
+#test(gradient.linear(red, green, blue).kind(), gradient.linear)
+
+--- gradient-stops ---
+#test(gradient.linear(red, green, blue).stops(), ((red, 0%), (green, 50%), (blue, 100%)))
+
+--- gradient-sample ---
+#test(gradient.linear(red, green, blue, space: rgb).sample(0%), red)
+#test(gradient.linear(red, green, blue, space: rgb).sample(25%), rgb("#97873b"))
+#test(gradient.linear(red, green, blue, space: rgb).sample(50%), green)
+#test(gradient.linear(red, green, blue, space: rgb).sample(75%), rgb("#17a08c"))
+#test(gradient.linear(red, green, blue, space: rgb).sample(100%), blue)
+
+--- gradient-space ---
+#test(gradient.linear(red, green, space: rgb).space(), rgb)
+#test(gradient.linear(red, green, space: oklab).space(), oklab)
+#test(gradient.linear(red, green, space: oklch).space(), oklch)
+#test(gradient.linear(red, green, space: cmyk).space(), cmyk)
+#test(gradient.linear(red, green, space: luma).space(), luma)
+#test(gradient.linear(red, green, space: color.linear-rgb).space(), color.linear-rgb)
+#test(gradient.linear(red, green, space: color.hsl).space(), color.hsl)
+#test(gradient.linear(red, green, space: color.hsv).space(), color.hsv)
+
+--- gradient-relative ---
+#test(gradient.linear(red, green, relative: "self").relative(), "self")
+#test(gradient.linear(red, green, relative: "parent").relative(), "parent")
+#test(gradient.linear(red, green).relative(), auto)
+
+--- gradient-angle ---
+#test(gradient.linear(red, green).angle(), 0deg)
+#test(gradient.linear(red, green, dir: ltr).angle(), 0deg)
+#test(gradient.linear(red, green, dir: rtl).angle(), 180deg)
+#test(gradient.linear(red, green, dir: ttb).angle(), 90deg)
+#test(gradient.linear(red, green, dir: btt).angle(), 270deg)
+
+--- gradient-repeat ---
+#test(
+ gradient.linear(red, green, blue).repeat(2).stops(),
+ ((red, 0%), (green, 25%), (blue, 50%), (red, 50%), (green, 75%), (blue, 100%))
+)
+#test(
+ gradient.linear(red, green, blue).repeat(2, mirror: true).stops(),
+ ((red, 0%), (green, 25%), (blue, 50%), (green, 75%), (red, 100%))
+)
+
+--- gradient-repr ---
+// Gradients
+#set page(width: 400pt)
+#set text(0.8em)
+#gradient.linear(blue, red) \
+#gradient.linear(blue, red, dir: ttb) \
+#gradient.linear(blue, red, angle: 45deg, relative: "self") \
+#gradient.linear(blue, red, angle: 45deg, space: rgb)
+
+--- issue-2902-gradient-oklch-panic ---
+// Minimal reproduction of #2902
+#set page(width: 15cm, height: auto, margin: 1em)
+#set block(width: 100%, height: 1cm, above: 2pt)
+
+// Oklch
+#block(fill: gradient.linear(red, purple, space: oklch))
+#block(fill: gradient.linear(..color.map.rainbow, space: oklch))
+#block(fill: gradient.linear(..color.map.plasma, space: oklch))
+
+--- issue-2902-gradient-oklab-panic ---
+#set page(width: 15cm, height: auto, margin: 1em)
+#set block(width: 100%, height: 1cm, above: 2pt)
+
+// Oklab
+#block(fill: gradient.linear(red, purple, space: oklab))
+#block(fill: gradient.linear(..color.map.rainbow, space: oklab))
+#block(fill: gradient.linear(..color.map.plasma, space: oklab))
+
+--- issue-gradient-cmyk-encode ---
+// Test that CMYK works on gradients
+#set page(margin: 0pt, width: 100pt, height: auto)
+
+#let violet = cmyk(75%, 80%, 0%, 0%)
+#let blue = cmyk(75%, 30%, 0%, 0%)
+
+#rect(
+ width: 100%,
+ height: 10pt,
+ fill: gradient.linear(violet, blue)
+)
+
+#rect(
+ width: 100%,
+ height: 10pt,
+ fill: gradient.linear(rgb(violet), rgb(blue))
+)
+
+// In PDF format, this gradient can look different from the others.
+// This is because PDF readers do weird things with CMYK.
+#rect(
+ width: 100%,
+ height: 10pt,
+ fill: gradient.linear(violet, blue, space: cmyk)
+)
diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ
new file mode 100644
index 0000000000..ac2d5af909
--- /dev/null
+++ b/tests/suite/visualize/image.typ
@@ -0,0 +1,122 @@
+// Test the `image` function.
+
+--- image-rgba-png-and-jpeg ---
+// Test loading different image formats.
+
+// Load an RGBA PNG image.
+#image("/assets/images/rhino.png")
+
+// Load an RGB JPEG image.
+#set page(height: 60pt)
+#image("/assets/images/tiger.jpg")
+
+--- image-sizing ---
+// Test configuring the size and fitting behaviour of images.
+
+// Set width and height explicitly.
+#box(image("/assets/images/rhino.png", width: 30pt))
+#box(image("/assets/images/rhino.png", height: 30pt))
+
+// Set width and height explicitly and force stretching.
+#image("/assets/images/monkey.svg", width: 100%, height: 20pt, fit: "stretch")
+
+// Make sure the bounding-box of the image is correct.
+#align(bottom + right, image("/assets/images/tiger.jpg", width: 40pt, alt: "A tiger"))
+
+--- image-fit ---
+// Test all three fit modes.
+#set page(height: 50pt, margin: 0pt)
+#grid(
+ columns: (1fr, 1fr, 1fr),
+ rows: 100%,
+ gutter: 3pt,
+ image("/assets/images/tiger.jpg", width: 100%, height: 100%, fit: "contain"),
+ image("/assets/images/tiger.jpg", width: 100%, height: 100%, fit: "cover"),
+ image("/assets/images/monkey.svg", width: 100%, height: 100%, fit: "stretch"),
+)
+
+--- image-jump-to-next-page ---
+// Does not fit to remaining height of page.
+#set page(height: 60pt)
+Stuff
+#image("/assets/images/rhino.png")
+
+--- image-baseline-with-box ---
+// Test baseline.
+A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B
+
+--- image-svg-complex ---
+// Test advanced SVG features.
+#image("/assets/images/pattern.svg")
+
+--- image-svg-text ---
+#set page(width: 250pt)
+
+#figure(
+ image("/assets/images/diagram.svg"),
+ caption: [A textful diagram],
+)
+
+--- image-svg-text-font ---
+#set page(width: 250pt)
+#show image: set text(font: ("Roboto", "Noto Serif CJK SC"))
+
+#figure(
+ image("/assets/images/chinese.svg"),
+ caption: [Bilingual text]
+)
+
+--- image-natural-dpi-sizing ---
+// Test that images aren't upscaled.
+// Image is just 48x80 at 220dpi. It should not be scaled to fit the page
+// width, but rather max out at its natural size.
+#image("/assets/images/f2t.jpg")
+
+--- image-file-not-found ---
+// Error: 8-29 file not found (searched at tests/suite/visualize/path/does/not/exist)
+#image("path/does/not/exist")
+
+--- image-bad-format ---
+// Error: 2-22 unknown image format
+#image("./image.typ")
+
+--- image-bad-svg ---
+// Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4)
+#image("/assets/images/bad.svg")
+
+--- image-decode-svg ---
+// Test parsing from svg data
+#image.decode(` `.text, format: "svg")
+
+--- image-decode-bad-svg ---
+// Error: 2-168 failed to parse SVG (missing root node)
+#image.decode(` `.text, format: "svg")
+
+--- image-decode-detect-format ---
+// Test format auto detect
+#image.decode(read("/assets/images/tiger.jpg", encoding: none), width: 80%)
+
+--- image-decode-specify-format ---
+// Test format manual
+#image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "jpg", width: 80%)
+
+--- image-decode-specify-wrong-format ---
+// Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.)
+#image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%)
+
+--- issue-870-image-rotation ---
+// Ensure that EXIF rotation is applied.
+// https://github.com/image-rs/image/issues/1045
+// File is from https://magnushoff.com/articles/jpeg-orientation/
+#image("/assets/images/f2t.jpg", width: 10pt)
+
+--- issue-measure-image ---
+// Test that image measurement doesn't turn `inf / some-value` into 0pt.
+#context {
+ let size = measure(image("/assets/images/tiger.jpg"))
+ test(size, (width: 1024pt, height: 670pt))
+}
+
+--- issue-2051-new-cm-svg ---
+#set text(font: "New Computer Modern")
+#image("/assets/images/diagram.svg")
diff --git a/tests/suite/visualize/line.typ b/tests/suite/visualize/line.typ
new file mode 100644
index 0000000000..7259f72ba8
--- /dev/null
+++ b/tests/suite/visualize/line.typ
@@ -0,0 +1,92 @@
+// Test lines.
+
+--- line-basic ---
+#set page(height: 60pt)
+#box({
+ set line(stroke: 0.75pt)
+ place(line(end: (0.4em, 0pt)))
+ place(line(start: (0pt, 0.4em), end: (0pt, 0pt)))
+ line(end: (0.6em, 0.6em))
+}) Hello #box(line(length: 1cm))!
+
+#line(end: (70%, 50%))
+
+--- line-positioning ---
+// Test the angle argument and positioning.
+
+#set page(fill: rgb("0B1026"))
+#set line(stroke: white)
+
+#let star(size, ..args) = box(width: size, height: size)[
+ #set text(spacing: 0%)
+ #set line(..args)
+ #set align(left)
+ #v(30%)
+ #place(line(length: +30%, start: (09.0%, 02%)))
+ #place(line(length: +30%, start: (38.7%, 02%), angle: -72deg))
+ #place(line(length: +30%, start: (57.5%, 02%), angle: 252deg))
+ #place(line(length: +30%, start: (57.3%, 02%)))
+ #place(line(length: -30%, start: (88.0%, 02%), angle: -36deg))
+ #place(line(length: +30%, start: (73.3%, 48%), angle: 252deg))
+ #place(line(length: -30%, start: (73.5%, 48%), angle: 36deg))
+ #place(line(length: +30%, start: (25.4%, 48%), angle: -36deg))
+ #place(line(length: +30%, start: (25.6%, 48%), angle: -72deg))
+ #place(line(length: +32%, start: (8.50%, 02%), angle: 34deg))
+]
+
+#align(center, grid(
+ columns: 3,
+ column-gutter: 10pt,
+ ..((star(20pt, stroke: 0.5pt),) * 9)
+))
+
+--- line-stroke ---
+// Some simple test lines
+#line(length: 60pt, stroke: red)
+#v(3pt)
+#line(length: 60pt, stroke: 2pt)
+#v(3pt)
+#line(length: 60pt, stroke: blue + 1.5pt)
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: "dashed"))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 4pt, cap: "round"))
+
+--- line-stroke-set ---
+// Set rules with stroke
+#set line(stroke: (paint: red, thickness: 1pt, cap: "butt", dash: "dash-dotted"))
+#line(length: 60pt)
+#v(3pt)
+#line(length: 60pt, stroke: blue)
+#v(3pt)
+#line(length: 60pt, stroke: (dash: none))
+
+--- line-stroke-dash ---
+// Dashing
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: ("dot", 1pt)))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: ("dot", 1pt, 4pt, 2pt)))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: (array: ("dot", 1pt, 4pt, 2pt), phase: 5pt)))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: ()))
+#v(3pt)
+#line(length: 60pt, stroke: (paint: red, thickness: 1pt, dash: (1pt, 3pt, 9pt)))
+
+--- line-stroke-field-typo ---
+// Error: 29-56 unexpected key "thicknes", valid keys are "paint", "thickness", "cap", "join", "dash", and "miter-limit"
+#line(length: 60pt, stroke: (paint: red, thicknes: 1pt))
+
+--- line-stroke-bad-dash-kind ---
+// Error: 29-55 expected "solid", "dotted", "densely-dotted", "loosely-dotted", "dashed", "densely-dashed", "loosely-dashed", "dash-dotted", "densely-dash-dotted", "loosely-dash-dotted", array, dictionary, none, or auto
+#line(length: 60pt, stroke: (paint: red, dash: "dash"))
+
+--- line-bad-point-array ---
+// Test errors.
+
+// Error: 12-19 point array must contain exactly two entries
+#line(end: (50pt,))
+
+--- line-bad-point-component-type ---
+// Error: 14-26 expected relative length, found angle
+#line(start: (3deg, 10pt), length: 5cm)
diff --git a/tests/suite/visualize/path.typ b/tests/suite/visualize/path.typ
new file mode 100644
index 0000000000..bdd3dc726e
--- /dev/null
+++ b/tests/suite/visualize/path.typ
@@ -0,0 +1,65 @@
+// Test paths.
+
+--- path ---
+#set page(height: 200pt, width: 200pt)
+#table(
+ columns: (1fr, 1fr),
+ rows: (1fr, 1fr),
+ align: center + horizon,
+ path(
+ fill: red,
+ closed: true,
+ ((0%, 0%), (4%, -4%)),
+ ((50%, 50%), (4%, -4%)),
+ ((0%, 50%), (4%, 4%)),
+ ((50%, 0%), (4%, 4%)),
+ ),
+ path(
+ fill: purple,
+ stroke: 1pt,
+ (0pt, 0pt),
+ (30pt, 30pt),
+ (0pt, 30pt),
+ (30pt, 0pt),
+ ),
+ path(
+ fill: blue,
+ stroke: 1pt,
+ closed: true,
+ ((30%, 0%), (35%, 30%), (-20%, 0%)),
+ ((30%, 60%), (-20%, 0%), (0%, 0%)),
+ ((50%, 30%), (60%, -30%), (60%, 0%)),
+ ),
+ path(
+ stroke: 5pt,
+ closed: true,
+ (0pt, 30pt),
+ (30pt, 30pt),
+ (15pt, 0pt),
+ ),
+)
+
+--- path-bad-vertex ---
+// Error: 7-9 path vertex must have 1, 2, or 3 points
+#path(())
+
+--- path-bad-point-count ---
+// Error: 7-47 path vertex must have 1, 2, or 3 points
+#path(((0%, 0%), (0%, 0%), (0%, 0%), (0%, 0%)))
+
+--- path-bad-point-array ---
+// Error: 7-31 point array must contain exactly two entries
+#path(((0%, 0%), (0%, 0%, 0%)))
+
+--- issue-path-in-sized-container ---
+// Paths used to implement `LayoutMultiple` rather than `LayoutSingle` without
+// fulfilling the necessary contract of respecting region expansion.
+#block(
+ fill: aqua,
+ width: 20pt,
+ height: 15pt,
+ path(
+ (0pt, 0pt),
+ (10pt, 10pt),
+ ),
+)
diff --git a/tests/suite/visualize/pattern.typ b/tests/suite/visualize/pattern.typ
new file mode 100644
index 0000000000..08051ed20e
--- /dev/null
+++ b/tests/suite/visualize/pattern.typ
@@ -0,0 +1,131 @@
+// Test patterns.
+
+--- pattern-line ---
+// Tests that simple patterns work.
+#set page(width: auto, height: auto, margin: 0pt)
+#let pat = pattern(size: (10pt, 10pt), line(stroke: 4pt, start: (0%, 0%), end: (100%, 100%)))
+#rect(width: 50pt, height: 50pt, fill: pat)
+
+--- pattern-lines ---
+#set page(width: auto, height: auto, margin: 0pt)
+
+#let pat = pattern(size: (10pt, 10pt), {
+ place(line(stroke: 4pt, start: (0%, 0%), end: (100%, 100%)))
+ place(line(stroke: 4pt, start: (100%,0%), end: (200%, 100%)))
+ place(line(stroke: 4pt, start: (0%,100%), end: (100%, 200%)))
+ place(line(stroke: 4pt, start: (-100%,0%), end: (0%, 100%)))
+ place(line(stroke: 4pt, start: (0%,-100%), end: (100%, 0%)))
+})
+#rect(width: 50pt, height: 50pt, fill: pat)
+
+--- pattern-relative-self ---
+// Test with relative set to `"self"`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt))
+ #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt))
+]
+
+#set page(fill: pat(), width: 100pt, height: 100pt)
+
+#rect(fill: pat(relative: "self"), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-relative-parent ---
+// Test with relative set to `"parent"`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt))
+ #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt))
+]
+
+#set page(fill: pat(), width: 100pt, height: 100pt)
+
+#rect(fill: pat(relative: "parent"), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-small ---
+// Tests small patterns for pixel accuracy.
+#box(
+ width: 8pt,
+ height: 1pt,
+ fill: pattern(size: (1pt, 1pt), square(size: 1pt, fill: black))
+)
+#v(-1em)
+#box(
+ width: 8pt,
+ height: 1pt,
+ fill: pattern(size: (2pt, 1pt), square(size: 1pt, fill: black))
+)
+
+--- pattern-zero-sized ---
+// Error: 15-52 pattern tile size must be non-zero
+// Hint: 15-52 try setting the size manually
+#line(stroke: pattern(path((0pt, 0pt), (1em, 0pt))))
+
+--- pattern-spacing-negative ---
+// Test with spacing set to `(-10pt, -10pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (-10pt, -10pt)), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-spacing-zero ---
+// Test with spacing set to `(0pt, 0pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (0pt, 0pt)), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-spacing-positive ---
+// Test with spacing set to `(10pt, 10pt)`
+#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
+ #square(width: 100%, height: 100%, stroke: 1pt, fill: blue)
+]
+
+#set page(width: 100pt, height: 100pt)
+
+#rect(fill: pat(spacing: (10pt, 10pt,)), width: 100%, height: 100%, stroke: 1pt)
+
+--- pattern-stroke ---
+// Test pattern on strokes
+#align(
+ center + top,
+ square(
+ size: 50pt,
+ stroke: 5pt + pattern(
+ size: (5pt, 5pt),
+ align(horizon + center, circle(fill: blue, radius: 2.5pt))
+ )
+ )
+)
+
+--- pattern-text ---
+// Test a pattern on some text
+// You shouldn't be able to see the text, if you can then
+// that means that the transform matrices are not being
+// applied to the text correctly.
+#let pat = pattern(
+ size: (30pt, 30pt),
+ relative: "parent",
+ square(size: 30pt, fill: gradient.conic(..color.map.rainbow))
+);
+
+#set page(
+ width: 140pt,
+ height: 140pt,
+ fill: pat
+)
+
+#rotate(45deg, scale(x: 50%, y: 70%, rect(
+ width: 100%,
+ height: 100%,
+ stroke: 1pt,
+)[
+ #lorem(10)
+
+ #set text(fill: pat)
+ #lorem(10)
+]))
diff --git a/tests/suite/visualize/polygon.typ b/tests/suite/visualize/polygon.typ
new file mode 100644
index 0000000000..a3f4c8ef14
--- /dev/null
+++ b/tests/suite/visualize/polygon.typ
@@ -0,0 +1,51 @@
+// Test polygons.
+
+--- polygon ---
+#set page(width: 50pt)
+#set polygon(stroke: 0.75pt, fill: blue)
+
+// These are not visible, but should also not give an error
+#polygon()
+#polygon((0em, 0pt))
+#polygon((0pt, 0pt), (10pt, 0pt))
+#polygon.regular(size: 0pt, vertices: 9)
+
+#polygon((5pt, 0pt), (0pt, 10pt), (10pt, 10pt))
+#polygon(
+ (0pt, 0pt), (5pt, 5pt), (10pt, 0pt),
+ (15pt, 5pt),
+ (5pt, 10pt)
+)
+#polygon(stroke: none, (5pt, 0pt), (0pt, 10pt), (10pt, 10pt))
+#polygon(stroke: 3pt, fill: none, (5pt, 0pt), (0pt, 10pt), (10pt, 10pt))
+
+// Relative size
+#polygon((0pt, 0pt), (100%, 5pt), (50%, 10pt))
+
+// Antiparallelogram
+#polygon((0pt, 5pt), (5pt, 0pt), (0pt, 10pt), (5pt, 15pt))
+
+// Self-intersections
+#polygon((0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt))
+
+// Regular polygon; should have equal side lengths
+#for k in range(3, 9) {polygon.regular(size: 30pt, vertices: k,)}
+
+--- polygon-line-join ---
+// Line joins
+#stack(
+ dir: ltr,
+ spacing: 1em,
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "round"),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "bevel"),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "miter"),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+ polygon(stroke: (thickness: 4pt, paint: blue, join: "miter", miter-limit: 20.0),
+ (0pt, 20pt), (15pt, 0pt), (0pt, 40pt), (15pt, 45pt)),
+)
+
+--- polygon-bad-point-array ---
+// Error: 10-17 point array must contain exactly two entries
+#polygon((50pt,))
diff --git a/tests/suite/visualize/rect.typ b/tests/suite/visualize/rect.typ
new file mode 100644
index 0000000000..f84fafcb11
--- /dev/null
+++ b/tests/suite/visualize/rect.typ
@@ -0,0 +1,107 @@
+// Test the `rect` function.
+
+--- rect ---
+// Default rectangle.
+#rect()
+
+--- rect-customization ---
+#set page(width: 150pt)
+
+// Fit to text.
+#rect(fill: conifer)[Textbox]
+
+// Empty with fixed width and height.
+#block(rect(
+ height: 15pt,
+ fill: rgb("46b3c2"),
+ stroke: 2pt + rgb("234994"),
+))
+
+// Fixed width, text height.
+#rect(width: 2cm, fill: rgb("9650d6"))[Fixed and padded]
+
+// Page width, fixed height.
+#rect(height: 1cm, width: 100%, fill: rgb("734ced"))[Topleft]
+
+// These are inline with text.
+{#box(rect(width: 0.5in, height: 7pt, fill: rgb("d6cd67")))
+ #box(rect(width: 0.5in, height: 7pt, fill: rgb("edd466")))
+ #box(rect(width: 0.5in, height: 7pt, fill: rgb("e3be62")))}
+
+// Rounded corners.
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ rect(width: 2cm, radius: 30%),
+ rect(width: 1cm, radius: (left: 10pt, right: 5pt)),
+ rect(width: 1.25cm, radius: (
+ top-left: 2pt,
+ top-right: 5pt,
+ bottom-right: 8pt,
+ bottom-left: 11pt
+ )),
+)
+
+// Different strokes.
+#set rect(stroke: (right: red))
+#rect(width: 100%, fill: lime, stroke: (x: 5pt, y: 1pt))
+
+--- rect-stroke ---
+// Rectangle strokes
+#rect(width: 20pt, height: 20pt, stroke: red)
+#v(3pt)
+#rect(width: 20pt, height: 20pt, stroke: (rest: red, top: (paint: blue, dash: "dashed")))
+#v(3pt)
+#rect(width: 20pt, height: 20pt, stroke: (thickness: 5pt, join: "round"))
+
+--- red-stroke-bad-type ---
+// Error: 15-21 expected length, color, gradient, pattern, dictionary, stroke, none, or auto, found array
+#rect(stroke: (1, 2))
+
+--- rect-fill-stroke ---
+#let variant = rect.with(width: 20pt, height: 10pt)
+#let items = for (i, item) in (
+ variant(stroke: none),
+ variant(),
+ variant(fill: none),
+ variant(stroke: 2pt),
+ variant(stroke: eastern),
+ variant(stroke: eastern + 2pt),
+ variant(fill: eastern),
+ variant(fill: eastern, stroke: none),
+ variant(fill: forest, stroke: none),
+ variant(fill: forest, stroke: conifer),
+ variant(fill: forest, stroke: black + 2pt),
+ variant(fill: forest, stroke: conifer + 2pt),
+).enumerate() {
+ (align(horizon)[#(i + 1).], item, [])
+}
+
+#grid(
+ columns: (auto, auto, 1fr, auto, auto, 0fr),
+ gutter: 5pt,
+ ..items,
+)
+
+--- rect-radius-bad-key ---
+// Error: 15-38 unexpected key "cake", valid keys are "top-left", "top-right", "bottom-right", "bottom-left", "left", "top", "right", "bottom", and "rest"
+#rect(radius: (left: 10pt, cake: 5pt))
+
+--- issue-1825-rect-overflow ---
+#set page(width: 17.8cm)
+#set par(justify: true)
+#rect(lorem(70))
+
+--- issue-3264-rect-negative-dimensions ---
+// Negative dimensions
+#rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse left]
+
+#rect(width: 1cm, fill: gradient.linear(red, blue))[Left]
+
+#align(center, rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse center])
+
+#align(center, rect(width: 1cm, fill: gradient.linear(red, blue))[Center])
+
+#align(right, rect(width: -1cm, fill: gradient.linear(red, blue))[Reverse right])
+
+#align(right, rect(width: 1cm, fill: gradient.linear(red, blue))[Right])
diff --git a/tests/suite/visualize/square.typ b/tests/suite/visualize/square.typ
new file mode 100644
index 0000000000..caa1fc21f1
--- /dev/null
+++ b/tests/suite/visualize/square.typ
@@ -0,0 +1,146 @@
+// Test the `square` function.
+
+--- square ---
+// Default square.
+#box(square())
+#box(square[hey!])
+
+--- square-auto-sized ---
+// Test auto-sized square.
+#square(fill: eastern)[
+ #set text(fill: white, weight: "bold")
+ Typst
+]
+
+--- square-relatively-sized-child ---
+// Test relative-sized child.
+#square(fill: eastern)[
+ #rect(width: 10pt, height: 5pt, fill: conifer)
+ #rect(width: 40%, height: 5pt, stroke: conifer)
+]
+
+--- square-contents-overflow ---
+// Test text overflowing height.
+#set page(width: 75pt, height: 100pt)
+#square(fill: conifer)[
+ But, soft! what light through yonder window breaks?
+]
+
+--- square-height-limited ---
+// Test that square does not overflow page.
+#set page(width: 100pt, height: 75pt)
+#square(fill: conifer)[
+ But, soft! what light through yonder window breaks?
+]
+
+--- square-size-width-and-height ---
+// Size wins over width and height.
+// Error: 09-20 unexpected argument: width
+#square(width: 10cm, height: 20cm, size: 1cm, fill: rgb("eb5278"))
+
+--- square-relative-size ---
+// Test relative width and height and size that is smaller
+// than default size.
+#set page(width: 120pt, height: 70pt)
+#set align(bottom)
+#let centered = align.with(center + horizon)
+#stack(
+ dir: ltr,
+ spacing: 1fr,
+ square(width: 50%, centered[A]),
+ square(height: 50%),
+ stack(
+ square(size: 10pt),
+ square(size: 20pt, centered[B])
+ ),
+)
+
+--- square-circle-alignment ---
+// Test alignment in automatically sized square and circle.
+#set text(8pt)
+#box(square(inset: 4pt)[
+ Hey there, #align(center + bottom, rotate(180deg, [you!]))
+])
+#box(circle(align(center + horizon, [Hey.])))
+
+--- square-circle-overspecified ---
+// Test that minimum wins if both width and height are given.
+#stack(
+ dir: ltr,
+ spacing: 2pt,
+ square(width: 20pt, height: 40pt),
+ circle(width: 20%, height: 100pt),
+)
+
+--- square-height-limited-stack ---
+// Test square that is limited by region size.
+#set page(width: 20pt, height: 10pt, margin: 0pt)
+#stack(dir: ltr, square(fill: forest), square(fill: conifer))
+
+--- square-overflow ---
+// Test that square doesn't overflow due to its aspect ratio.
+#set page(width: 40pt, height: 25pt, margin: 5pt)
+#square(width: 100%)
+#square(width: 100%)[Hello there]
+
+--- square-size-relative-invalid ---
+// Size cannot be relative because we wouldn't know
+// relative to which axis.
+// Error: 15-18 expected length or auto, found ratio
+#square(size: 50%)
+
+--- square-rect-rounded ---
+#set square(size: 20pt, stroke: 4pt)
+
+// no radius for non-rounded corners
+#stack(
+ dir: ltr,
+ square(),
+ h(10pt),
+ square(radius: 0pt),
+ h(10pt),
+ square(radius: -10pt),
+)
+
+#stack(
+ dir: ltr,
+ square(),
+ h(10pt),
+ square(radius: 0%),
+ h(10pt),
+ square(radius: -10%),
+)
+
+// small values for small radius
+#stack(
+ dir: ltr,
+ square(radius: 1pt),
+ h(10pt),
+ square(radius: 5%),
+ h(10pt),
+ square(radius: 2pt),
+)
+
+// large values for large radius or circle
+#stack(
+ dir: ltr,
+ square(radius: 8pt),
+ h(10pt),
+ square(radius: 10pt),
+ h(10pt),
+ square(radius: 12pt),
+)
+
+#stack(
+ dir: ltr,
+ square(radius: 45%),
+ h(10pt),
+ square(radius: 50%),
+ h(10pt),
+ square(radius: 55%),
+)
+
+--- square-base ---
+// Test that square sets correct base for its content.
+#set page(height: 80pt)
+#square(width: 40%, rect(width: 60%, height: 80%))
diff --git a/tests/suite/visualize/stroke.typ b/tests/suite/visualize/stroke.typ
new file mode 100644
index 0000000000..b03c96c5d8
--- /dev/null
+++ b/tests/suite/visualize/stroke.typ
@@ -0,0 +1,171 @@
+// Test lines.
+
+--- stroke-constructor ---
+// Converting to stroke
+#assert.eq(stroke(red).paint, red)
+#assert.eq(stroke(red).thickness, auto)
+#assert.eq(stroke(2pt).paint, auto)
+#assert.eq(stroke((cap: "round", paint: blue)).cap, "round")
+#assert.eq(stroke((cap: auto, paint: blue)).cap, auto)
+#assert.eq(stroke((cap: auto, paint: blue)).thickness, auto)
+
+// Constructing with named arguments
+#assert.eq(stroke(paint: blue, thickness: 8pt), 8pt + blue)
+#assert.eq(stroke(thickness: 2pt), stroke(2pt))
+#assert.eq(stroke(cap: "round").thickness, auto)
+#assert.eq(stroke(cap: "round", thickness: auto).thickness, auto)
+
+--- stroke-constructor-unknown-key ---
+// Error: 9-21 unexpected key "foo", valid keys are "paint", "thickness", "cap", "join", "dash", and "miter-limit"
+#stroke((foo: "bar"))
+
+--- stroke-fields-simple ---
+// Test stroke fields for simple strokes.
+#test((1em + blue).paint, blue)
+#test((1em + blue).thickness, 1em)
+#test((1em + blue).cap, auto)
+#test((1em + blue).join, auto)
+#test((1em + blue).dash, auto)
+#test((1em + blue).miter-limit, auto)
+
+--- stroke-fields-complex ---
+// Test complex stroke fields.
+#let r1 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: none))
+#let r2 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: (3pt, "dot", 4em)))
+#let r3 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", dash: (array: (3pt, "dot", 4em), phase: 5em)))
+#let s1 = r1.stroke
+#let s2 = r2.stroke
+#let s3 = r3.stroke
+#test(s1.paint, cmyk(1%, 2%, 3%, 4%))
+#test(s1.thickness, 4em + 2pt)
+#test(s1.cap, "round")
+#test(s1.join, "bevel")
+#test(s1.miter-limit, 5.0)
+#test(s3.miter-limit, auto)
+#test(s1.dash, none)
+#test(s2.dash, (array: (3pt, "dot", 4em), phase: 0pt))
+#test(s3.dash, (array: (3pt, "dot", 4em), phase: 5em))
+
+--- stroke-zero-thickness ---
+// 0pt strokes must function exactly like 'none' strokes and not draw anything
+#rect(width: 10pt, height: 10pt, stroke: none)
+#rect(width: 10pt, height: 10pt, stroke: 0pt)
+#rect(width: 10pt, height: 10pt, stroke: none, fill: blue)
+#rect(width: 10pt, height: 10pt, stroke: 0pt + red, fill: blue)
+
+#line(length: 30pt, stroke: 0pt)
+#line(length: 30pt, stroke: (paint: red, thickness: 0pt, dash: ("dot", 1pt)))
+
+#table(columns: 2, stroke: none)[A][B]
+#table(columns: 2, stroke: 0pt)[A][B]
+
+#path(
+ fill: red,
+ stroke: none,
+ closed: true,
+ ((0%, 0%), (4%, -4%)),
+ ((50%, 50%), (4%, -4%)),
+ ((0%, 50%), (4%, 4%)),
+ ((50%, 0%), (4%, 4%)),
+)
+
+#path(
+ fill: red,
+ stroke: 0pt,
+ closed: true,
+ ((0%, 0%), (4%, -4%)),
+ ((50%, 50%), (4%, -4%)),
+ ((0%, 50%), (4%, 4%)),
+ ((50%, 0%), (4%, 4%)),
+)
+
+--- stroke-text ---
+#set text(size: 20pt)
+#set page(width: auto)
+#let v = [测试字体Test]
+
+#text(stroke: 0.3pt + red, v)
+
+#text(stroke: 0.7pt + red, v)
+
+#text(stroke: 7pt + red, v)
+
+#text(stroke: (paint: blue, thickness: 1pt, dash: "dashed"), v)
+
+#text(stroke: 1pt + gradient.linear(..color.map.rainbow), v)
+
+--- stroke-folding ---
+// Test stroke folding.
+#let sq(..args) = box(square(size: 10pt, ..args))
+
+#set square(stroke: none)
+#sq()
+#set square(stroke: auto)
+#sq()
+#sq(fill: teal)
+#sq(stroke: 2pt)
+#sq(stroke: blue)
+#sq(fill: teal, stroke: blue)
+#sq(fill: teal, stroke: 2pt + blue)
+
+--- stroke-composition ---
+// Test stroke composition.
+#set square(stroke: 4pt)
+#set text(font: "Roboto")
+#stack(
+ dir: ltr,
+ square(
+ stroke: (left: red, top: yellow, right: green, bottom: blue),
+ radius: 50%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+ h(0.5cm),
+ square(
+ stroke: (left: red, top: yellow + 8pt, right: green, bottom: blue + 2pt),
+ radius: 50%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+ h(0.5cm),
+ square(
+ stroke: (left: red, top: yellow, right: green, bottom: blue),
+ radius: 100%, align(center+horizon)[*G*],
+ inset: 8pt
+ ),
+)
+
+// Join between different solid strokes
+#set square(size: 20pt, stroke: 2pt)
+#set square(stroke: (left: green + 4pt, top: black + 2pt, right: blue, bottom: black + 2pt))
+#stack(
+ dir: ltr,
+ square(),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 1pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 8pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 100pt)),
+)
+
+// Join between solid and dotted strokes
+#set square(stroke: (left: green + 4pt, top: black + 2pt, right: (paint: blue, dash: "dotted"), bottom: (paint: black, dash: "dotted")))
+#stack(
+ dir: ltr,
+ square(),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 1pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 8pt)),
+ h(0.2cm),
+ square(radius: (top-left: 0pt, rest: 100pt)),
+)
+
+--- issue-3700-deformed-stroke ---
+// Test shape fill & stroke for specific values that used to make the stroke
+// deformed.
+#rect(
+ radius: 1mm,
+ width: 100%,
+ height: 10pt,
+ stroke: (left: rgb("46b3c2") + 16.0mm),
+)
diff --git a/tests/typ/autocomplete/showcase.typ b/tests/typ/autocomplete/showcase.typ
deleted file mode 100644
index 8ea94f2e3d..0000000000
--- a/tests/typ/autocomplete/showcase.typ
+++ /dev/null
@@ -1,13 +0,0 @@
-// Autocomplete: true
-// Ref: false
-
----
-// Autocomplete contains: -1 "int", "if conditional"
-// Autocomplete excludes: -1 "foo"
-#i
-
----
-
-// Autocomplete contains: -1 "insert", "remove", "len", "all"
-// Autocomplete excludes: -1 "foobar", "foo",
-#().
diff --git a/tests/typ/bugs/1050-terms-indent.typ b/tests/typ/bugs/1050-terms-indent.typ
deleted file mode 100644
index 82376820af..0000000000
--- a/tests/typ/bugs/1050-terms-indent.typ
+++ /dev/null
@@ -1,11 +0,0 @@
-#set page(width: 200pt)
-#set par(first-line-indent: 0.5cm)
-
-- #lorem(10)
-- #lorem(10)
-
-+ #lorem(10)
-+ #lorem(10)
-
-/ Term 1: #lorem(10)
-/ Term 2: #lorem(10)
diff --git a/tests/typ/bugs/1240-stack-fr.typ b/tests/typ/bugs/1240-stack-fr.typ
deleted file mode 100644
index fa49dce762..0000000000
--- a/tests/typ/bugs/1240-stack-fr.typ
+++ /dev/null
@@ -1,18 +0,0 @@
-// This issue is sort of horrible: When you write `h(1fr)` in a `stack` instead
-// of directly `1fr`, things go awry. To fix this, we now transparently detect
-// h/v children.
-//
-// https://github.com/typst/typst/issues/1240
-
----
-#stack(dir: ltr, [a], 1fr, [b], 1fr, [c])
-#stack(dir: ltr, [a], h(1fr), [b], h(1fr), [c])
-
----
-#set page(height: 60pt)
-#stack(
- dir: ltr,
- spacing: 1fr,
- stack([a], 1fr, [b]),
- stack([a], v(1fr), [b]),
-)
diff --git a/tests/typ/bugs/1597-cite-footnote.typ b/tests/typ/bugs/1597-cite-footnote.typ
deleted file mode 100644
index cc231e2a7b..0000000000
--- a/tests/typ/bugs/1597-cite-footnote.typ
+++ /dev/null
@@ -1,12 +0,0 @@
-// Tests that when a citation footnote is pushed to next page, things still
-// work as expected.
-//
-// Issue: https://github.com/typst/typst/issues/1597
-
----
-#set page(height: 60pt)
-#lorem(4)
-
-#footnote[@netwok]
-#show bibliography: none
-#bibliography("/assets/bib/works.bib")
diff --git a/tests/typ/bugs/2044-invalid-parsed-ident.typ b/tests/typ/bugs/2044-invalid-parsed-ident.typ
deleted file mode 100644
index 5e4b560caa..0000000000
--- a/tests/typ/bugs/2044-invalid-parsed-ident.typ
+++ /dev/null
@@ -1,6 +0,0 @@
-// In this bug, the dot at the end was causing the right parenthesis to be
-// parsed as an identifier instead of the closing right parenthesis.
-// Issue: https://github.com/typst/typst/issues/2044
-
-$floor(phi.alt.)$
-$floor(phi.alt. )$
diff --git a/tests/typ/bugs/2105-linebreak-tofu.typ b/tests/typ/bugs/2105-linebreak-tofu.typ
deleted file mode 100644
index 4dd5a24412..0000000000
--- a/tests/typ/bugs/2105-linebreak-tofu.typ
+++ /dev/null
@@ -1 +0,0 @@
-#linebreak()中文
diff --git a/tests/typ/bugs/2595-float-overlap.typ b/tests/typ/bugs/2595-float-overlap.typ
deleted file mode 100644
index 7c7f68c936..0000000000
--- a/tests/typ/bugs/2595-float-overlap.typ
+++ /dev/null
@@ -1,13 +0,0 @@
-#set page(height: 80pt)
-
-Start.
-
-#place(auto, float: true, [
- #block(height: 100%, width: 100%, fill: aqua)
-])
-
-#place(auto, float: true, [
- #block(height: 100%, width: 100%, fill: red)
-])
-
-#lorem(20)
diff --git a/tests/typ/bugs/2650-cjk-latin-spacing-meta.typ b/tests/typ/bugs/2650-cjk-latin-spacing-meta.typ
deleted file mode 100644
index 12c7ea41dd..0000000000
--- a/tests/typ/bugs/2650-cjk-latin-spacing-meta.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// https://github.com/typst/typst/issues/2650
-
-测a试
-
-测#context [a]试
diff --git a/tests/typ/bugs/2715-float-order.typ b/tests/typ/bugs/2715-float-order.typ
deleted file mode 100644
index af0684a1cf..0000000000
--- a/tests/typ/bugs/2715-float-order.typ
+++ /dev/null
@@ -1,19 +0,0 @@
-#set page(height: 180pt)
-#set figure(placement: auto)
-
-#figure(
- rect(height: 60pt),
- caption: [Rectangle I],
-)
-
-#figure(
- rect(height: 50pt),
- caption: [Rectangle II],
-)
-
-#figure(
- circle(),
- caption: [Circle],
-)
-
-#lorem(20)
diff --git a/tests/typ/bugs/2821-missing-fields.typ b/tests/typ/bugs/2821-missing-fields.typ
deleted file mode 100644
index 0fec20437e..0000000000
--- a/tests/typ/bugs/2821-missing-fields.typ
+++ /dev/null
@@ -1,9 +0,0 @@
-// Issue #2821: Setting a figure's supplement to none removes the field
-// Ref: false
-
----
-#show figure.caption: it => {
- assert(it.has("supplement"))
- assert(it.supplement == none)
-}
-#figure([], caption: [], supplement: none)
diff --git a/tests/typ/bugs/2902-gradient-oklch-panic.typ b/tests/typ/bugs/2902-gradient-oklch-panic.typ
deleted file mode 100644
index 6e09df5217..0000000000
--- a/tests/typ/bugs/2902-gradient-oklch-panic.typ
+++ /dev/null
@@ -1,20 +0,0 @@
-// Minimal reproduction of #2902
-// Ref: false
-
----
-#set page(width: 15cm, height: auto, margin: 1em)
-#set block(width: 100%, height: 1cm, above: 2pt)
-
-// Oklch
-#block(fill: gradient.linear(red, purple, space: oklch))
-#block(fill: gradient.linear(..color.map.rainbow, space: oklch))
-#block(fill: gradient.linear(..color.map.plasma, space: oklch))
-
----
-#set page(width: 15cm, height: auto, margin: 1em)
-#set block(width: 100%, height: 1cm, above: 2pt)
-
-// Oklab
-#block(fill: gradient.linear(red, purple, space: oklab))
-#block(fill: gradient.linear(..color.map.rainbow, space: oklab))
-#block(fill: gradient.linear(..color.map.plasma, space: oklab))
diff --git a/tests/typ/bugs/3082-chinese-punctuation.typ b/tests/typ/bugs/3082-chinese-punctuation.typ
deleted file mode 100644
index 82cab6f619..0000000000
--- a/tests/typ/bugs/3082-chinese-punctuation.typ
+++ /dev/null
@@ -1,4 +0,0 @@
-#set text(font: "Noto Serif CJK TC", lang: "zh")
-#set page(width: 230pt)
-
-課有手冬,朱得過已誰卜服見以大您即乙太邊良,因且行肉因和拉幸,念姐遠米巴急(abc0),松黃貫誰。
diff --git a/tests/typ/bugs/3110-no-type-ctor-or-field.typ b/tests/typ/bugs/3110-no-type-ctor-or-field.typ
deleted file mode 100644
index 61d6525313..0000000000
--- a/tests/typ/bugs/3110-no-type-ctor-or-field.typ
+++ /dev/null
@@ -1,15 +0,0 @@
-// Issue #3110: let the error message report the type name.
-// https://github.com/typst/typst/issues/3110
-// Ref: false
-
----
-// Error: 2-9 type content does not have a constructor
-#content()
-
----
-// Error: 6-12 type integer does not contain field `MAXVAL`
-#int.MAXVAL
-
----
-// Error: 6-18 type string does not contain field `from-unïcode`
-#str.from-unïcode(97)
diff --git a/tests/typ/bugs/3154-array-dict-mut-entry.typ b/tests/typ/bugs/3154-array-dict-mut-entry.typ
deleted file mode 100644
index b5a52814bb..0000000000
--- a/tests/typ/bugs/3154-array-dict-mut-entry.typ
+++ /dev/null
@@ -1,109 +0,0 @@
-// Issue #3154: Confusing errors from methods supposed to return a mutable entry
-// https://github.com/typst/typst/issues/3154
-// Ref: false
-
----
-#{
- let array = ()
- // Error: 3-16 array is empty
- array.first()
-}
-
----
-#{
- let array = ()
- // Error: 3-16 array is empty
- array.first() = 9
-}
-
----
-#{
- let array = ()
- // Error: 3-15 array is empty
- array.last()
-}
-
----
-#{
- let array = ()
- // Error: 3-15 array is empty
- array.last() = 9
-}
-
----
-#{
- let array = (1,)
- // Error: 3-14 array index out of bounds (index: 1, len: 1) and no default value was specified
- array.at(1)
-}
-
----
-#{
- let array = (1,)
- test(array.at(1, default: 0), 0)
-}
-
----
-#{
- let array = (1,)
- // Error: 3-14 array index out of bounds (index: 1, len: 1)
- array.at(1) = 9
-}
-
----
-#{
- let array = (1,)
- // Error: 3-26 array index out of bounds (index: 1, len: 1)
- array.at(1, default: 0) = 9
-}
-
----
-#{
- let dict = (a: 1)
- // Error: 3-15 dictionary does not contain key "b" and no default value was specified
- dict.at("b")
-}
-
----
-#{
- let dict = (a: 1)
- test(dict.at("b", default: 0), 0)
-}
-
----
-#{
- let dict = (a: 1)
- // Error: 3-15 dictionary does not contain key "b"
- // Hint: 3-15 use `insert` to add or update values
- dict.at("b") = 9
-}
-
----
-#{
- let dict = (a: 1)
- // Error: 3-27 dictionary does not contain key "b"
- // Hint: 3-27 use `insert` to add or update values
- dict.at("b", default: 0) = 9
-}
-
----
-#{
- let dict = (a: 1)
- // Error: 8-9 dictionary does not contain key "b"
- dict.b
-}
-
----
-#{
- let dict = (a: 1)
- dict.b = 9
- test(dict, (a: 1, b: 9))
-}
-
----
-#{
- let dict = (a: 1)
- // Error: 3-9 dictionary does not contain key "b"
- // Hint: 3-9 use `insert` to add or update values
- dict.b += 9
-}
diff --git a/tests/typ/bugs/3232-dict-wrong-keys.typ b/tests/typ/bugs/3232-dict-wrong-keys.typ
deleted file mode 100644
index 61d9e8b8a2..0000000000
--- a/tests/typ/bugs/3232-dict-wrong-keys.typ
+++ /dev/null
@@ -1,23 +0,0 @@
-// Issue #3232: Confusing "expected relative length or dictionary, found dictionary"
-// https://github.com/typst/typst/issues/3232
-// Ref: false
-
----
-// Error: 16-58 unexpected keys "unexpected" and "unexpected-too"
-#block(outset: (unexpected: 0.5em, unexpected-too: 0.2em), [Hi])
-
----
-// Error: 14-56 unexpected keys "unexpected" and "unexpected-too"
-#box(radius: (unexpected: 0.5em, unexpected-too: 0.5em), [Hi])
-
----
-// Error: 16-49 unexpected key "unexpected", valid keys are "left", "top", "right", "bottom", "x", "y", and "rest"
-#block(outset: (unexpected: 0.2em, right: 0.5em), [Hi]) // The 1st key is unexpected
-
----
-// Error: 14-50 unexpected key "unexpected", valid keys are "top-left", "top-right", "bottom-right", "bottom-left", "left", "top", "right", "bottom", and "rest"
-#box(radius: (top-left: 0.5em, unexpected: 0.5em), [Hi]) // The 2nd key is unexpected
-
----
-#block(outset: (:), [Hi]) // Ok
-#box(radius: (:), [Hi]) // Ok
diff --git a/tests/typ/bugs/3275-loop-errors.typ b/tests/typ/bugs/3275-loop-errors.typ
deleted file mode 100644
index 9fdd2961b8..0000000000
--- a/tests/typ/bugs/3275-loop-errors.typ
+++ /dev/null
@@ -1,67 +0,0 @@
-// Issue #3275: clearer errors for loops, https://github.com/typst/typst/issues/3275
-// Ref: false
-
----
-// Normal variable.
-#for x in (1, 2) {}
-#for x in (a: 1, b: 2) {}
-#for x in "foo" {}
-#for x in bytes("😊") {}
-
----
-// Placeholder.
-#for _ in (1, 2) {}
-#for _ in (a: 1, b: 2) {}
-#for _ in "foo" {}
-#for _ in bytes("😊") {}
-
----
-// Destructuring.
-#for (a,b,c) in (("a", 1, bytes(())), ("b", 2, bytes(""))) {}
-#for (a, ..) in (("a", 1, bytes(())), ("b", 2, bytes(""))) {}
-#for (k, v) in (a: 1, b: 2, c: 3) {}
-#for (.., v) in (a: 1, b: 2, c: 3) {}
-
----
-// Error: 11-17 cannot loop over content
-#for x in [1, 2] {}
-
----
-// Error: 11-25 cannot loop over arguments
-#for _ in arguments("a") {}
-
----
-// Error: 16-21 cannot loop over integer
-#for (x, y) in 12306 {}
-
----
-// Error: 16-22 cannot loop over content
-#for (x, y) in [1, 2] {}
-
----
-// Error: 6-12 cannot destructure values of string
-#for (x, y) in "foo" {}
-
----
-// Error: 6-12 cannot destructure string
-#for (x, y) in ("foo", "bar") {}
-
----
-// Error: 6-12 cannot destructure values of bytes
-#for (x, y) in bytes("😊") {}
-
----
-// Error: 6-12 cannot destructure bytes
-#for (x, y) in (bytes((1,2)), bytes((1,2))) {}
-
----
-// Error: 6-12 cannot destructure integer
-#for (x, y) in (1, 2) {}
-
----
-// Error: 10-11 not enough elements to destructure
-#for (x, y) in ((1,), (2,)) {}
-
----
-// Error: 6-12 too many elements to destructure
-#for (x, y) in ((1,2,3), (4,5,6)) {}
diff --git a/tests/typ/bugs/3502-colon-space.typ b/tests/typ/bugs/3502-colon-space.typ
deleted file mode 100644
index 35f38a9b2a..0000000000
--- a/tests/typ/bugs/3502-colon-space.typ
+++ /dev/null
@@ -1,14 +0,0 @@
-// Test that a space after a named parameter is permissible.
-// https://github.com/typst/typst/issues/3502
-// Ref: false
-
----
-#let f( param : v ) = param
-#test(f( param /* ok */ : 2 ), 2)
-
----
-#let ( key : /* hi */ binding ) = ( key: "ok" )
-#test(binding, "ok")
-
----
-#test(( key : "value" ).key, "value")
diff --git a/tests/typ/bugs/3586-figure-caption-separator.typ b/tests/typ/bugs/3586-figure-caption-separator.typ
deleted file mode 100644
index ee992c50ef..0000000000
--- a/tests/typ/bugs/3586-figure-caption-separator.typ
+++ /dev/null
@@ -1,7 +0,0 @@
-// Test that figure caption separator is synthesized correctly.
-// https://github.com/typst/typst/issues/3586
-// Ref: false
-
----
-#show figure.caption: c => test(c.separator, [#": "])
-#figure(table[], caption: [This is a test caption])
diff --git a/tests/typ/bugs/3601-empty-raw.typ b/tests/typ/bugs/3601-empty-raw.typ
deleted file mode 100644
index 3fb39acad3..0000000000
--- a/tests/typ/bugs/3601-empty-raw.typ
+++ /dev/null
@@ -1,7 +0,0 @@
-// Test that empty raw block with `typ` language doesn't cause a crash.
-// https://github.com/typst/typst/issues/3601
-// Ref: false
-
----
-```typ
-```
diff --git a/tests/typ/bugs/3641-float-loop.typ b/tests/typ/bugs/3641-float-loop.typ
deleted file mode 100644
index 4021fb4f27..0000000000
--- a/tests/typ/bugs/3641-float-loop.typ
+++ /dev/null
@@ -1,11 +0,0 @@
-// Flow layout should terminate!
-// https://github.com/typst/typst/issues/3641
-//
-// This is not yet ideal: The heading should not move to the second page, but
-// that's a separate bug and not a regression.
-
----
-#set page(height: 40pt)
-
-= Heading
-#lorem(6)
diff --git a/tests/typ/bugs/3650-italic-equation.typ b/tests/typ/bugs/3650-italic-equation.typ
deleted file mode 100644
index c9b4754311..0000000000
--- a/tests/typ/bugs/3650-italic-equation.typ
+++ /dev/null
@@ -1,4 +0,0 @@
-_abc $sin(x) "abc"$_ \
-$italic(sin(x) "abc" #box[abc])$ \
-*abc $sin(x) "abc"$* \
-$bold(sin(x) "abc" #box[abc])$ \
diff --git a/tests/typ/bugs/3658-math-size.typ b/tests/typ/bugs/3658-math-size.typ
deleted file mode 100644
index 63c020b20f..0000000000
--- a/tests/typ/bugs/3658-math-size.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// https://github.com/typst/typst/issues/3658
-
----
-$ #rect[$1/2$] $
-$#rect[$1/2$]$
diff --git a/tests/typ/bugs/870-image-rotation.typ b/tests/typ/bugs/870-image-rotation.typ
deleted file mode 100644
index 5d7b5597b1..0000000000
--- a/tests/typ/bugs/870-image-rotation.typ
+++ /dev/null
@@ -1,6 +0,0 @@
-// Ensure that EXIF rotation is applied.
-// https://github.com/image-rs/image/issues/1045
-
----
-// File is from https://magnushoff.com/articles/jpeg-orientation/
-#image("/assets/images/f2t.jpg", width: 10pt)
diff --git a/tests/typ/bugs/args-sink.typ b/tests/typ/bugs/args-sink.typ
deleted file mode 100644
index 4f7492ac4e..0000000000
--- a/tests/typ/bugs/args-sink.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Test bugs with argument sinks.
-
----
-#let foo(..body) = repr(body.pos())
-#foo(a: "1", b: "2", 1, 2, 3, 4, 5, 6)
diff --git a/tests/typ/bugs/args-underscore.typ b/tests/typ/bugs/args-underscore.typ
deleted file mode 100644
index ca3c0ff895..0000000000
--- a/tests/typ/bugs/args-underscore.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Test that lone underscore works.
-// Ref: false
-
----
-#test((1, 2, 3).map(_ => {}).len(), 3)
diff --git a/tests/typ/bugs/bibliography-math.typ b/tests/typ/bugs/bibliography-math.typ
deleted file mode 100644
index 3aab4b888d..0000000000
--- a/tests/typ/bugs/bibliography-math.typ
+++ /dev/null
@@ -1,4 +0,0 @@
-#set page(width: 200pt)
-
-@Zee04
-#bibliography("/assets/bib/works_too.bib", style: "mla")
diff --git a/tests/typ/bugs/bidi-tofus.typ b/tests/typ/bugs/bidi-tofus.typ
deleted file mode 100644
index 3b43b28053..0000000000
--- a/tests/typ/bugs/bidi-tofus.typ
+++ /dev/null
@@ -1,7 +0,0 @@
-// Test that shaping missing characters in both left-to-right and
-// right-to-left directions does not cause a crash.
-
----
-#"\u{590}\u{591}\u{592}\u{593}"
-
-#"\u{30000}\u{30001}\u{30002}\u{30003}"
diff --git a/tests/typ/bugs/block-width-box.typ b/tests/typ/bugs/block-width-box.typ
deleted file mode 100644
index a039bc6667..0000000000
--- a/tests/typ/bugs/block-width-box.typ
+++ /dev/null
@@ -1,6 +0,0 @@
-// Test box in 100% width block.
-
----
-#block(width: 100%, fill: red, box("a box"))
-
-#block(width: 100%, fill: red, [#box("a box") #box()])
diff --git a/tests/typ/bugs/cite-locate.typ b/tests/typ/bugs/cite-locate.typ
deleted file mode 100644
index 699bb085d1..0000000000
--- a/tests/typ/bugs/cite-locate.typ
+++ /dev/null
@@ -1,23 +0,0 @@
-// Test citation in other introspection.
-
----
-#set page(width: 180pt)
-#set heading(numbering: "1")
-
-#outline(
- title: [List of Figures],
- target: figure.where(kind: image),
-)
-
-#pagebreak()
-
-= Introduction
-#figure(
- rect[-- PIRATE --],
- caption: [A pirate @arrgh in @intro],
-)
-
-#context [Citation @distress on page #here().page()]
-
-#pagebreak()
-#bibliography("/assets/bib/works.bib", style: "chicago-notes")
diff --git a/tests/typ/bugs/cite-show-set.typ b/tests/typ/bugs/cite-show-set.typ
deleted file mode 100644
index f476dd496c..0000000000
--- a/tests/typ/bugs/cite-show-set.typ
+++ /dev/null
@@ -1,9 +0,0 @@
-// Test show set rules on citations.
-
----
-#show cite: set text(red)
-A @netwok @arrgh.
-B #cite() #cite().
-
-#show bibliography: none
-#bibliography("/assets/bib/works.bib")
diff --git a/tests/typ/bugs/clamp-panic.typ b/tests/typ/bugs/clamp-panic.typ
deleted file mode 100644
index 5f167c76b1..0000000000
--- a/tests/typ/bugs/clamp-panic.typ
+++ /dev/null
@@ -1,3 +0,0 @@
-#set page(height: 20pt, margin: 0pt)
-#v(22pt)
-#block(fill: red, width: 100%, height: 10pt, radius: 4pt)
diff --git a/tests/typ/bugs/columns-1.typ b/tests/typ/bugs/columns-1.typ
deleted file mode 100644
index 96a4d0e5e5..0000000000
--- a/tests/typ/bugs/columns-1.typ
+++ /dev/null
@@ -1,12 +0,0 @@
-// The well-known columns bug.
-
----
-#set page(height: 70pt)
-
-Hallo
-#columns(2)[
- = A
- Text
- = B
- Text
-]
diff --git a/tests/typ/bugs/emoji-linebreak.typ b/tests/typ/bugs/emoji-linebreak.typ
deleted file mode 100644
index 2f7e74e7fd..0000000000
--- a/tests/typ/bugs/emoji-linebreak.typ
+++ /dev/null
@@ -1,6 +0,0 @@
-// Test that there are no linebreaks in composite emoji (issue #80).
-
----
-#set page(width: 50pt, height: auto)
-#h(99%) 🏳️🌈
-🏳️🌈
diff --git a/tests/typ/bugs/equation-numbering-reference.typ b/tests/typ/bugs/equation-numbering-reference.typ
deleted file mode 100644
index 3423f02298..0000000000
--- a/tests/typ/bugs/equation-numbering-reference.typ
+++ /dev/null
@@ -1,15 +0,0 @@
-// In this bug, the hint and error messages for an equation
-// being reference mentioned that it was a "heading" and was
-// lacking the proper path.
-// Ref: false
-
----
-#set page(height: 70pt)
-
-$
- Delta = b^2 - 4 a c
-$
-
-// Error: 14-24 cannot reference equation without numbering
-// Hint: 14-24 you can enable equation numbering with `#set math.equation(numbering: "1.")`
-Looks at the @quadratic formula.
\ No newline at end of file
diff --git a/tests/typ/bugs/flow-1.typ b/tests/typ/bugs/flow-1.typ
deleted file mode 100644
index 425a0ce8e7..0000000000
--- a/tests/typ/bugs/flow-1.typ
+++ /dev/null
@@ -1,11 +0,0 @@
-// In this bug, the first line of the second paragraph was on its page alone an
-// the rest moved down. The reason was that the second block resulted in
-// overlarge frames because the region wasn't finished properly.
-
----
-#set page(height: 70pt)
-#block[This file tests a bug where an almost empty page occurs.]
-#block[
- The text in this second block was torn apart and split up for
- some reason beyond my knowledge.
-]
diff --git a/tests/typ/bugs/flow-2.typ b/tests/typ/bugs/flow-2.typ
deleted file mode 100644
index 5ffffd5837..0000000000
--- a/tests/typ/bugs/flow-2.typ
+++ /dev/null
@@ -1,10 +0,0 @@
-// In this bug, the first part of the paragraph moved down to the second page
-// because trailing leading wasn't trimmed, resulting in an overlarge frame.
-
----
-#set page(height: 60pt)
-#v(19pt)
-#block[
- But, soft! what light through yonder window breaks?
- It is the east, and Juliet is the sun.
-]
diff --git a/tests/typ/bugs/flow-3.typ b/tests/typ/bugs/flow-3.typ
deleted file mode 100644
index 71af1914c9..0000000000
--- a/tests/typ/bugs/flow-3.typ
+++ /dev/null
@@ -1,12 +0,0 @@
-// In this bug, there was a bit of space below the heading because weak spacing
-// directly before a layout-induced column or page break wasn't trimmed.
-
----
-#set page(height: 60pt)
-#rect(inset: 0pt, columns(2)[
- Text
- #v(12pt)
- Hi
- #v(10pt, weak: true)
- At column break.
-])
diff --git a/tests/typ/bugs/flow-4.typ b/tests/typ/bugs/flow-4.typ
deleted file mode 100644
index f49873f500..0000000000
--- a/tests/typ/bugs/flow-4.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// In this bug, a frame intended for the second region ended up in the first.
-
----
-#set page(height: 105pt)
-#block(lorem(20))
diff --git a/tests/typ/bugs/flow-5.typ b/tests/typ/bugs/flow-5.typ
deleted file mode 100644
index 5e580b9e83..0000000000
--- a/tests/typ/bugs/flow-5.typ
+++ /dev/null
@@ -1,13 +0,0 @@
-// This bug caused an index-out-of-bounds panic when layouting paragraphs needed
-// multiple reorderings.
-
----
-#set page(height: 200pt)
-#lorem(30)
-
-#figure(placement: auto, block(height: 100%))
-
-#lorem(10)
-
-#lorem(10)
-
diff --git a/tests/typ/bugs/fold-vector.typ b/tests/typ/bugs/fold-vector.typ
deleted file mode 100644
index 5d57ad3336..0000000000
--- a/tests/typ/bugs/fold-vector.typ
+++ /dev/null
@@ -1,20 +0,0 @@
-// Test fold order of vectors.
-
----
-#set text(features: (liga: 1))
-#set text(features: (liga: 0))
-fi
-
----
-#underline(stroke: aqua + 4pt)[
- #underline[Hello]
-]
-
----
-#let c = counter("mycounter")
-#c.update(1)
-#locate(loc => [
- #c.update(2)
- #c.at(loc) \
- Second: #locate(loc => c.at(loc))
-])
diff --git a/tests/typ/bugs/footnote-keep-multiple.typ b/tests/typ/bugs/footnote-keep-multiple.typ
deleted file mode 100644
index e4efe3ce86..0000000000
--- a/tests/typ/bugs/footnote-keep-multiple.typ
+++ /dev/null
@@ -1,10 +0,0 @@
-// Test that the logic that keeps footnote entry together with
-// their markers also works for multiple footnotes in a single
-// line or frame (here, there are two lines, but they are one
-// unit due to orphan prevention).
-
----
-#set page(height: 100pt)
-#v(40pt)
-A #footnote[a] \
-B #footnote[b]
diff --git a/tests/typ/bugs/footnote-list.typ b/tests/typ/bugs/footnote-list.typ
deleted file mode 100644
index ceece0caf2..0000000000
--- a/tests/typ/bugs/footnote-list.typ
+++ /dev/null
@@ -1,11 +0,0 @@
-// Test that footnotes in lists do not produce extraneous page breaks. The list
-// layout itself does not currently react to the footnotes layout, weakening the
-// "footnote and its entry are on the same page" invariant somewhat, but at
-// least there shouldn't be extra page breaks.
-
----
-#set page(height: 100pt)
-#block(height: 50pt, width: 100%, fill: aqua)
-
-- #footnote[1]
-- #footnote[2]
diff --git a/tests/typ/bugs/gradient-cmyk-encode.typ b/tests/typ/bugs/gradient-cmyk-encode.typ
deleted file mode 100644
index 5e0b58dc68..0000000000
--- a/tests/typ/bugs/gradient-cmyk-encode.typ
+++ /dev/null
@@ -1,27 +0,0 @@
-// Test that CMYK works on gradients
-
----
-#set page(margin: 0pt, width: 200pt, height: auto)
-
-#let violet = cmyk(75%, 80%, 0%, 0%)
-#let blue = cmyk(75%, 30%, 0%, 0%)
-
-#rect(
- width: 100%,
- height: 30pt,
- fill: gradient.linear(violet, blue)
-)
-
-#rect(
- width: 100%,
- height: 30pt,
- fill: gradient.linear(rgb(violet), rgb(blue))
-)
-
-// In PDF format, this gradient can look different from the others.
-// This is because PDF readers do weird things with CMYK.
-#rect(
- width: 100%,
- height: 30pt,
- fill: gradient.linear(violet, blue, space: cmyk)
-)
diff --git a/tests/typ/bugs/grid-1.typ b/tests/typ/bugs/grid-1.typ
deleted file mode 100644
index c583cfe586..0000000000
--- a/tests/typ/bugs/grid-1.typ
+++ /dev/null
@@ -1,16 +0,0 @@
-// Test that grid base for auto rows makes sense.
-
----
-#set page(height: 150pt)
-#table(
- columns: (1.5cm, auto),
- rows: (auto, auto),
- rect(width: 100%, fill: red),
- rect(width: 100%, fill: blue),
- rect(width: 100%, height: 50%, fill: green),
-)
-
----
-#rect(width: 100%, height: 1em)
-- #rect(width: 100%, height: 1em)
- - #rect(width: 100%, height: 1em)
diff --git a/tests/typ/bugs/grid-2.typ b/tests/typ/bugs/grid-2.typ
deleted file mode 100644
index b7528b7bdc..0000000000
--- a/tests/typ/bugs/grid-2.typ
+++ /dev/null
@@ -1,20 +0,0 @@
-// Grid now skips a remaining region when one of the cells
-// doesn't fit into it at all.
-
----
-#set page(height: 100pt)
-#grid(
- columns: (2cm, auto),
- rows: (auto, auto),
- rect(width: 100%, fill: red),
- rect(width: 100%, fill: blue),
- rect(width: 100%, height: 80%, fill: green),
- [hello \ darkness #parbreak() my \ old \ friend \ I],
- rect(width: 100%, height: 20%, fill: blue),
- polygon(fill: red, (0%, 0%), (100%, 0%), (100%, 20%))
-)
-
----
-#set page(height: 60pt)
-#lorem(5)
-- #lorem(5)
diff --git a/tests/typ/bugs/grid-3.typ b/tests/typ/bugs/grid-3.typ
deleted file mode 100644
index 19317c5038..0000000000
--- a/tests/typ/bugs/grid-3.typ
+++ /dev/null
@@ -1,8 +0,0 @@
-// Ensure that the list does not jump to the third page.
-
----
-#set page(height: 70pt)
-#v(40pt)
-The following:
-+ A
-+ B
diff --git a/tests/typ/bugs/grid-4.typ b/tests/typ/bugs/grid-4.typ
deleted file mode 100644
index 691bf877a1..0000000000
--- a/tests/typ/bugs/grid-4.typ
+++ /dev/null
@@ -1,17 +0,0 @@
-// Ensure gutter rows at the top or bottom of a region are skipped.
-
----
-#set page(height: 10em)
-
-#table(
- row-gutter: 1.5em,
- inset: 0pt,
- rows: (1fr, auto),
- [a],
- [],
- [],
- [f],
- [e\ e],
- [],
- [a]
-)
diff --git a/tests/typ/bugs/hide-meta.typ b/tests/typ/bugs/hide-meta.typ
deleted file mode 100644
index 8d2c7cb660..0000000000
--- a/tests/typ/bugs/hide-meta.typ
+++ /dev/null
@@ -1,24 +0,0 @@
-// Test that metadata of hidden stuff stays available.
-
----
-#set cite(style: "chicago-notes")
-
-A pirate. @arrgh \
-#set text(2pt)
-#hide[
- A @arrgh pirate.
- #bibliography("/assets/bib/works.bib")
-]
-
----
-#set text(8pt)
-#outline()
-#set text(2pt)
-#hide(block(grid(
- [= A],
- [= B],
- block(grid(
- [= C],
- [= D],
- ))
-)))
diff --git a/tests/typ/bugs/int-constructor.typ b/tests/typ/bugs/int-constructor.typ
deleted file mode 100644
index 0bdce6121e..0000000000
--- a/tests/typ/bugs/int-constructor.typ
+++ /dev/null
@@ -1,7 +0,0 @@
-// Test that integer -> integer conversion doesn't do a roundtrip through float.
-// Ref: false
-
----
-#let x = 9223372036854775800
-#test(type(x), int)
-#test(int(x), x)
diff --git a/tests/typ/bugs/justify-hanging-indent.typ b/tests/typ/bugs/justify-hanging-indent.typ
deleted file mode 100644
index 511aa172e5..0000000000
--- a/tests/typ/bugs/justify-hanging-indent.typ
+++ /dev/null
@@ -1,6 +0,0 @@
-// Test that combination of justification and hanging indent doesn't result in
-// an underfull first line.
-
----
-#set par(hanging-indent: 2.5cm, justify: true)
-#lorem(5)
diff --git a/tests/typ/bugs/label-fields-dict.typ b/tests/typ/bugs/label-fields-dict.typ
deleted file mode 100644
index 05c7006adf..0000000000
--- a/tests/typ/bugs/label-fields-dict.typ
+++ /dev/null
@@ -1,31 +0,0 @@
-// Tests whether the label is accessible through the has, field,
-// and fields accessors
-// Ref: false
-
----
-// Test whether the label is accessible through the has method
-#show heading: it => {
- assert(it.has("label"))
- it
-}
-
-= Hello, world!
-
----
-// Test whether the label is accessible through the field method
-#show heading: it => {
- assert(str(it.label) == "my_label")
- it
-}
-
-= Hello, world!
-
----
-// Test whether the label is accessible through the fields method
-#show heading: it => {
- assert("label" in it.fields())
- assert(str(it.fields().label) == "my_label")
- it
-}
-
-= Hello, world!
diff --git a/tests/typ/bugs/layout-infinite-lengths.typ b/tests/typ/bugs/layout-infinite-lengths.typ
deleted file mode 100644
index 7fbc62163b..0000000000
--- a/tests/typ/bugs/layout-infinite-lengths.typ
+++ /dev/null
@@ -1,25 +0,0 @@
-// Test that passing infinite lengths to drawing primitives does not crash Typst.
-
----
-#set page(width: auto, height: auto)
-
-// Error: 58-59 cannot expand into infinite width
-#layout(size => grid(columns: (size.width, size.height))[a][b][c][d])
-
----
-#set page(width: auto, height: auto)
-
-// Error: 17-66 cannot create grid with infinite height
-#layout(size => grid(rows: (size.width, size.height))[a][b][c][d])
-
----
-#set page(width: auto, height: auto)
-
-// Error: 17-41 cannot create line with infinite length
-#layout(size => line(length: size.width))
-
----
-#set page(width: auto, height: auto)
-
-// Error: 17-54 cannot create polygon with infinite size
-#layout(size => polygon((0pt,0pt), (0pt, size.width)))
diff --git a/tests/typ/bugs/line-align.typ b/tests/typ/bugs/line-align.typ
deleted file mode 100644
index 0518eaaa94..0000000000
--- a/tests/typ/bugs/line-align.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Test right-aligning a line and a rectangle.
-
----
-#align(right, line(length: 30%))
-#align(right, rect())
diff --git a/tests/typ/bugs/linebreak-no-justifiables.typ b/tests/typ/bugs/linebreak-no-justifiables.typ
deleted file mode 100644
index ab1b2732b5..0000000000
--- a/tests/typ/bugs/linebreak-no-justifiables.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Test breaking a line without justifiables.
-
----
-#set par(justify: true)
-#block(width: 1cm, fill: aqua, lorem(2))
diff --git a/tests/typ/bugs/mat-aug-color.typ b/tests/typ/bugs/mat-aug-color.typ
deleted file mode 100644
index c2e617d6cc..0000000000
--- a/tests/typ/bugs/mat-aug-color.typ
+++ /dev/null
@@ -1,9 +0,0 @@
-// https://github.com/typst/typst/issues/2268
-// The augment line should be of the same color as the text
-#set text(
- font: "New Computer Modern",
- lang: "en",
- fill: yellow,
-)
-
-$mat(augment: #1, M, v) arrow.r.squiggly mat(augment: #1, R, b)$
diff --git a/tests/typ/bugs/math-eval.typ b/tests/typ/bugs/math-eval.typ
deleted file mode 100644
index 31450b8dce..0000000000
--- a/tests/typ/bugs/math-eval.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Evaluating a math expr should renders the same as an equation
-
-#eval(mode: "math", "f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)")
-
-$f(a) = cases(a + b\, space space x >= 3,a + b\, space space x = 5)$
diff --git a/tests/typ/bugs/math-number-spacing.typ b/tests/typ/bugs/math-number-spacing.typ
deleted file mode 100644
index 9450caca95..0000000000
--- a/tests/typ/bugs/math-number-spacing.typ
+++ /dev/null
@@ -1,9 +0,0 @@
-// Test spacing after numbers in math.
-
----
-$
-10degree \
-10 degree \
-10.1degree \
-10.1 degree
-$
diff --git a/tests/typ/bugs/math-realize.typ b/tests/typ/bugs/math-realize.typ
deleted file mode 100644
index 10d8b78e00..0000000000
--- a/tests/typ/bugs/math-realize.typ
+++ /dev/null
@@ -1,47 +0,0 @@
-// Test that content in math can be realized without breaking
-// nested equations.
-
----
-#let my = $pi$
-#let f1 = box(baseline: 10pt, [f])
-#let f2 = context f1
-#show math.vec: [nope]
-
-$ pi a $
-$ my a $
-$ 1 + sqrt(x/2) + sqrt(#hide($x/2$)) $
-$ a x #link("url", $+ b$) $
-$ f f1 f2 $
-$ vec(1,2) * 2 $
-
----
-$ x^2 #hide[$(>= phi.alt) union y^2 0$] z^2 $
-Hello #hide[there $x$]
-and #hide[$ f(x) := x^2 $]
-
----
-// Test equations can embed equation pieces built by functions
-#let foo(v1, v2) = {
- // Return an equation piece that would've been rendered in
- // inline style if the piece is not embedded
- $v1 v2^2$
-}
-#let bar(v1, v2) = {
- // Return an equation piece that would've been rendered in
- // block style if the piece is not embedded
- $ v1 v2^2 $
-}
-#let baz(..sink) = {
- // Return an equation piece built by joining arrays
- sink.pos().map(x => $hat(#x)$).join(sym.and)
-}
-
-Inline $2 foo(alpha, (M+foo(a, b)))$.
-
-Inline $2 bar(alpha, (M+foo(a, b)))$.
-
-Inline $2 baz(x,y,baz(u, v))$.
-
-$ 2 foo(alpha, (M+foo(a, b))) $
-$ 2 bar(alpha, (M+foo(a, b))) $
-$ 2 baz(x,y,baz(u, v)) $
diff --git a/tests/typ/bugs/math-shift.typ b/tests/typ/bugs/math-shift.typ
deleted file mode 100644
index 4a833e312a..0000000000
--- a/tests/typ/bugs/math-shift.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// https://github.com/typst/typst/issues/2214
-// The math content should also be affected by the TextElem baseline.
-
-hello #text(baseline: -5pt)[123 #sym.WW\orld]\
-hello #text(baseline: -5pt)[$123 WW#text[or]$ld]\
diff --git a/tests/typ/bugs/math-text-break.typ b/tests/typ/bugs/math-text-break.typ
deleted file mode 100644
index a8aa1d0a6b..0000000000
--- a/tests/typ/bugs/math-text-break.typ
+++ /dev/null
@@ -1,4 +0,0 @@
-// Test text with linebreaks in math.
-
----
-$ x := "a\nb\nc\nd\ne" $
diff --git a/tests/typ/bugs/measure-image.typ b/tests/typ/bugs/measure-image.typ
deleted file mode 100644
index bd8703b3fc..0000000000
--- a/tests/typ/bugs/measure-image.typ
+++ /dev/null
@@ -1,8 +0,0 @@
-// Test that image measurement doesn't turn `inf / some-value` into 0pt.
-// Ref: false
-
----
-#context {
- let size = measure(image("/assets/images/tiger.jpg"))
- test(size, (width: 1024pt, height: 670pt))
-}
diff --git a/tests/typ/bugs/new-cm-svg.typ b/tests/typ/bugs/new-cm-svg.typ
deleted file mode 100644
index eeafcbbdb1..0000000000
--- a/tests/typ/bugs/new-cm-svg.typ
+++ /dev/null
@@ -1,2 +0,0 @@
-#set text(font: "New Computer Modern")
-#image("/assets/images/diagram.svg")
diff --git a/tests/typ/bugs/newline-mode.typ b/tests/typ/bugs/newline-mode.typ
deleted file mode 100644
index 30545eeee4..0000000000
--- a/tests/typ/bugs/newline-mode.typ
+++ /dev/null
@@ -1,24 +0,0 @@
-// Test newline continuations.
-
----
-#{
- "hello"
- .clusters()
- if false {
-
- }
- else {
- ("1", "2")
- }
-}
-
----
-#"hello"
- .codepoints()
-
-#if false {
-
-}
-else {
- ("1", "2")
-}
diff --git a/tests/typ/bugs/pagebreak-bibliography.typ b/tests/typ/bugs/pagebreak-bibliography.typ
deleted file mode 100644
index 257043a372..0000000000
--- a/tests/typ/bugs/pagebreak-bibliography.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Test weak pagebreak before bibliography.
-
----
-#pagebreak(weak: true)
-#bibliography("/assets/bib/works.bib")
diff --git a/tests/typ/bugs/pagebreak-numbering.typ b/tests/typ/bugs/pagebreak-numbering.typ
deleted file mode 100644
index a9fae3e44f..0000000000
--- a/tests/typ/bugs/pagebreak-numbering.typ
+++ /dev/null
@@ -1,12 +0,0 @@
-// https://github.com/typst/typst/issues/2095
-// The empty page 2 should not have a page number
-
-#set page(numbering: none)
-This and next page should not be numbered
-
-#pagebreak(weak: true, to: "odd")
-
-#set page(numbering: "1")
-#counter(page).update(1)
-
-This page should
diff --git a/tests/typ/bugs/pagebreak-set-style.typ b/tests/typ/bugs/pagebreak-set-style.typ
deleted file mode 100644
index 1ac24652c9..0000000000
--- a/tests/typ/bugs/pagebreak-set-style.typ
+++ /dev/null
@@ -1,12 +0,0 @@
-// https://github.com/typst/typst/issues/2162
-// The styles should not be applied to the pagebreak empty page,
-// it should only be applied after that.
-
-#pagebreak(to: "even") // We should now skip to page 2
-
-Some text on page 2
-
-#pagebreak(to: "even") // We should now skip to page 4
-
-#set page(fill: orange) // This sets the color of the page starting from page 4
-Some text on page 4
diff --git a/tests/typ/bugs/parameter-pattern.typ b/tests/typ/bugs/parameter-pattern.typ
deleted file mode 100644
index 31b07f2ca0..0000000000
--- a/tests/typ/bugs/parameter-pattern.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Test that underscore works in parameter patterns.
-// Ref: false
-
----
-#test((1, 2, 3).zip((1, 2, 3)).map(((_, x)) => x), (1, 2, 3))
diff --git a/tests/typ/bugs/parenthesized.typ b/tests/typ/bugs/parenthesized.typ
deleted file mode 100644
index f8f3190f24..0000000000
--- a/tests/typ/bugs/parenthesized.typ
+++ /dev/null
@@ -1,98 +0,0 @@
-// Ref: false
-// Test bugs related to destructuring and parenthesized parsing.
-
----
-// https://github.com/typst/typst/issues/1338
-#let foo = "foo"
-#let bar = "bar"
-// Error: 8-9 expected expression, found underscore
-// Error: 16-17 expected expression, found underscore
-#(foo: _, bar: _)
-
----
-// https://github.com/typst/typst/issues/1342
-// Error: 5-8 expected named or keyed pair, found identifier
-// Error: 10-13 expected named or keyed pair, found identifier
-#(: foo, bar)
-
----
-// https://github.com/typst/typst/issues/1351
-// Error: 17-22 expected pattern, found string
-#let foo((test: "bar")) = {}
-
----
-// https://github.com/typst/typst/issues/3014
-// Error: 8-17 expected expression, found named pair
-#(box, fill: red)
-
----
-// https://github.com/typst/typst/issues/3144
-#let f(a: 10) = a(1) + 1
-#test(f(a: _ => 5), 6)
-
----
-// Error: 17-20 missing argument: pattern parameter
-#let f(a: 10) = a() + 1
-#f(a: _ => 5)
-
----
-// This wasn't allowed.
-#let ((x)) = 1
-#test(x, 1)
-
----
-// This also wasn't allowed.
-#let ((a, b)) = (1, 2)
-#test(a, 1)
-#test(b, 2)
-
----
-// This was unintentionally allowed ...
-// Error: 9 expected equals sign
-#let (a)
-
----
-// ... where this wasn't.
-// Error: 12 expected equals sign
-#let (a, b)
-
----
-// This wasn't allowed before the bug fix ...
-#let f(..) = {}
-#f(arg: 1)
-
----
-// ... but this was.
-#let f(..x) = {}
-#f(arg: 1)
-
----
-// Here, `best` was accessed as a variable, where it shouldn't have.
-#{
- (best: _) = (best: "brr")
-}
-
----
-// Same here.
-#{
- let array = (1, 2, 3, 4)
- (test: array.at(1), best: _) = (test: "baz", best: "brr")
- test(array, (1, "baz", 3, 4))
-}
-
----
-// Here, `a` is not duplicate, where it was previously identified as one.
-#let f((a: b), (c,), a) = (a, b, c)
-#test(f((a: 1), (2,), 3), (3, 1, 2))
-
----
-// Ensure that we can't have non-atomic closures.
-#let x = 1
-#let c = [#(x) => (1, 2)]
-#test(c.children.last(), [(1, 2)]))
-
----
-// Ensure that we can't have non-atomic destructuring.
-#let x = 1
-#let c = [#() = ()]
-#test(c.children.last(), [()])
diff --git a/tests/typ/bugs/place-base.typ b/tests/typ/bugs/place-base.typ
deleted file mode 100644
index 4a0bd02903..0000000000
--- a/tests/typ/bugs/place-base.typ
+++ /dev/null
@@ -1,7 +0,0 @@
-// Test that placement is relative to container and not itself.
-
----
-#set page(height: 80pt, margin: 0pt)
-#place(right, dx: -70%, dy: 20%, [First])
-#place(left, dx: 20%, dy: 60%, [Second])
-#place(center + horizon, dx: 25%, dy: 25%, [Third])
diff --git a/tests/typ/bugs/place-pagebreak.typ b/tests/typ/bugs/place-pagebreak.typ
deleted file mode 100644
index bc04af1a39..0000000000
--- a/tests/typ/bugs/place-pagebreak.typ
+++ /dev/null
@@ -1,7 +0,0 @@
-// Test placing on an already full page.
-// It shouldn't result in a page break.
-
----
-#set page(height: 40pt)
-#block(height: 100%)
-#place(bottom + right)[Hello world]
diff --git a/tests/typ/bugs/place-spacing.typ b/tests/typ/bugs/place-spacing.typ
deleted file mode 100644
index 4d7b5fe38c..0000000000
--- a/tests/typ/bugs/place-spacing.typ
+++ /dev/null
@@ -1,15 +0,0 @@
-// Test that placed elements don't add extra block spacing.
-
----
-#show figure: set block(spacing: 4em)
-
-Paragraph before float.
-#figure(rect(), placement: bottom)
-Paragraph after float.
-
----
-#show place: set block(spacing: 4em)
-
-Paragraph before place.
-#place(rect())
-Paragraph after place.
diff --git a/tests/typ/bugs/raw-color-overwrite.typ b/tests/typ/bugs/raw-color-overwrite.typ
deleted file mode 100644
index ec306ef1a0..0000000000
--- a/tests/typ/bugs/raw-color-overwrite.typ
+++ /dev/null
@@ -1,13 +0,0 @@
-// Test that the color of a raw block is not overwritten
-
----
-
-#show raw: set text(fill: blue)
-
-`Hello, World!`
-
-```rs
-fn main() {
- println!("Hello, World!");
-}
-```
\ No newline at end of file
diff --git a/tests/typ/bugs/smartquotes-in-outline.typ b/tests/typ/bugs/smartquotes-in-outline.typ
deleted file mode 100644
index 1ecfcdc40d..0000000000
--- a/tests/typ/bugs/smartquotes-in-outline.typ
+++ /dev/null
@@ -1,4 +0,0 @@
-#set page(width: 15em)
-#outline()
-
-= "This" "is" "a" "test"
diff --git a/tests/typ/bugs/smartquotes-on-newline.typ b/tests/typ/bugs/smartquotes-on-newline.typ
deleted file mode 100644
index 3180350ea0..0000000000
--- a/tests/typ/bugs/smartquotes-on-newline.typ
+++ /dev/null
@@ -1,7 +0,0 @@
-// Test that smart quotes are inferred correctly across newlines.
-
----
-"test"#linebreak()"test"
-
-"test"\
-"test"
diff --git a/tests/typ/bugs/spacing-behaviour.typ b/tests/typ/bugs/spacing-behaviour.typ
deleted file mode 100644
index a2a30b8adb..0000000000
--- a/tests/typ/bugs/spacing-behaviour.typ
+++ /dev/null
@@ -1,9 +0,0 @@
-// Test that metadata after spacing does not force a new paragraph.
-
----
-#{
- h(1em)
- counter(heading).update(4)
- [Hello ]
- counter(heading).display()
-}
diff --git a/tests/typ/bugs/square-base.typ b/tests/typ/bugs/square-base.typ
deleted file mode 100644
index d8339c1ace..0000000000
--- a/tests/typ/bugs/square-base.typ
+++ /dev/null
@@ -1,5 +0,0 @@
-// Test that square sets correct base for its content.
-
----
-#set page(height: 80pt)
-#square(width: 40%, rect(width: 60%, height: 80%))
diff --git a/tests/typ/bugs/subelement-panic.typ b/tests/typ/bugs/subelement-panic.typ
deleted file mode 100644
index fcad83bc31..0000000000
--- a/tests/typ/bugs/subelement-panic.typ
+++ /dev/null
@@ -1,40 +0,0 @@
-// Test that figure captions don't cause panics.
-// Ref: false
-
----
-// #2530
-#figure(caption: [test])[].caption
-
----
-// #2165
-#figure.caption[]
-
----
-// #2328
-// Error: 4-43 footnote entry must have a location
-// Hint: 4-43 try using a query or a show rule to customize the footnote instead
-HI#footnote.entry(clearance: 2.5em)[There]
-
----
-// Enum item (pre-emptive)
-#enum.item(none)[Hello]
-#enum.item(17)[Hello]
-
----
-// List item (pre-emptive)
-#list.item[Hello]
-
----
-// Term item (pre-emptive)
-#terms.item[Hello][World!]
-
----
-// Outline entry (pre-emptive)
-// Error: 2-48 cannot outline text
-#outline.entry(1, [Hello], [World!], none, [1])
-
----
-// Outline entry (pre-emptive, improved error)
-// Error: 2-55 heading must have a location
-// Hint: 2-55 try using a query or a show rule to customize the outline.entry instead
-#outline.entry(1, heading[Hello], [World!], none, [1])
diff --git a/tests/typ/bugs/table-lines.typ b/tests/typ/bugs/table-lines.typ
deleted file mode 100644
index 7e9540121a..0000000000
--- a/tests/typ/bugs/table-lines.typ
+++ /dev/null
@@ -1,10 +0,0 @@
-// Ensure no empty lines before a table that doesn't fit into the first page.
-
----
-#set page(height: 50pt)
-
-Hello
-#table(
- columns: 4,
- [1], [2], [3], [4]
-)
diff --git a/tests/typ/bugs/table-row-missing.typ b/tests/typ/bugs/table-row-missing.typ
deleted file mode 100644
index d72305baec..0000000000
--- a/tests/typ/bugs/table-row-missing.typ
+++ /dev/null
@@ -1,8 +0,0 @@
-// Test that a table row isn't wrongly treated like a gutter row.
-
----
-#set page(height: 70pt)
-#table(
- rows: 16pt,
- ..range(6).map(str).flatten(),
-)
diff --git a/tests/typ/compiler/array.typ b/tests/typ/compiler/array.typ
deleted file mode 100644
index 4a1948ff7f..0000000000
--- a/tests/typ/compiler/array.typ
+++ /dev/null
@@ -1,370 +0,0 @@
-// Test arrays.
-// Ref: false
-
----
-// Ref: true
-
-#set page(width: 150pt)
-
-// Empty.
-#()
-
-// Not an array, just a parenthesized expression.
-#(1)
-
-// One item and trailing comma.
-#(-1,)
-
-// No trailing comma.
-#(true, false)
-
-// Multiple lines and items and trailing comma.
-#("1"
- , rgb("002")
- ,)
-
----
-// Test the `len` method.
-#test(().len(), 0)
-#test(("A", "B", "C").len(), 3)
-
----
-// Test lvalue and rvalue access.
-#{
- let array = (1, 2)
- array.at(1) += 5 + array.at(0)
- test(array, (1, 8))
-}
-
----
-// Test different lvalue method.
-#{
- let array = (1, 2, 3)
- array.first() = 7
- array.at(1) *= 8
- test(array, (7, 16, 3))
-}
-
----
-// Test rvalue out of bounds.
-// Error: 2-17 array index out of bounds (index: 5, len: 3) and no default value was specified
-#(1, 2, 3).at(5)
-
----
-// Test lvalue out of bounds.
-#{
- let array = (1, 2, 3)
- // Error: 3-14 array index out of bounds (index: 3, len: 3)
- array.at(3) = 5
-}
-
----
-// Test default value.
-#test((1, 2, 3).at(2, default: 5), 3)
-#test((1, 2, 3).at(3, default: 5), 5)
-
----
-// Test remove with default value.
-
-#{
- let array = (1, 2, 3)
- test(array.remove(2, default: 5), 3)
-}
-
-#{
- let array = (1, 2, 3)
- test(array.remove(3, default: 5), 5)
-}
-
----
-// Test bad lvalue.
-// Error: 2:3-2:14 cannot mutate a temporary value
-#let array = (1, 2, 3)
-#(array.len() = 4)
-
----
-// Test bad lvalue.
-// Error: 2:9-2:13 type array has no method `yolo`
-#let array = (1, 2, 3)
-#(array.yolo() = 4)
-
----
-// Test negative indices.
-#{
- let array = (1, 2, 3, 4)
- test(array.at(0), 1)
- test(array.at(-1), 4)
- test(array.at(-2), 3)
- test(array.at(-3), 2)
- test(array.at(-4), 1)
-}
-
----
-// The the `first` and `last` methods.
-#test((1,).first(), 1)
-#test((2,).last(), 2)
-#test((1, 2, 3).first(), 1)
-#test((1, 2, 3).last(), 3)
-
----
-// Error: 2-12 array is empty
-#().first()
-
----
-// Error: 2-11 array is empty
-#().last()
-
----
-// Test the `push` and `pop` methods.
-#{
- let tasks = (a: (1, 2, 3), b: (4, 5, 6))
- test(tasks.at("a").pop(), 3)
- tasks.b.push(7)
- test(tasks.a, (1, 2))
- test(tasks.at("b"), (4, 5, 6, 7))
-}
-
----
-// Test the `insert` and `remove` methods.
-#{
- let array = (0, 1, 2, 4, 5)
- array.insert(3, 3)
- test(array, range(6))
- array.remove(1)
- test(array, (0, 2, 3, 4, 5))
-}
-
----
-// Error: 2:2-2:18 missing argument: index
-#let numbers = ()
-#numbers.insert()
-
----
-// Test the `slice` method.
-#test((1, 2, 3, 4).slice(2), (3, 4))
-#test(range(10).slice(2, 6), (2, 3, 4, 5))
-#test(range(10).slice(4, count: 3), (4, 5, 6))
-#test(range(10).slice(-5, count: 2), (5, 6))
-#test((1, 2, 3).slice(2, -2), ())
-#test((1, 2, 3).slice(-2, 2), (2,))
-#test((1, 2, 3).slice(-3, 2), (1, 2))
-#test("ABCD".split("").slice(1, -1).join("-"), "A-B-C-D")
-
----
-// Error: 2-30 array index out of bounds (index: 12, len: 10)
-#range(10).slice(9, count: 3)
-
----
-// Error: 2-24 array index out of bounds (index: -4, len: 3)
-#(1, 2, 3).slice(0, -4)
-
----
-// Test the `position` method.
-#test(("Hi", "❤️", "Love").position(s => s == "❤️"), 1)
-#test(("Bye", "💘", "Apart").position(s => s == "❤️"), none)
-#test(("A", "B", "CDEF", "G").position(v => v.len() > 2), 2)
-
----
-// Test the `filter` method.
-#test(().filter(calc.even), ())
-#test((1, 2, 3, 4).filter(calc.even), (2, 4))
-#test((7, 3, 2, 5, 1).filter(x => x < 5), (3, 2, 1))
-
----
-// Test the `map` method.
-#test(().map(x => x * 2), ())
-#test((2, 3).map(x => x * 2), (4, 6))
-
----
-// Test the `fold` method.
-#test(().fold("hi", grid), "hi")
-#test((1, 2, 3, 4).fold(0, (s, x) => s + x), 10)
-
----
-// Error: 20-22 unexpected argument
-#(1, 2, 3).fold(0, () => none)
-
----
-// Test the `sum` method.
-#test(().sum(default: 0), 0)
-#test(().sum(default: []), [])
-#test((1, 2, 3).sum(), 6)
-
----
-// Error: 2-10 cannot calculate sum of empty array with no default
-#().sum()
-
----
-// Test the `product` method.
-#test(().product(default: 0), 0)
-#test(().product(default: []), [])
-#test(([ab], 3).product(), [ab]*3)
-#test((1, 2, 3).product(), 6)
-
----
-// Error: 2-14 cannot calculate product of empty array with no default
-#().product()
-
----
-// Test the `rev` method.
-#test(range(3).rev(), (2, 1, 0))
-
----
-// Test the `join` method.
-#test(().join(), none)
-#test((1,).join(), 1)
-#test(("a", "b", "c").join(), "abc")
-#test("(" + ("a", "b", "c").join(", ") + ")", "(a, b, c)")
-
----
-// Error: 2-22 cannot join boolean with boolean
-#(true, false).join()
-
----
-// Error: 2-20 cannot join string with integer
-#("a", "b").join(1)
-
----
-// Test joining content.
-// Ref: true
-#([One], [Two], [Three]).join([, ], last: [ and ]).
-
----
-// Test the `intersperse` method
-#test(().intersperse("a"), ())
-#test((1,).intersperse("a"), (1,))
-#test((1, 2).intersperse("a"), (1, "a", 2))
-#test((1, 2, "b").intersperse("a"), (1, "a", 2, "a", "b"))
-
----
-// Test the `chunks` method.
-#test(().chunks(10), ())
-#test((1, 2, 3).chunks(10), ((1, 2, 3),))
-#test((1, 2, 3, 4, 5, 6).chunks(3), ((1, 2, 3), (4, 5, 6)))
-#test((1, 2, 3, 4, 5, 6, 7, 8).chunks(3), ((1, 2, 3), (4, 5, 6), (7, 8)))
-
-#test(().chunks(10, exact: true), ())
-#test((1, 2, 3).chunks(10, exact: true), ())
-#test((1, 2, 3, 4, 5, 6).chunks(3, exact: true), ((1, 2, 3), (4, 5, 6)))
-#test((1, 2, 3, 4, 5, 6, 7, 8).chunks(3, exact: true), ((1, 2, 3), (4, 5, 6)))
-
----
-// Error: 19-20 number must be positive
-#(1, 2, 3).chunks(0)
-
----
-// Error: 19-21 number must be positive
-#(1, 2, 3).chunks(-5)
-
----
-// Test the `sorted` method.
-#test(().sorted(), ())
-#test(().sorted(key: x => x), ())
-#test(((true, false) * 10).sorted(), (false,) * 10 + (true,) * 10)
-#test(("it", "the", "hi", "text").sorted(), ("hi", "it", "text", "the"))
-#test(("I", "the", "hi", "text").sorted(key: x => x), ("I", "hi", "text", "the"))
-#test(("I", "the", "hi", "text").sorted(key: x => x.len()), ("I", "hi", "the", "text"))
-#test((2, 1, 3, 10, 5, 8, 6, -7, 2).sorted(), (-7, 1, 2, 2, 3, 5, 6, 8, 10))
-#test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x), (-10, -7, -5, 1, 2, 2, 3, 6, 8))
-#test((2, 1, 3, -10, -5, 8, 6, -7, 2).sorted(key: x => x * x), (1, 2, 2, 3, -5, 6, -7, 8, -10))
-
----
-// Error: 12-18 unexpected argument
-#().sorted(x => x)
-
----
-// Test the `zip` method.
-#test(().zip(()), ())
-#test((1,).zip(()), ())
-#test((1,).zip((2,)), ((1, 2),))
-#test((1, 2).zip((3, 4)), ((1, 3), (2, 4)))
-#test((1, 2, 3, 4).zip((5, 6)), ((1, 5), (2, 6)))
-#test(((1, 2), 3).zip((4, 5)), (((1, 2), 4), (3, 5)))
-#test((1, "hi").zip((true, false)), ((1, true), ("hi", false)))
-#test((1, 2, 3).zip((3, 4, 5), (6, 7, 8)), ((1, 3, 6), (2, 4, 7), (3, 5, 8)))
-#test(().zip((), ()), ())
-#test((1,).zip((2,), (3,)), ((1, 2, 3),))
-#test((1, 2, 3).zip(), ((1,), (2,), (3,)))
-#test(array.zip(()), ())
-
-
----
-// Test the `enumerate` method.
-#test(().enumerate(), ())
-#test(().enumerate(start: 5), ())
-#test(("a", "b", "c").enumerate(), ((0, "a"), (1, "b"), (2, "c")))
-#test(("a", "b", "c").enumerate(start: 1), ((1, "a"), (2, "b"), (3, "c")))
-#test(("a", "b", "c").enumerate(start: 42), ((42, "a"), (43, "b"), (44, "c")))
-#test(("a", "b", "c").enumerate(start: -7), ((-7, "a"), (-6, "b"), (-5, "c")))
-
----
-// Test the `dedup` method.
-#test(().dedup(), ())
-#test((1,).dedup(), (1,))
-#test((1, 1).dedup(), (1,))
-#test((1, 2, 1).dedup(), (1, 2))
-#test(("Jane", "John", "Eric").dedup(), ("Jane", "John", "Eric"))
-#test(("Jane", "John", "Eric", "John").dedup(), ("Jane", "John", "Eric"))
-
----
-// Test the `dedup` with the `key` argument.
-#test((1, 2, 3, 4, 5, 6).dedup(key: x => calc.rem(x, 2)), (1, 2))
-#test((1, 2, 3, 4, 5, 6).dedup(key: x => calc.rem(x, 3)), (1, 2, 3))
-#test(("Hello", "World", "Hi", "There").dedup(key: x => x.len()), ("Hello", "Hi"))
-#test(("Hello", "World", "Hi", "There").dedup(key: x => x.at(0)), ("Hello", "World", "There"))
-
----
-// Error: 9-26 unexpected argument: val
-#().zip(val: "applicable")
-
----
-// Error: 13-30 unexpected argument: val
-#().zip((), val: "applicable")
-
----
-// Error: 32-37 cannot divide by zero
-#(1, 2, 0, 3).sorted(key: x => 5 / x)
-
----
-// Error: 2-26 cannot compare content and content
-#([Hi], [There]).sorted()
-
----
-// Error: 2-26 cannot compare 3em with 2pt
-#(1pt, 2pt, 3em).sorted()
-
----
-// Error: 42-52 unexpected argument
-#((k: "a", v: 2), (k: "b", v: 1)).sorted(it => it.v)
-
----
-// Error: 2-18 array index out of bounds (index: -4, len: 3) and no default value was specified
-#(1, 2, 3).at(-4)
-
----
-// Error: 3-4 unclosed delimiter
-#{(}
-
-// Error: 2-3 unclosed delimiter
-#{)}
-
-// Error: 4-6 unexpected end of block comment
-#(1*/2)
-
-// Error: 6-8 invalid number suffix: u
-#(1, 1u 2)
-
-// Error: 3-4 unexpected comma
-#(,1)
-
-// Missing expression makes named pair incomplete, making this an empty array.
-// Error: 5 expected expression
-#(a:)
-
-// Named pair after this is already identified as an array.
-// Error: 6-10 expected expression, found named pair
-#(1, b: 2)
-
-// Keyed pair after this is already identified as an array.
-// Error: 6-14 expected expression, found keyed pair
-#(1, "key": 2)
diff --git a/tests/typ/compiler/bench.typ b/tests/typ/compiler/bench.typ
deleted file mode 100644
index 7276776d0f..0000000000
--- a/tests/typ/compiler/bench.typ
+++ /dev/null
@@ -1,47 +0,0 @@
-// Ref: false
-
-// Configuration with `page` and `font` functions.
-#set page(width: 450pt, margin: 1cm)
-
-// There are variables and they can take normal values like strings, ...
-#let city = "Berlin"
-
-// ... but also "content" values. While these contain markup,
-// they are also values and can be summed, stored in arrays etc.
-// There are also more standard control flow structures, like #if and #for.
-#let university = [*Technische Universität #city*]
-#let faculty = [*Fakultät II, Institut for Mathematik*]
-
-// The `box` function just places content into a rectangular container. When
-// the only argument to a function is a content block, the parentheses can be
-// omitted (i.e. `f[a]` is the same as `f([a])`).
-#box[
- // Backslash adds a forced line break.
- #university \
- #faculty \
- Sekretariat MA \
- Dr. Max Mustermann \
- Ola Nordmann, John Doe
-]
-#align(right, box[*WiSe 2019/2020* \ Woche 3])
-
-// Adds vertical spacing.
-#v(6mm)
-
-// If the last argument to a function is a content block, we can also place it
-// behind the parentheses.
-#align(center)[
- // Markdown-like syntax for headings.
- ==== 3. Übungsblatt Computerorientierte Mathematik II #v(4mm)
- *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) #v(4mm)
- *Alle Antworten sind zu beweisen.*
-]
-
-*1. Aufgabe* #align(right)[(1 + 1 + 2 Punkte)]
-
-Ein _Binärbaum_ ist ein Wurzelbaum, in dem jeder Knoten ≤ 2 Kinder hat.
-Die Tiefe eines Knotens _v_ ist die Länge des eindeutigen Weges von der Wurzel
-zu _v_, und die Höhe von _v_ ist die Länge eines längsten (absteigenden) Weges
-von _v_ zu einem Blatt. Die Höhe des Baumes ist die Höhe der Wurzel.
-
-#v(6mm)
diff --git a/tests/typ/compiler/block.typ b/tests/typ/compiler/block.typ
deleted file mode 100644
index 48c9fefcd3..0000000000
--- a/tests/typ/compiler/block.typ
+++ /dev/null
@@ -1,145 +0,0 @@
-// Test code blocks.
-// Ref: false
-
----
-// Ref: true
-
-// Evaluates to join of none, [My ] and the two loop bodies.
-#{
- let parts = ("my fri", "end.")
- [Hello, ]
- for s in parts [#s]
-}
-
-// Evaluates to join of the content and strings.
-#{
- [How]
- if true {
- " are"
- }
- [ ]
- if false [Nope]
- [you] + "?"
-}
-
----
-// Nothing evaluates to none.
-#test({}, none)
-
-// Let evaluates to none.
-#test({ let v = 0 }, none)
-
-// Evaluates to single expression.
-#test({ "hello" }, "hello")
-
-// Evaluates to string.
-#test({ let x = "m"; x + "y" }, "my")
-
-// Evaluated to int.
-#test({
- let x = 1
- let y = 2
- x + y
-}, 3)
-
-// String is joined with trailing none, evaluates to string.
-#test({
- type("")
- none
-}, str)
-
----
-// Some things can't be joined.
-#{
- [A]
- // Error: 3-4 cannot join content with integer
- 1
- [B]
-}
-
----
-// Block directly in markup also creates a scope.
-#{ let x = 1 }
-
-// Error: 7-8 unknown variable: x
-#test(x, 1)
-
----
-// Block in expression does create a scope.
-#let a = {
- let b = 1
- b
-}
-
-#test(a, 1)
-
-// Error: 3-4 unknown variable: b
-#{b}
-
----
-// Double block creates a scope.
-#{{
- import "module.typ": b
- test(b, 1)
-}}
-
-// Error: 2-3 unknown variable: b
-#b
-
----
-// Multiple nested scopes.
-#{
- let a = "a1"
- {
- let a = "a2"
- {
- test(a, "a2")
- let a = "a3"
- test(a, "a3")
- }
- test(a, "a2")
- }
- test(a, "a1")
-}
-
----
-// Content blocks also create a scope.
-#[#let x = 1]
-
-// Error: 2-3 unknown variable: x
-#x
-
----
-// Multiple unseparated expressions in one line.
-
-// Error: 2-4 invalid number suffix: u
-#1u
-
-// Should output `1`.
-// Error: 4 expected semicolon or line break
-#{1 2}
-
-// Should output `2`.
-// Error: 13 expected semicolon or line break
-// Error: 23 expected semicolon or line break
-#{let x = -1 let y = 3 x + y}
-
-// Should output `3`.
-#{
- // Error: 7-10 expected pattern, found string
- for "v"
-
- // Error: 8 expected keyword `in`
- // Error: 22 expected block
- for v let z = 1 + 2
-
- z
-}
-
----
-// Error: 2-3 unclosed delimiter
-#{
-
----
-// Error: 2-3 unexpected closing brace
-#}
diff --git a/tests/typ/compiler/break-continue.typ b/tests/typ/compiler/break-continue.typ
deleted file mode 100644
index 4c4738bbe4..0000000000
--- a/tests/typ/compiler/break-continue.typ
+++ /dev/null
@@ -1,162 +0,0 @@
-// Test break and continue in loops.
-// Ref: false
-
----
-// Test break.
-
-#let var = 0
-#let error = false
-
-#for i in range(10) {
- var += i
- if i > 5 {
- break
- error = true
- }
-}
-
-#test(var, 21)
-#test(error, false)
-
----
-// Test joining with break.
-
-#let i = 0
-#let x = while true {
- i += 1
- str(i)
- if i >= 5 {
- "."
- break
- }
-}
-
-#test(x, "12345.")
-
----
-// Test continue.
-
-#let i = 0
-#let x = 0
-
-#while x < 8 {
- i += 1
- if calc.rem(i, 3) == 0 {
- continue
- }
- x += i
-}
-
-// If continue did not work, this would equal 10.
-#test(x, 12)
-
----
-// Test joining with continue.
-
-#let x = for i in range(5) {
- "a"
- if calc.rem(i, 3) == 0 {
- "_"
- continue
- }
- str(i)
-}
-
-#test(x, "a_a1a2a_a4")
-
----
-// Test break outside of loop.
-#let f() = {
- // Error: 3-8 cannot break outside of loop
- break
-}
-
-#for i in range(1) {
- f()
-}
-
----
-// Test break in function call.
-#let identity(x) = x
-#let out = for i in range(5) {
- "A"
- identity({
- "B"
- break
- })
- "C"
-}
-
-#test(out, "AB")
-
----
-// Test continue outside of loop.
-
-// Error: 12-20 cannot continue outside of loop
-#let x = { continue }
-
----
-// Error: 2-10 cannot continue outside of loop
-#continue
-
----
-// Ref: true
-// Should output `Hello World 🌎`.
-#for _ in range(10) {
- [Hello ]
- [World #{
- [🌎]
- break
- }]
-}
-
----
-// Ref: true
-// Should output `Some` in red, `Some` in blue and `Last` in green.
-// Everything should be in smallcaps.
-#for color in (red, blue, green, yellow) [
- #set text(font: "Roboto")
- #show: it => text(fill: color, it)
- #smallcaps(if color != green [
- Some
- ] else [
- Last
- #break
- ])
-]
-
----
-// Ref: true
-// Test break in set rule.
-// Should output `Hi` in blue.
-#for i in range(10) {
- [Hello]
- set text(blue, ..break)
- [Not happening]
-}
-
----
-// Test second block during break flow.
-// Ref: true
-
-#for i in range(10) {
- table(
- { [A]; break },
- for _ in range(3) [B]
- )
-}
-
----
-// Ref: true
-// Test continue while destructuring.
-// Should output "one = I \ two = II \ one = I".
-#for num in (1, 2, 3, 1) {
- let (word, roman) = if num == 1 {
- ("one", "I")
- } else if num == 2 {
- ("two", "II")
- } else {
- continue
- }
- [#word = #roman \ ]
-}
diff --git a/tests/typ/compiler/call.typ b/tests/typ/compiler/call.typ
deleted file mode 100644
index 0c225a1c82..0000000000
--- a/tests/typ/compiler/call.typ
+++ /dev/null
@@ -1,111 +0,0 @@
-// Test function calls.
-// Ref: false
-
----
-// Ref: true
-
-// Omitted space.
-#let f() = {}
-#[#f()*Bold*]
-
-// Call return value of function with body.
-#let f(x, body) = (y) => [#x] + body + [#y]
-#f(1)[2](3)
-
-// Don't parse this as a function.
-#test (it)
-
-#let f(body) = body
-#f[A]
-#f()[A]
-#f([A])
-
-#let g(a, b) = a + b
-#g[A][B]
-#g([A], [B])
-#g()[A][B]
-
----
-// Trailing comma.
-#test(1 + 1, 2,)
-
-// Call function assigned to variable.
-#let alias = type
-#test(alias(alias), type)
-
-// Callee expressions.
-#{
- // Wrapped in parens.
- test((type)("hi"), str)
-
- // Call the return value of a function.
- let adder(dx) = x => x + dx
- test(adder(2)(5), 7)
-}
-
----
-// Error: 26-30 duplicate argument: font
-#set text(font: "Arial", font: "Helvetica")
-
----
-// Error: 4-15 the argument `amount` is positional
-// Hint: 4-15 try removing `amount:`
-#h(amount: 0.5)
-
----
-// Error: 2-6 expected function, found boolean
-#true()
-
----
-#let x = "x"
-
-// Error: 2-3 expected function, found string
-#x()
-
----
-#let f(x) = x
-
-// Error: 2-6 expected function, found integer
-#f(1)(2)
-
----
-#let f(x) = x
-
-// Error: 2-6 expected function, found content
-#f[1](2)
-
----
-// Error: 7-8 unexpected colon
-#func(:)
-
-// Error: 10-12 unexpected end of block comment
-#func(a:1*/)
-
-// Error: 8 expected comma
-#func(1 2)
-
-// Error: 7-8 expected identifier, found integer
-// Error: 9 expected expression
-#func(1:)
-
-// Error: 7-8 expected identifier, found integer
-#func(1:2)
-
-// Error: 7-12 expected identifier, found string
-#func("abc": 2)
-
-// Error: 7-10 expected identifier, found group
-#func((x):1)
-
----
-// Error: 6-7 unclosed delimiter
-#func[`a]`
-
----
-// Error: 7-8 unclosed delimiter
-#{func(}
-
----
-// Error: 6-7 unclosed delimiter
-// Error: 1:7-2:1 unclosed string
-#func("]
diff --git a/tests/typ/compiler/color.typ b/tests/typ/compiler/color.typ
deleted file mode 100644
index ac83355d62..0000000000
--- a/tests/typ/compiler/color.typ
+++ /dev/null
@@ -1,101 +0,0 @@
-// Test color modification methods.
-
----
-// Test CMYK color conversion.
-#let c = cmyk(50%, 64%, 16%, 17%)
-#stack(
- dir: ltr,
- spacing: 1fr,
- rect(width: 1cm, fill: cmyk(69%, 11%, 69%, 41%)),
- rect(width: 1cm, fill: c),
- rect(width: 1cm, fill: c.negate(space: cmyk)),
-)
-
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: c.lighten(x * 10%)))
-}
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: c.darken(x * 10%)))
-}
-
----
-// The the different color spaces
-#let col = rgb(50%, 64%, 16%)
-#box(square(size: 9pt, fill: col))
-#box(square(size: 9pt, fill: rgb(col)))
-#box(square(size: 9pt, fill: oklab(col)))
-#box(square(size: 9pt, fill: oklch(col)))
-#box(square(size: 9pt, fill: luma(col)))
-#box(square(size: 9pt, fill: cmyk(col)))
-#box(square(size: 9pt, fill: color.linear-rgb(col)))
-#box(square(size: 9pt, fill: color.hsl(col)))
-#box(square(size: 9pt, fill: color.hsv(col)))
-
----
-// Colors outside the sRGB gamut.
-#box(square(size: 9pt, fill: oklab(90%, -0.2, -0.1)))
-#box(square(size: 9pt, fill: oklch(50%, 0.5, 0deg)))
-
----
-// Test hue rotation
-#let col = rgb(50%, 64%, 16%)
-
-// Oklch
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg)))
-}
-
-// HSL
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsl)))
-}
-
-// HSV
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: rgb(col).rotate(x * 36deg, space: color.hsv)))
-}
-
----
-// Test saturation
-#let col = color.hsl(180deg, 0%, 50%)
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: col.saturate(x * 10%)))
-}
-
-#let col = color.hsl(180deg, 100%, 50%)
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: col.desaturate(x * 10%)))
-}
-
-#let col = color.hsv(180deg, 0%, 50%)
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: col.saturate(x * 10%)))
-}
-
-#let col = color.hsv(180deg, 100%, 50%)
-#for x in range(0, 11) {
- box(square(size: 9pt, fill: col.desaturate(x * 10%)))
-}
-
----
-// Test gray color modification.
-// Ref: false
-#test-repr(luma(20%).lighten(50%), luma(60%))
-#test-repr(luma(80%).darken(20%), luma(64%))
-#test-repr(luma(80%).negate(space: luma), luma(20%))
-
----
-// Test alpha modification.
-// Ref: false
-#test-repr(luma(100%, 100%).transparentize(50%), luma(100%, 50%))
-#test-repr(luma(100%, 100%).transparentize(75%), luma(100%, 25%))
-#test-repr(luma(100%, 50%).transparentize(50%), luma(100%, 25%))
-#test-repr(luma(100%, 10%).transparentize(250%), luma(100%, 0%))
-#test-repr(luma(100%, 40%).transparentize(-50%), luma(100%, 70%))
-#test-repr(luma(100%, 0%).transparentize(-100%), luma(100%, 100%))
-
-#test-repr(luma(100%, 50%).opacify(50%), luma(100%, 75%))
-#test-repr(luma(100%, 20%).opacify(100%), luma(100%, 100%))
-#test-repr(luma(100%, 100%).opacify(250%), luma(100%, 100%))
-#test-repr(luma(100%, 50%).opacify(-50%), luma(100%, 25%))
-#test-repr(luma(100%, 0%).opacify(0%), luma(100%, 0%))
diff --git a/tests/typ/compiler/comment.typ b/tests/typ/compiler/comment.typ
deleted file mode 100644
index 31025de696..0000000000
--- a/tests/typ/compiler/comment.typ
+++ /dev/null
@@ -1,34 +0,0 @@
-// Test line and block comments.
-
----
-// Line comment acts as spacing.
-A// you
-B
-
-// Block comment does not act as spacing, nested block comments.
-C/*
- /* */
-*/D
-
-// Works in code.
-#test(type(/*1*/ 1) //
-, int)
-
-// End of block comment in line comment.
-// Hello */
-
-// Nested "//" doesn't count as line comment.
-/* // */
-E
-
-/*//*/
-This is a comment.
-*/*/
-
----
-// End should not appear without start.
-// Error: 7-9 unexpected end of block comment
-/* */ */
-
-// Unterminated is okay.
-/*
diff --git a/tests/typ/compiler/construct.typ b/tests/typ/compiler/construct.typ
deleted file mode 100644
index da077e7e26..0000000000
--- a/tests/typ/compiler/construct.typ
+++ /dev/null
@@ -1,31 +0,0 @@
-// Test constructors.
-
----
-// Ensure that constructor styles aren't passed down the tree.
-// The inner list should have no extra indent.
-#set par(leading: 2pt)
-#list(body-indent: 20pt, [First], list[A][B])
-
----
-// Ensure that constructor styles win, but not over outer styles.
-// The outer paragraph should be right-aligned,
-// but the B should be center-aligned.
-#set list(marker: [>])
-#list(marker: [--])[
- #rect(width: 2cm, fill: conifer, inset: 4pt, list[A])
-]
-
----
-// The inner rectangle should also be yellow here.
-// (and therefore invisible)
-#[#set rect(fill: yellow);#text(1em, rect(inset: 5pt, rect()))]
-
----
-// The inner rectangle should not be yellow here.
-A #box(rect(fill: yellow, inset: 5pt, rect())) B
-
----
-// The constructor property should still work
-// when there are recursive show rules.
-#show enum: set text(blue)
-#enum(numbering: "(a)", [A], enum[B])
diff --git a/tests/typ/compiler/content-field.typ b/tests/typ/compiler/content-field.typ
deleted file mode 100644
index 96ce1dca25..0000000000
--- a/tests/typ/compiler/content-field.typ
+++ /dev/null
@@ -1,63 +0,0 @@
-// Tests content field access.
-
----
-// Ensure that fields from set rules are materialized into the element before
-// a show rule runs.
-#set table(columns: (10pt, auto))
-#show table: it => it.columns
-#table[A][B][C][D]
-
----
-// Test it again with a different element.
-#set heading(numbering: "(I)")
-#show heading: set text(size: 11pt, weight: "regular")
-#show heading: it => it.numbering
-= Heading
-
----
-// Test it with query.
-#set raw(lang: "rust")
-#context query().first().lang
-`raw`
-
----
-// Integrated test for content fields.
-#let compute(equation, ..vars) = {
- let vars = vars.named()
- let f(elem) = {
- let func = elem.func()
- if func == text {
- let text = elem.text
- if regex("^\d+$") in text {
- int(text)
- } else if text in vars {
- int(vars.at(text))
- } else {
- panic("unknown math variable: " + text)
- }
- } else if func == math.attach {
- let value = f(elem.base)
- if elem.has("t") {
- value = calc.pow(value, f(elem.t))
- }
- value
- } else if elem.has("children") {
- elem
- .children
- .filter(v => v != [ ])
- .split[+]
- .map(xs => xs.fold(1, (prod, v) => prod * f(v)))
- .fold(0, (sum, v) => sum + v)
- }
- }
- let result = f(equation.body)
- [With ]
- vars
- .pairs()
- .map(p => $#p.first() = #p.last()$)
- .join(", ", last: " and ")
- [ we have:]
- $ equation = result $
-}
-
-#compute($x y + y^2$, x: 2, y: 3)
diff --git a/tests/typ/compiler/delayed-error.typ b/tests/typ/compiler/delayed-error.typ
deleted file mode 100644
index eff6b85bae..0000000000
--- a/tests/typ/compiler/delayed-error.typ
+++ /dev/null
@@ -1,11 +0,0 @@
-// Test that errors in show rules are delayed: There can be multiple at once.
-
----
-// Error: 21-34 panicked with: "hey1"
-#show heading: _ => panic("hey1")
-
-// Error: 20-33 panicked with: "hey2"
-#show strong: _ => panic("hey2")
-
-= Hello
-*strong*
diff --git a/tests/typ/compiler/dict.typ b/tests/typ/compiler/dict.typ
deleted file mode 100644
index 552b243c9f..0000000000
--- a/tests/typ/compiler/dict.typ
+++ /dev/null
@@ -1,160 +0,0 @@
-// Test dictionaries.
-// Ref: false
-
----
-// Ref: true
-
-// Empty
-#(:)
-
-// Two pairs and string key.
-#let dict = (normal: 1, "spacy key": 2)
-#dict
-
-#test(dict.normal, 1)
-#test(dict.at("spacy key"), 2)
-
----
-// Test lvalue and rvalue access.
-#{
- let dict = (a: 1, "b b": 1)
- dict.at("b b") += 1
- dict.state = (ok: true, err: false)
- test(dict, (a: 1, "b b": 2, state: (ok: true, err: false)))
- test(dict.state.ok, true)
- dict.at("state").ok = false
- test(dict.state.ok, false)
- test(dict.state.err, false)
-}
-
----
-// Test rvalue missing key.
-#{
- let dict = (a: 1, b: 2)
- // Error: 11-23 dictionary does not contain key "c" and no default value was specified
- let x = dict.at("c")
-}
-
----
-// Test default value.
-#test((a: 1, b: 2).at("b", default: 3), 2)
-#test((a: 1, b: 2).at("c", default: 3), 3)
-
----
-// Test insert.
-#{
- let dict = (a: 1, b: 2)
- dict.insert("b", 3)
- test(dict, (a: 1, b: 3))
- dict.insert("c", 5)
- test(dict, (a: 1, b: 3, c: 5))
-}
-
----
-// Test remove with default value.
-#{
- let dict = (a: 1, b: 2)
- test(dict.remove("b", default: 3), 2)
-}
-
-#{
- let dict = (a: 1, b: 2)
- test(dict.remove("c", default: 3), 3)
-}
-
----
-// Missing lvalue is not automatically none-initialized.
-#{
- let dict = (:)
- // Error: 3-9 dictionary does not contain key "b"
- // Hint: 3-9 use `insert` to add or update values
- dict.b += 1
-}
-
----
-// Test dictionary methods.
-#let dict = (a: 3, c: 2, b: 1)
-#test("c" in dict, true)
-#test(dict.len(), 3)
-#test(dict.values(), (3, 2, 1))
-#test(dict.pairs().map(p => p.first() + str(p.last())).join(), "a3c2b1")
-
-#dict.remove("c")
-#test("c" in dict, false)
-#test(dict, (a: 3, b: 1))
-
----
-// Test dictionary constructor
-#dictionary(sys).at("version")
-#dictionary(sys).at("no_crash", default: none)
-
----
-// Test that removal keeps order.
-#let dict = (a: 1, b: 2, c: 3, d: 4)
-#dict.remove("b")
-#test(dict.keys(), ("a", "c", "d"))
-
----
-// Error: 24-29 duplicate key: first
-#(first: 1, second: 2, first: 3)
-
----
-// Error: 17-20 duplicate key: a
-#(a: 1, "b": 2, "a": 3)
-
----
-// Simple expression after already being identified as a dictionary.
-// Error: 9-10 expected named or keyed pair, found identifier
-#(a: 1, b)
-
-// Identified as dictionary due to initial colon.
-// The boolean key is allowed for now since it will only cause an error at the evaluation stage.
-// Error: 4-5 expected named or keyed pair, found integer
-// Error: 17 expected expression
-#(:1 b:"", true:)
-
----
-// Error: 3-15 cannot mutate a temporary value
-#((key: "val").other = "some")
-
----
-#{
- let dict = (
- call-me: () => 1,
- )
- // Error: 8-15 type dictionary has no method `call-me`
- // Hint: 8-15 to call the function stored in the dictionary, surround the field access with parentheses, e.g. `(dict.call-me)(..)`
- dict.call-me()
-}
-
----
-#{
- let dict = (
- nonfunc: 1
- )
-
- // Error: 8-15 type dictionary has no method `nonfunc`
- // Hint: 8-15 did you mean to access the field `nonfunc`?
- dict.nonfunc()
-}
-
----
-#let a = "hello"
-#let b = "world"
-#let c = "value"
-#let d = "conflict"
-
-#assert.eq(((a): b), ("hello": "world"))
-#assert.eq(((a): 1, (a): 2), ("hello": 2))
-#assert.eq((hello: 1, (a): 2), ("hello": 2))
-#assert.eq((a + b: c, (a + b): d, (a): "value2", a: "value3"), ("helloworld": "conflict", "hello": "value2", "a": "value3"))
-
----
-// Error: 7-10 expected identifier, found group
-// Error: 12-14 expected pattern, found integer
-#let ((a): 10) = "world"
-
----
-// Error: 3-7 expected string, found boolean
-// Error: 16-18 expected string, found integer
-#(true: false, 42: 3)
diff --git a/tests/typ/compiler/embedded-expr.typ b/tests/typ/compiler/embedded-expr.typ
deleted file mode 100644
index ee6e07f903..0000000000
--- a/tests/typ/compiler/embedded-expr.typ
+++ /dev/null
@@ -1,22 +0,0 @@
-// Test embedded expressions.
-// Ref: false
-
----
-// Error: 6-8 expected pattern, found keyword `as`
-// Hint: 6-8 keyword `as` is not allowed as an identifier; try `as_` instead
-#let as = 1 + 2
-
----
-#{
- // Error: 7-9 expected pattern, found keyword `as`
- // Hint: 7-9 keyword `as` is not allowed as an identifier; try `as_` instead
- let as = 10
-}
-
----
-// Error: 2-2 expected expression
-#
-
----
-// Error: 2-2 expected expression
-# hello
diff --git a/tests/typ/compiler/field.typ b/tests/typ/compiler/field.typ
deleted file mode 100644
index 35768ec5ae..0000000000
--- a/tests/typ/compiler/field.typ
+++ /dev/null
@@ -1,200 +0,0 @@
-// Test field access.
-// Ref: false
-
----
-// Test field on dictionary.
-#let dict = (nothing: "ness", hello: "world")
-#test(dict.nothing, "ness")
-#{
- let world = dict
- .hello
-
- test(world, "world")
-}
-
----
-// Test fields on elements.
-#show list: it => {
- test(it.children.len(), 3)
-}
-
-- A
-- B
-- C
-
----
-// Test fields on function scopes.
-#enum.item
-#assert.eq
-#assert.ne
-
----
-// Error: 9-16 function `assert` does not contain field `invalid`
-#assert.invalid
-
----
-// Error: 7-14 function `enum` does not contain field `invalid`
-#enum.invalid
-
----
-// Error: 7-14 function `enum` does not contain field `invalid`
-#enum.invalid()
-
----
-// Closures cannot have fields.
-#let f(x) = x
-// Error: 4-11 cannot access fields on user-defined functions
-#f.invalid
-
----
-// Error: 6-13 dictionary does not contain key "invalid"
-#(:).invalid
-
----
-// Error: 8-10 cannot access fields on type boolean
-#false.ok
-
----
-// Error: 25-28 content does not contain field "fun"
-#show heading: it => it.fun
-= A
-
----
-// Error: 9-13 cannot access fields on type boolean
-#{false.true}
-
----
-// Test relative length fields.
-#test((100% + 2em + 2pt).ratio, 100%)
-#test((100% + 2em + 2pt).length, 2em + 2pt)
-#test((100% + 2pt).length, 2pt)
-#test((100% + 2pt - 2pt).length, 0pt)
-#test((56% + 2pt - 56%).ratio, 0%)
-
----
-// Test length fields.
-#test((1pt).em, 0.0)
-#test((1pt).abs, 1pt)
-#test((3em).em, 3.0)
-#test((3em).abs, 0pt)
-#test((2em + 2pt).em, 2.0)
-#test((2em + 2pt).abs, 2pt)
-
----
-// Test stroke fields for simple strokes.
-#test((1em + blue).paint, blue)
-#test((1em + blue).thickness, 1em)
-#test((1em + blue).cap, auto)
-#test((1em + blue).join, auto)
-#test((1em + blue).dash, auto)
-#test((1em + blue).miter-limit, auto)
-
----
-// Test complex stroke fields.
-#let r1 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: none))
-#let r2 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", miter-limit: 5.0, dash: (3pt, "dot", 4em)))
-#let r3 = rect(stroke: (paint: cmyk(1%, 2%, 3%, 4%), thickness: 4em + 2pt, cap: "round", join: "bevel", dash: (array: (3pt, "dot", 4em), phase: 5em)))
-#let s1 = r1.stroke
-#let s2 = r2.stroke
-#let s3 = r3.stroke
-#test(s1.paint, cmyk(1%, 2%, 3%, 4%))
-#test(s1.thickness, 4em + 2pt)
-#test(s1.cap, "round")
-#test(s1.join, "bevel")
-#test(s1.miter-limit, 5.0)
-#test(s3.miter-limit, auto)
-#test(s1.dash, none)
-#test(s2.dash, (array: (3pt, "dot", 4em), phase: 0pt))
-#test(s3.dash, (array: (3pt, "dot", 4em), phase: 5em))
-
----
-// Test 2d alignment 'horizontal' field.
-#test((start + top).x, start)
-#test((end + top).x, end)
-#test((left + top).x, left)
-#test((right + top).x, right)
-#test((center + top).x, center)
-#test((start + bottom).x, start)
-#test((end + bottom).x, end)
-#test((left + bottom).x, left)
-#test((right + bottom).x, right)
-#test((center + bottom).x, center)
-#test((start + horizon).x, start)
-#test((end + horizon).x, end)
-#test((left + horizon).x, left)
-#test((right + horizon).x, right)
-#test((center + horizon).x, center)
-#test((top + start).x, start)
-#test((bottom + end).x, end)
-#test((horizon + center).x, center)
-
----
-// Test 2d alignment 'vertical' field.
-#test((start + top).y, top)
-#test((end + top).y, top)
-#test((left + top).y, top)
-#test((right + top).y, top)
-#test((center + top).y, top)
-#test((start + bottom).y, bottom)
-#test((end + bottom).y, bottom)
-#test((left + bottom).y, bottom)
-#test((right + bottom).y, bottom)
-#test((center + bottom).y, bottom)
-#test((start + horizon).y, horizon)
-#test((end + horizon).y, horizon)
-#test((left + horizon).y, horizon)
-#test((right + horizon).y, horizon)
-#test((center + horizon).y, horizon)
-#test((top + start).y, top)
-#test((bottom + end).y, bottom)
-#test((horizon + center).y, horizon)
-
----
-#{
- let object = sym.eq.not
- // Error: 3-9 cannot mutate fields on symbol
- object.property = "value"
-}
-
----
-#{
- let object = [hi]
- // Error: 3-9 cannot mutate fields on content
- object.property = "value"
-}
-
----
-#{
- let object = calc
- // Error: 3-9 cannot mutate fields on module
- object.property = "value"
-}
-
----
-#{
- let object = calc.sin
- // Error: 3-9 cannot mutate fields on function
- object.property = "value"
-}
-
----
-#{
- let object = none
- // Error: 3-9 none does not have accessible fields
- object.property = "value"
-}
-
----
-#{
- let object = 10
- // Error: 3-9 integer does not have accessible fields
- object.property = "value"
-}
-
----
-#{
- let s = 1pt + red
- // Error: 3-4 fields on stroke are not yet mutable
- // Hint: 3-4 try creating a new stroke with the updated field value instead
- s.thickness = 5pt
-}
diff --git a/tests/typ/compiler/highlight.typ b/tests/typ/compiler/highlight.typ
deleted file mode 100644
index 1cbeaf9d56..0000000000
--- a/tests/typ/compiler/highlight.typ
+++ /dev/null
@@ -1,42 +0,0 @@
-#set page(width: auto)
-
-```typ
-#set hello()
-#set hello()
-#set hello.world()
-#set hello.my.world()
-#let foo(x) = x * 2
-#show heading: func
-#show module.func: func
-#show module.func: it => {}
-#foo(ident: ident)
-#hello
-#hello()
-#box[]
-#hello.world
-#hello.world()
-#hello().world()
-#hello.my.world
-#hello.my.world()
-#hello.my().world
-#hello.my().world()
-#{ hello }
-#{ hello() }
-#{ hello.world() }
-$ hello $
-$ hello() $
-$ box[] $
-$ hello.world $
-$ hello.world() $
-$ hello.my.world() $
-$ f_zeta(x), f_zeta(x)/1 $
-$ emph(hello.my.world()) $
-$ emph(hello.my().world) $
-$ emph(hello.my().world()) $
-$ #hello $
-$ #hello() $
-$ #hello.world $
-$ #hello.world() $
-$ #box[] $
-#if foo []
-```
diff --git a/tests/typ/compiler/hint.typ b/tests/typ/compiler/hint.typ
deleted file mode 100644
index 1a5efcaa3e..0000000000
--- a/tests/typ/compiler/hint.typ
+++ /dev/null
@@ -1,41 +0,0 @@
-// Test hints on diagnostics.
-// Ref: false
-
----
-// Error: 1:17-1:19 expected length, found integer: a length needs a unit - did you mean 12pt?
-#set text(size: 12)
-
----
-#{
- let a = 2
- a = 1-a
- a = a -1
-
- // Error: 7-10 unknown variable: a-1
- // Hint: 7-10 if you meant to use subtraction, try adding spaces around the minus sign
- a = a-1
-}
-
----
-#{
- // Error: 3-6 unknown variable: a-1
- // Hint: 3-6 if you meant to use subtraction, try adding spaces around the minus sign
- a-1 = 2
-}
-
----
-= Heading
-
-// Error: 1:20-1:26 cannot reference heading without numbering
-// Hint: 1:20-1:26 you can enable heading numbering with `#set heading(numbering: "1.")`
-Can not be used as @intro
-
----
-// This test is more of a tooling test. It checks if hint annotation validation
-// can be turned off.
-// Hints: false
-
-= Heading
-
-// Error: 1:20-1:26 cannot reference heading without numbering
-Can not be used as @intro
diff --git a/tests/typ/compiler/import.typ b/tests/typ/compiler/import.typ
deleted file mode 100644
index 5c3a05c930..0000000000
--- a/tests/typ/compiler/import.typ
+++ /dev/null
@@ -1,262 +0,0 @@
-// Test function and module imports.
-// Ref: false
-
----
-// Test basic syntax and semantics.
-// Ref: true
-
-// Test that this will be overwritten.
-#let value = [foo]
-
-// Import multiple things.
-#import "module.typ": fn, value
-#fn[Like and Subscribe!]
-#value
-
-// Should output `bye`.
-// Stop at semicolon.
-#import "module.typ": a, c;bye
-
----
-// An item import.
-#import "module.typ": item
-#test(item(1, 2), 3)
-
-// Code mode
-{
- import "module.typ": b
- test(b, 1)
-}
-
-// A wildcard import.
-#import "module.typ": *
-
-// It exists now!
-#test(d, 3)
-
----
-// A renamed item import.
-#import "module.typ": item as something
-#test(something(1, 2), 3)
-
-// Mixing renamed and not renamed items.
-#import "module.typ": fn, b as val, item as other
-#test(val, 1)
-#test(other(1, 2), 3)
-
----
-// Test importing from function scopes.
-// Ref: true
-
-#import enum: item
-#import assert.with(true): *
-
-#enum(
- item(1)[First],
- item(5)[Fifth]
-)
-#eq(10, 10)
-#ne(5, 6)
-
----
-// Test renaming items imported from function scopes.
-#import assert: eq as aseq
-#aseq(10, 10)
-
----
-// A module import without items.
-#import "module.typ"
-#test(module.b, 1)
-#test(module.item(1, 2), 3)
-#test(module.push(2), 3)
-
----
-// A renamed module import without items.
-#import "module.typ" as other
-#test(other.b, 1)
-#test(other.item(1, 2), 3)
-#test(other.push(2), 3)
-
----
-// Mixing renamed module and items.
-#import "module.typ" as newname: b as newval, item
-#test(newname.b, 1)
-#test(newval, 1)
-#test(item(1, 2), 3)
-#test(newname.item(1, 2), 3)
-
----
-// Renamed module import with function scopes.
-#import enum as othernum
-#test(enum, othernum)
-
----
-// Mixing renamed module import from function with renamed item import.
-#import assert as asrt
-#import asrt: ne as asne
-#asne(1, 2)
-
----
-// Edge case for module access that isn't fixed.
-#import "module.typ"
-
-// Works because the method name isn't categorized as mutating.
-#test((module,).at(0).item(1, 2), 3)
-
-// Doesn't work because of mutating name.
-// Error: 2-11 cannot mutate a temporary value
-#(module,).at(0).push()
-
----
-// Who needs whitespace anyways?
-#import"module.typ":*
-
-// Allow the trailing comma.
-#import "module.typ": a, c,
-
----
-// Usual importing syntax also works for function scopes
-#let d = (e: enum)
-#import d.e
-#import d.e as renamed
-#import d.e: item
-#item(2)[a]
-
----
-// Warning: 23-27 unnecessary import rename to same name
-#import enum: item as item
-
----
-// Warning: 17-21 unnecessary import rename to same name
-#import enum as enum
-
----
-// Warning: 17-21 unnecessary import rename to same name
-#import enum as enum: item
-// Warning: 17-21 unnecessary import rename to same name
-// Warning: 31-35 unnecessary import rename to same name
-#import enum as enum: item as item
-
----
-// No warning on a case that isn't obviously pathological
-#import "module.typ" as module
-
----
-// Can't import from closures.
-#let f(x) = x
-// Error: 9-10 cannot import from user-defined functions
-#import f: x
-
----
-// Can't import from closures, despite renaming.
-#let f(x) = x
-// Error: 9-10 cannot import from user-defined functions
-#import f as g
-
----
-// Can't import from closures, despite modifiers.
-#let f(x) = x
-// Error: 9-18 cannot import from user-defined functions
-#import f.with(5): x
-
----
-// Error: 9-18 cannot import from user-defined functions
-#import () => {5}: x
-
----
-// Error: 9-10 expected path, module, function, or type, found integer
-#import 5: something
-
----
-// Error: 9-10 expected path, module, function, or type, found integer
-#import 5 as x
-
----
-// Error: 9-11 failed to load file (is a directory)
-#import "": name
-
----
-// Error: 9-11 failed to load file (is a directory)
-#import "" as x
-
----
-// Error: 9-20 file not found (searched at typ/compiler/lib/0.2.1)
-#import "lib/0.2.1"
-
----
-// Error: 9-20 file not found (searched at typ/compiler/lib/0.2.1)
-#import "lib/0.2.1" as x
-
----
-// Some non-text stuff.
-// Error: 9-35 file is not valid utf-8
-#import "/assets/images/rhino.png"
-
----
-// Unresolved import.
-// Error: 23-35 unresolved import
-#import "module.typ": non_existing
-
----
-// Cyclic import of this very file.
-// Error: 9-23 cyclic import
-#import "./import.typ"
-
----
-// Cyclic import in other file.
-#import "./modules/cycle1.typ": *
-
-This is never reached.
-
----
-// Renaming does not import the old name (without items).
-#import "module.typ" as something
-// Error: 7-12 unknown variable: mymod
-#test(mymod.b, 1)
-
----
-// Renaming does not import the old name (with items).
-#import "module.typ" as something: b as other
-// Error: 7-12 unknown variable: mymod
-#test(mymod.b, 1)
-
----
-// Error: 8 expected expression
-#import
-
----
-// Error: 26-29 unexpected string
-#import "module.typ": a, "b", c
-
----
-// Error: 23-24 unexpected equals sign
-#import "module.typ": =
-
----
-// An additional trailing comma.
-// Error: 31-32 unexpected comma
-#import "module.typ": a, b, c,,
-
----
-// Error: 2:2 expected semicolon or line break
-#import "module.typ
-"stuff
-
----
-// A star in the list.
-// Error: 26-27 unexpected star
-#import "module.typ": a, *, b
-
----
-// An item after a star.
-// Error: 24 expected semicolon or line break
-#import "module.typ": *, a
-
----
-// Error: 14-15 unexpected colon
-// Error: 16-17 unexpected integer
-#import "": a: 1
-
----
-// Error: 14 expected comma
-#import "": a b
diff --git a/tests/typ/compiler/include.typ b/tests/typ/compiler/include.typ
deleted file mode 100644
index 586e869bb4..0000000000
--- a/tests/typ/compiler/include.typ
+++ /dev/null
@@ -1,32 +0,0 @@
-// Test module includes.
-
----
-#set page(width: 200pt)
-
-= Document
-
-// Include a file
-#include "modules/chap1.typ"
-
-// Expression as a file name.
-#let chap2 = include "modu" + "les/chap" + "2.typ"
-
--- _Intermission_ --
-#chap2
-
----
-#{
- // Error: 19-38 file not found (searched at typ/compiler/modules/chap3.typ)
- let x = include "modules/chap3.typ"
-}
-
----
-#include "modules/chap1.typ"
-
-// The variables of the file should not appear in this scope.
-// Error: 2-6 unknown variable: name
-#name
-
----
-// Error: 18 expected semicolon or line break
-#include "hi.typ" Hi
diff --git a/tests/typ/compiler/let.typ b/tests/typ/compiler/let.typ
deleted file mode 100644
index 411509ff55..0000000000
--- a/tests/typ/compiler/let.typ
+++ /dev/null
@@ -1,302 +0,0 @@
-// Test let bindings.
-
----
-// Automatically initialized with none.
-#let x
-#test(x, none)
-
-// Manually initialized with one.
-#let z = 1
-#test(z, 1)
-
-// Syntax sugar for function definitions.
-#let fill = conifer
-#let f(body) = rect(width: 2cm, fill: fill, inset: 5pt, body)
-#f[Hi!]
-
----
-// Termination.
-
-// Terminated by line break.
-#let v1 = 1
-One
-
-// Terminated by semicolon.
-#let v2 = 2; Two
-
-// Terminated by semicolon and line break.
-#let v3 = 3;
-Three
-
-#test(v1, 1)
-#test(v2, 2)
-#test(v3, 3)
-
----
-// Test what constitutes a valid Typst identifier.
-// Ref: false
-#let name = 1
-#test(name, 1)
-#let name_ = 1
-#test(name_, 1)
-#let name-2 = 1
-#test(name-2, 1)
-#let name_2 = 1
-#test(name_2, 1)
-#let __name = 1
-#test(__name, 1)
-#let ůñıćóðė = 1
-#test(ůñıćóðė, 1)
-
----
-// Test parenthesised assignments.
-// Ref: false
-#let (a) = (1, 2)
-
----
-// Ref: false
-// Simple destructuring.
-#let (a, b) = (1, 2)
-#test(a, 1)
-#test(b, 2)
-
----
-// Ref: false
-#let (a,) = (1,)
-#test(a, 1)
-
----
-// Ref: false
-// Destructuring with multiple placeholders.
-#let (a, _, c, _) = (1, 2, 3, 4)
-#test(a, 1)
-#test(c, 3)
-
----
-// Ref: false
-// Destructuring with a sink.
-#let (a, b, ..c) = (1, 2, 3, 4, 5, 6)
-#test(a, 1)
-#test(b, 2)
-#test(c, (3, 4, 5, 6))
-
----
-// Ref: false
-// Destructuring with a sink in the middle.
-#let (a, ..b, c) = (1, 2, 3, 4, 5, 6)
-#test(a, 1)
-#test(b, (2, 3, 4, 5))
-#test(c, 6)
-
----
-// Ref: false
-// Destructuring with an empty sink.
-#let (..a, b, c) = (1, 2)
-#test(a, ())
-#test(b, 1)
-#test(c, 2)
-
----
-// Ref: false
-// Destructuring with an empty sink.
-#let (a, ..b, c) = (1, 2)
-#test(a, 1)
-#test(b, ())
-#test(c, 2)
-
----
-// Ref: false
-// Destructuring with an empty sink.
-#let (a, b, ..c) = (1, 2)
-#test(a, 1)
-#test(b, 2)
-#test(c, ())
-
----
-// Ref: false
-// Destructuring with an empty sink and empty array.
-#let (..a) = ()
-#test(a, ())
-
----
-// Ref: false
-// Destructuring with unnamed sink.
-#let (a, .., b) = (1, 2, 3, 4)
-#test(a, 1)
-#test(b, 4)
-
-// Error: 10-11 duplicate binding: a
-#let (a, a) = (1, 2)
-
-// Error: 12-15 only one destructuring sink is allowed
-#let (..a, ..a) = (1, 2)
-
-// Error: 12-13 duplicate binding: a
-#let (a, ..a) = (1, 2)
-
-// Error: 13-14 duplicate binding: a
-#let (a: a, a) = (a: 1, b: 2)
-
-// Error: 13-20 expected pattern, found function call
-#let (a, b: b.at(0)) = (a: 1, b: 2)
-
-// Error: 7-14 expected pattern, found function call
-#let (a.at(0),) = (1,)
-
----
-// Error: 13-14 not enough elements to destructure
-#let (a, b, c) = (1, 2)
-
----
-// Error: 7-10 not enough elements to destructure
-#let (..a, b, c, d) = (1, 2)
-
----
-// Error: 6-12 cannot destructure boolean
-#let (a, b) = true
-
----
-// Ref: false
-// Simple destructuring.
-#let (a: a, b, x: c) = (a: 1, b: 2, x: 3)
-#test(a, 1)
-#test(b, 2)
-#test(c, 3)
-
----
-// Ref: false
-// Destructuring with a sink.
-#let (a: _, ..b) = (a: 1, b: 2, c: 3)
-#test(b, (b: 2, c: 3))
-
----
-// Ref: false
-// Destructuring with a sink in the middle.
-#let (a: _, ..b, c: _) = (a: 1, b: 2, c: 3)
-#test(b, (b: 2))
-
----
-// Ref: false
-// Destructuring with an empty sink.
-#let (a: _, ..b) = (a: 1)
-#test(b, (:))
-
----
-// Ref: false
-// Destructuring with an empty sink and empty dict.
-#let (..a) = (:)
-#test(a, (:))
-
----
-// Ref: false
-// Destructuring with unnamed sink.
-#let (a, ..) = (a: 1, b: 2)
-#test(a, 1)
-
----
-// Ref: false
-// Nested destructuring.
-#let ((a, b), (key: c)) = ((1, 2), (key: 3))
-#test((a, b, c), (1, 2, 3))
-
----
-// Keyed destructuring is not currently supported.
-// Error: 7-18 expected pattern, found string
-#let ("spacy key": val) = ("spacy key": 123)
-#val
-
----
-// Keyed destructuring is not currently supported.
-#let x = "spacy key"
-// Error: 7-10 expected identifier, found group
-#let ((x): v) = ("spacy key": 123)
-
----
-// Trailing placeholders.
-// Error: 10-11 not enough elements to destructure
-#let (a, _, _, _, _) = (1,)
-#test(a, 1)
-
----
-// Error: 10-13 expected pattern, found string
-// Error: 18-19 expected pattern, found integer
-#let (a: "a", b: 2) = (a: 1, b: 2)
-
----
-// Error: 10-11 dictionary does not contain key "b"
-#let (a, b) = (a: 1)
-
----
-// Error: 10-11 dictionary does not contain key "b"
-#let (a, b: b) = (a: 1)
-
----
-// Error: 7-11 cannot destructure named pattern from an array
-#let (a: a, b) = (1, 2, 3)
-
----
-// Error: 5 expected pattern
-#let
-
-// Error: 6 expected pattern
-#{let}
-
-// Error: 6-9 expected pattern, found string
-#let "v"
-
-// Error: 7 expected semicolon or line break
-#let v 1
-
-// Error: 9 expected expression
-#let v =
-
-// Error: 6-9 expected pattern, found string
-#let "v" = 1
-
-// Terminated because expression ends.
-// Error: 12 expected semicolon or line break
-#let v4 = 4 Four
-
-// Terminated by semicolon even though we are in a paren group.
-// Error: 18 expected expression
-// Error: 11-12 unclosed delimiter
-#let v5 = (1, 2 + ; Five
-
-// Error: 9-13 expected pattern, found boolean
-#let (..true) = false
-
----
-#let _ = 4
-
-#for _ in range(2) []
-
-// Error: 2-3 unexpected underscore
-#_
-
-// Error: 8-9 expected expression, found underscore
-#lorem(_)
-
-// Error: 3-4 expected expression, found underscore
-#(_,)
-
-// Error: 3-4 expected expression, found underscore
-#{_}
-
-// Error: 8-9 expected expression, found underscore
-#{ 1 + _ }
-
----
-// Error: 13 expected equals sign
-#let func(x)
-
-// Error: 15 expected expression
-#let func(x) =
-
----
-// Error: 12 expected equals sign
-#let (func)(x)
-
----
-// Error: 12 expected equals sign
-// Error: 15-15 expected semicolon or line break
-#let (func)(x) = 3
diff --git a/tests/typ/compiler/methods.typ b/tests/typ/compiler/methods.typ
deleted file mode 100644
index 8d5484ed55..0000000000
--- a/tests/typ/compiler/methods.typ
+++ /dev/null
@@ -1,287 +0,0 @@
-// Test method calls.
-// Ref: false
-
----
-// Test whitespace around dot.
-#test( "Hi there" . split() , ("Hi", "there"))
-
----
-// Test mutating indexed value.
-#{
- let matrix = (((1,), (2,)), ((3,), (4,)))
- matrix.at(1).at(0).push(5)
- test(matrix, (((1,), (2,)), ((3, 5), (4,))))
-}
-
----
-// Test multiline chain in code block.
-#{
- let rewritten = "Hello. This is a sentence. And one more."
- .split(".")
- .map(s => s.trim())
- .filter(s => s != "")
- .map(s => s + "!")
- .join("\n ")
-
- test(rewritten, "Hello!\n This is a sentence!\n And one more!")
-}
-
----
-// Test .at() default values for content.
-#test(auto, [a].at("doesn't exist", default: auto))
-
----
-// Error: 2:10-2:13 type array has no method `fun`
-#let numbers = ()
-#numbers.fun()
-
----
-// Error: 2:4-2:10 type content has no method `stroke`
-// Hint: 2:4-2:10 did you mean to access the field `stroke`?
-#let l = line(stroke: red)
-#l.stroke()
-
----
-// Error: 2:2-2:43 cannot mutate a temporary value
-#let numbers = (1, 2, 3)
-#numbers.map(v => v / 2).sorted().map(str).remove(4)
-
----
-// Error: 2:3-2:19 cannot mutate a temporary value
-#let numbers = (1, 2, 3)
-#(numbers.sorted() = 1)
-
----
-// Error: 2-5 cannot mutate a constant: box
-#box.push(1)
-
----
-// Test content fields method.
-#test([a].fields(), (text: "a"))
-#test([a *b*].fields(), (children: ([a], [ ], strong[b])))
-
----
-// Test length unit conversions.
-#test((500.934pt).pt(), 500.934)
-#test((3.3453cm).cm(), 3.3453)
-#test((4.3452mm).mm(), 4.3452)
-#test((5.345in).inches(), 5.345)
-#test((500.333666999pt).pt(), 500.333666999)
-#test((3.5234354cm).cm(), 3.5234354)
-#test((4.12345678mm).mm(), 4.12345678)
-#test((5.333666999in).inches(), 5.333666999)
-#test((4.123456789123456mm).mm(), 4.123456789123456)
-#test((254cm).mm(), 2540.0)
-#test(calc.round((254cm).inches(), digits: 2), 100.0)
-#test((2540mm).cm(), 254.0)
-#test(calc.round((2540mm).inches(), digits: 2), 100.0)
-#test((100in).pt(), 7200.0)
-#test(calc.round((100in).cm(), digits: 2), 254.0)
-#test(calc.round((100in).mm(), digits: 2), 2540.0)
-#test(5em.abs.cm(), 0.0)
-#test((5em + 6in).abs.inches(), 6.0)
-
----
-// Test length `to-absolute` method.
-#set text(size: 12pt)
-#context {
- test((6pt).to-absolute(), 6pt)
- test((6pt + 10em).to-absolute(), 126pt)
- test((10em).to-absolute(), 120pt)
-}
-
-#set text(size: 64pt)
-#context {
- test((6pt).to-absolute(), 6pt)
- test((6pt + 10em).to-absolute(), 646pt)
- test((10em).to-absolute(), 640pt)
-}
-
----
-// Error: 2-21 cannot convert a length with non-zero em units (`-6pt + 10.5em`) to pt
-// Hint: 2-21 use `length.abs.pt()` instead to ignore its em component
-#(10.5em - 6pt).pt()
-
----
-// Error: 2-12 cannot convert a length with non-zero em units (`3em`) to cm
-// Hint: 2-12 use `length.abs.cm()` instead to ignore its em component
-#(3em).cm()
-
----
-// Error: 2-20 cannot convert a length with non-zero em units (`-226.77pt + 93em`) to mm
-// Hint: 2-20 use `length.abs.mm()` instead to ignore its em component
-#(93em - 80mm).mm()
-
----
-// Error: 2-24 cannot convert a length with non-zero em units (`432pt + 4.5em`) to inches
-// Hint: 2-24 use `length.abs.inches()` instead to ignore its em component
-#(4.5em + 6in).inches()
-
----
-// Test color kind method.
-#test(rgb(1, 2, 3, 4).space(), rgb)
-#test(cmyk(4%, 5%, 6%, 7%).space(), cmyk)
-#test(luma(40).space(), luma)
-#test(rgb(1, 2, 3, 4).space() != luma, true)
-
----
-// Test color '.components()' without conversions
-
-#let test-components(col, ref, has-alpha: true) = {
- // Perform an approximate scalar comparison.
- let are-equal((a, b)) = {
- let to-float(x) = if type(x) == angle { x.rad() } else { float(x) }
- let epsilon = 1e-4 // The maximum error between both numbers
- assert.eq(type(a), type(b))
- calc.abs(to-float(a) - to-float(b)) < epsilon
- }
-
- let ref-without-alpha = if has-alpha { ref.slice(0, -1) } else { ref }
- assert.eq(col.components().len(), ref.len())
- assert(col.components().zip(ref).all(are-equal))
- assert(col.components(alpha: false).zip(ref-without-alpha).all(are-equal))
-}
-#test-components(rgb(1, 2, 3, 4), (0.39%, 0.78%, 1.18%, 1.57%))
-#test-components(luma(40), (15.69%, 100%))
-#test-components(luma(40, 50%), (15.69%, 50%))
-#test-components(cmyk(4%, 5%, 6%, 7%), (4%, 5%, 6%, 7%), has-alpha: false)
-#test-components(oklab(10%, 0.2, 0.4), (10%, 0.2, 0.4, 100%))
-#test-components(oklch(10%, 0.2, 90deg), (10%, 0.2, 90deg, 100%))
-#test-components(oklab(10%, 50%, 200%), (10%, 0.2, 0.8, 100%))
-#test-components(oklch(10%, 50%, 90deg), (10%, 0.2, 90deg, 100%))
-#test-components(color.linear-rgb(10%, 20%, 30%), (10%, 20%, 30%, 100%))
-#test-components(color.hsv(10deg, 20%, 30%), (10deg, 20%, 30%, 100%))
-#test-components(color.hsl(10deg, 20%, 30%), (10deg, 20%, 30%, 100%))
-
----
-// Test color conversions.
-#test(rgb(1, 2, 3).to-hex(), "#010203")
-#test(rgb(1, 2, 3, 4).to-hex(), "#01020304")
-#test(luma(40).to-hex(), "#282828")
-#test-repr(cmyk(4%, 5%, 6%, 7%).to-hex(), "#e0dcda")
-#test-repr(rgb(cmyk(4%, 5%, 6%, 7%)), rgb(87.84%, 86.27%, 85.49%, 100%))
-#test-repr(rgb(luma(40%)), rgb(40%, 40%, 40%))
-#test-repr(cmyk(luma(40)), cmyk(11.76%, 10.67%, 10.51%, 14.12%))
-#test-repr(cmyk(rgb(1, 2, 3)), cmyk(66.67%, 33.33%, 0%, 98.82%))
-#test-repr(luma(rgb(1, 2, 3)), luma(0.73%))
-#test-repr(color.hsl(luma(40)), color.hsl(0deg, 0%, 15.69%))
-#test-repr(color.hsv(luma(40)), color.hsv(0deg, 0%, 15.69%))
-#test-repr(color.linear-rgb(luma(40)), color.linear-rgb(2.12%, 2.12%, 2.12%))
-#test-repr(color.linear-rgb(rgb(1, 2, 3)), color.linear-rgb(0.03%, 0.06%, 0.09%))
-#test-repr(color.hsl(rgb(1, 2, 3)), color.hsl(-150deg, 50%, 0.78%))
-#test-repr(color.hsv(rgb(1, 2, 3)), color.hsv(-150deg, 66.67%, 1.18%))
-#test-repr(oklab(luma(40)), oklab(27.68%, 0.0, 0.0, 100%))
-#test-repr(oklab(rgb(1, 2, 3)), oklab(8.23%, -0.004, -0.007, 100%))
-#test-repr(oklch(oklab(40%, 0.2, 0.2)), oklch(40%, 0.283, 45deg, 100%))
-#test-repr(oklch(luma(40)), oklch(27.68%, 0.0, 72.49deg, 100%))
-#test-repr(oklch(rgb(1, 2, 3)), oklch(8.23%, 0.008, 240.75deg, 100%))
-
----
-// Test gradient functions.
-#test(gradient.linear(red, green, blue).kind(), gradient.linear)
-#test(gradient.linear(red, green, blue).stops(), ((red, 0%), (green, 50%), (blue, 100%)))
-#test(gradient.linear(red, green, blue, space: rgb).sample(0%), red)
-#test(gradient.linear(red, green, blue, space: rgb).sample(25%), rgb("#97873b"))
-#test(gradient.linear(red, green, blue, space: rgb).sample(50%), green)
-#test(gradient.linear(red, green, blue, space: rgb).sample(75%), rgb("#17a08c"))
-#test(gradient.linear(red, green, blue, space: rgb).sample(100%), blue)
-#test(gradient.linear(red, green, space: rgb).space(), rgb)
-#test(gradient.linear(red, green, space: oklab).space(), oklab)
-#test(gradient.linear(red, green, space: oklch).space(), oklch)
-#test(gradient.linear(red, green, space: cmyk).space(), cmyk)
-#test(gradient.linear(red, green, space: luma).space(), luma)
-#test(gradient.linear(red, green, space: color.linear-rgb).space(), color.linear-rgb)
-#test(gradient.linear(red, green, space: color.hsl).space(), color.hsl)
-#test(gradient.linear(red, green, space: color.hsv).space(), color.hsv)
-#test(gradient.linear(red, green, relative: "self").relative(), "self")
-#test(gradient.linear(red, green, relative: "parent").relative(), "parent")
-#test(gradient.linear(red, green).relative(), auto)
-#test(gradient.linear(red, green).angle(), 0deg)
-#test(gradient.linear(red, green, dir: ltr).angle(), 0deg)
-#test(gradient.linear(red, green, dir: rtl).angle(), 180deg)
-#test(gradient.linear(red, green, dir: ttb).angle(), 90deg)
-#test(gradient.linear(red, green, dir: btt).angle(), 270deg)
-#test(
- gradient.linear(red, green, blue).repeat(2).stops(),
- ((red, 0%), (green, 25%), (blue, 50%), (red, 50%), (green, 75%), (blue, 100%))
-)
-#test(
- gradient.linear(red, green, blue).repeat(2, mirror: true).stops(),
- ((red, 0%), (green, 25%), (blue, 50%), (green, 75%), (red, 100%))
-)
-
----
-// Test alignment methods.
-#test(start.axis(), "horizontal")
-#test(end.axis(), "horizontal")
-#test(left.axis(), "horizontal")
-#test(right.axis(), "horizontal")
-#test(center.axis(), "horizontal")
-#test(top.axis(), "vertical")
-#test(bottom.axis(), "vertical")
-#test(horizon.axis(), "vertical")
-#test(start.inv(), end)
-#test(end.inv(), start)
-#test(left.inv(), right)
-#test(right.inv(), left)
-#test(center.inv(), center)
-#test(top.inv(), bottom)
-#test(bottom.inv(), top)
-#test(horizon.inv(), horizon)
-
----
-// Test 2d alignment methods.
-#test((start + top).inv(), (end + bottom))
-#test((end + top).inv(), (start + bottom))
-#test((left + top).inv(), (right + bottom))
-#test((right + top).inv(), (left + bottom))
-#test((center + top).inv(), (center + bottom))
-#test((start + bottom).inv(), (end + top))
-#test((end + bottom).inv(), (start + top))
-#test((left + bottom).inv(), (right + top))
-#test((right + bottom).inv(), (left + top))
-#test((center + bottom).inv(), (center + top))
-#test((start + horizon).inv(), (end + horizon))
-#test((end + horizon).inv(), (start + horizon))
-#test((left + horizon).inv(), (right + horizon))
-#test((right + horizon).inv(), (left + horizon))
-#test((center + horizon).inv(), (center + horizon))
-#test((top + start).inv(), (end + bottom))
-#test((bottom + end).inv(), (start + top))
-#test((horizon + center).inv(), (center + horizon))
-
----
-// Test direction methods.
-#test(ltr.axis(), "horizontal")
-#test(rtl.axis(), "horizontal")
-#test(ttb.axis(), "vertical")
-#test(btt.axis(), "vertical")
-#test(ltr.start(), left)
-#test(rtl.start(), right)
-#test(ttb.start(), top)
-#test(btt.start(), bottom)
-#test(ltr.end(), right)
-#test(rtl.end(), left)
-#test(ttb.end(), bottom)
-#test(btt.end(), top)
-#test(ltr.inv(), rtl)
-#test(rtl.inv(), ltr)
-#test(ttb.inv(), btt)
-#test(btt.inv(), ttb)
-
----
-// Test angle methods.
-#test(1rad.rad(), 1.0)
-#test(1.23rad.rad(), 1.23)
-#test(0deg.rad(), 0.0)
-#test(2deg.deg(), 2.0)
-#test(2.94deg.deg(), 2.94)
-#test(0rad.deg(), 0.0)
-
----
-// Test date methods.
-#test(datetime(day: 1, month: 1, year: 2000).ordinal(), 1);
-#test(datetime(day: 1, month: 3, year: 2000).ordinal(), 31 + 29 + 1);
-#test(datetime(day: 31, month: 12, year: 2000).ordinal(), 366);
-#test(datetime(day: 1, month: 3, year: 2001).ordinal(), 31 + 28 + 1);
-#test(datetime(day: 31, month: 12, year: 2001).ordinal(), 365);
diff --git a/tests/typ/compiler/ops-assoc.typ b/tests/typ/compiler/ops-assoc.typ
deleted file mode 100644
index ec128c61b4..0000000000
--- a/tests/typ/compiler/ops-assoc.typ
+++ /dev/null
@@ -1,18 +0,0 @@
-// Test operator associativity.
-// Ref: false
-
----
-// Math operators are left-associative.
-#test(10 / 2 / 2 == (10 / 2) / 2, true)
-#test(10 / 2 / 2 == 10 / (2 / 2), false)
-#test(1 / 2 * 3, 1.5)
-
----
-// Assignment is right-associative.
-{
- let x = 1
- let y = 2
- x = y = "ok"
- test(x, none)
- test(y, "ok")
-}
diff --git a/tests/typ/compiler/ops-invalid.typ b/tests/typ/compiler/ops-invalid.typ
deleted file mode 100644
index 64e3a87864..0000000000
--- a/tests/typ/compiler/ops-invalid.typ
+++ /dev/null
@@ -1,134 +0,0 @@
-// Test invalid operations.
-// Ref: false
-
----
-// Error: 4 expected expression
-#(-)
-
----
-// Error: 10 expected expression
-#test({1+}, 1)
-
----
-// Error: 10 expected expression
-#test({2*}, 2)
-
----
-// Error: 3-13 cannot apply unary '+' to content
-#(+([] + []))
-
----
-// Error: 3-6 cannot apply '-' to string
-#(-"")
-
----
-// Error: 3-9 cannot apply 'not' to array
-#(not ())
-
----
-// Error: 3-19 cannot compare relative length and ratio
-#(30% + 1pt <= 40%)
-
----
-// Error: 3-14 cannot compare 1em with 10pt
-#(1em <= 10pt)
-
----
-// Error: 3-22 cannot compare 2.2 with NaN
-#(2.2 <= float("nan"))
-
----
-// Error: 3-26 cannot compare integer and string
-#((0, 1, 3) > (0, 1, "a"))
-
----
-// Error: 3-42 cannot compare 3.5 with NaN
-#((0, "a", 3.5) <= (0, "a", float("nan")))
-
----
-// Error: 3-12 cannot divide by zero
-#(1.2 / 0.0)
-
----
-// Error: 3-8 cannot divide by zero
-#(1 / 0)
-
----
-// Error: 3-15 cannot divide by zero
-#(15deg / 0deg)
-
----
-// Special messages for +, -, * and /.
-// Error: 3-10 cannot add integer and string
-#(1 + "2", 40% - 1)
-
----
-// Error: 15-23 cannot add integer and string
-#{ let x = 1; x += "2" }
-
----
-// Error: 4-13 cannot divide ratio by length
-#( 10% / 5pt )
-
----
-// Error: 3-12 cannot divide these two lengths
-#(1em / 5pt)
-
----
-// Error: 3-19 cannot divide relative length by ratio
-#((10% + 1pt) / 5%)
-
----
-// Error: 3-28 cannot divide these two relative lengths
-#((10% + 1pt) / (20% + 1pt))
-
----
-// Error: 13-20 cannot subtract integer from ratio
-#((1234567, 40% - 1))
-
----
-// Error: 3-11 cannot multiply integer with boolean
-#(2 * true)
-
----
-// Error: 3-11 cannot divide integer by length
-#(3 / 12pt)
-
----
-// Error: 3-10 number must be at least zero
-#(-1 * "")
-
----
-// Error: 4-5 unknown variable: x
-#((x) = "")
-
----
-// Error: 4-5 unknown variable: x
-#((x,) = (1,))
-
----
-// Error: 3-8 cannot mutate a temporary value
-#(1 + 2 += 3)
-
----
-// Error: 2:3-2:8 cannot apply 'not' to string
-#let x = "Hey"
-#(not x = "a")
-
----
-// Error: 7-8 unknown variable: x
-#(1 + x += 3)
-
----
-// Error: 3-4 unknown variable: z
-#(z = 1)
-
----
-// Error: 3-7 cannot mutate a constant: rect
-#(rect = "hi")
-
----
-// Works if we define rect beforehand
-// (since then it doesn't resolve to the standard library version anymore).
-#let rect = ""
-#(rect = "hi")
diff --git a/tests/typ/compiler/ops-prec.typ b/tests/typ/compiler/ops-prec.typ
deleted file mode 100644
index d3fe01b5fa..0000000000
--- a/tests/typ/compiler/ops-prec.typ
+++ /dev/null
@@ -1,36 +0,0 @@
-// Test operator precedence.
-// Ref: false
-
----
-// Multiplication binds stronger than addition.
-#test(1+2*-3, -5)
-
-// Subtraction binds stronger than comparison.
-#test(3 == 5 - 2, true)
-
-// Boolean operations bind stronger than '=='.
-#test("a" == "a" and 2 < 3, true)
-#test(not "b" == "b", false)
-
----
-// Assignment binds stronger than boolean operations.
-// Error: 2:3-2:8 cannot mutate a temporary value
-#let x = false
-#(not x = "a")
-
----
-// Precedence doesn't matter for chained unary operators.
-// Error: 3-12 cannot apply '-' to boolean
-#(-not true)
-
----
-// Not in handles precedence.
-#test(-1 not in (1, 2, 3), true)
-
----
-// Parentheses override precedence.
-#test((1), 1)
-#test((1+2)*-3, -9)
-
-// Error: 8-9 unclosed delimiter
-#test({(1 + 1}, 2)
diff --git a/tests/typ/compiler/ops.typ b/tests/typ/compiler/ops.typ
deleted file mode 100644
index e148dd1941..0000000000
--- a/tests/typ/compiler/ops.typ
+++ /dev/null
@@ -1,359 +0,0 @@
-// Test binary expressions.
-// Ref: false
-
----
-// Test adding content.
-// Ref: true
-#([*Hello* ] + [world!])
-
----
-// Test math operators.
-
-// Test plus and minus.
-#for v in (1, 3.14, 12pt, 45deg, 90%, 13% + 10pt, 6.3fr) {
- // Test plus.
- test(+v, v)
-
- // Test minus.
- test(-v, -1 * v)
- test(--v, v)
-
- // Test combination.
- test(-++ --v, -v)
-}
-
-#test(-(4 + 2), 6-12)
-
-// Addition.
-#test(2 + 4, 6)
-#test("a" + "b", "ab")
-#test("a" + if false { "b" }, "a")
-#test("a" + if true { "b" }, "ab")
-#test(13 * "a" + "bbbbbb", "aaaaaaaaaaaaabbbbbb")
-#test((1, 2) + (3, 4), (1, 2, 3, 4))
-#test((a: 1) + (b: 2, c: 3), (a: 1, b: 2, c: 3))
-
----
-// Error: 3-26 value is too large
-#(9223372036854775807 + 1)
-
----
-// Subtraction.
-#test(1-4, 3*-1)
-#test(4cm - 2cm, 2cm)
-#test(1e+2-1e-2, 99.99)
-
-// Multiplication.
-#test(2 * 4, 8)
-
-// Division.
-#test(12pt/.4, 30pt)
-#test(7 / 2, 3.5)
-
-// Combination.
-#test(3-4 * 5 < -10, true)
-#test({ let x; x = 1 + 4*5 >= 21 and { x = "a"; x + "b" == "ab" }; x }, true)
-
-// With block.
-#test(if true {
- 1
-} + 2, 3)
-
-// Mathematical identities.
-#let nums = (
- 1, 3.14,
- 12pt, 3em, 12pt + 3em,
- 45deg,
- 90%,
- 13% + 10pt, 5% + 1em + 3pt,
- 2.3fr,
-)
-
-#for v in nums {
- // Test plus and minus.
- test(v + v - v, v)
- test(v - v - v, -v)
-
- // Test plus/minus and multiplication.
- test(v - v, 0 * v)
- test(v + v, 2 * v)
-
- // Integer addition does not give a float.
- if type(v) != int {
- test(v + v, 2.0 * v)
- }
-
- if type(v) != relative and ("pt" not in repr(v) or "em" not in repr(v)) {
- test(v / v, 1.0)
- }
-}
-
-// Make sure length, ratio and relative length
-// - can all be added to / subtracted from each other,
-// - multiplied with integers and floats,
-// - divided by integers and floats.
-#let dims = (10pt, 1em, 10pt + 1em, 30%, 50% + 3cm, 40% + 2em + 1cm)
-#for a in dims {
- for b in dims {
- test(type(a + b), type(a - b))
- }
-
- for b in (7, 3.14) {
- test(type(a * b), type(a))
- test(type(b * a), type(a))
- test(type(a / b), type(a))
- }
-}
-
-// Test division of different numeric types with zero components.
-#for a in (0pt, 0em, 0%) {
- for b in (10pt, 10em, 10%) {
- test((2 * b) / b, 2)
- test((a + b * 2) / b, 2)
- test(b / (b * 2 + a), 0.5)
- }
-}
-
----
-// Test numbers with alternative bases.
-#test(0x10, 16)
-#test(0b1101, 13)
-#test(0xA + 0xa, 0x14)
-
----
-// Error: 2-7 invalid binary number: 0b123
-#0b123
-
----
-// Error: 2-8 invalid hexadecimal number: 0x123z
-#0x123z
-
----
-// Test that multiplying infinite numbers by certain units does not crash.
-#(float("inf") * 1pt)
-#(float("inf") * 1em)
-#(float("inf") * (1pt + 1em))
-
----
-// Test that trying to produce a NaN scalar (such as in lengths) does not crash.
-#let infpt = float("inf") * 1pt
-#test(infpt - infpt, 0pt)
-#test(infpt + (-infpt), 0pt)
-// TODO: this result is surprising
-#test(infpt / float("inf"), 0pt)
-
----
-// Test boolean operators.
-
-// Test not.
-#test(not true, false)
-#test(not false, true)
-
-// And.
-#test(false and false, false)
-#test(false and true, false)
-#test(true and false, false)
-#test(true and true, true)
-
-// Or.
-#test(false or false, false)
-#test(false or true, true)
-#test(true or false, true)
-#test(true or true, true)
-
-// Short-circuiting.
-#test(false and dont-care, false)
-#test(true or dont-care, true)
-
----
-// Test equality operators.
-
-// Most things compare by value.
-#test(1 == "hi", false)
-#test(1 == 1.0, true)
-#test(30% == 30% + 0cm, true)
-#test(1in == 0% + 72pt, true)
-#test(30% == 30% + 1cm, false)
-#test("ab" == "a" + "b", true)
-#test(() == (1,), false)
-#test((1, 2, 3) == (1, 2.0) + (3,), true)
-#test((:) == (a: 1), false)
-#test((a: 2 - 1.0, b: 2) == (b: 2, a: 1), true)
-#test("a" != "a", false)
-
-// Functions compare by identity.
-#test(test == test, true)
-#test((() => {}) == (() => {}), false)
-
-// Content compares field by field.
-#let t = [a]
-#test(t == t, true)
-#test([] == [], true)
-#test([a] == [a], true)
-#test(grid[a] == grid[a], true)
-#test(grid[a] == grid[b], false)
-
----
-// Test comparison operators.
-
-#test(13 * 3 < 14 * 4, true)
-#test(5 < 10, true)
-#test(5 > 5, false)
-#test(5 <= 5, true)
-#test(5 <= 4, false)
-#test(45deg < 1rad, true)
-#test(10% < 20%, true)
-#test(50% < 40% + 0pt, false)
-#test(40% + 0pt < 50% + 0pt, true)
-#test(1em < 2em, true)
-#test((0, 1, 2, 4) < (0, 1, 2, 5), true)
-#test((0, 1, 2, 4) < (0, 1, 2, 3), false)
-#test((0, 1, 2, 3.3) > (0, 1, 2, 4), false)
-#test((0, 1, 2) < (0, 1, 2, 3), true)
-#test((0, 1, "b") > (0, 1, "a", 3), true)
-#test((0, 1.1, 3) >= (0, 1.1, 3), true)
-#test((0, 1, datetime(day: 1, month: 12, year: 2023)) <= (0, 1, datetime(day: 1, month: 12, year: 2023), 3), true)
-#test(("a", 23, 40, "b") > ("a", 23, 40), true)
-#test(() <= (), true)
-#test(() >= (), true)
-#test(() <= (1,), true)
-#test((1,) <= (), false)
-
----
-// Test assignment operators.
-
-#let x = 0
-#(x = 10) #test(x, 10)
-#(x -= 5) #test(x, 5)
-#(x += 1) #test(x, 6)
-#(x *= x) #test(x, 36)
-#(x /= 2.0) #test(x, 18.0)
-#(x = "some") #test(x, "some")
-#(x += "thing") #test(x, "something")
-
----
-// Test destructuring assignments.
-
-#let a = none
-#let b = none
-#let c = none
-#((a,) = (1,))
-#test(a, 1)
-
-#((_, a, b, _) = (1, 2, 3, 4))
-#test(a, 2)
-#test(b, 3)
-
-#((a, b, ..c) = (1, 2, 3, 4, 5, 6))
-#test(a, 1)
-#test(b, 2)
-#test(c, (3, 4, 5, 6))
-
-#((a: a, b, x: c) = (a: 1, b: 2, x: 3))
-#test(a, 1)
-#test(b, 2)
-#test(c, 3)
-
-#let a = (1, 2)
-#((a: a.at(0), b) = (a: 3, b: 4))
-#test(a, (3, 2))
-#test(b, 4)
-
-#let a = (1, 2)
-#((a.at(0), b) = (3, 4))
-#test(a, (3, 2))
-#test(b, 4)
-
-#((a, ..b) = (1, 2, 3, 4))
-#test(a, 1)
-#test(b, (2, 3, 4))
-
-#let a = (1, 2)
-#((b, ..a.at(0)) = (1, 2, 3, 4))
-#test(a, ((2, 3, 4), 2))
-#test(b, 1)
-
----
-// Test comma placement in destructuring assignment.
-#let array = (1, 2, 3)
-#((key: array.at(1)) = (key: "hi"))
-#test(array, (1, "hi", 3))
-
-#let array = (1, 2, 3)
-#((array.at(1)) = ("hi"))
-#test(array, (1, "hi", 3))
-
-#let array = (1, 2, 3)
-#((array.at(1),) = ("hi",))
-#test(array, (1, "hi", 3))
-
-#let array = (1, 2, 3)
-#((array.at(1)) = ("hi",))
-#test(array, (1, ("hi",), 3))
-
----
-// Test nested destructuring assignment.
-#let a
-#let b
-#let c
-#(((a, b), (key: c)) = ((1, 2), (key: 3)))
-#test((a, b, c), (1, 2, 3))
-
----
-#let array = (1, 2, 3)
-// Error: 3-17 cannot destructure string
-#((array.at(1),) = ("hi"))
-#test(array, (1, ("hi",), 3))
-
----
-// Error: 3-6 cannot mutate a constant: box
-#(box = 1)
-
----
-// Test `in` operator.
-#test("hi" in "worship", true)
-#test("hi" in ("we", "hi", "bye"), true)
-#test("Hey" in "abHeyCd", true)
-#test("Hey" in "abheyCd", false)
-#test(5 in range(10), true)
-#test(12 in range(10), false)
-#test("" in (), false)
-#test("key" in (key: "value"), true)
-#test("value" in (key: "value"), false)
-#test("Hey" not in "abheyCd", true)
-#test("a" not
-/* fun comment? */ in "abc", false)
-
----
-// Error: 10 expected keyword `in`
-#("a" not)
-
----
-// Test `with` method.
-
-// Apply positional arguments.
-#let add(x, y) = x + y
-#test(add.with(2)(3), 5)
-#test(add.with(2, 3)(), 5)
-#test(add.with(2).with(3)(), 5)
-#test((add.with(2))(4), 6)
-#test((add.with(2).with(3))(), 5)
-
-// Make sure that named arguments are overridable.
-#let inc(x, y: 1) = x + y
-#test(inc(1), 2)
-
-#let inc2 = inc.with(y: 2)
-#test(inc2(2), 4)
-#test(inc2(2, y: 4), 6)
-
-// Apply arguments to an argument sink.
-#let times(..sink) = {
- let res = sink.pos().product()
- if sink.named().at("negate", default: false) { res *= -1 }
- res
-}
-#test((times.with(2, negate: true).with(5))(), -10)
-#test((times.with(2).with(5).with(negate: true))(), -10)
-#test((times.with(2).with(5, negate: true))(), -10)
-#test((times.with(2).with(negate: true))(5), -10)
diff --git a/tests/typ/compiler/packages.typ b/tests/typ/compiler/packages.typ
deleted file mode 100644
index 0d3fda5813..0000000000
--- a/tests/typ/compiler/packages.typ
+++ /dev/null
@@ -1,69 +0,0 @@
-// Test package imports
-// Ref: false
-
----
-// Test import without items.
-#import "@test/adder:0.1.0"
-#test(adder.add(2, 8), 10)
-
----
-// Test import with items.
-#import "@test/adder:0.1.0": add
-#test(add(2, 8), 10)
-
----
-// Test too high required compiler version.
-// Error: 9-29 package requires typst 1.0.0 or newer (current version is VERSION)
-#import "@test/future:0.1.0": future
-
----
-// Error: 9-13 `@` is not a valid package namespace
-#import "@@": *
-
----
-// Error: 9-16 package specification is missing name
-#import "@heya": *
-
----
-// Error: 9-15 `123` is not a valid package namespace
-#import "@123": *
-
----
-// Error: 9-17 package specification is missing name
-#import "@test/": *
-
----
-// Error: 9-22 package specification is missing version
-#import "@test/mypkg": *
-
----
-// Error: 9-20 `$$$` is not a valid package name
-#import "@test/$$$": *
-
----
-// Error: 9-23 package specification is missing version
-#import "@test/mypkg:": *
-
----
-// Error: 9-24 version number is missing minor version
-#import "@test/mypkg:0": *
-
----
-// Error: 9-29 `latest` is not a valid major version
-#import "@test/mypkg:latest": *
-
----
-// Error: 9-29 `-3` is not a valid major version
-#import "@test/mypkg:-3.0.0": *
-
----
-// Error: 9-26 version number is missing patch version
-#import "@test/mypkg:0.3": *
-
----
-// Error: 9-27 version number is missing patch version
-#import "@test/mypkg:0.3.": *
-
----
-// Error: 9-28 file not found (searched at typ/compiler/#test/mypkg:1.0.0)
-#import "#test/mypkg:1.0.0": *
diff --git a/tests/typ/compiler/plugin-oob.typ b/tests/typ/compiler/plugin-oob.typ
deleted file mode 100644
index 4d1ba20539..0000000000
--- a/tests/typ/compiler/plugin-oob.typ
+++ /dev/null
@@ -1,14 +0,0 @@
-// Test Out Of Bound read/write in WebAssembly plugins communication.
-// Ref: false
-
----
-#let p = plugin("/assets/plugins/plugin-oob.wasm")
-
-// Error: 2-14 plugin tried to read out of bounds: pointer 0x40000000 is out of bounds for read of length 1
-#p.read_oob()
-
----
-#let p = plugin("/assets/plugins/plugin-oob.wasm")
-
-// Error: 2-27 plugin tried to write out of bounds: pointer 0x40000000 is out of bounds for write of length 3
-#p.write_oob(bytes("xyz"))
diff --git a/tests/typ/compiler/plugin.typ b/tests/typ/compiler/plugin.typ
deleted file mode 100644
index e727355ff2..0000000000
--- a/tests/typ/compiler/plugin.typ
+++ /dev/null
@@ -1,36 +0,0 @@
-// Test WebAssembly plugins.
-// Ref: false
-
----
-#let p = plugin("/assets/plugins/hello.wasm")
-#test(p.hello(), bytes("Hello from wasm!!!"))
-#test(p.double_it(bytes("hey!")), bytes("hey!.hey!"))
-#test(
- p.shuffle(bytes("value1"), bytes("value2"), bytes("value3")),
- bytes("value3-value1-value2"),
-)
-
----
-#let p = plugin("/assets/plugins/hello.wasm")
-
-// Error: 2-20 plugin function takes 0 arguments, but 1 was given
-#p.hello(bytes(""))
-
----
-#let p = plugin("/assets/plugins/hello.wasm")
-
-// Error: 10-14 expected bytes, found boolean
-// Error: 27-29 expected bytes, found integer
-#p.hello(true, bytes(()), 10)
-
----
-#let p = plugin("/assets/plugins/hello.wasm")
-
-// Error: 2-17 plugin errored with: This is an `Err`
-#p.returns_err()
-
----
-#let p = plugin("/assets/plugins/hello.wasm")
-
-// Error: 2-16 plugin panicked: wasm `unreachable` instruction executed
-#p.will_panic()
diff --git a/tests/typ/compiler/raw.typ b/tests/typ/compiler/raw.typ
deleted file mode 100644
index 3084146dae..0000000000
--- a/tests/typ/compiler/raw.typ
+++ /dev/null
@@ -1,170 +0,0 @@
-// Test new raw parser
-// Ref: false
-
----
-#let empty = (
- name: "empty",
- input: ``,
- text: "",
-)
-
-#let backtick = (
- name: "backtick",
- input: ``` ` ```,
- text: "`",
- block: false,
-)
-
-#let lang-backtick = (
- name: "lang-backtick",
- input: ```js ` ```,
- lang: "js",
- text: "`",
- block: false,
-)
-
-// The language tag stops on space
-#let lang-space = (
- name: "lang-space",
- input: ```js test ```,
- lang: "js",
- text: "test ",
- block: false,
-)
-
-// The language tag stops on newline
-#let lang-newline = (
- name: "lang-newline",
- input: ```js
-test
-```,
- lang: "js",
- text: "test",
- block: true,
-)
-
-// The first line and the last line are ignored
-#let blocky = (
- name: "blocky",
- input: {
-```
-test
-```
-},
- text: "test",
- block: true,
-)
-
-// A blocky raw should handle dedents
-#let blocky-dedent = (
- name: "blocky-dedent",
- input: {
-```
- test
- ```
- },
- text: "test",
- block: true,
-)
-
-// When there is content in the first line, it should exactly eat a whitespace char.
-#let blocky-dedent-firstline = (
- name: "blocky-dedent-firstline",
- input: ``` test
- ```,
- text: "test",
- block: true,
-)
-
-// When there is content in the first line, it should exactly eat a whitespace char.
-#let blocky-dedent-firstline2 = (
- name: "blocky-dedent-firstline2",
- input: ``` test
-```,
- text: "test",
- block: true,
-)
-
-// The first line is not affected by dedent, and the middle lines don't consider the whitespace prefix of the first line.
-#let blocky-dedent-firstline3 = (
- name: "blocky-dedent-firstline3",
- input: ``` test
- test2
- ```,
- text: "test\n test2",
- block: true,
-)
-
-// The first line is not affected by dedent, and the middle lines don't consider the whitespace prefix of the first line.
-#let blocky-dedent-firstline4 = (
- name: "blocky-dedent-firstline4",
- input: ``` test
- test2
- ```,
- text: " test\ntest2",
- block: true,
-)
-
-#let blocky-dedent-lastline = (
- name: "blocky-dedent-lastline",
- input: ```
- test
- ```,
- text: " test",
- block: true,
-)
-
-#let blocky-dedent-lastline2 = (
- name: "blocky-dedent-lastline2",
- input: ```
- test
- ```,
- text: "test",
- block: true,
-)
-
-#let blocky-tab = (
- name: "blocky-tab",
- input: {
-```
- test
-```
-},
- text: "\ttest",
- block: true,
-)
-
-#let blocky-tab-dedent = (
- name: "blocky-tab-dedent",
- input: {
-```
- test
-
- ```
-},
- text: "test\n ",
- block: true,
-)
-
-#let cases = (
- empty,
- backtick,
- lang-backtick,
- lang-space,
- lang-newline,
- blocky,
- blocky-dedent,
- blocky-dedent-firstline,
- blocky-dedent-firstline2,
- blocky-dedent-firstline3,
- blocky-dedent-lastline,
- blocky-dedent-lastline2,
- blocky-tab,
- blocky-tab-dedent,
-)
-
-#for c in cases {
- assert.eq(c.text, c.input.text, message: "in point " + c.name + ", expect " + repr(c.text) + ", got " + repr(c.input.text) + "")
- let block = c.at("block", default: false)
- assert.eq(block, c.input.block, message: "in point " + c.name + ", expect " + repr(block) + ", got " + repr(c.input.block) + "")
-}
diff --git a/tests/typ/compiler/repr-color-gradient.typ b/tests/typ/compiler/repr-color-gradient.typ
deleted file mode 100644
index ef15897426..0000000000
--- a/tests/typ/compiler/repr-color-gradient.typ
+++ /dev/null
@@ -1,23 +0,0 @@
-// Test representation of values in the document.
-
----
-// Colors
-#set page(width: 400pt)
-#set text(0.8em)
-#blue \
-#color.linear-rgb(blue) \
-#oklab(blue) \
-#oklch(blue) \
-#cmyk(blue) \
-#color.hsl(blue) \
-#color.hsv(blue) \
-#luma(blue)
-
----
-// Gradients
-#set page(width: 400pt)
-#set text(0.8em)
-#gradient.linear(blue, red) \
-#gradient.linear(blue, red, dir: ttb) \
-#gradient.linear(blue, red, angle: 45deg, relative: "self") \
-#gradient.linear(blue, red, angle: 45deg, space: rgb)
diff --git a/tests/typ/compiler/select-where-styles.typ b/tests/typ/compiler/select-where-styles.typ
deleted file mode 100644
index 028be2e92e..0000000000
--- a/tests/typ/compiler/select-where-styles.typ
+++ /dev/null
@@ -1,91 +0,0 @@
-// Test that where selectors also work with settable fields.
-
----
-// Test that where selectors also trigger on set rule fields.
-#show raw.where(block: false): box.with(
- fill: luma(220),
- inset: (x: 3pt, y: 0pt),
- outset: (y: 3pt),
- radius: 2pt,
-)
-
-This is #raw("fn main() {}") some text.
-
----
-// Note: This show rule is horribly inefficient because it triggers for
-// every individual text element. But it should still work.
-#show text.where(lang: "de"): set text(red)
-
-#set text(lang: "es")
-Hola, mundo!
-
-#set text(lang: "de")
-Hallo Welt!
-
-#set text(lang: "en")
-Hello World!
-
----
-// Test that folding is taken into account.
-#set text(5pt)
-#set text(2em)
-
-#[
- #show text.where(size: 2em): set text(blue)
- 2em not blue
-]
-
-#[
- #show text.where(size: 10pt): set text(blue)
- 10pt blue
-]
-
----
-// Test again that folding is taken into account.
-#set rect(width: 40pt, height: 10pt)
-#set rect(stroke: blue)
-#set rect(stroke: 2pt)
-
-#{
- show rect.where(stroke: blue): "Not Triggered"
- rect()
-}
-#{
- show rect.where(stroke: 2pt): "Not Triggered"
- rect()
-}
-#{
- show rect.where(stroke: 2pt + blue): "Triggered"
- rect()
-}
-
----
-// Test that resolving is *not* taken into account.
-#set line(start: (1em, 1em + 2pt))
-
-#{
- show line.where(start: (1em, 1em + 2pt)): "Triggered"
- line()
-}
-#{
- show line.where(start: (10pt, 12pt)): "Not Triggered"
- line()
-}
-
-
----
-// Test again that resolving is *not* taken into account.
-#set text(hyphenate: auto)
-
-#[
- #show text.where(hyphenate: auto): underline
- Auto
-]
-#[
- #show text.where(hyphenate: true): underline
- True
-]
-#[
- #show text.where(hyphenate: false): underline
- False
-]
diff --git a/tests/typ/compiler/selector-logical.typ b/tests/typ/compiler/selector-logical.typ
deleted file mode 100644
index 5369e4c747..0000000000
--- a/tests/typ/compiler/selector-logical.typ
+++ /dev/null
@@ -1,126 +0,0 @@
-//Tests for logical (and/or) selectors
-
----
-= A
-== B
-#figure([Cat], kind: "cat", supplement: [Other])
-=== D
-= E
-#figure([Frog], kind: "frog", supplement: none)
-#figure([Giraffe], kind: "giraffe", supplement: none)
-#figure([GiraffeCat], kind: "cat", supplement: [Other])
-= H
-#figure([Iguana], kind: "iguana", supplement: none)
-== I
-
-#let test-selector(selector, ref) = context {
- test(query(selector).map(e => e.body), ref)
-}
-
-// Test `or`.
-#test-selector(
- heading.where(level: 1).or(heading.where(level: 3)),
- ([A], [D], [E], [H]),
-)
-
-#test-selector(
- heading.where(level: 1).or(
- heading.where(level: 3),
- figure.where(kind: "frog"),
- ),
- ([A], [D], [E], [Frog], [H]),
-)
-
-#test-selector(
- heading.where(level: 1).or(
- heading.where(level: 2),
- figure.where(kind: "frog"),
- figure.where(kind: "cat"),
- ),
- ([A], [B], [Cat], [E], [Frog], [GiraffeCat], [H], [I]),
-)
-
-#test-selector(
- figure.where(kind: "dog").or(heading.where(level: 3)),
- ([D],),
-)
-
-#test-selector(
- figure.where(kind: "dog").or(figure.where(kind: "fish")),
- (),
-)
-
-// Test `or` duplicates removal.
-#test-selector(
- heading.where(level: 1).or(heading.where(level: 1)),
- ([A], [E], [H]),
-)
-
-// Test `and`.
-#test-selector(
- figure.where(kind: "cat").and(figure.where(kind: "frog")),
- (),
-)
-
-// Test `or` with `before`/`after`
-#test-selector(
- selector(heading)
- .before()
- .or(selector(figure).before()),
- ([A], [B], [Cat], [D], [E]),
-)
-
-#test-selector(
- heading.where(level: 2)
- .after()
- .or(selector(figure).after()),
- ([Frog], [Giraffe], [GiraffeCat], [Iguana], [I]),
-)
-
-// Test `and` with `after`
-#test-selector(
- figure.where(kind: "cat")
- .and(figure.where(supplement: [Other]))
- .after(),
- ([GiraffeCat],),
-)
-
-// Test `and` (with nested `or`)
-#test-selector(
- heading.where(level: 2)
- .or(heading.where(level: 3))
- .and(heading.where(level: 2).or(heading.where(level: 1))),
- ([B], [I]),
-)
-
-#test-selector(
- heading.where(level: 2)
- .or(heading.where(level: 3), heading.where(level:1))
- .and(
- heading.where(level: 2).or(heading.where(level: 1)),
- heading.where(level: 3).or(heading.where(level: 1)),
- ),
- ([A], [E], [H]),
-)
-
-// Test `and` with `or` and `before`/`after`
-#test-selector(
- heading.where(level: 1).before()
- .or(heading.where(level: 3).before())
- .and(
- heading.where(level: 1).before()
- .or(heading.where(level: 2).before())
- ),
- ([A], [E]),
-)
-
-#test-selector(
- heading.where(level: 1).before(, inclusive: false)
- .or(selector(figure).after())
- .and(figure.where(kind: "iguana").or(
- figure.where(kind: "frog"),
- figure.where(kind: "cat"),
- heading.where(level: 1).after(),
- )),
- ([Frog], [GiraffeCat], [Iguana])
-)
diff --git a/tests/typ/compiler/set.typ b/tests/typ/compiler/set.typ
deleted file mode 100644
index 23b3a7c60a..0000000000
--- a/tests/typ/compiler/set.typ
+++ /dev/null
@@ -1,66 +0,0 @@
-// General tests for set.
-
----
-// Test that text is affected by instantiation-site bold.
-#let x = [World]
-Hello *#x*
-
----
-// Test that lists are affected by correct indents.
-#let fruit = [
- - Apple
- - Orange
- #list(body-indent: 20pt)[Pear]
-]
-
-- Fruit
-#[#set list(indent: 10pt)
- #fruit]
-- No more fruit
-
----
-// Test that that block spacing and text style are respected from
-// the outside, but the more specific fill is respected.
-#set block(spacing: 4pt)
-#set text(style: "italic", fill: eastern)
-#let x = [And the forest #parbreak() lay silent!]
-#text(fill: forest, x)
-
----
-// Test that scoping works as expected.
-#{
- if true {
- set text(blue)
- [Blue ]
- }
- [Not blue]
-}
-
----
-// Test relative path resolving in layout phase.
-#let choice = ("monkey.svg", "rhino.png", "tiger.jpg")
-#set enum(numbering: n => {
- let path = "/assets/images/" + choice.at(n - 1)
- move(dy: -0.15em, image(path, width: 1em, height: 1em))
-})
-
-+ Monkey
-+ Rhino
-+ Tiger
-
----
-// Test conditional set.
-#show ref: it => {
- set text(red) if it.target ==