diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01b3e8c3a..41f17d137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ env: RUSTFLAGS: "-Dwarnings" RUSTDOCFLAGS: "-Dwarnings" TYPST_TESTS_EXTENDED: true + PKG_CONFIG_i686-unknown-linux-gnu: /usr/bin/i686-linux-gnu-pkgconf jobs: # This allows us to have one branch protection rule for the full test matrix. @@ -27,30 +28,43 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] + bits: [64] + include: + - os: ubuntu-latest + bits: 32 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - if: startsWith(matrix.os, 'ubuntu-') && matrix.bits == 32 + run: | + sudo dpkg --add-architecture i386 + sudo apt update + sudo apt install -y gcc-multilib libssl-dev:i386 pkg-config:i386 + - uses: dtolnay/rust-toolchain@1.85.0 + with: + targets: ${{ matrix.bits == 32 && 'i686-unknown-linux-gnu' || '' }} - uses: Swatinem/rust-cache@v2 - - run: cargo test --workspace --no-run - - run: cargo test --workspace --no-fail-fast + with: + key: ${{ matrix.bits }} + - run: cargo test --workspace --no-run ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} + - run: cargo test --workspace --no-fail-fast ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} - name: Upload rendered test output if: failure() uses: actions/upload-artifact@v4 with: - name: tests-rendered-${{ matrix.os }} + name: tests-rendered-${{ matrix.os }}-${{ matrix.bits }} path: tests/store/render/** retention-days: 3 - name: Update test artifacts if: failure() run: | - cargo test --workspace --test tests -- --update + cargo test --workspace --test tests ${{ matrix.bits == 32 && '--target i686-unknown-linux-gnu' || '' }} -- --update echo 'updated_artifacts=1' >> "$GITHUB_ENV" - name: Upload updated reference output (for use if the test changes are desired) if: failure() && env.updated_artifacts uses: actions/upload-artifact@v4 with: - name: tests-updated-${{ matrix.os }} + name: tests-updated-${{ matrix.os }}-${{ matrix.bits }} path: tests/ref/** retention-days: 3 @@ -59,7 +73,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 @@ -73,7 +87,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.80.0 + - uses: dtolnay/rust-toolchain@1.83.0 - uses: Swatinem/rust-cache@v2 - run: cargo check --workspace diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5be6bfa2c..0d235aec5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 with: target: ${{ matrix.target }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dfa836d18..33c5343c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,8 +37,8 @@ Below are some signs of a good PR: - Adds/changes as little code and as few interfaces as possible. Should changes to larger-scale abstractions be necessary, these should be discussed throughout the implementation process. -- Adds tests if appropriate (with reference images for visual tests). See the - [testing] readme for more details. +- Adds tests if appropriate (with reference output for visual/HTML tests). See + the [testing] readme for more details. - Contains documentation comments on all new Rust types. - Comes with brief documentation for all new Typst definitions (elements/functions), ideally with a concise example that fits into ~5-10 diff --git a/Cargo.lock b/Cargo.lock index ca99a97ec..85698d8bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler2" @@ -8,18 +8,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -46,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -61,36 +49,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -104,9 +93,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -192,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -213,9 +202,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "by_address" @@ -225,23 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -257,9 +232,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" -version = "1.1.24" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -292,14 +267,14 @@ checksum = "7588475145507237ded760e52bf2f1085495245502033756d28ea72ade0e498b" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -337,9 +312,9 @@ dependencies = [ [[package]] name = "citationberg" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92fea693c83bd967604be367dc1e1b4895625eabafec2eec66c51092e18e700e" +checksum = "e4595e03beafb40235070080b5286d3662525efc622cca599585ff1d63f844fa" dependencies = [ "quick-xml 0.36.2", "serde", @@ -347,9 +322,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -357,9 +332,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -370,18 +345,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.32" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" +checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -391,15 +366,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" -version = "0.2.23" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ "clap", "roff", @@ -423,23 +398,24 @@ dependencies = [ [[package]] name = "codex" -version = "0.1.0" -source = "git+https://github.com/typst/codex?rev=343a9b1#343a9b199430681ba3ca0e2242097c6419492d55" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "724d27a0ee38b700e5e164350e79aba601a0db673ac47fce1cb74c3e38864036" [[package]] name = "color-print" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee543c60ff3888934877a5671f45494dd27ed4ba25c6670b9a7576b7ed7a8c0" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" dependencies = [ "color-print-proc-macro", ] [[package]] name = "color-print-proc-macro" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ff1a80c5f3cb1ca7c06ffdd71b6a6dd6d8f896c42141fbd43f50ed28dcdb93" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" dependencies = [ "nom", "proc-macro2", @@ -455,9 +431,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "comemo" @@ -468,7 +444,7 @@ dependencies = [ "comemo-macros", "once_cell", "parking_lot", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -500,9 +476,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core_maths" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" dependencies = [ "libm", ] @@ -518,18 +494,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -546,21 +522,21 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "csv" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" dependencies = [ "csv-core", "itoa", @@ -594,9 +570,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", @@ -605,23 +581,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -643,9 +619,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "ecow" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54bfbb1708988623190a6c4dbedaeaf0f53c20c6395abd6a01feb327b3146f4b" +checksum = "e42fc0a93992b20c58b99e59d61eaf1635a25bfbe49e4275c34ba0aee98119ba" dependencies = [ "serde", ] @@ -706,12 +682,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -732,15 +708,15 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -759,9 +735,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -773,15 +749,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -[[package]] -name = "float-cmp" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" -dependencies = [ - "num-traits", -] - [[package]] name = "fnv" version = "1.0.7" @@ -789,13 +756,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "font-types" -version = "0.7.3" +name = "foldhash" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" -dependencies = [ - "bytemuck", -] +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "fontconfig-parser" @@ -808,9 +772,9 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.22.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716" +checksum = "37be9fc20d966be438cd57a45767f73349477fb0f85ce86e000557f787298afb" dependencies = [ "fontconfig-parser", "log", @@ -882,7 +846,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -911,36 +887,30 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] [[package]] name = "hayagriva" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3635c2577f77499c9dc3dceeef2e64e6c146e711b1861507a0f15b20641348" +checksum = "954907554bb7fcba29a4f917c2d43e289ec21b69d872ccf97db160eca6caeed8" dependencies = [ "biblatex", "ciborium", "citationberg", - "indexmap 2.6.0", + "indexmap 2.7.1", "numerals", "paste", "serde", "serde_yaml 0.9.34+deprecated", - "thiserror", + "thiserror 1.0.69", "unic-langid", "unicode-segmentation", "unscanny", @@ -1034,6 +1004,30 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + [[package]] name = "icu_properties" version = "1.5.1" @@ -1138,12 +1132,23 @@ checksum = "f739ee737260d955e330bc83fdeaaf1631f7fb7ed218761d3c04bb13bb7d79df" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1154,9 +1159,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -1196,9 +1201,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -1206,19 +1211,13 @@ dependencies = [ "serde", ] -[[package]] -name = "indexmap-nostd" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" - [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "inotify-sys", "libc", ] @@ -1259,9 +1258,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -1274,18 +1273,19 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "kamadak-exif" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +checksum = "1130d80c7374efad55a117d715a3af9368f0fa7a2c54573afc15a188cd984837" dependencies = [ "mutate_once", ] @@ -1310,38 +1310,6 @@ dependencies = [ "libc", ] -[[package]] -name = "krilla" -version = "0.3.0" -source = "git+https://github.com/LaurenzV/krilla?rev=ffdd1aa#ffdd1aa56a5c70711cc2863ce056cc2925bf4468" -dependencies = [ - "base64", - "bumpalo", - "comemo", - "flate2", - "float-cmp 0.10.0", - "fontdb", - "gif", - "image-webp", - "imagesize", - "miniz_oxide", - "once_cell", - "pdf-writer", - "rayon", - "resvg", - "rustybuzz", - "siphasher 1.0.1", - "skrifa", - "subsetter", - "tiny-skia", - "tiny-skia-path", - "usvg", - "xmp-writer", - "yoke", - "zune-jpeg", - "zune-png", -] - [[package]] name = "kurbo" version = "0.11.1" @@ -1354,33 +1322,33 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.159" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libdeflate-sys" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b14a6afa4e2e1d343fd793a1c0a7e5857a73a2697c2ff2c98ac00d6c4ecc820" +checksum = "413b667c8a795fcbe6287a75a8ce92b1dae928172c716fe95044cb2ec7877941" dependencies = [ "cc", ] [[package]] name = "libdeflater" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17fe2badabdaf756f620748311e99ef99a5fdd681562dfd343fdb16ed7d4797" +checksum = "d78376c917eec0550b9c56c858de50e1b7ebf303116487562e624e63ce51453a" dependencies = [ "libdeflate-sys", ] [[package]] name = "libfuzzer-sys" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9569d2f74e257076d8c6bfa73fb505b46b851e51ddaecc825944aa3bed17fa" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" dependencies = [ "arbitrary", "cc", @@ -1388,9 +1356,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" @@ -1398,7 +1366,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -1411,9 +1379,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "lipsum" @@ -1427,9 +1395,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" dependencies = [ "serde", ] @@ -1452,9 +1420,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lzma-sys" @@ -1490,9 +1458,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", "simd-adler32", @@ -1500,14 +1468,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.48.0", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", ] [[package]] @@ -1524,9 +1492,9 @@ checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -1551,12 +1519,11 @@ dependencies = [ [[package]] name = "notify" -version = "6.1.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943" dependencies = [ - "bitflags 2.6.0", - "crossbeam-channel", + "bitflags 2.8.0", "filetime", "fsevent-sys", "inotify", @@ -1564,10 +1531,17 @@ dependencies = [ "libc", "log", "mio", + "notify-types", "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "num-bigint" version = "0.4.6" @@ -1610,18 +1584,15 @@ checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31" [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "open" -version = "5.3.0" +version = "5.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" dependencies = [ "is-wsl", "libc", @@ -1630,11 +1601,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "foreign-types", "libc", @@ -1656,24 +1627,24 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.3.2+3.3.2" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", @@ -1690,22 +1661,19 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "oxipng" -version = "9.1.2" +version = "9.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec25597808aff9f632f018f0fe8985c6f670598ac5241d220a9f2d32ff46812e" +checksum = "aa3202b10a7ffac89508bb091fe420048c47926b37c5ff84d78dc8af7044fa86" dependencies = [ "bitvec", - "clap", - "clap_mangen", "crossbeam-channel", "filetime", - "indexmap 2.6.0", + "indexmap 2.7.1", "libdeflater", "log", "rayon", "rgb", "rustc-hash", - "rustc_version", "zopfli", ] @@ -1753,7 +1721,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1764,16 +1732,17 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" -version = "0.12.0" -source = "git+https://github.com/LaurenzV/pdf-writer?rev=f95a19c#f95a19c07a1b3e3ee021c1199e91f19badb57d46" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "itoa", "memchr", "ryu", @@ -1787,9 +1756,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -1797,9 +1766,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -1807,9 +1776,9 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", @@ -1820,11 +1789,11 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -1855,7 +1824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64", - "indexmap 2.6.0", + "indexmap 2.7.1", "quick-xml 0.32.0", "serde", "time", @@ -1863,9 +1832,9 @@ dependencies = [ [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -1876,15 +1845,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "postcard" -version = "1.0.10" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -1909,18 +1878,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" dependencies = [ "cc", ] @@ -1931,7 +1900,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "getopts", "memchr", "unicase", @@ -1970,9 +1939,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2028,41 +1997,31 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "read-fonts" -version = "0.22.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a04b892cb6f91951f144c33321843790c8574c825aafdb16d815fd7183b5229" -dependencies = [ - "bytemuck", - "font-types", -] - [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.11", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2072,9 +2031,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2089,9 +2048,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "resvg" -version = "0.44.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958" +checksum = "c7314563c59c7ce31c18e23ad3dd092c37b928a0fa4e1c0a1a6504351ab411d1" dependencies = [ "gif", "image-webp", @@ -2137,37 +2096,28 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustybuzz" @@ -2175,7 +2125,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "core_maths", "log", @@ -2189,9 +2139,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -2204,9 +2154,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -2223,7 +2173,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation", "core-foundation-sys", "libc", @@ -2232,9 +2182,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -2253,24 +2203,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2279,9 +2229,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -2316,7 +2266,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "itoa", "ryu", "serde", @@ -2352,35 +2302,19 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" -[[package]] -name = "skrifa" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" -dependencies = [ - "bytemuck", - "read-fonts", -] - [[package]] name = "slotmap" version = "1.0.7" @@ -2427,17 +2361,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp 0.9.0", + "float-cmp", ] [[package]] name = "string-interner" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6a0d765f5807e98a091107bae0a56ea3799f66a5de47b2c84c94a39c09974e" +checksum = "1a3275464d7a9f2d4cac57c89c2ef96a8524dba2864c8d6f82e3980baf136f9b" dependencies = [ - "cfg-if", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] @@ -2472,23 +2405,44 @@ dependencies = [ [[package]] name = "subsetter" version = "0.2.0" -source = "git+https://github.com/typst/subsetter?rev=172416a#172416a806246e6e9010d400d5690ca7a026e53d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f98178f34057d4d4de93d68104007c6dea4dfac930204a69ab4622daefa648" + +[[package]] +name = "svg2pdf" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5014c9dadcf318fb7ef8c16438e95abcc9de1ae24d60d5bccc64c55100c50364" +dependencies = [ + "fontdb", + "image", + "log", + "miniz_oxide", + "once_cell", + "pdf-writer", + "resvg", + "siphasher", + "subsetter", + "tiny-skia", + "ttf-parser", + "usvg", +] [[package]] name = "svgtypes" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", - "siphasher 1.0.1", + "siphasher", ] [[package]] name = "syn" -version = "2.0.79" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -2523,7 +2477,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] @@ -2536,9 +2490,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" +checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" dependencies = [ "filetime", "libc", @@ -2547,12 +2501,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2569,9 +2524,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix", "windows-sys 0.59.0", @@ -2585,18 +2540,38 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -2605,9 +2580,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -2626,9 +2601,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2685,9 +2660,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -2721,11 +2696,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ - "indexmap 2.6.0", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", @@ -2743,9 +2718,9 @@ dependencies = [ [[package]] name = "two-face" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccd4843ea031c609fe9c16cae00e9657bad8a9f735a3cc2e420955d802b4268" +checksum = "384eda438ddf62e2c6f39a174452d952d9d9df5a8ad5ade22198609f8dcaf852" dependencies = [ "once_cell", "serde", @@ -2760,7 +2735,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.12.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2777,12 +2752,12 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577" +version = "0.13.1" +source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a" [[package]] name = "typst-cli" -version = "0.12.0" +version = "0.13.1" dependencies = [ "chrono", "clap", @@ -2827,12 +2802,12 @@ dependencies = [ [[package]] name = "typst-dev-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-dev-assets?rev=b07d156#b07d1560143d6883887358d30edb25cb12fcf5b9" +version = "0.13.1" +source = "git+https://github.com/typst/typst-dev-assets?rev=9879589#9879589f4b3247b12c5e694d0d7fa86d4d8a198e" [[package]] name = "typst-docs" -version = "0.12.0" +version = "0.13.1" dependencies = [ "clap", "ecow", @@ -2847,18 +2822,20 @@ dependencies = [ "typst-assets", "typst-dev-assets", "typst-render", + "typst-utils", + "unicode-math-class", "unscanny", "yaml-front-matter", ] [[package]] name = "typst-eval" -version = "0.12.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", "if_chain", - "indexmap 2.6.0", + "indexmap 2.7.1", "stacker", "toml", "typst-library", @@ -2871,7 +2848,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.12.0" +version = "0.13.1" dependencies = [ "comemo", "libfuzzer-sys", @@ -2883,7 +2860,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.12.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2897,7 +2874,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.12.0" +version = "0.13.1" dependencies = [ "comemo", "ecow", @@ -2914,7 +2891,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.12.0" +version = "0.13.1" dependencies = [ "dirs", "ecow", @@ -2924,6 +2901,8 @@ dependencies = [ "native-tls", "once_cell", "openssl", + "serde", + "serde_json", "tar", "typst-assets", "typst-library", @@ -2935,7 +2914,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.12.0" +version = "0.13.1" dependencies = [ "az", "bumpalo", @@ -2965,10 +2944,10 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.12.0" +version = "0.13.1" dependencies = [ "az", - "bitflags 2.6.0", + "bitflags 2.8.0", "bumpalo", "chinese-number", "ciborium", @@ -2983,10 +2962,11 @@ dependencies = [ "icu_provider", "icu_provider_blob", "image", - "indexmap 2.6.0", + "indexmap 2.7.1", "kamadak-exif", "kurbo", "lipsum", + "memchr", "palette", "phf", "png", @@ -3000,7 +2980,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml 0.9.34+deprecated", - "siphasher 1.0.1", + "siphasher", "smallvec", "syntect", "time", @@ -3015,6 +2995,7 @@ dependencies = [ "typst-timing", "typst-utils", "unicode-math-class", + "unicode-normalization", "unicode-segmentation", "unscanny", "usvg", @@ -3024,7 +3005,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.12.0" +version = "0.13.1" dependencies = [ "heck", "proc-macro2", @@ -3034,24 +3015,33 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.12.0" +version = "0.13.1" dependencies = [ + "arrayvec", + "base64", "bytemuck", "comemo", "ecow", "image", - "krilla", + "indexmap 2.7.1", + "miniz_oxide", + "pdf-writer", + "serde", + "subsetter", + "svg2pdf", + "ttf-parser", "typst-assets", "typst-library", "typst-macros", "typst-syntax", "typst-timing", "typst-utils", + "xmp-writer", ] [[package]] name = "typst-realize" -version = "0.12.0" +version = "0.13.1" dependencies = [ "arrayvec", "bumpalo", @@ -3067,7 +3057,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.12.0" +version = "0.13.1" dependencies = [ "bytemuck", "comemo", @@ -3083,12 +3073,13 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.12.0" +version = "0.13.1" dependencies = [ "base64", "comemo", "ecow", "flate2", + "image", "ttf-parser", "typst-library", "typst-macros", @@ -3100,7 +3091,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.12.0" +version = "0.13.1" dependencies = [ "ecow", "serde", @@ -3116,7 +3107,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.12.0" +version = "0.13.1" dependencies = [ "clap", "comemo", @@ -3129,6 +3120,7 @@ dependencies = [ "typst", "typst-assets", "typst-dev-assets", + "typst-html", "typst-library", "typst-pdf", "typst-render", @@ -3140,22 +3132,24 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.12.0" +version = "0.13.1" dependencies = [ "parking_lot", "serde", "serde_json", + "web-sys", ] [[package]] name = "typst-utils" -version = "0.12.0" +version = "0.13.1" dependencies = [ "once_cell", "portable-atomic", "rayon", - "siphasher 1.0.1", + "siphasher", "thin-vec", + "unicode-math-class", ] [[package]] @@ -3179,18 +3173,15 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" @@ -3206,9 +3197,9 @@ checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-math-class" @@ -3269,9 +3260,9 @@ checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" [[package]] name = "ureq" -version = "2.10.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" dependencies = [ "base64", "flate2", @@ -3285,9 +3276,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3297,9 +3288,9 @@ dependencies = [ [[package]] name = "usvg" -version = "0.44.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" +checksum = "6803057b5cbb426e9fb8ce2216f3a9b4ca1dd2c705ba3cbebc13006e437735fd" dependencies = [ "base64", "data-url", @@ -3312,7 +3303,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -3322,6 +3313,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3363,25 +3360,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.93" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -3390,9 +3396,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3400,9 +3406,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -3413,15 +3419,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasmi" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7a1acc721dd73e4fff2dc3796cc3efda6e008369e859a20fdbe058bddeebc3" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" dependencies = [ "arrayvec", "multi-stash", @@ -3430,23 +3439,23 @@ dependencies = [ "wasmi_collections", "wasmi_core", "wasmi_ir", - "wasmparser-nostd", + "wasmparser", ] [[package]] name = "wasmi_collections" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142fda775f9cda587681ff0ec63c7a7e5679dc95da75f3f9b7e3979ce3506a5b" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" dependencies = [ "string-interner", ] [[package]] name = "wasmi_core" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "281a49ca3c12c8efa052cb67758454fc861d80ab5a03def352e04eb08c20beb2" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" dependencies = [ "downcast-rs", "libm", @@ -3454,20 +3463,31 @@ dependencies = [ [[package]] name = "wasmi_ir" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbadcf529808086a74bacd3ce8aedece444a847292198a56dcde920d1fb213c" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" dependencies = [ "wasmi_core", ] [[package]] -name = "wasmparser-nostd" -version = "0.100.2" +name = "wasmparser" +version = "0.221.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" +checksum = "9845c470a2e10b61dd42c385839cdd6496363ed63b5c9e420b5488b77bd22083" dependencies = [ - "indexmap-nostd", + "bitflags 2.8.0", + "indexmap 2.7.1", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", ] [[package]] @@ -3491,16 +3511,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -3509,7 +3520,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3518,22 +3529,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -3542,46 +3538,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3594,48 +3572,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3644,13 +3598,28 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.5.5" @@ -3668,9 +3637,9 @@ dependencies = [ [[package]] name = "xattr" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" dependencies = [ "libc", "linux-raw-sys", @@ -3691,8 +3660,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" -version = "0.3.0" -source = "git+https://github.com/LaurenzV/xmp-writer?rev=1c2b8ae9#1c2b8ae9c217ceeec39b86cf5e215b67fe8870db" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487" [[package]] name = "xz2" @@ -3724,9 +3694,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -3736,9 +3706,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -3769,18 +3739,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", @@ -3825,18 +3795,18 @@ dependencies = [ [[package]] name = "zip" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" dependencies = [ "arbitrary", "crc32fast", "crossbeam-utils", "displaydoc", "flate2", - "indexmap 2.6.0", + "indexmap 2.7.1", "memchr", - "thiserror", + "thiserror 2.0.11", "zopfli", ] @@ -3860,30 +3830,11 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] - -[[package]] -name = "zune-png" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d29c085769c6f29effea890f093120ac019375fdc789d2a496ba8ba96c77509" -dependencies = [ - "zune-core", - "zune-inflate", -] diff --git a/Cargo.toml b/Cargo.toml index 08ca9dc64..0bfd92821 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ default-members = ["crates/typst-cli"] resolver = "2" [workspace.package] -version = "0.12.0" -rust-version = "1.80" # also change in ci.yml +version = "0.13.1" +rust-version = "1.83" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" homepage = "https://typst.app" @@ -16,24 +16,24 @@ keywords = ["typst"] readme = "README.md" [workspace.dependencies] -typst = { path = "crates/typst", version = "0.12.0" } -typst-cli = { path = "crates/typst-cli", version = "0.12.0" } -typst-eval = { path = "crates/typst-eval", version = "0.12.0" } -typst-html = { path = "crates/typst-html", version = "0.12.0" } -typst-ide = { path = "crates/typst-ide", version = "0.12.0" } -typst-kit = { path = "crates/typst-kit", version = "0.12.0" } -typst-layout = { path = "crates/typst-layout", version = "0.12.0" } -typst-library = { path = "crates/typst-library", version = "0.12.0" } -typst-macros = { path = "crates/typst-macros", version = "0.12.0" } -typst-pdf = { path = "crates/typst-pdf", version = "0.12.0" } -typst-realize = { path = "crates/typst-realize", version = "0.12.0" } -typst-render = { path = "crates/typst-render", version = "0.12.0" } -typst-svg = { path = "crates/typst-svg", version = "0.12.0" } -typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } -typst-timing = { path = "crates/typst-timing", version = "0.12.0" } -typst-utils = { path = "crates/typst-utils", version = "0.12.0" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" } +typst = { path = "crates/typst", version = "0.13.1" } +typst-cli = { path = "crates/typst-cli", version = "0.13.1" } +typst-eval = { path = "crates/typst-eval", version = "0.13.1" } +typst-html = { path = "crates/typst-html", version = "0.13.1" } +typst-ide = { path = "crates/typst-ide", version = "0.13.1" } +typst-kit = { path = "crates/typst-kit", version = "0.13.1" } +typst-layout = { path = "crates/typst-layout", version = "0.13.1" } +typst-library = { path = "crates/typst-library", version = "0.13.1" } +typst-macros = { path = "crates/typst-macros", version = "0.13.1" } +typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" } +typst-realize = { path = "crates/typst-realize", version = "0.13.1" } +typst-render = { path = "crates/typst-render", version = "0.13.1" } +typst-svg = { path = "crates/typst-svg", version = "0.13.1" } +typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } +typst-timing = { path = "crates/typst-timing", version = "0.13.1" } +typst-utils = { path = "crates/typst-utils", version = "0.13.1" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "9879589" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" @@ -47,18 +47,18 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "343a9b1" } +codex = "0.1.1" color-print = "0.3.6" comemo = "0.4" csv = "1" ctrlc = "3.4.1" -dirs = "5" +dirs = "6" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" flate2 = "1" -fontdb = { version = "0.22", default-features = false } +fontdb = { version = "0.21", default-features = false } fs_extra = "1.3" -hayagriva = "0.8" +hayagriva = "0.8.1" heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } @@ -67,16 +67,16 @@ icu_provider_adapters = "1.4" icu_provider_blob = "1.4" icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" -image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] } +image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } -kamadak-exif = "0.5" -krilla = { git = "https://github.com/LaurenzV/krilla", rev = "ffdd1aa", features = ["comemo", "rayon", "svg"] } +kamadak-exif = "0.6" kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" +memchr = "2" miniz_oxide = "0.8" native-tls = "0.2" -notify = "6" +notify = "8" once_cell = "1" open = "5.0.1" openssl = "0.10" @@ -84,7 +84,7 @@ oxipng = { version = "9.0", default-features = false, features = ["filetime", "p palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } parking_lot = "0.12.1" pathdiff = "0.2" -pdf-writer = "0.12" +pdf-writer = "0.12.1" phf = { version = "0.11", features = ["macros"] } pixglyph = "0.5.1" png = "0.17" @@ -96,7 +96,7 @@ quote = "1" rayon = "1.7.0" regex = "1" regex-syntax = "0.8" -resvg = { version = "0.44", default-features = false, features = ["raster-images"] } +resvg = { version = "0.43", default-features = false, features = ["raster-images"] } roxmltree = "0.20" rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] } rustybuzz = "0.18" @@ -123,21 +123,23 @@ tiny_http = "0.12" tiny-skia = "0.11" toml = { version = "0.8", default-features = false, features = ["parse", "display"] } ttf-parser = "0.24.1" -two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] } +two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] } typed-arena = "2" -unicode-bidi = "0.3.13" +unicode-bidi = "0.3.18" unicode-ident = "1.0" unicode-math-class = "0.1" unicode-script = "0.5" +unicode-normalization = "0.1.24" unicode-segmentation = "1" unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } -usvg = { version = "0.44", default-features = false, features = ["text"] } +usvg = { version = "0.43", default-features = false, features = ["text"] } walkdir = "2" -wasmi = "0.39.0" +wasmi = "0.40.0" +web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" -xmp-writer = "0.3" +xmp-writer = "0.3.1" xz2 = { version = "0.1", features = ["static"] } yaml-front-matter = "0.1" zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/README.md b/README.md index 5d5c4798a..41f465152 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,19 @@

Documentation + > Typst App + > Discord Server + > Apache-2 License + > Jobs at Typst + >

Typst is a new markup-based typesetting system that is designed to be as powerful @@ -39,7 +39,7 @@ A [gentle introduction][tutorial] to Typst is available in our documentation. However, if you want to see the power of Typst encapsulated in one image, here it is:

- Example + Example

@@ -113,7 +113,9 @@ Typst's CLI is available from different sources: - You can install Typst through different package managers. Note that the versions in the package managers might lag behind the latest release. - - Linux: View [Typst on Repology][repology] + - Linux: + - View [Typst on Repology][repology] + - View [Typst's Snap][snap] - macOS: `brew install typst` - Windows: `winget install --id Typst.Typst` @@ -254,3 +256,4 @@ instant preview. To achieve these goals, we follow three core design principles: [contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md [packages]: https://github.com/typst/packages/ [`comemo`]: https://github.com/typst/comemo/ +[snap]: https://snapcraft.io/typst diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index f6aaa4b1f..d6855d100 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -241,10 +241,6 @@ pub struct CompileArgs { #[arg(long = "pages", value_delimiter = ',')] pub pages: Option>, - /// The version of the produced PDF. - #[arg(long = "pdf-version")] - pub pdf_version: Option, - /// One (or multiple comma-separated) PDF standards that Typst will enforce /// conformance with. #[arg(long = "pdf-standard", value_delimiter = ',')] @@ -467,45 +463,19 @@ pub enum Feature { display_possible_values!(Feature); -/// A PDF version. -#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] -#[allow(non_camel_case_types)] -pub enum PdfVersion { - /// PDF 1.4. - #[value(name = "1.4")] - V_1_4, - /// PDF 1.5. - #[value(name = "1.5")] - V_1_5, - /// PDF 1.5. - #[value(name = "1.6")] - V_1_6, - /// PDF 1.7. - #[value(name = "1.7")] - V_1_7, -} - -display_possible_values!(PdfVersion); - /// A PDF standard that Typst can enforce conformance with. #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[allow(non_camel_case_types)] pub enum PdfStandard { - /// PDF/A-1b. - #[value(name = "a-1b")] - A_1b, + /// PDF 1.7. + #[value(name = "1.7")] + V_1_7, /// PDF/A-2b. #[value(name = "a-2b")] A_2b, - /// PDF/A-2u. - #[value(name = "a-2u")] - A_2u, - /// PDF/A-3u. + /// PDF/A-3b. #[value(name = "a-3b")] A_3b, - /// PDF/A-2b. - #[value(name = "a-3u")] - A_3u, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index e2ee21c1b..ae71e298c 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -6,8 +6,9 @@ use std::path::{Path, PathBuf}; use chrono::{DateTime, Datelike, Timelike, Utc}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term; -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use parking_lot::RwLock; +use pathdiff::diff_paths; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use typst::diag::{ bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, @@ -17,11 +18,11 @@ use typst::html::HtmlDocument; use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::syntax::{FileId, Source, Span}; use typst::WorldExt; -use typst_pdf::{PdfOptions, Timestamp, Validator}; +use typst_pdf::{PdfOptions, PdfStandards, Timestamp}; use crate::args::{ CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, - PdfStandard, PdfVersion, WatchCommand, + PdfStandard, WatchCommand, }; #[cfg(feature = "http-server")] use crate::server::HtmlServer; @@ -62,10 +63,9 @@ pub struct CompileConfig { /// Opens the output file with the default viewer or a specific program after /// compilation. pub open: Option>, - /// The version that should be used to export the PDF. - pub pdf_version: Option, - /// A list of standards the PDF should conform to. - pub pdf_standard: Vec, + /// One (or multiple comma-separated) PDF standards that Typst will enforce + /// conformance with. + pub pdf_standards: PdfStandards, /// A path to write a Makefile rule describing the current compilation. pub make_deps: Option, /// The PPI (pixels per inch) to use for PNG export. @@ -130,6 +130,19 @@ impl CompileConfig { PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) }); + let pdf_standards = { + let list = args + .pdf_standard + .iter() + .map(|standard| match standard { + PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, + PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, + PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, + }) + .collect::>(); + PdfStandards::new(&list)? + }; + #[cfg(feature = "http-server")] let server = match watch { Some(command) @@ -146,16 +159,15 @@ impl CompileConfig { output, output_format, pages, + pdf_standards, creation_timestamp: args.world.creation_timestamp, make_deps: args.make_deps.clone(), ppi: args.ppi, diagnostic_format: args.process.diagnostic_format, open: args.open.clone(), - pdf_version: args.pdf_version, export_cache: ExportCache::new(), #[cfg(feature = "http-server")] server, - pdf_standard: args.pdf_standard.clone(), }) } } @@ -177,7 +189,7 @@ pub fn compile_once( match output { // Export the PDF / PNG. - Ok(()) => { + Ok(outputs) => { let duration = start.elapsed(); if config.watching { @@ -191,7 +203,7 @@ pub fn compile_once( print_diagnostics(world, &[], &warnings, config.diagnostic_format) .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; - write_make_deps(world, config)?; + write_make_deps(world, config, outputs)?; open_output(config)?; } @@ -215,12 +227,15 @@ pub fn compile_once( fn compile_and_export( world: &mut SystemWorld, config: &mut CompileConfig, -) -> Warned> { +) -> Warned>> { match config.output_format { OutputFormat::Html => { let Warned { output, warnings } = typst::compile::(world); let result = output.and_then(|document| export_html(&document, config)); - Warned { output: result, warnings } + Warned { + output: result.map(|()| vec![config.output.clone()]), + warnings, + } } _ => { let Warned { output, warnings } = typst::compile::(world); @@ -246,9 +261,14 @@ fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult< } /// Export to a paged target format. -fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> { +fn export_paged( + document: &PagedDocument, + config: &CompileConfig, +) -> SourceResult> { match config.output_format { - OutputFormat::Pdf => export_pdf(document, config), + OutputFormat::Pdf => { + export_pdf(document, config).map(|()| vec![config.output.clone()]) + } OutputFormat::Png => { export_image(document, config, ImageExportFormat::Png).at(Span::detached()) } @@ -275,37 +295,11 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult< }) } }; - - let validator = match config.pdf_standard.first() { - None => Validator::None, - Some(s) => { - if config.pdf_standard.len() > 1 { - bail!(Span::detached(), "cannot export using more than one PDF standard"; - hint: "typst currently only supports export using \ - one standard at the same time"); - } else { - match s { - PdfStandard::A_1b => Validator::A1_B, - PdfStandard::A_2b => Validator::A2_B, - PdfStandard::A_2u => Validator::A2_U, - PdfStandard::A_3b => Validator::A3_B, - PdfStandard::A_3u => Validator::A3_U, - } - } - } - }; - let options = PdfOptions { ident: Smart::Auto, timestamp, page_ranges: config.pages.clone(), - pdf_version: config.pdf_version.map(|v| match v { - PdfVersion::V_1_4 => typst_pdf::PdfVersion::Pdf14, - PdfVersion::V_1_5 => typst_pdf::PdfVersion::Pdf15, - PdfVersion::V_1_6 => typst_pdf::PdfVersion::Pdf16, - PdfVersion::V_1_7 => typst_pdf::PdfVersion::Pdf17, - }), - validator, + standards: config.pdf_standards.clone(), }; let buffer = typst_pdf::pdf(document, &options)?; config @@ -342,7 +336,7 @@ fn export_image( document: &PagedDocument, config: &CompileConfig, fmt: ImageExportFormat, -) -> StrResult<()> { +) -> StrResult> { // Determine whether we have indexable templates in output let can_handle_multiple = match config.output { Output::Stdout => false, @@ -356,7 +350,7 @@ fn export_image( .iter() .enumerate() .filter(|(i, _)| { - config.pages.as_ref().map_or(true, |exported_page_ranges| { + config.pages.as_ref().is_none_or(|exported_page_ranges| { exported_page_ranges.includes_page_index(*i) }) }) @@ -398,7 +392,7 @@ fn export_image( && config.export_cache.is_cached(*i, &page.frame) && path.exists() { - return Ok(()); + return Ok(Output::Path(path.to_path_buf())); } Output::Path(path.to_owned()) @@ -407,11 +401,9 @@ fn export_image( }; export_image_page(config, page, &output, fmt)?; - Ok(()) + Ok(output) }) - .collect::, EcoString>>()?; - - Ok(()) + .collect::>>() } mod output_template { @@ -516,14 +508,25 @@ impl ExportCache { /// Writes a Makefile rule describing the relationship between the output and /// its dependencies to the path specified by the --make-deps argument, if it /// was provided. -fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> { +fn write_make_deps( + world: &mut SystemWorld, + config: &CompileConfig, + outputs: Vec, +) -> StrResult<()> { let Some(ref make_deps_path) = config.make_deps else { return Ok(()) }; - let Output::Path(output_path) = &config.output else { - bail!("failed to create make dependencies file because output was stdout") - }; - let Some(output_path) = output_path.as_os_str().to_str() else { + let Ok(output_paths) = outputs + .into_iter() + .filter_map(|o| match o { + Output::Path(path) => Some(path.into_os_string().into_string()), + Output::Stdout => None, + }) + .collect::, _>>() + else { bail!("failed to create make dependencies file because output path was not valid unicode") }; + if output_paths.is_empty() { + bail!("failed to create make dependencies file because output was stdout") + } // Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't // perfect as some special characters can't be escaped. @@ -537,6 +540,10 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult res.push('$'); slashes = 0; } + ':' => { + res.push('\\'); + slashes = 0; + } ' ' | '\t' => { // `munge`'s source contains a comment here that says: "A // space or tab preceded by 2N+1 backslashes represents N @@ -559,18 +566,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult fn write( make_deps_path: &Path, - output_path: &str, + output_paths: Vec, root: PathBuf, dependencies: impl Iterator, ) -> io::Result<()> { let mut file = File::create(make_deps_path)?; + let current_dir = std::env::current_dir()?; + let relative_root = diff_paths(&root, ¤t_dir).unwrap_or(root.clone()); - file.write_all(munge(output_path).as_bytes())?; + for (i, output_path) in output_paths.into_iter().enumerate() { + if i != 0 { + file.write_all(b" ")?; + } + file.write_all(munge(&output_path).as_bytes())?; + } file.write_all(b":")?; for dependency in dependencies { - let Some(dependency) = - dependency.strip_prefix(&root).unwrap_or(&dependency).to_str() - else { + let relative_dependency = match dependency.strip_prefix(&root) { + Ok(root_relative_dependency) => { + relative_root.join(root_relative_dependency) + } + Err(_) => dependency, + }; + let Some(relative_dependency) = relative_dependency.to_str() else { // Silently skip paths that aren't valid unicode so we still // produce a rule that will work for the other paths that can be // processed. @@ -578,14 +596,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult }; file.write_all(b" ")?; - file.write_all(munge(dependency).as_bytes())?; + file.write_all(munge(relative_dependency).as_bytes())?; } file.write_all(b"\n")?; Ok(()) } - write(make_deps_path, output_path, world.root().to_owned(), world.dependencies()) + write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies()) .map_err(|err| { eco_format!("failed to create make dependencies file due to IO error ({err})") }) diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index 91132fc30..0813d8ffd 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> { // Perform initial compilation. timer.record(&mut world, |world| compile_once(world, &mut config))??; - // Watch all dependencies of the initial compilation. - watcher.update(world.dependencies())?; - // Recompile whenever something relevant happens. loop { + // Watch all dependencies of the most recent compilation. + watcher.update(world.dependencies())?; + // Wait until anything relevant happens. watcher.wait()?; @@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> { // Evict the cache. comemo::evict(10); - - // Adjust the file watching. - watcher.update(world.dependencies())?; } } @@ -204,6 +201,10 @@ impl Watcher { let event = event .map_err(|err| eco_format!("failed to watch dependencies ({err})"))?; + if !is_relevant_event_kind(&event.kind) { + continue; + } + // Workaround for notify-rs' implicit unwatch on remove/rename // (triggered by some editors when saving files) with the // inotify backend. By keeping track of the potentially @@ -224,7 +225,17 @@ impl Watcher { } } - relevant |= self.is_event_relevant(&event); + // Don't recompile because the output file changed. + // FIXME: This doesn't work properly for multifile image export. + if event + .paths + .iter() + .all(|path| is_same_file(path, &self.output).unwrap_or(false)) + { + continue; + } + + relevant = true; } // If we found a relevant event or if any of the missing files now @@ -234,32 +245,23 @@ impl Watcher { } } } +} - /// Whether a watch event is relevant for compilation. - fn is_event_relevant(&self, event: ¬ify::Event) -> bool { - // Never recompile because the output file changed. - if event - .paths - .iter() - .all(|path| is_same_file(path, &self.output).unwrap_or(false)) - { - return false; - } - - match &event.kind { - notify::EventKind::Any => true, - notify::EventKind::Access(_) => false, - notify::EventKind::Create(_) => true, - notify::EventKind::Modify(kind) => match kind { - notify::event::ModifyKind::Any => true, - notify::event::ModifyKind::Data(_) => true, - notify::event::ModifyKind::Metadata(_) => false, - notify::event::ModifyKind::Name(_) => true, - notify::event::ModifyKind::Other => false, - }, - notify::EventKind::Remove(_) => true, - notify::EventKind::Other => false, - } +/// Whether a kind of watch event is relevant for compilation. +fn is_relevant_event_kind(kind: ¬ify::EventKind) -> bool { + match kind { + notify::EventKind::Any => true, + notify::EventKind::Access(_) => false, + notify::EventKind::Create(_) => true, + notify::EventKind::Modify(kind) => match kind { + notify::event::ModifyKind::Any => true, + notify::event::ModifyKind::Data(_) => true, + notify::event::ModifyKind::Metadata(_) => false, + notify::event::ModifyKind::Name(_) => true, + notify::event::ModifyKind::Other => false, + }, + notify::EventKind::Remove(_) => true, + notify::EventKind::Other => false, } } diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index af6cf228f..12e80d273 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -305,7 +305,7 @@ impl FileSlot { ) -> FileResult { self.file.get_or_init( || read(self.id, project_root, package_storage), - |data, _| Ok(data.into()), + |data, _| Ok(Bytes::new(data)), ) } } diff --git a/crates/typst-eval/src/access.rs b/crates/typst-eval/src/access.rs index 9bcac4d68..22a6b7f3d 100644 --- a/crates/typst-eval/src/access.rs +++ b/crates/typst-eval/src/access.rs @@ -30,12 +30,14 @@ impl Access for ast::Ident<'_> { fn access<'a>(self, vm: &'a mut Vm) -> SourceResult<&'a mut Value> { let span = self.span(); if vm.inspected == Some(span) { - if let Ok(value) = vm.scopes.get(&self).cloned() { - vm.trace(value); + if let Ok(binding) = vm.scopes.get(&self) { + vm.trace(binding.read().clone()); } } - let value = vm.scopes.get_mut(&self).at(span)?; - Ok(value) + vm.scopes + .get_mut(&self) + .and_then(|b| b.write().map_err(Into::into)) + .at(span) } } diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index fc934cef5..1ca7b4b8f 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -6,13 +6,12 @@ use typst_library::diag::{ }; use typst_library::engine::{Engine, Sink, Traced}; use typst_library::foundations::{ - Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, - NativeElement, Scope, Scopes, Value, + Arg, Args, Binding, Capturer, Closure, Content, Context, Func, NativeElement, Scope, + Scopes, SymbolElem, Value, }; use typst_library::introspection::Introspector; use typst_library::math::LrElem; use typst_library::routines::Routines; -use typst_library::text::TextElem; use typst_library::World; use typst_syntax::ast::{self, AstNode, Ident}; use typst_syntax::{Span, Spanned, SyntaxNode}; @@ -197,7 +196,7 @@ pub fn eval_closure( // Provide the closure itself for recursive calls. if let Some(name) = name { - vm.define(name, Value::Func(func.clone())); + vm.define(name, func.clone()); } let num_pos_args = args.to_pos().len(); @@ -316,22 +315,25 @@ fn eval_field_call( (target, args) }; - if let Value::Plugin(plugin) = &target { - // Call plugins by converting args to bytes. - let bytes = args.all::()?; - args.finish()?; - let value = plugin.call(&field, bytes).at(span)?.into_value(); - Ok(FieldCall::Resolved(value)) - } else if let Some(callee) = target.ty().scope().get(&field) { + let field_span = field.span(); + let sink = (&mut vm.engine, field_span); + if let Some(callee) = target.ty().scope().get(&field) { args.insert(0, target_expr.span(), target); - Ok(FieldCall::Normal(callee.clone(), args)) + Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args)) + } else if let Value::Content(content) = &target { + if let Some(callee) = content.elem().scope().get(&field) { + args.insert(0, target_expr.span(), target); + Ok(FieldCall::Normal(callee.read_checked(sink).clone(), args)) + } else { + bail!(missing_field_call_error(target, field)) + } } else if matches!( target, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) ) { // Certain value types may have their own ways to access method fields. // e.g. `$arrow.r(v)$`, `table.cell[..]` - let value = target.field(&field).at(field.span())?; + let value = target.field(&field, sink).at(field_span)?; Ok(FieldCall::Normal(value, args)) } else { // Otherwise we cannot call this field. @@ -341,8 +343,20 @@ fn eval_field_call( /// Produce an error when we cannot call the field. fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { - let mut error = - error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str()); + let mut error = match &target { + Value::Content(content) => error!( + field.span(), + "element {} has no method `{}`", + content.elem().name(), + field.as_str(), + ), + _ => error!( + field.span(), + "type {} has no method `{}`", + target.ty(), + field.as_str() + ), + }; match target { Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => { @@ -352,7 +366,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { field.as_str(), )); } - _ if target.field(&field).is_ok() => { + _ if target.field(&field, ()).is_ok() => { error.hint(eco_format!( "did you mean to access the field `{}`?", field.as_str(), @@ -360,6 +374,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { } _ => {} } + error } @@ -382,16 +397,16 @@ fn wrap_args_in_math( let mut body = Content::empty(); for (i, arg) in args.all::()?.into_iter().enumerate() { if i > 0 { - body += TextElem::packed(','); + body += SymbolElem::packed(','); } body += arg; } if trailing_comma { - body += TextElem::packed(','); + body += SymbolElem::packed(','); } Ok(Value::Content( callee.display().spanned(callee_span) - + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) + + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) .pack() .spanned(args.span), )) @@ -445,15 +460,13 @@ impl<'a> CapturesVisitor<'a> { // Identifiers that shouldn't count as captures because they // actually bind a new name are handled below (individually through // the expressions that contain them). - Some(ast::Expr::Ident(ident)) => { - self.capture(ident.get(), ident.span(), Scopes::get) - } + Some(ast::Expr::Ident(ident)) => self.capture(ident.get(), Scopes::get), Some(ast::Expr::MathIdent(ident)) => { - self.capture(ident.get(), ident.span(), Scopes::get_in_math) + self.capture(ident.get(), Scopes::get_in_math) } // Code and content blocks create a scope. - Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { + Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => { self.internal.enter(); for child in node.children() { self.visit(child); @@ -503,7 +516,7 @@ impl<'a> CapturesVisitor<'a> { // A let expression contains a binding, but that binding is only // active after the body is evaluated. - Some(ast::Expr::Let(expr)) => { + Some(ast::Expr::LetBinding(expr)) => { if let Some(init) = expr.init() { self.visit(init.to_untyped()); } @@ -516,7 +529,7 @@ impl<'a> CapturesVisitor<'a> { // A for loop contains one or two bindings in its pattern. These are // active after the iterable is evaluated but before the body is // evaluated. - Some(ast::Expr::For(expr)) => { + Some(ast::Expr::ForLoop(expr)) => { self.visit(expr.iterable().to_untyped()); self.internal.enter(); @@ -531,7 +544,7 @@ impl<'a> CapturesVisitor<'a> { // An import contains items, but these are active only after the // path is evaluated. - Some(ast::Expr::Import(expr)) => { + Some(ast::Expr::ModuleImport(expr)) => { self.visit(expr.source().to_untyped()); if let Some(ast::Imports::Items(items)) = expr.imports() { for item in items.iter() { @@ -557,32 +570,34 @@ impl<'a> CapturesVisitor<'a> { /// Bind a new internal variable. fn bind(&mut self, ident: ast::Ident) { - self.internal.top.define_ident(ident, Value::None); + // The concrete value does not matter as we only use the scoping + // mechanism of `Scopes`, not the values themselves. + self.internal + .top + .bind(ident.get().clone(), Binding::detached(Value::None)); } /// Capture a variable if it isn't internal. fn capture( &mut self, ident: &EcoString, - span: Span, - getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>, + getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Binding>, ) { - if self.internal.get(ident).is_err() { - let Some(value) = self - .external - .map(|external| getter(external, ident).ok()) - .unwrap_or(Some(&Value::None)) - else { - return; - }; - - self.captures.define_captured( - ident.clone(), - value.clone(), - self.capturer, - span, - ); + if self.internal.get(ident).is_ok() { + return; } + + let binding = match self.external { + Some(external) => match getter(external, ident) { + Ok(binding) => binding.capture(self.capturer), + Err(_) => return, + }, + // The external scopes are only `None` when we are doing IDE capture + // analysis, in which case the concrete value doesn't matter. + None => Binding::detached(Value::None), + }; + + self.captures.bind(ident.clone(), binding); } } @@ -685,8 +700,7 @@ mod tests { // Named-params. test(s, "$ foo(bar: y) $", &["foo"]); - // This should be updated when we improve named-param parsing: - test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]); + test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]); // Field access in math. test(s, "$ foo.bar $", &["foo"]); diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index 34373fd4a..9078418e4 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -30,7 +30,7 @@ fn eval_code<'a>( while let Some(expr) = exprs.next() { let span = expr.span(); let value = match expr { - ast::Expr::Set(set) => { + ast::Expr::SetRule(set) => { let styles = set.eval(vm)?; if vm.flow.is_some() { break; @@ -39,7 +39,7 @@ fn eval_code<'a>( let tail = eval_code(vm, exprs)?.display(); Value::Content(tail.styled_with_map(styles)) } - ast::Expr::Show(show) => { + ast::Expr::ShowRule(show) => { let recipe = show.eval(vm)?; if vm.flow.is_some() { break; @@ -94,11 +94,12 @@ impl Eval for ast::Expr<'_> { Self::Label(v) => v.eval(vm), Self::Ref(v) => v.eval(vm).map(Value::Content), Self::Heading(v) => v.eval(vm).map(Value::Content), - Self::List(v) => v.eval(vm).map(Value::Content), - Self::Enum(v) => v.eval(vm).map(Value::Content), - Self::Term(v) => v.eval(vm).map(Value::Content), + Self::ListItem(v) => v.eval(vm).map(Value::Content), + Self::EnumItem(v) => v.eval(vm).map(Value::Content), + Self::TermItem(v) => v.eval(vm).map(Value::Content), Self::Equation(v) => v.eval(vm).map(Value::Content), Self::Math(v) => v.eval(vm).map(Value::Content), + Self::MathText(v) => v.eval(vm).map(Value::Content), Self::MathIdent(v) => v.eval(vm), Self::MathShorthand(v) => v.eval(vm), Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), @@ -115,8 +116,8 @@ impl Eval for ast::Expr<'_> { Self::Float(v) => v.eval(vm), Self::Numeric(v) => v.eval(vm), Self::Str(v) => v.eval(vm), - Self::Code(v) => v.eval(vm), - Self::Content(v) => v.eval(vm).map(Value::Content), + Self::CodeBlock(v) => v.eval(vm), + Self::ContentBlock(v) => v.eval(vm).map(Value::Content), Self::Array(v) => v.eval(vm).map(Value::Array), Self::Dict(v) => v.eval(vm).map(Value::Dict), Self::Parenthesized(v) => v.eval(vm), @@ -125,19 +126,19 @@ impl Eval for ast::Expr<'_> { Self::Closure(v) => v.eval(vm), Self::Unary(v) => v.eval(vm), Self::Binary(v) => v.eval(vm), - Self::Let(v) => v.eval(vm), - Self::DestructAssign(v) => v.eval(vm), - Self::Set(_) => bail!(forbidden("set")), - Self::Show(_) => bail!(forbidden("show")), + Self::LetBinding(v) => v.eval(vm), + Self::DestructAssignment(v) => v.eval(vm), + Self::SetRule(_) => bail!(forbidden("set")), + Self::ShowRule(_) => bail!(forbidden("show")), Self::Contextual(v) => v.eval(vm).map(Value::Content), Self::Conditional(v) => v.eval(vm), - Self::While(v) => v.eval(vm), - Self::For(v) => v.eval(vm), - Self::Import(v) => v.eval(vm), - Self::Include(v) => v.eval(vm).map(Value::Content), - Self::Break(v) => v.eval(vm), - Self::Continue(v) => v.eval(vm), - Self::Return(v) => v.eval(vm), + Self::WhileLoop(v) => v.eval(vm), + Self::ForLoop(v) => v.eval(vm), + Self::ModuleImport(v) => v.eval(vm), + Self::ModuleInclude(v) => v.eval(vm).map(Value::Content), + Self::LoopBreak(v) => v.eval(vm), + Self::LoopContinue(v) => v.eval(vm), + Self::FuncReturn(v) => v.eval(vm), }? .spanned(span); @@ -153,7 +154,13 @@ impl Eval for ast::Ident<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - vm.scopes.get(&self).cloned().at(self.span()) + let span = self.span(); + Ok(vm + .scopes + .get(&self) + .at(span)? + .read_checked((&mut vm.engine, span)) + .clone()) } } @@ -309,8 +316,9 @@ impl Eval for ast::FieldAccess<'_> { fn eval(self, vm: &mut Vm) -> SourceResult { let value = self.target().eval(vm)?; let field = self.field(); + let field_span = field.span(); - let err = match value.field(&field).at(field.span()) { + let err = match value.field(&field, (&mut vm.engine, field_span)).at(field_span) { Ok(value) => return Ok(value), Err(err) => err, }; diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 5b67c0608..1b1641487 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -4,9 +4,9 @@ use typst_library::diag::{ bail, error, warning, At, FileError, SourceResult, Trace, Tracepoint, }; use typst_library::engine::Engine; -use typst_library::foundations::{Content, Module, Value}; +use typst_library::foundations::{Binding, Content, Module, Value}; use typst_library::World; -use typst_syntax::ast::{self, AstNode}; +use typst_syntax::ast::{self, AstNode, BareImportError}; use typst_syntax::package::{PackageManifest, PackageSpec}; use typst_syntax::{FileId, Span, VirtualPath}; @@ -16,11 +16,11 @@ impl Eval for ast::ModuleImport<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - let source = self.source(); - let source_span = source.span(); - let mut source = source.eval(vm)?; - let new_name = self.new_name(); - let imports = self.imports(); + let source_expr = self.source(); + let source_span = source_expr.span(); + + let mut source = source_expr.eval(vm)?; + let mut is_str = false; match &source { Value::Func(func) => { @@ -32,6 +32,7 @@ impl Eval for ast::ModuleImport<'_> { Value::Module(_) => {} Value::Str(path) => { source = Value::Module(import(&mut vm.engine, path, source_span)?); + is_str = true; } v => { bail!( @@ -42,6 +43,8 @@ impl Eval for ast::ModuleImport<'_> { } } + // If there is a rename, import the source itself under that name. + let new_name = self.new_name(); if let Some(new_name) = new_name { if let ast::Expr::Ident(ident) = self.source() { if ident.as_str() == new_name.as_str() { @@ -54,21 +57,42 @@ impl Eval for ast::ModuleImport<'_> { } // Define renamed module on the scope. - vm.scopes.top.define_ident(new_name, source.clone()); + vm.define(new_name, source.clone()); } let scope = source.scope().unwrap(); - match imports { + match self.imports() { None => { - // Only import here if there is no rename. if new_name.is_none() { - let name: EcoString = source.name().unwrap().into(); - vm.scopes.top.define(name, source); + match self.bare_name() { + // Bare dynamic string imports are not allowed. + Ok(name) + if !is_str || matches!(source_expr, ast::Expr::Str(_)) => + { + if matches!(source_expr, ast::Expr::Ident(_)) { + vm.engine.sink.warn(warning!( + source_expr.span(), + "this import has no effect", + )); + } + vm.scopes.top.bind(name, Binding::new(source, source_span)); + } + Ok(_) | Err(BareImportError::Dynamic) => bail!( + source_span, "dynamic import requires an explicit name"; + hint: "you can name the import with `as`" + ), + Err(BareImportError::PathInvalid) => bail!( + source_span, "module name would not be a valid identifier"; + hint: "you can rename the import with `as`", + ), + // Bad package spec would have failed the import already. + Err(BareImportError::PackageInvalid) => unreachable!(), + } } } Some(ast::Imports::Wildcard) => { - for (var, value, span) in scope.iter() { - vm.scopes.top.define_spanned(var.clone(), value.clone(), span); + for (var, binding) in scope.iter() { + vm.scopes.top.bind(var.clone(), binding.clone()); } } Some(ast::Imports::Items(items)) => { @@ -78,7 +102,7 @@ impl Eval for ast::ModuleImport<'_> { let mut scope = scope; while let Some(component) = &path.next() { - let Some(value) = scope.get(component) else { + let Some(binding) = scope.get(component) else { errors.push(error!(component.span(), "unresolved import")); break; }; @@ -86,6 +110,7 @@ impl Eval for ast::ModuleImport<'_> { if path.peek().is_some() { // Nested import, as this is not the last component. // This must be a submodule. + let value = binding.read(); let Some(submodule) = value.scope() else { let error = if matches!(value, Value::Func(function) if function.scope().is_none()) { @@ -128,7 +153,7 @@ impl Eval for ast::ModuleImport<'_> { } } - vm.define(item.bound_name(), value.clone()); + vm.bind(item.bound_name(), binding.clone()); } } } @@ -211,7 +236,7 @@ fn resolve_package( // Evaluate the manifest. let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); let bytes = engine.world.file(manifest_id).at(span)?; - let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; + let string = bytes.as_str().map_err(FileError::from).at(span)?; let manifest: PackageManifest = toml::from_str(string) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) .at(span)?; diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs index 3a5ebe1fc..5beefa912 100644 --- a/crates/typst-eval/src/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -33,7 +33,7 @@ fn eval_markup<'a>( while let Some(expr) = exprs.next() { match expr { - ast::Expr::Set(set) => { + ast::Expr::SetRule(set) => { let styles = set.eval(vm)?; if vm.flow.is_some() { break; @@ -41,7 +41,7 @@ fn eval_markup<'a>( seq.push(eval_markup(vm, exprs)?.styled_with_map(styles)) } - ast::Expr::Show(show) => { + ast::Expr::ShowRule(show) => { let recipe = show.eval(vm)?; if vm.flow.is_some() { break; diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index 51dc0a3d5..0e271a089 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -1,11 +1,11 @@ use ecow::eco_format; use typst_library::diag::{At, SourceResult}; -use typst_library::foundations::{Content, NativeElement, Symbol, Value}; +use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value}; use typst_library::math::{ AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, }; use typst_library::text::TextElem; -use typst_syntax::ast::{self, AstNode}; +use typst_syntax::ast::{self, AstNode, MathTextKind}; use crate::{Eval, Vm}; @@ -20,11 +20,28 @@ impl Eval for ast::Math<'_> { } } +impl Eval for ast::MathText<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + match self.get() { + MathTextKind::Character(c) => Ok(SymbolElem::packed(c)), + MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())), + } + } +} + impl Eval for ast::MathIdent<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - vm.scopes.get_in_math(&self).cloned().at(self.span()) + let span = self.span(); + Ok(vm + .scopes + .get_in_math(&self) + .at(span)? + .read_checked((&mut vm.engine, span)) + .clone()) } } @@ -102,6 +119,7 @@ impl Eval for ast::MathRoot<'_> { type Output = Content; fn eval(self, vm: &mut Vm) -> SourceResult { + // Use `TextElem` to match `MathTextKind::Number` above. let index = self.index().map(|i| TextElem::packed(eco_format!("{i}"))); let radicand = self.radicand().eval_display(vm)?; Ok(RootElem::new(radicand).with_index(index).pack()) diff --git a/crates/typst-eval/src/rules.rs b/crates/typst-eval/src/rules.rs index 646354d4b..f4c1563f3 100644 --- a/crates/typst-eval/src/rules.rs +++ b/crates/typst-eval/src/rules.rs @@ -45,7 +45,7 @@ impl Eval for ast::ShowRule<'_> { let transform = self.transform(); let transform = match transform { - ast::Expr::Set(set) => Transformation::Style(set.eval(vm)?), + ast::Expr::SetRule(set) => Transformation::Style(set.eval(vm)?), expr => expr.eval(vm)?.cast::().at(transform.span())?, }; diff --git a/crates/typst-eval/src/vm.rs b/crates/typst-eval/src/vm.rs index a5cbb6fa0..52cfb4b5b 100644 --- a/crates/typst-eval/src/vm.rs +++ b/crates/typst-eval/src/vm.rs @@ -1,7 +1,7 @@ use comemo::Tracked; use typst_library::diag::warning; use typst_library::engine::Engine; -use typst_library::foundations::{Context, IntoValue, Scopes, Value}; +use typst_library::foundations::{Binding, Context, IntoValue, Scopes, Value}; use typst_library::World; use typst_syntax::ast::{self, AstNode}; use typst_syntax::Span; @@ -42,13 +42,23 @@ impl<'a> Vm<'a> { self.engine.world } - /// Define a variable in the current scope. + /// Bind a value to an identifier. + /// + /// This will create a [`Binding`] with the value and the identifier's span. pub fn define(&mut self, var: ast::Ident, value: impl IntoValue) { - let value = value.into_value(); + self.bind(var, Binding::new(value, var.span())); + } + + /// Insert a binding into the current scope. + /// + /// This will insert the value into the top-most scope and make it available + /// for dynamic tracing, assisting IDE functionality. + pub fn bind(&mut self, var: ast::Ident, binding: Binding) { if self.inspected == Some(var.span()) { - self.trace(value.clone()); + self.trace(binding.read().clone()); } - // This will become an error in the parser if 'is' becomes a keyword. + + // This will become an error in the parser if `is` becomes a keyword. if var.get() == "is" { self.engine.sink.warn(warning!( var.span(), @@ -58,7 +68,8 @@ impl<'a> Vm<'a> { hint: "try `is_` instead" )); } - self.scopes.top.define_ident(var, value); + + self.scopes.top.bind(var.get().clone(), binding); } /// Trace a value. diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index b87b0e1d6..612f923fc 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::foundations::Repr; -use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode}; +use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag}; use typst_library::layout::Frame; use typst_syntax::Span; @@ -12,15 +12,19 @@ pub fn html(document: &HtmlDocument) -> SourceResult { w.buf.push_str(""); write_indent(&mut w); write_element(&mut w, &document.root)?; + if w.pretty { + w.buf.push('\n'); + } Ok(w.buf) } #[derive(Default)] struct Writer { + /// The output buffer. buf: String, - /// current indentation level + /// The current indentation level level: usize, - /// pretty printing enabled? + /// Whether pretty printing is enabled. pretty: bool, } @@ -85,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { let pretty = w.pretty; if !element.children.is_empty() { - w.pretty &= is_pretty(element); + let pretty_inside = allows_pretty_inside(element.tag) + && element.children.iter().any(|node| match node { + HtmlNode::Element(child) => wants_pretty_around(child.tag), + _ => false, + }); + + w.pretty &= pretty_inside; let mut indent = w.pretty; w.level += 1; for c in &element.children { - let pretty_child = match c { + let pretty_around = match c { HtmlNode::Tag(_) => continue, - HtmlNode::Element(element) => is_pretty(element), + HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag), HtmlNode::Text(..) | HtmlNode::Frame(_) => false, }; - if core::mem::take(&mut indent) || pretty_child { + if core::mem::take(&mut indent) || pretty_around { write_indent(w); } write_node(w, c)?; - indent = pretty_child; + indent = pretty_around; } w.level -= 1; - write_indent(w) + write_indent(w); } w.pretty = pretty; @@ -115,9 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { Ok(()) } -/// Whether the element should be pretty-printed. -fn is_pretty(element: &HtmlElement) -> bool { - tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta) +/// Whether we are allowed to add an extra newline at the start and end of the +/// element's contents. +/// +/// Technically, users can change CSS `display` properties such that the +/// insertion of whitespace may actually impact the visual output. For example, +/// shows how adding CSS +/// rules to `

` can make it sensitive to whitespace. For this reason, we +/// should also respect the `style` tag in the future. +fn allows_pretty_inside(tag: HtmlTag) -> bool { + (tag::is_block_by_default(tag) && tag != tag::pre) + || tag::is_tabular_by_default(tag) + || tag == tag::li +} + +/// Whether newlines should be added before and after the element if the parent +/// allows it. +/// +/// In contrast to `allows_pretty_inside`, which is purely spec-driven, this is +/// more subjective and depends on preference. +fn wants_pretty_around(tag: HtmlTag) -> bool { + allows_pretty_inside(tag) || tag::is_metadata(tag) || tag == tag::pre } /// Escape a character. diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index ffd8e2505..aa769976e 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -14,9 +14,9 @@ use typst_library::html::{ use typst_library::introspection::{ Introspector, Locator, LocatorLink, SplitLocator, TagElem, }; -use typst_library::layout::{Abs, Axes, BoxElem, Region, Size}; +use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; use typst_library::model::{DocumentInfo, ParElem}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_library::World; use typst_syntax::Span; @@ -83,8 +83,8 @@ fn html_document_impl( )?; let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; + let introspector = Introspector::html(&output); let root = root_element(output, &info)?; - let introspector = Introspector::html(&root); Ok(HtmlDocument { info, root, introspector }) } @@ -139,7 +139,9 @@ fn html_fragment_impl( let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::HtmlFragment, + // No need to know about the `FragmentKind` because we handle both + // uniformly. + RealizationKind::HtmlFragment(&mut FragmentKind::Block), &mut engine, &mut locator, &arenas, @@ -189,7 +191,8 @@ fn handle( }; output.push(element.into()); } else if let Some(elem) = child.to_packed::() { - let children = handle_list(engine, locator, elem.children.iter(&styles))?; + let children = + html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; output.push( HtmlElement::new(tag::p) .with_children(children) @@ -197,13 +200,34 @@ fn handle( .into(), ); } else if let Some(elem) = child.to_packed::() { - // FIXME: Very incomplete and hacky, but makes boxes kind fulfill their - // purpose for now. + // TODO: This is rather incomplete. if let Some(body) = elem.body(styles) { let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; - output.extend(children); + output.push( + HtmlElement::new(tag::span) + .with_attr(attr::style, "display: inline-block;") + .with_children(children) + .spanned(elem.span()) + .into(), + ) } + } else if let Some((elem, body)) = + child + .to_packed::() + .and_then(|elem| match elem.body(styles) { + Some(BlockBody::Content(body)) => Some((elem, body)), + _ => None, + }) + { + // TODO: This is rather incomplete. + let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::div) + .with_children(children) + .spanned(elem.span()) + .into(), + ); } else if child.is::() { output.push(HtmlNode::text(' ', child.span())); } else if let Some(elem) = child.to_packed::() { @@ -283,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement { /// Determine which kind of output the user generated. fn classify_output(mut output: Vec) -> SourceResult { - let len = output.len(); + let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); for node in &mut output { let HtmlNode::Element(elem) = node else { continue }; let tag = elem.tag; let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); - match (tag, len) { + match (tag, count) { (tag::html, 1) => return Ok(OutputKind::Html(take())), (tag::body, 1) => return Ok(OutputKind::Body(take())), (tag::html | tag::body, _) => bail!( elem.span, "`{}` element must be the only element in the document", - elem.tag + elem.tag, ), _ => {} } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 5c2b500a0..91fa53f9a 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { } // Behind existing atom or identifier: "$a|$" or "$abc|$". - if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { + if matches!( + ctx.leaf.kind(), + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent + ) { ctx.from = ctx.leaf.offset(); math_completions(ctx); return true; @@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { // Behind an expression plus dot: "emoji.|". if_chain! { if ctx.leaf.kind() == SyntaxKind::Dot - || (ctx.leaf.kind() == SyntaxKind::Text + || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText) && ctx.leaf.text() == "."); if ctx.leaf.range().end == ctx.cursor; if let Some(prev) = ctx.leaf.prev_sibling(); @@ -398,13 +401,31 @@ fn field_access_completions( value: &Value, styles: &Option, ) { - for (name, value, _) in value.ty().scope().iter() { - ctx.call_completion(name.clone(), value); + let scopes = { + let ty = value.ty().scope(); + let elem = match value { + Value::Content(content) => Some(content.elem().scope()), + _ => None, + }; + elem.into_iter().chain(Some(ty)) + }; + + // Autocomplete methods from the element's or type's scope. We only complete + // those which have a `self` parameter. + for (name, binding) in scopes.flat_map(|scope| scope.iter()) { + let Ok(func) = binding.read().clone().cast::() else { continue }; + if func + .params() + .and_then(|params| params.first()) + .is_some_and(|param| param.name == "self") + { + ctx.call_completion(name.clone(), binding.read()); + } } if let Some(scope) = value.scope() { - for (name, value, _) in scope.iter() { - ctx.call_completion(name.clone(), value); + for (name, binding) in scope.iter() { + ctx.call_completion(name.clone(), binding.read()); } } @@ -414,7 +435,7 @@ fn field_access_completions( // with method syntax; // 2. We can unwrap the field's value since it's a field belonging to // this value's type, so accessing it should not fail. - ctx.value_completion(field, &value.field(field).unwrap()); + ctx.value_completion(field, &value.field(field, ()).unwrap()); } match value { @@ -452,16 +473,6 @@ fn field_access_completions( } } } - Value::Plugin(plugin) => { - for name in plugin.iter() { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: name.clone(), - apply: None, - detail: None, - }) - } - } _ => {} } } @@ -506,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { // "#import "path.typ": a, b, |". if_chain! { if let Some(prev) = ctx.leaf.prev_sibling(); - if let Some(ast::Expr::Import(import)) = prev.get().cast(); + if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast(); if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(source) = prev.children().find(|child| child.is::()); then { @@ -521,11 +532,13 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { if_chain! { if ctx.leaf.kind() == SyntaxKind::Ident; if let Some(parent) = ctx.leaf.parent(); - if parent.kind() == SyntaxKind::ImportItems; + if parent.kind() == SyntaxKind::ImportItemPath; if let Some(grand) = parent.parent(); - if let Some(ast::Expr::Import(import)) = grand.get().cast(); + if grand.kind() == SyntaxKind::ImportItems; + if let Some(great) = grand.parent(); + if let Some(ast::Expr::ModuleImport(import)) = great.get().cast(); if let Some(ast::Imports::Items(items)) = import.imports(); - if let Some(source) = grand.children().find(|child| child.is::()); + if let Some(source) = great.children().find(|child| child.is::()); then { ctx.from = ctx.leaf.offset(); import_item_completions(ctx, items, &source); @@ -549,9 +562,9 @@ fn import_item_completions<'a>( ctx.snippet_completion("*", "*", "Import everything."); } - for (name, value, _) in scope.iter() { + for (name, binding) in scope.iter() { if existing.iter().all(|item| item.original_name().as_str() != name) { - ctx.value_completion(name.clone(), value); + ctx.value_completion(name.clone(), binding.read()); } } } @@ -664,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool { if let Some(args) = parent.get().cast::(); if let Some(grand) = parent.parent(); if let Some(expr) = grand.get().cast::(); - let set = matches!(expr, ast::Expr::Set(_)); + let set = matches!(expr, ast::Expr::SetRule(_)); if let Some(callee) = match expr { ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), + ast::Expr::SetRule(set) => Some(set.target()), _ => None, }; then { @@ -815,19 +828,8 @@ fn param_value_completions<'a>( ) { if param.name == "font" { ctx.font_completions(); - } else if param.name == "path" { - ctx.file_completions_with_extensions(match func.name() { - Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], - Some("csv") => &["csv"], - Some("plugin") => &["wasm"], - Some("cbor") => &["cbor"], - Some("json") => &["json"], - Some("toml") => &["toml"], - Some("xml") => &["xml"], - Some("yaml") => &["yml", "yaml"], - Some("bibliography") => &["bib", "yml", "yaml"], - _ => &[], - }); + } else if let Some(extensions) = path_completion(func, param) { + ctx.file_completions_with_extensions(extensions); } else if func.name() == Some("figure") && param.name == "body" { ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure."); ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure."); @@ -836,6 +838,28 @@ fn param_value_completions<'a>( ctx.cast_completions(¶m.input); } +/// Returns which file extensions to complete for the given parameter if any. +fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> { + Some(match (func.name(), param.name) { + (Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], + (Some("csv"), "source") => &["csv"], + (Some("plugin"), "source") => &["wasm"], + (Some("cbor"), "source") => &["cbor"], + (Some("json"), "source") => &["json"], + (Some("toml"), "source") => &["toml"], + (Some("xml"), "source") => &["xml"], + (Some("yaml"), "source") => &["yml", "yaml"], + (Some("bibliography"), "sources") => &["bib", "yml", "yaml"], + (Some("bibliography"), "style") => &["csl"], + (Some("cite"), "style") => &["csl"], + (Some("raw"), "syntaxes") => &["sublime-syntax"], + (Some("raw"), "theme") => &["tmtheme"], + (Some("embed"), "path") => &[], + (None, "path") => &[], + _ => return None, + }) +} + /// Resolve a callee expression to a global function. fn resolve_global_callee<'a>( ctx: &CompletionContext<'a>, @@ -843,13 +867,11 @@ fn resolve_global_callee<'a>( ) -> Option<&'a Func> { let globals = globals(ctx.world, ctx.leaf); let value = match callee { - ast::Expr::Ident(ident) => globals.get(&ident)?, + ast::Expr::Ident(ident) => globals.get(&ident)?.read(), ast::Expr::FieldAccess(access) => match access.target() { - ast::Expr::Ident(target) => match globals.get(&target)? { - Value::Module(module) => module.field(&access.field()).ok()?, - Value::Func(func) => func.field(&access.field()).ok()?, - _ => return None, - }, + ast::Expr::Ident(target) => { + globals.get(&target)?.read().scope()?.get(&access.field())?.read() + } _ => return None, }, _ => return None, @@ -1441,7 +1463,7 @@ impl<'a> CompletionContext<'a> { let mut defined = BTreeMap::>::new(); named_items(self.world, self.leaf.clone(), |item| { let name = item.name(); - if !name.is_empty() && item.value().as_ref().map_or(true, filter) { + if !name.is_empty() && item.value().as_ref().is_none_or(filter) { defined.insert(name.clone(), item.value()); } @@ -1461,7 +1483,8 @@ impl<'a> CompletionContext<'a> { } } - for (name, value, _) in globals(self.world, self.leaf).iter() { + for (name, binding) in globals(self.world, self.leaf).iter() { + let value = binding.read(); if filter(value) && !defined.contains_key(name) { self.value_completion_full(Some(name.clone()), value, parens, None, None); } @@ -1504,14 +1527,13 @@ impl BracketMode { #[cfg(test)] mod tests { + use std::borrow::Borrow; use std::collections::BTreeSet; use typst::layout::PagedDocument; - use typst::syntax::{FileId, Source, VirtualPath}; - use typst::World; use super::{autocomplete, Completion}; - use crate::tests::{SourceExt, TestWorld}; + use crate::tests::{FilePos, TestWorld, WorldLike}; /// Quote a string. macro_rules! q { @@ -1583,60 +1605,50 @@ mod tests { } #[track_caller] - fn test(text: &str, cursor: isize) -> Response { - let world = TestWorld::new(text); - test_with_world(&world, cursor) + fn test(world: impl WorldLike, pos: impl FilePos) -> Response { + let world = world.acquire(); + let world = world.borrow(); + let doc = typst::compile(world).output.ok(); + test_with_doc(world, pos, doc.as_ref()) } #[track_caller] - fn test_with_world(world: &TestWorld, cursor: isize) -> Response { - let doc = typst::compile(&world).output.ok(); - test_full(world, &world.main, doc.as_ref(), cursor) - } - - #[track_caller] - fn test_with_path(world: &TestWorld, path: &str, cursor: isize) -> Response { - let doc = typst::compile(&world).output.ok(); - let id = FileId::new(None, VirtualPath::new(path)); - let source = world.source(id).unwrap(); - test_full(world, &source, doc.as_ref(), cursor) - } - - #[track_caller] - fn test_full( - world: &TestWorld, - source: &Source, + fn test_with_doc( + world: impl WorldLike, + pos: impl FilePos, doc: Option<&PagedDocument>, - cursor: isize, ) -> Response { - autocomplete(world, doc, source, source.cursor(cursor), true) + let world = world.acquire(); + let world = world.borrow(); + let (source, cursor) = pos.resolve(world); + autocomplete(world, doc, &source, cursor, true) } #[test] fn test_autocomplete_hash_expr() { - test("#i", 2).must_include(["int", "if conditional"]); + test("#i", -1).must_include(["int", "if conditional"]); } #[test] fn test_autocomplete_array_method() { - test("#().", 4).must_include(["insert", "remove", "len", "all"]); - test("#{ let x = (1, 2, 3); x. }", -2).must_include(["at", "push", "pop"]); + test("#().", -1).must_include(["insert", "remove", "len", "all"]); + test("#{ let x = (1, 2, 3); x. }", -3).must_include(["at", "push", "pop"]); } /// Test that extra space before '.' is handled correctly. #[test] fn test_autocomplete_whitespace() { - test("#() .", 5).must_exclude(["insert", "remove", "len", "all"]); - test("#{() .}", 6).must_include(["insert", "remove", "len", "all"]); - test("#() .a", 6).must_exclude(["insert", "remove", "len", "all"]); - test("#{() .a}", 7).must_include(["at", "any", "all"]); + test("#() .", -1).must_exclude(["insert", "remove", "len", "all"]); + test("#{() .}", -2).must_include(["insert", "remove", "len", "all"]); + test("#() .a", -1).must_exclude(["insert", "remove", "len", "all"]); + test("#{() .a}", -2).must_include(["at", "any", "all"]); } /// Test that the `before_window` doesn't slice into invalid byte /// boundaries. #[test] fn test_autocomplete_before_window_char_boundary() { - test("😀😀 #text(font: \"\")", -2); + test("😀😀 #text(font: \"\")", -3); } /// Ensure that autocompletion for `#cite(|)` completes bibligraphy labels, @@ -1653,7 +1665,7 @@ mod tests { let end = world.main.len_bytes(); world.main.edit(end..end, " #cite()"); - test_full(&world, &world.main, doc.as_ref(), -1) + test_with_doc(&world, -2, doc.as_ref()) .must_include(["netwok", "glacier-melt", "supplement"]) .must_exclude(["bib"]); } @@ -1677,13 +1689,13 @@ mod tests { #[test] fn test_autocomplete_positional_param() { // No string given yet. - test("#numbering()", -1).must_include(["string", "integer"]); + test("#numbering()", -2).must_include(["string", "integer"]); // String is already given. - test("#numbering(\"foo\", )", -1) + test("#numbering(\"foo\", )", -2) .must_include(["integer"]) .must_exclude(["string"]); // Integer is already given, but numbering is variadic. - test("#numbering(\"foo\", 1, )", -1) + test("#numbering(\"foo\", 1, )", -2) .must_include(["integer"]) .must_exclude(["string"]); } @@ -1698,14 +1710,14 @@ mod tests { "#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)", ); - test_with_world(&world, -1) + test(&world, -2) .must_include(["clrs", "aqua"]) .must_exclude(["nums", "a", "b"]); } #[test] fn test_autocomplete_packages() { - test("#import \"@\"", -1).must_include([q!("@preview/example:0.1.0")]); + test("#import \"@\"", -2).must_include([q!("@preview/example:0.1.0")]); } #[test] @@ -1719,28 +1731,63 @@ mod tests { .with_asset_at("assets/rhino.png", "rhino.png") .with_asset_at("data/example.csv", "example.csv"); - test_with_path(&world, "main.typ", -1) + test(&world, -2) .must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")]) .must_exclude([q!("assets/tiger.jpg")]); - test_with_path(&world, "content/c.typ", -1) + test(&world, ("content/c.typ", -2)) .must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")]) .must_exclude([q!("c.typ")]); - test_with_path(&world, "content/a.typ", -1) + test(&world, ("content/a.typ", -2)) .must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")]) .must_exclude([q!("../data/example.csv"), q!("b.typ")]); - test_with_path(&world, "content/b.typ", -2) - .must_include([q!("../data/example.csv")]); + test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]); } #[test] fn test_autocomplete_figure_snippets() { - test("#figure()", -1) + test("#figure()", -2) .must_apply("image", "image(\"${}\"),") .must_apply("table", "table(\n ${}\n),"); - test("#figure(cap)", -1).must_apply("caption", "caption: [${}]"); + test("#figure(cap)", -2).must_apply("caption", "caption: [${}]"); + } + + #[test] + fn test_autocomplete_import_items() { + let world = TestWorld::new("#import \"other.typ\": ") + .with_source("second.typ", "#import \"other.typ\": th") + .with_source("other.typ", "#let this = 1; #let that = 2"); + + test(&world, ("main.typ", 21)) + .must_include(["*", "this", "that"]) + .must_exclude(["figure"]); + test(&world, ("second.typ", 23)) + .must_include(["this", "that"]) + .must_exclude(["*", "figure"]); + } + + #[test] + fn test_autocomplete_type_methods() { + test("#\"hello\".", -1).must_include(["len", "contains"]); + test("#table().", -1).must_exclude(["cell"]); + } + + #[test] + fn test_autocomplete_content_methods() { + test("#show outline.entry: it => it.\n#outline()\n= Hi", 30) + .must_include(["indented", "body", "page"]); + } + + #[test] + fn test_autocomplete_symbol_variants() { + test("#sym.arrow.", -1) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); + test("$ arrow. $", -3) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); } } diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs index c789430a2..69d702b3b 100644 --- a/crates/typst-ide/src/definition.rs +++ b/crates/typst-ide/src/definition.rs @@ -55,8 +55,8 @@ pub fn definition( } } - if let Some(value) = globals(world, &leaf).get(&name) { - return Some(Definition::Std(value.clone())); + if let Some(binding) = globals(world, &leaf).get(&name) { + return Some(Definition::Std(binding.read().clone())); } } @@ -86,6 +86,7 @@ pub fn definition( #[cfg(test)] mod tests { + use std::borrow::Borrow; use std::ops::Range; use typst::foundations::{IntoValue, NativeElement}; @@ -93,7 +94,7 @@ mod tests { use typst::WorldExt; use super::{definition, Definition}; - use crate::tests::{SourceExt, TestWorld}; + use crate::tests::{FilePos, TestWorld, WorldLike}; type Response = (TestWorld, Option); @@ -132,23 +133,19 @@ mod tests { } #[track_caller] - fn test(text: &str, cursor: isize, side: Side) -> Response { - let world = TestWorld::new(text); - test_with_world(world, cursor, side) - } - - #[track_caller] - fn test_with_world(world: TestWorld, cursor: isize, side: Side) -> Response { - let doc = typst::compile(&world).output.ok(); - let source = &world.main; - let def = definition(&world, doc.as_ref(), source, source.cursor(cursor), side); - (world, def) + fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response { + let world = world.acquire(); + let world = world.borrow(); + let doc = typst::compile(world).output.ok(); + let (source, cursor) = pos.resolve(world); + let def = definition(world, doc.as_ref(), &source, cursor, side); + (world.clone(), def) } #[test] fn test_definition_let() { - test("#let x; #x", 9, Side::After).must_be_at("main.typ", 5..6); - test("#let x() = {}; #x", 16, Side::After).must_be_at("main.typ", 5..6); + test("#let x; #x", -2, Side::After).must_be_at("main.typ", 5..6); + test("#let x() = {}; #x", -2, Side::After).must_be_at("main.typ", 5..6); } #[test] @@ -158,33 +155,33 @@ mod tests { // The span is at the args here because that's what the function value's // span is. Not ideal, but also not too big of a big deal. - test_with_world(world, -1, Side::Before).must_be_at("other.typ", 8..11); + test(&world, -2, Side::Before).must_be_at("other.typ", 8..11); } #[test] fn test_definition_cross_file() { let world = TestWorld::new("#import \"other.typ\": x; #x") .with_source("other.typ", "#let x = 1"); - test_with_world(world, -1, Side::After).must_be_at("other.typ", 5..6); + test(&world, -2, Side::After).must_be_at("other.typ", 5..6); } #[test] fn test_definition_import() { let world = TestWorld::new("#import \"other.typ\" as o: x") .with_source("other.typ", "#let x = 1"); - test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0); + test(&world, 14, Side::Before).must_be_at("other.typ", 0..0); } #[test] fn test_definition_include() { let world = TestWorld::new("#include \"other.typ\"") .with_source("other.typ", "Hello there"); - test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0); + test(&world, 14, Side::Before).must_be_at("other.typ", 0..0); } #[test] fn test_definition_ref() { - test("#figure[] See @hi", 21, Side::After).must_be_at("main.typ", 1..9); + test("#figure[] See @hi", -2, Side::After).must_be_at("main.typ", 1..9); } #[test] diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index ba62b0ab9..428335426 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -73,7 +73,10 @@ pub fn jump_from_click( let Some(id) = span.id() else { continue }; let source = world.source(id).ok()?; let node = source.find(span)?; - let pos = if node.kind() == SyntaxKind::Text { + let pos = if matches!( + node.kind(), + SyntaxKind::Text | SyntaxKind::MathText + ) { let range = node.range(); let mut offset = range.start + usize::from(span_offset); if (click.x - pos.x) > width / 2.0 { @@ -115,7 +118,7 @@ pub fn jump_from_cursor( cursor: usize, ) -> Vec { fn is_text(node: &LinkedNode) -> bool { - node.get().kind() == SyntaxKind::Text + matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText) } let root = LinkedNode::new(source.root()); @@ -182,12 +185,13 @@ mod tests { //! )) //! ``` + use std::borrow::Borrow; use std::num::NonZeroUsize; use typst::layout::{Abs, Point, Position}; use super::{jump_from_click, jump_from_cursor, Jump}; - use crate::tests::TestWorld; + use crate::tests::{FilePos, TestWorld, WorldLike}; fn point(x: f64, y: f64) -> Point { Point::new(Abs::pt(x), Abs::pt(y)) @@ -211,10 +215,11 @@ mod tests { } #[track_caller] - fn test_click(text: &str, click: Point, expected: Option) { - let world = TestWorld::new(text); - let doc = typst::compile(&world).output.unwrap(); - let jump = jump_from_click(&world, &doc, &doc.pages[0].frame, click); + fn test_click(world: impl WorldLike, click: Point, expected: Option) { + let world = world.acquire(); + let world = world.borrow(); + let doc = typst::compile(world).output.unwrap(); + let jump = jump_from_click(world, &doc, &doc.pages[0].frame, click); if let (Some(Jump::Position(pos)), Some(Jump::Position(expected))) = (&jump, &expected) { @@ -227,10 +232,12 @@ mod tests { } #[track_caller] - fn test_cursor(text: &str, cursor: usize, expected: Option) { - let world = TestWorld::new(text); - let doc = typst::compile(&world).output.unwrap(); - let pos = jump_from_cursor(&doc, &world.main, cursor); + fn test_cursor(world: impl WorldLike, pos: impl FilePos, expected: Option) { + let world = world.acquire(); + let world = world.borrow(); + let doc = typst::compile(world).output.unwrap(); + let (source, cursor) = pos.resolve(world); + let pos = jump_from_cursor(&doc, &source, cursor); assert_eq!(!pos.is_empty(), expected.is_some()); if let (Some(pos), Some(expected)) = (pos.first(), expected) { assert_eq!(pos.page, expected.page); @@ -257,6 +264,11 @@ mod tests { test_click(s, point(21.0, 12.0), cursor(56)); } + #[test] + fn test_jump_from_click_math() { + test_click("$a + b$", point(28.0, 14.0), cursor(5)); + } + #[test] fn test_jump_from_cursor() { let s = "*Hello* #box[ABC] World"; @@ -264,6 +276,11 @@ mod tests { test_cursor(s, 14, pos(1, 37.55, 16.58)); } + #[test] + fn test_jump_from_cursor_math() { + test_cursor("$a + b$", -3, pos(1, 27.51, 16.83)); + } + #[test] fn test_backlink() { let s = "#footnote[Hi]"; diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index 18262f701..93fdc5dd5 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use typst::foundations::{Module, Value}; use typst::syntax::ast::AstNode; -use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode}; +use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use crate::{analyze_import, IdeWorld}; @@ -30,38 +30,38 @@ pub fn named_items( if let Some(v) = node.cast::() { let imports = v.imports(); - let source = node - .children() - .find(|child| child.is::()) - .and_then(|source: LinkedNode| { - Some((analyze_import(world, &source)?, source)) - }); - let source = source.as_ref(); + let source = v.source(); + + let source_value = node + .find(source.span()) + .and_then(|source| analyze_import(world, &source)); + let source_value = source_value.as_ref(); + + let module = source_value.and_then(|value| match value { + Value::Module(module) => Some(module), + _ => None, + }); + + let name_and_span = match (imports, v.new_name()) { + // ```plain + // import "foo" as name + // import "foo" as name: .. + // ``` + (_, Some(name)) => Some((name.get().clone(), name.span())), + // ```plain + // import "foo" + // ``` + (None, None) => v.bare_name().ok().map(|name| (name, source.span())), + // ```plain + // import "foo": .. + // ``` + (Some(..), None) => None, + }; // Seeing the module itself. - if let Some((value, source)) = source { - let site = match (imports, v.new_name()) { - // ```plain - // import "foo" as name; - // import "foo" as name: ..; - // ``` - (_, Some(name)) => Some(name.to_untyped()), - // ```plain - // import "foo"; - // ``` - (None, None) => Some(source.get()), - // ```plain - // import "foo": ..; - // ``` - (Some(..), None) => None, - }; - - if let Some((site, value)) = - site.zip(value.clone().cast::().ok()) - { - if let Some(res) = recv(NamedItem::Module(&value, site)) { - return Some(res); - } + if let Some((name, span)) = name_and_span { + if let Some(res) = recv(NamedItem::Module(&name, span, module)) { + return Some(res); } } @@ -75,9 +75,13 @@ pub fn named_items( // import "foo": *; // ``` Some(ast::Imports::Wildcard) => { - if let Some(scope) = source.and_then(|(value, _)| value.scope()) { - for (name, value, span) in scope.iter() { - let item = NamedItem::Import(name, span, Some(value)); + if let Some(scope) = source_value.and_then(Value::scope) { + for (name, binding) in scope.iter() { + let item = NamedItem::Import( + name, + binding.span(), + Some(binding.read()), + ); if let Some(res) = recv(item) { return Some(res); } @@ -89,18 +93,26 @@ pub fn named_items( // ``` Some(ast::Imports::Items(items)) => { for item in items.iter() { - let original = item.original_name(); - let bound = item.bound_name(); - let scope = source.and_then(|(value, _)| value.scope()); - let span = scope - .and_then(|s| s.get_span(&original)) - .unwrap_or(Span::detached()) - .or(bound.span()); + let mut iter = item.path().iter(); + let mut binding = source_value + .and_then(Value::scope) + .zip(iter.next()) + .and_then(|(scope, first)| scope.get(&first)); - let value = scope.and_then(|s| s.get(&original)); - if let Some(res) = - recv(NamedItem::Import(bound.get(), span, value)) - { + for ident in iter { + binding = binding.and_then(|binding| { + binding.read().scope()?.get(&ident) + }); + } + + let bound = item.bound_name(); + let (span, value) = match binding { + Some(binding) => (binding.span(), Some(binding.read())), + None => (bound.span(), None), + }; + + let item = NamedItem::Import(bound.get(), span, value); + if let Some(res) = recv(item) { return Some(res); } } @@ -169,8 +181,8 @@ pub enum NamedItem<'a> { Var(ast::Ident<'a>), /// A function item. Fn(ast::Ident<'a>), - /// A (imported) module item. - Module(&'a Module, &'a SyntaxNode), + /// A (imported) module. + Module(&'a EcoString, Span, Option<&'a Module>), /// An imported item. Import(&'a EcoString, Span, Option<&'a Value>), } @@ -180,7 +192,7 @@ impl<'a> NamedItem<'a> { match self { NamedItem::Var(ident) => ident.get(), NamedItem::Fn(ident) => ident.get(), - NamedItem::Module(value, _) => value.name(), + NamedItem::Module(name, _, _) => name, NamedItem::Import(name, _, _) => name, } } @@ -188,7 +200,7 @@ impl<'a> NamedItem<'a> { pub(crate) fn value(&self) -> Option { match self { NamedItem::Var(..) | NamedItem::Fn(..) => None, - NamedItem::Module(value, _) => Some(Value::Module((*value).clone())), + NamedItem::Module(_, _, value) => value.cloned().map(Value::Module), NamedItem::Import(_, _, value) => value.cloned(), } } @@ -196,7 +208,7 @@ impl<'a> NamedItem<'a> { pub(crate) fn span(&self) -> Span { match *self { NamedItem::Var(name) | NamedItem::Fn(name) => name.span(), - NamedItem::Module(_, site) => site.span(), + NamedItem::Module(_, span, _) => span, NamedItem::Import(_, span, _) => span, } } @@ -220,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option> { ast::Expr::FuncCall(call) => { DerefTarget::Callee(expr_node.find(call.callee().span())?) } - ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?), + ast::Expr::SetRule(set) => { + DerefTarget::Callee(expr_node.find(set.target().span())?) + } ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => { DerefTarget::VarAccess(expr_node) } @@ -266,53 +280,105 @@ pub enum DerefTarget<'a> { #[cfg(test)] mod tests { + use std::borrow::Borrow; + + use ecow::EcoString; + use typst::foundations::Value; use typst::syntax::{LinkedNode, Side}; - use crate::{named_items, tests::TestWorld}; + use super::named_items; + use crate::tests::{FilePos, TestWorld, WorldLike}; + + type Response = Vec<(EcoString, Option)>; + + trait ResponseExt { + fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self; + fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self; + fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self; + } + + impl ResponseExt for Response { + #[track_caller] + fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self { + for item in includes { + assert!( + self.iter().any(|v| v.0 == item), + "{item:?} was not contained in {self:?}", + ); + } + self + } + + #[track_caller] + fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self { + for item in excludes { + assert!( + !self.iter().any(|v| v.0 == item), + "{item:?} was wrongly contained in {self:?}", + ); + } + self + } + + #[track_caller] + fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self { + assert!( + self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value), + "{name_value:?} was not contained in {self:?}", + ); + self + } + } #[track_caller] - fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool { - let world = TestWorld::new(text); - - let src = world.main.clone(); - let node = LinkedNode::new(src.root()); + fn test(world: impl WorldLike, pos: impl FilePos) -> Response { + let world = world.acquire(); + let world = world.borrow(); + let (source, cursor) = pos.resolve(world); + let node = LinkedNode::new(source.root()); let leaf = node.leaf_at(cursor, Side::After).unwrap(); - - let res = named_items(&world, leaf, |s| { - if containing == s.name() { - return Some(true); - } - - None + let mut items = vec![]; + named_items(world, leaf, |s| { + items.push((s.name().clone(), s.value().clone())); + None::<()> }); - - res.unwrap_or_default() + items } #[test] - fn test_simple_named_items() { - // Has named items - assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a")); - assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a")); - - // Doesn't have named items - assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b")); + fn test_named_items_simple() { + let s = "#let a = 1;#let b = 2;"; + test(s, 8).must_include(["a"]).must_exclude(["b"]); + test(s, 15).must_include(["b"]); } #[test] - fn test_param_named_items() { - // Has named items - assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a")); - assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a")); + fn test_named_items_param() { + let pos = "#let f(a) = 1;#let b = 2;"; + test(pos, 12).must_include(["a"]); + test(pos, 19).must_include(["b", "f"]).must_exclude(["a"]); - // Doesn't have named items - assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a")); - assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b")); + let named = "#let f(a: b) = 1;#let b = 2;"; + test(named, 15).must_include(["a", "f"]).must_exclude(["b"]); } #[test] - fn test_import_named_items() { - // Cannot test much. - assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a")); + fn test_named_items_import() { + test("#import \"foo.typ\"", 2).must_include(["foo"]); + test("#import \"foo.typ\" as bar", 2) + .must_include(["bar"]) + .must_exclude(["foo"]); + } + + #[test] + fn test_named_items_import_items() { + test("#import \"foo.typ\": a; #(a);", 2) + .must_include(["a"]) + .must_exclude(["foo"]); + + let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);") + .with_source("foo.typ", "#import \"a.typ\"") + .with_source("a.typ", "#let b = 1;"); + test(&world, 2).must_include_value(("b", Some(&Value::Int(1)))); } } diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index 5a73fa375..6678ab841 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -1,4 +1,6 @@ +use std::borrow::Borrow; use std::collections::HashMap; +use std::sync::Arc; use ecow::EcoString; use typst::diag::{FileError, FileResult}; @@ -13,10 +15,10 @@ use typst::{Library, World}; use crate::IdeWorld; /// A world for IDE testing. +#[derive(Clone)] pub struct TestWorld { pub main: Source, - assets: HashMap, - sources: HashMap, + files: Arc, base: &'static TestBase, } @@ -29,8 +31,7 @@ impl TestWorld { let main = Source::new(Self::main_id(), text.into()); Self { main, - assets: HashMap::new(), - sources: HashMap::new(), + files: Arc::new(TestFiles::default()), base: singleton!(TestBase, TestBase::default()), } } @@ -39,7 +40,7 @@ impl TestWorld { pub fn with_source(mut self, path: &str, text: &str) -> Self { let id = FileId::new(None, VirtualPath::new(path)); let source = Source::new(id, text.into()); - self.sources.insert(id, source); + Arc::make_mut(&mut self.files).sources.insert(id, source); self } @@ -54,8 +55,8 @@ impl TestWorld { pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self { let id = FileId::new(None, VirtualPath::new(path)); let data = typst_dev_assets::get_by_name(filename).unwrap(); - let bytes = Bytes::from_static(data); - self.assets.insert(id, bytes); + let bytes = Bytes::new(data); + Arc::make_mut(&mut self.files).assets.insert(id, bytes); self } @@ -81,7 +82,7 @@ impl World for TestWorld { fn source(&self, id: FileId) -> FileResult { if id == self.main.id() { Ok(self.main.clone()) - } else if let Some(source) = self.sources.get(&id) { + } else if let Some(source) = self.files.sources.get(&id) { Ok(source.clone()) } else { Err(FileError::NotFound(id.vpath().as_rootless_path().into())) @@ -89,7 +90,7 @@ impl World for TestWorld { } fn file(&self, id: FileId) -> FileResult { - match self.assets.get(&id) { + match self.files.assets.get(&id) { Some(bytes) => Ok(bytes.clone()), None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())), } @@ -111,8 +112,8 @@ impl IdeWorld for TestWorld { fn files(&self) -> Vec { std::iter::once(self.main.id()) - .chain(self.sources.keys().copied()) - .chain(self.assets.keys().copied()) + .chain(self.files.sources.keys().copied()) + .chain(self.files.assets.keys().copied()) .collect() } @@ -133,20 +134,11 @@ impl IdeWorld for TestWorld { } } -/// Extra methods for [`Source`]. -pub trait SourceExt { - /// Negative cursors index from the back. - fn cursor(&self, cursor: isize) -> usize; -} - -impl SourceExt for Source { - fn cursor(&self, cursor: isize) -> usize { - if cursor < 0 { - self.len_bytes().checked_add_signed(cursor).unwrap() - } else { - cursor as usize - } - } +/// Test-specific files. +#[derive(Default, Clone)] +struct TestFiles { + assets: HashMap, + sources: HashMap, } /// Shared foundation of all test worlds. @@ -160,7 +152,7 @@ impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); Self { @@ -186,3 +178,58 @@ fn library() -> Library { lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into()))); lib } + +/// The input to a test: Either just a string or a full `TestWorld`. +pub trait WorldLike { + type World: Borrow; + + fn acquire(self) -> Self::World; +} + +impl<'a> WorldLike for &'a TestWorld { + type World = &'a TestWorld; + + fn acquire(self) -> Self::World { + self + } +} + +impl WorldLike for &str { + type World = TestWorld; + + fn acquire(self) -> Self::World { + TestWorld::new(self) + } +} + +/// Specifies a position in a file for a test. +pub trait FilePos { + fn resolve(self, world: &TestWorld) -> (Source, usize); +} + +impl FilePos for isize { + #[track_caller] + fn resolve(self, world: &TestWorld) -> (Source, usize) { + (world.main.clone(), cursor(&world.main, self)) + } +} + +impl FilePos for (&str, isize) { + #[track_caller] + fn resolve(self, world: &TestWorld) -> (Source, usize) { + let id = FileId::new(None, VirtualPath::new(self.0)); + let source = world.source(id).unwrap(); + let cursor = cursor(&source, self.1); + (source, cursor) + } +} + +/// Resolve a signed index (negative from the back) to a unsigned index. +#[track_caller] +fn cursor(source: &Source, cursor: isize) -> usize { + if cursor < 0 { + source.len_bytes().checked_add_signed(cursor + 1).unwrap() + } else { + cursor as usize + } +} diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 4eaaeda1f..cbfffe530 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use typst::engine::Sink; -use typst::foundations::{repr, Capturer, CastInfo, Repr, Value}; +use typst::foundations::{repr, Binding, Capturer, CastInfo, Repr, Value}; use typst::layout::{Length, PagedDocument}; use typst::syntax::ast::AstNode; use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; @@ -201,12 +201,17 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option(); if let Some(ast::Expr::Ident(callee)) = match expr { ast::Expr::FuncCall(call) => Some(call.callee()), - ast::Expr::Set(set) => Some(set.target()), + ast::Expr::SetRule(set) => Some(set.target()), _ => None, }; // Find metadata about the function. - if let Some(Value::Func(func)) = world.library().global.scope().get(&callee); + if let Some(Value::Func(func)) = world + .library() + .global + .scope() + .get(&callee) + .map(Binding::read); then { (func, named) } else { return None; } }; @@ -274,10 +279,12 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option { #[cfg(test)] mod tests { + use std::borrow::Borrow; + use typst::syntax::Side; use super::{tooltip, Tooltip}; - use crate::tests::{SourceExt, TestWorld}; + use crate::tests::{FilePos, TestWorld, WorldLike}; type Response = Option; @@ -308,21 +315,17 @@ mod tests { } #[track_caller] - fn test(text: &str, cursor: isize, side: Side) -> Response { - let world = TestWorld::new(text); - test_with_world(&world, cursor, side) - } - - #[track_caller] - fn test_with_world(world: &TestWorld, cursor: isize, side: Side) -> Response { - let source = &world.main; - let doc = typst::compile(&world).output.ok(); - tooltip(world, doc.as_ref(), source, source.cursor(cursor), side) + fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response { + let world = world.acquire(); + let world = world.borrow(); + let (source, cursor) = pos.resolve(world); + let doc = typst::compile(world).output.ok(); + tooltip(world, doc.as_ref(), &source, cursor, side) } #[test] fn test_tooltip() { - test("#let x = 1 + 2", 14, Side::After).must_be_none(); + test("#let x = 1 + 2", -1, Side::After).must_be_none(); test("#let x = 1 + 2", 5, Side::After).must_be_code("3"); test("#let x = 1 + 2", 6, Side::Before).must_be_code("3"); test("#let x = 1 + 2", 6, Side::Before).must_be_code("3"); @@ -330,7 +333,7 @@ mod tests { #[test] fn test_tooltip_empty_contextual() { - test("#{context}", 10, Side::Before).must_be_code("context()"); + test("#{context}", -1, Side::Before).must_be_code("context()"); } #[test] @@ -354,12 +357,18 @@ mod tests { .must_be_text("This closure captures `f` and `y`"); } + #[test] + fn test_tooltip_import() { + let world = TestWorld::new("#import \"other.typ\": a, b") + .with_source("other.typ", "#let (a, b, c) = (1, 2, 3)"); + test(&world, -5, Side::After).must_be_code("1"); + } + #[test] fn test_tooltip_star_import() { let world = TestWorld::new("#import \"other.typ\": *") .with_source("other.typ", "#let (a, b, c) = (1, 2, 3)"); - test_with_world(&world, 21, Side::Before).must_be_none(); - test_with_world(&world, 21, Side::After) - .must_be_text("This star imports `a`, `b`, and `c`"); + test(&world, -2, Side::Before).must_be_none(); + test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`"); } } diff --git a/crates/typst-ide/src/utils.rs b/crates/typst-ide/src/utils.rs index cd66ec8f0..d5d584e2b 100644 --- a/crates/typst-ide/src/utils.rs +++ b/crates/typst-ide/src/utils.rs @@ -171,7 +171,7 @@ where self.find_iter(content.fields().iter().map(|(_, v)| v))?; } Value::Module(module) => { - self.find_iter(module.scope().iter().map(|(_, v, _)| v))?; + self.find_iter(module.scope().iter().map(|(_, b)| b.read()))?; } _ => {} } diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 266eba0b4..52aa407c3 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } once_cell = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } tar = { workspace = true, optional = true } ureq = { workspace = true, optional = true } diff --git a/crates/typst-kit/src/fonts.rs b/crates/typst-kit/src/fonts.rs index 83e13fd8f..c15d739ec 100644 --- a/crates/typst-kit/src/fonts.rs +++ b/crates/typst-kit/src/fonts.rs @@ -13,6 +13,7 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; use fontdb::{Database, Source}; +use typst_library::foundations::Bytes; use typst_library::text::{Font, FontBook, FontInfo}; use typst_timing::TimingScope; @@ -52,9 +53,8 @@ impl FontSlot { .as_ref() .expect("`path` is not `None` if `font` is uninitialized"), ) - .ok()? - .into(); - Font::new(data, self.index) + .ok()?; + Font::new(Bytes::new(data), self.index) }) .clone() } @@ -196,7 +196,7 @@ impl FontSearcher { #[cfg(feature = "embed-fonts")] fn add_embedded(&mut self) { for data in typst_assets::fonts() { - let buffer = typst_library::foundations::Bytes::from_static(data); + let buffer = Bytes::new(data); for (i, font) in Font::iter(buffer).enumerate() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index e7eb71ee4..172d8740a 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -5,10 +5,9 @@ use std::path::{Path, PathBuf}; use ecow::eco_format; use once_cell::sync::OnceCell; +use serde::Deserialize; use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; -use typst_syntax::package::{ - PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec, -}; +use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec}; use crate::download::{Downloader, Progress}; @@ -32,7 +31,7 @@ pub struct PackageStorage { /// The downloader used for fetching the index and packages. downloader: Downloader, /// The cached index of the default namespace. - index: OnceCell>, + index: OnceCell>, } impl PackageStorage { @@ -42,6 +41,18 @@ impl PackageStorage { package_cache_path: Option, package_path: Option, downloader: Downloader, + ) -> Self { + Self::with_index(package_cache_path, package_path, downloader, OnceCell::new()) + } + + /// Creates a new package storage with a pre-defined index. + /// + /// Useful for testing. + fn with_index( + package_cache_path: Option, + package_path: Option, + downloader: Downloader, + index: OnceCell>, ) -> Self { Self { package_cache_path: package_cache_path.or_else(|| { @@ -51,7 +62,7 @@ impl PackageStorage { dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR)) }), downloader, - index: OnceCell::new(), + index, } } @@ -109,6 +120,7 @@ impl PackageStorage { // version. self.download_index()? .iter() + .filter_map(|value| MinimalPackageInfo::deserialize(value).ok()) .filter(|package| package.name == spec.name) .map(|package| package.version) .max() @@ -131,7 +143,7 @@ impl PackageStorage { } /// Download the package index. The result of this is cached for efficiency. - pub fn download_index(&self) -> StrResult<&[PackageInfo]> { + pub fn download_index(&self) -> StrResult<&[serde_json::Value]> { self.index .get_or_try_init(|| { let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); @@ -186,3 +198,54 @@ impl PackageStorage { }) } } + +/// Minimal information required about a package to determine its latest +/// version. +#[derive(Deserialize)] +struct MinimalPackageInfo { + name: String, + version: PackageVersion, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lazy_deser_index() { + let storage = PackageStorage::with_index( + None, + None, + Downloader::new("typst/test"), + OnceCell::with_value(vec![ + serde_json::json!({ + "name": "charged-ieee", + "version": "0.1.0", + "entrypoint": "lib.typ", + }), + serde_json::json!({ + "name": "unequivocal-ams", + // This version number is currently not valid, so this package + // can't be parsed. + "version": "0.2.0-dev", + "entrypoint": "lib.typ", + }), + ]), + ); + + let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "charged-ieee".into(), + }); + assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 })); + + let ams_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "unequivocal-ams".into(), + }); + assert_eq!( + ams_version, + Err("failed to find package @preview/unequivocal-ams".into()) + ) + } +} diff --git a/crates/typst-layout/src/flow/block.rs b/crates/typst-layout/src/flow/block.rs index 71eacc1ce..6c2c3923d 100644 --- a/crates/typst-layout/src/flow/block.rs +++ b/crates/typst-layout/src/flow/block.rs @@ -364,6 +364,12 @@ fn breakable_pod<'a>( /// Distribute a fixed height spread over existing regions into a new first /// height and a new backlog. +/// +/// Note that, if the given height fits within the first region, no backlog is +/// generated and the first region's height shrinks to fit exactly the given +/// height. In particular, negative and zero heights always fit in any region, +/// so such heights are always directly returned as the new first region +/// height. fn distribute<'a>( height: Abs, mut regions: Regions, @@ -371,7 +377,19 @@ fn distribute<'a>( ) -> (Abs, &'a mut [Abs]) { // Build new region heights from old regions. let mut remaining = height; + + // Negative and zero heights always fit, so just keep them. + // No backlog is generated. + if remaining <= Abs::zero() { + buf.push(remaining); + return (buf[0], &mut buf[1..]); + } + loop { + // This clamp is safe (min <= max), as 'remaining' won't be negative + // due to the initial check above (on the first iteration) and due to + // stopping on 'remaining.approx_empty()' below (for the second + // iteration onwards). let limited = regions.size.y.clamp(Abs::zero(), remaining); buf.push(limited); remaining -= limited; diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 12cfa152e..2c14f7a37 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -20,12 +20,16 @@ use typst_library::model::ParElem; use typst_library::routines::{Pair, Routines}; use typst_library::text::TextElem; use typst_library::World; +use typst_utils::SliceExt; -use super::{layout_multi_block, layout_single_block}; +use super::{layout_multi_block, layout_single_block, FlowMode}; +use crate::inline::ParSituation; +use crate::modifiers::layout_and_modify; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. #[typst_macros::time] +#[allow(clippy::too_many_arguments)] pub fn collect<'a>( engine: &mut Engine, bump: &'a Bump, @@ -33,6 +37,7 @@ pub fn collect<'a>( locator: Locator<'a>, base: Size, expand: bool, + mode: FlowMode, ) -> SourceResult>> { Collector { engine, @@ -42,9 +47,9 @@ pub fn collect<'a>( base, expand, output: Vec::with_capacity(children.len()), - last_was_par: false, + par_situation: ParSituation::First, } - .run() + .run(mode) } /// State for collection. @@ -56,12 +61,20 @@ struct Collector<'a, 'x, 'y> { expand: bool, locator: SplitLocator<'a>, output: Vec>, - last_was_par: bool, + par_situation: ParSituation, } impl<'a> Collector<'a, '_, '_> { /// Perform the collection. - fn run(mut self) -> SourceResult>> { + fn run(self, mode: FlowMode) -> SourceResult>> { + match mode { + FlowMode::Root | FlowMode::Block => self.run_block(), + FlowMode::Inline => self.run_inline(), + } + } + + /// Perform collection for block-level children. + fn run_block(mut self) -> SourceResult>> { for &(child, styles) in self.children { if let Some(elem) = child.to_packed::() { self.output.push(Child::Tag(&elem.tag)); @@ -94,6 +107,42 @@ impl<'a> Collector<'a, '_, '_> { Ok(self.output) } + /// Perform collection for inline-level children. + fn run_inline(mut self) -> SourceResult>> { + // Extract leading and trailing tags. + let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::()); + let inner = &self.children[start..end]; + + // Compute the shared styles, ignoring tags. + let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default(); + + // Layout the lines. + let lines = crate::inline::layout_inline( + self.engine, + inner, + &mut self.locator, + styles, + self.base, + self.expand, + )? + .into_frames(); + + for (c, _) in &self.children[..start] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + let leading = ParElem::leading_in(styles); + self.lines(lines, leading, styles); + + for (c, _) in &self.children[end..] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + Ok(self.output) + } + /// Collect vertical spacing into a relative or fractional child. fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { self.output.push(match elem.amount { @@ -109,24 +158,35 @@ impl<'a> Collector<'a, '_, '_> { elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { - let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); - let spacing = ParElem::spacing_in(styles); - let costs = TextElem::costs_in(styles); - - let lines = crate::layout_inline( + let lines = crate::inline::layout_par( + elem, self.engine, - &elem.children, self.locator.next(&elem.span()), styles, - self.last_was_par, self.base, self.expand, + self.par_situation, )? .into_frames(); + let spacing = elem.spacing(styles); + let leading = elem.leading(styles); + self.output.push(Child::Rel(spacing.into(), 4)); + self.lines(lines, leading, styles); + + self.output.push(Child::Rel(spacing.into(), 4)); + self.par_situation = ParSituation::Consecutive; + + Ok(()) + } + + /// Collect laid-out lines. + fn lines(&mut self, lines: Vec, leading: Abs, styles: StyleChain<'a>) { + let align = AlignElem::alignment_in(styles).resolve(styles); + let costs = TextElem::costs_in(styles); + // Determine whether to prevent widow and orphans. let len = lines.len(); let prevent_orphans = @@ -165,11 +225,6 @@ impl<'a> Collector<'a, '_, '_> { self.output .push(Child::Line(self.boxed(LineChild { frame, align, need }))); } - - self.output.push(Child::Rel(spacing.into(), 4)); - self.last_was_par = true; - - Ok(()) } /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on @@ -218,7 +273,7 @@ impl<'a> Collector<'a, '_, '_> { }; self.output.push(spacing(elem.below(styles))); - self.last_was_par = false; + self.par_situation = ParSituation::Other; } /// Collects a placed element into a [`PlacedChild`]. @@ -377,8 +432,9 @@ fn layout_single_impl( route: Route::extend(route), }; - layout_single_block(elem, &mut engine, locator, styles, region) - .map(|frame| frame.post_processed(styles)) + layout_and_modify(styles, |styles| { + layout_single_block(elem, &mut engine, locator, styles, region) + }) } /// A child that encapsulates a prepared breakable block. @@ -473,11 +529,8 @@ fn layout_multi_impl( route: Route::extend(route), }; - layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { - for frame in &mut fragment { - frame.post_process(styles); - } - fragment + layout_and_modify(styles, |styles| { + layout_multi_block(elem, &mut engine, locator, styles, regions) }) } @@ -579,20 +632,23 @@ impl PlacedChild<'_> { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); + let styles = self.styles.chain(&aligned); - let mut frame = crate::layout_frame( - engine, - &self.elem.body, - self.locator.relayout(), - self.styles.chain(&aligned), - Region::new(base, Axes::splat(false)), - )?; + let mut frame = layout_and_modify(styles, |styles| { + crate::layout_frame( + engine, + &self.elem.body, + self.locator.relayout(), + styles, + Region::new(base, Axes::splat(false)), + ) + })?; if self.float { frame.set_parent(self.elem.location().unwrap()); } - Ok(frame.post_processed(self.styles)) + Ok(frame) }) } diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 326456752..76af8f650 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -15,9 +15,11 @@ use typst_library::model::{ FootnoteElem, FootnoteEntry, LineNumberingScope, Numbering, ParLineMarker, }; use typst_syntax::Span; -use typst_utils::NonZeroExt; +use typst_utils::{NonZeroExt, Numeric}; -use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; +use super::{ + distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work, +}; /// Composes the contents of a single page/region. A region can have multiple /// columns/subregions. @@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { migratable: bool, ) -> FlowResult<()> { // Footnotes are only supported at the root level. - if !self.config.root { + if self.config.mode != FlowMode::Root { return Ok(()); } @@ -374,7 +376,11 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { let mut relayout = false; let mut regions = *regions; - let mut migratable = migratable && !breakable && regions.may_progress(); + + // The first footnote's origin frame should be migratable if the region + // may progress (already checked by the footnote function) and if the + // origin frame isn't breakable (checked here). + let mut migratable = migratable && !breakable; for (y, elem) in notes { // The amount of space used by the in-flow content that contains the @@ -464,11 +470,35 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { // If the first frame is empty, then none of its content fit. If // possible, we then migrate the origin frame to the next region to // uphold the footnote invariant (that marker and entry are on the same - // page). If not, we just queue the footnote for the next page. + // page). If not, we just queue the footnote for the next page, but + // only if that would actually make a difference (that is, if the + // footnote isn't alone in the page after not fitting in any previous + // pages, as it probably won't ever fit then). + // + // Note that a non-zero flow need also indicates that queueing would + // make a difference, because the flow need is subtracted from the + // available height in the entry's pod even if what caused that need + // wasn't considered for the input `regions`. For example, floats just + // pass the `regions` they received along to their footnotes, which + // don't take into account the space occupied by the floats themselves, + // but they do indicate their footnotes have a non-zero flow need, so + // queueing them can matter as, in the following pages, the flow need + // will be set to zero and the footnote will be alone in the page. + // Then, `may_progress()` will also be false (this time, correctly) and + // the footnote is laid out, as queueing wouldn't improve the lack of + // space anymore and would result in an infinite loop. + // + // However, it is worth noting that migration does take into account + // the original region, before inserting what prompted the flow need. + // Logically, if moving the original frame can't improve the lack of + // space, then migration should be inhibited. The space occupied by the + // original frame is not relevant for that check. Therefore, + // `regions.may_progress()` must still be checked separately for + // migration, regardless of the presence of flow need. if first.is_empty() && exist_non_empty_frame { - if migratable { + if migratable && regions.may_progress() { return Err(Stop::Finish(false)); - } else { + } else if regions.may_progress() || !flow_need.is_zero() { self.footnote_queue.push(elem); return Ok(()); } diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index df716b338..cba228bcd 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -25,7 +25,7 @@ use typst_library::layout::{ Regions, Rel, Size, }; use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::TextElem; use typst_library::World; use typst_utils::{NonZeroExt, Numeric}; @@ -140,9 +140,10 @@ fn layout_fragment_impl( engine.route.check_layout_depth().at(content.span())?; + let mut kind = FragmentKind::Block; let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::LayoutFragment, + RealizationKind::LayoutFragment(&mut kind), &mut engine, &mut locator, &arenas, @@ -158,56 +159,45 @@ fn layout_fragment_impl( regions, columns, column_gutter, - false, + kind.into(), ) } +/// The mode a flow can be laid out in. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum FlowMode { + /// A root flow with block-level elements. Like `FlowMode::Block`, but can + /// additionally host footnotes and line numbers. + Root, + /// A flow whose children are block-level elements. + Block, + /// A flow whose children are inline-level elements. + Inline, +} + +impl From for FlowMode { + fn from(value: FragmentKind) -> Self { + match value { + FragmentKind::Inline => Self::Inline, + FragmentKind::Block => Self::Block, + } + } +} + /// Lays out realized content into regions, potentially with columns. #[allow(clippy::too_many_arguments)] -pub(crate) fn layout_flow( +pub fn layout_flow<'a>( engine: &mut Engine, - children: &[Pair], - locator: &mut SplitLocator, - shared: StyleChain, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, mut regions: Regions, columns: NonZeroUsize, column_gutter: Rel, - root: bool, + mode: FlowMode, ) -> SourceResult { // Prepare configuration that is shared across the whole flow. - let config = Config { - root, - shared, - columns: { - let mut count = columns.get(); - if !regions.size.x.is_finite() { - count = 1; - } - - let gutter = column_gutter.relative_to(regions.base().x); - let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64; - let dir = TextElem::dir_in(shared); - ColumnConfig { count, width, gutter, dir } - }, - footnote: FootnoteConfig { - separator: FootnoteEntry::separator_in(shared), - clearance: FootnoteEntry::clearance_in(shared), - gap: FootnoteEntry::gap_in(shared), - expand: regions.expand.x, - }, - line_numbers: root.then(|| LineNumberConfig { - scope: ParLine::numbering_scope_in(shared), - default_clearance: { - let width = if PageElem::flipped_in(shared) { - PageElem::height_in(shared) - } else { - PageElem::width_in(shared) - }; - (0.026 * width.unwrap_or_default()) - .clamp(Em::new(0.75).resolve(shared), Em::new(2.5).resolve(shared)) - }, - }), - }; + let config = configuration(shared, regions, columns, column_gutter, mode); // Collect the elements into pre-processed children. These are much easier // to handle than the raw elements. @@ -219,6 +209,7 @@ pub(crate) fn layout_flow( locator.next(&()), Size::new(config.columns.width, regions.full), regions.expand.x, + mode, )?; let mut work = Work::new(&children); @@ -241,6 +232,55 @@ pub(crate) fn layout_flow( Ok(Fragment::frames(finished)) } +/// Determine the flow's configuration. +fn configuration<'x>( + shared: StyleChain<'x>, + regions: Regions, + columns: NonZeroUsize, + column_gutter: Rel, + mode: FlowMode, +) -> Config<'x> { + Config { + mode, + shared, + columns: { + let mut count = columns.get(); + if !regions.size.x.is_finite() { + count = 1; + } + + let gutter = column_gutter.relative_to(regions.base().x); + let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64; + let dir = TextElem::dir_in(shared); + ColumnConfig { count, width, gutter, dir } + }, + footnote: FootnoteConfig { + separator: FootnoteEntry::separator_in(shared), + clearance: FootnoteEntry::clearance_in(shared), + gap: FootnoteEntry::gap_in(shared), + expand: regions.expand.x, + }, + line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig { + scope: ParLine::numbering_scope_in(shared), + default_clearance: { + let width = if PageElem::flipped_in(shared) { + PageElem::height_in(shared) + } else { + PageElem::width_in(shared) + }; + + // Clamp below is safe (min <= max): if the font size is + // negative, we set min = max = 0; otherwise, + // `0.75 * size <= 2.5 * size` for zero and positive sizes. + (0.026 * width.unwrap_or_default()).clamp( + Em::new(0.75).resolve(shared).max(Abs::zero()), + Em::new(2.5).resolve(shared).max(Abs::zero()), + ) + }, + }), + } +} + /// The work that is left to do by flow layout. /// /// The lifetimes 'a and 'b are used across flow layout: @@ -312,7 +352,7 @@ impl<'a, 'b> Work<'a, 'b> { struct Config<'x> { /// Whether this is the root flow, which can host footnotes and line /// numbers. - root: bool, + mode: FlowMode, /// The styles shared by the whole flow. This is used for footnotes and line /// numbers. shared: StyleChain<'x>, @@ -354,6 +394,16 @@ struct LineNumberConfig { /// Where line numbers are reset. scope: LineNumberingScope, /// The default clearance for `auto`. + /// + /// This value should be relative to the page's width, such that the + /// clearance between line numbers and text is small when the page is, + /// itself, small. However, that could cause the clearance to be too small + /// or too large when considering the current text size; in particular, a + /// larger text size would require more clearance to be able to tell line + /// numbers apart from text, whereas a smaller text size requires less + /// clearance so they aren't way too far apart. Therefore, the default + /// value is a percentage of the page width clamped between `0.75em` and + /// `2.5em`. default_clearance: Abs, } diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 7c94617dc..af47ff72f 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,6 +3,7 @@ use std::fmt::Debug; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Resolve, StyleChain}; +use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable}; use typst_library::layout::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -13,8 +14,8 @@ use typst_syntax::Span; use typst_utils::{MaybeReverseIter, Numeric}; use super::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, - LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup, + generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, + LineSegment, Rowspan, UnbreakableRowGroup, }; /// Performs grid layout. @@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> { let size = Size::new(available, height); let pod = Region::new(size, Axes::splat(false)); - let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame(); + let frame = + layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame(); resolved.set_max(frame.width() - already_covered_width); } @@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> { }; let frames = - cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); + layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames(); // Skip the first region if one cell in it is empty. Then, // remeasure. @@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> { // rows. pod.full = self.regions.full; } - let frame = cell - .layout(engine, disambiguator, self.styles, pod)? - .into_frame(); + let frame = + layout_cell(cell, engine, disambiguator, self.styles, pod)? + .into_frame(); let mut pos = pos; if self.is_rtl { // In the grid, cell colspans expand to the right, @@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> { // Push the layouted frames into the individual output frames. let fragment = - cell.layout(engine, disambiguator, self.styles, pod)?; + layout_cell(cell, engine, disambiguator, self.styles, pod)?; for (output, frame) in outputs.iter_mut().zip(fragment) { let mut pos = pos; if self.is_rtl { @@ -1375,7 +1377,7 @@ impl<'a> GridLayouter<'a> { .footer .as_ref() .and_then(Repeatable::as_repeated) - .map_or(true, |footer| footer.start != header.end) + .is_none_or(|footer| footer.start != header.end) && self.lrows.last().is_some_and(|row| row.index() < header.end) && !in_last_with_offset( self.regions, @@ -1444,7 +1446,7 @@ impl<'a> GridLayouter<'a> { .iter_mut() .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) .filter(|rowspan| { - rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) + rowspan.max_resolved_row.is_none_or(|max_row| y > max_row) }) { // If the first region wasn't defined yet, it will have the @@ -1492,7 +1494,7 @@ impl<'a> GridLayouter<'a> { // laid out at the first frame of the row). // Any rowspans ending before this row are laid out even // on this row's first frame. - if laid_out_footer_start.map_or(true, |footer_start| { + if laid_out_footer_start.is_none_or(|footer_start| { // If this is a footer row, then only lay out this rowspan // if the rowspan is contained within the footer. y < footer_start || rowspan.y >= footer_start @@ -1578,5 +1580,5 @@ pub(super) fn points( /// our case, headers). pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { regions.backlog.is_empty() - && regions.last.map_or(true, |height| regions.size.y + offset == height) + && regions.last.is_none_or(|height| regions.size.y + offset == height) } diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 3e89612a1..7549673f1 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -1,41 +1,11 @@ -use std::num::NonZeroUsize; use std::sync::Arc; use typst_library::foundations::{AlternativeFold, Fold}; +use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable}; use typst_library::layout::Abs; use typst_library::visualize::Stroke; -use super::{CellGrid, LinePosition, Repeatable, RowPiece}; - -/// Represents an explicit grid line (horizontal or vertical) specified by the -/// user. -pub struct Line { - /// The index of the track after this line. This will be the index of the - /// row a horizontal line is above of, or of the column right after a - /// vertical line. - /// - /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols` - /// or `grid.rows`, ignoring gutter tracks, as appropriate). - pub index: usize, - /// The index of the track at which this line starts being drawn. - /// This is the first column a horizontal line appears in, or the first row - /// a vertical line appears in. - /// - /// Must be within `0..tracks.len()` minus gutter tracks. - pub start: usize, - /// The index after the last track through which the line is drawn. - /// Thus, the line is drawn through tracks `start..end` (note that `end` is - /// exclusive). - /// - /// Must be within `1..=tracks.len()` minus gutter tracks. - /// `None` indicates the line should go all the way to the end. - pub end: Option, - /// The line's stroke. This is `None` when the line is explicitly used to - /// override a previously specified line. - pub stroke: Option>>, - /// The line's position in relation to the track with its index. - pub position: LinePosition, -} +use super::RowPiece; /// Indicates which priority a particular grid line segment should have, based /// on the highest priority configuration that defined the segment's stroke. @@ -493,7 +463,7 @@ pub fn hline_stroke_at_column( // region, we have the last index, and (as a failsafe) we don't have the // last row of cells above us. let use_bottom_border_stroke = !in_last_region - && local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) + && local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len()) && y == grid.rows.len(); let bottom_y = if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; @@ -588,13 +558,13 @@ pub fn hline_stroke_at_column( #[cfg(test)] mod test { + use std::num::NonZeroUsize; use typst_library::foundations::Content; use typst_library::introspection::Locator; + use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition}; use typst_library::layout::{Axes, Sides, Sizing}; use typst_utils::NonZeroExt; - use super::super::cells::Entry; - use super::super::Cell; use super::*; fn sample_cell() -> Cell<'static> { diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs index 769bef8c5..1b4380f0a 100644 --- a/crates/typst-layout/src/grid/mod.rs +++ b/crates/typst-layout/src/grid/mod.rs @@ -1,40 +1,44 @@ -mod cells; mod layouter; mod lines; mod repeated; mod rowspans; -pub use self::cells::{Cell, CellGrid}; pub use self::layouter::GridLayouter; -use std::num::NonZeroUsize; -use std::sync::Arc; - -use ecow::eco_format; -use typst_library::diag::{SourceResult, Trace, Tracepoint}; +use typst_library::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::foundations::{Fold, Packed, Smart, StyleChain}; +use typst_library::foundations::{Packed, StyleChain}; use typst_library::introspection::Locator; -use typst_library::layout::{ - Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length, - OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, -}; -use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; -use typst_library::text::TextElem; -use typst_library::visualize::{Paint, Stroke}; -use typst_syntax::Span; +use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell}; +use typst_library::layout::{Fragment, GridElem, Regions}; +use typst_library::model::TableElem; -use self::cells::{ - LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem, -}; use self::layouter::RowPiece; use self::lines::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line, - LineSegment, + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment, }; -use self::repeated::{Footer, Header, Repeatable}; use self::rowspans::{Rowspan, UnbreakableRowGroup}; +/// Layout the cell into the given regions. +/// +/// The `disambiguator` indicates which instance of this cell this should be +/// layouted as. For normal cells, it is always `0`, but for headers and +/// footers, it indicates the index of the header/footer among all. See the +/// [`Locator`] docs for more details on the concepts behind this. +pub fn layout_cell( + cell: &Cell, + engine: &mut Engine, + disambiguator: usize, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + let mut locator = cell.locator.relayout(); + if disambiguator > 0 { + locator = locator.split().next_inner(disambiguator as u128); + } + crate::layout_fragment(engine, &cell.body, locator, styles, regions) +} + /// Layout the grid. #[typst_macros::time(span = elem.span())] pub fn layout_grid( @@ -44,54 +48,8 @@ pub fn layout_grid( styles: StyleChain, regions: Regions, ) -> SourceResult { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the grid when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); - let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { - GridChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - GridChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - GridChild::Item(item) => { - ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) - } - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - - // Measure the columns and layout the grid row-by-row. - layouter.layout(engine) + let grid = grid_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } /// Layout the table. @@ -103,314 +61,6 @@ pub fn layout_table( styles: StyleChain, regions: Regions, ) -> SourceResult { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the table when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); - let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { - TableChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - TableChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - TableChild::Item(item) => { - ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) - } - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - layouter.layout(engine) -} - -fn grid_item_to_resolvable( - item: &GridItem, - styles: StyleChain, -) -> ResolvableGridItem> { - match item { - GridItem::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - GridItem::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } -} - -fn table_item_to_resolvable( - item: &TableItem, - styles: StyleChain, -) -> ResolvableGridItem> { - match item { - TableItem::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - TableItem::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } -} - -impl ResolvableCell for Packed { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the table is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on table cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } -} - -impl ResolvableCell for Packed { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the grid is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on grid cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } + let grid = table_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 8d08d56db..22d2a09ef 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,50 +1,11 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; +use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; use typst_library::layout::{Abs, Axes, Frame, Regions}; use super::layouter::GridLayouter; use super::rowspans::UnbreakableRowGroup; -/// A repeatable grid header. Starts at the first row. -pub struct Header { - /// The index after the last row included in this header. - pub end: usize, -} - -/// A repeatable grid footer. Stops at the last row. -pub struct Footer { - /// The first row included in this footer. - pub start: usize, -} - -/// A possibly repeatable grid object. -/// It still exists even when not repeatable, but must not have additional -/// considerations by grid layout, other than for consistency (such as making -/// a certain group of rows unbreakable). -pub enum Repeatable { - Repeated(T), - NotRepeated(T), -} - -impl Repeatable { - /// Gets the value inside this repeatable, regardless of whether - /// it repeats. - pub fn unwrap(&self) -> &T { - match self { - Self::Repeated(repeated) => repeated, - Self::NotRepeated(not_repeated) => not_repeated, - } - } - - /// Returns `Some` if the value is repeated, `None` otherwise. - pub fn as_repeated(&self) -> Option<&T> { - match self { - Self::Repeated(repeated) => Some(repeated), - Self::NotRepeated(_) => None, - } - } -} - impl GridLayouter<'_> { /// Layouts the header's rows. /// Skips regions as necessary. diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 93d4c960d..21992ed02 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -1,12 +1,12 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::Resolve; +use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_utils::MaybeReverseIter; use super::layouter::{in_last_with_offset, points, Row, RowPiece}; -use super::repeated::Repeatable; -use super::{Cell, GridLayouter}; +use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. pub struct Rowspan { @@ -141,7 +141,7 @@ impl GridLayouter<'_> { } // Push the layouted frames directly into the finished frames. - let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; + let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; let (current_region, current_rrows) = current_region_data.unzip(); for ((i, finished), frame) in self .finished @@ -588,7 +588,7 @@ impl GridLayouter<'_> { measurement_data: &CellMeasurementData<'_>, ) -> bool { if sizes.len() <= 1 - && sizes.first().map_or(true, |&first_frame_size| { + && sizes.first().is_none_or(|&first_frame_size| { first_frame_size <= measurement_data.height_in_this_region }) { diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index f44d68873..3e5b7d8bd 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -1,16 +1,17 @@ use std::ffi::OsStr; -use typst_library::diag::{bail, warning, At, SourceResult, StrResult}; +use typst_library::diag::{warning, At, SourceResult, StrResult}; use typst_library::engine::Engine; -use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, }; -use typst_library::loading::Readable; +use typst_library::loading::DataSource; use typst_library::text::families; use typst_library::visualize::{ - Image, ImageElem, ImageFit, ImageFormat, Path, RasterFormat, VectorFormat, + Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind, + RasterImage, SvgImage, VectorFormat, }; /// Layout the image. @@ -26,17 +27,17 @@ pub fn layout_image( // Take the format that was explicitly defined, or parse the extension, // or try to detect the format. - let data = elem.data(); + let Derived { source, derived: data } = &elem.source; let format = match elem.format(styles) { Smart::Custom(v) => v, - Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?, + Smart::Auto => determine_format(source, data).at(span)?, }; // Warn the user if the image contains a foreign object. Not perfect // because the svg could also be encoded, but that's an edge case. if format == ImageFormat::Vector(VectorFormat::Svg) { let has_foreign_object = - data.as_str().is_some_and(|s| s.contains(">(), - elem.flatten_text(styles), - ) - .at(span)?; + let kind = match format { + ImageFormat::Raster(format) => ImageKind::Raster( + RasterImage::new( + data.clone(), + format, + elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), + ) + .at(span)?, + ), + ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( + SvgImage::with_fonts( + data.clone(), + engine.world, + &families(styles).map(|f| f.as_str()).collect::>(), + ) + .at(span)?, + ), + }; + + let image = Image::new(kind, elem.alt(styles), elem.scaling(styles)); // Determine the image's pixel aspect ratio. let pxw = image.width(); @@ -83,6 +95,8 @@ pub fn layout_image( } else { // If neither is forced, take the natural image size at the image's // DPI bounded by the available space. + // + // Division by DPI is fine since it's guaranteed to be positive. let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); Size::new( @@ -113,31 +127,29 @@ pub fn layout_image( // Create a clipping group if only part of the image should be visible. if fit == ImageFit::Cover && !target.fits(fitted) { - frame.clip(Path::rect(frame.size())); + frame.clip(Curve::rect(frame.size())); } Ok(frame) } -/// Determine the image format based on path and data. -fn determine_format(path: &str, data: &Readable) -> StrResult { - let ext = std::path::Path::new(path) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); +/// Try to determine the image format based on the data. +fn determine_format(source: &DataSource, data: &Bytes) -> StrResult { + if let DataSource::Path(path) = source { + let ext = std::path::Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); - Ok(match ext.as_str() { - "png" => ImageFormat::Raster(RasterFormat::Png), - "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), - "gif" => ImageFormat::Raster(RasterFormat::Gif), - "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), - _ => match &data { - Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), - Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { - Some(f) => ImageFormat::Raster(f), - None => bail!("unknown image format"), - }, - }, - }) + match ext.as_str() { + "png" => return Ok(ExchangeFormat::Png.into()), + "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), + "gif" => return Ok(ExchangeFormat::Gif.into()), + "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), + _ => {} + } + } + + Ok(ImageFormat::detect(data).ok_or("unknown image format")?) } diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs index 6dfbc9696..e21928d3c 100644 --- a/crates/typst-layout/src/inline/box.rs +++ b/crates/typst-layout/src/inline/box.rs @@ -11,7 +11,7 @@ use typst_utils::Numeric; use crate::flow::unbreakable_pod; use crate::shapes::{clip_rect, fill_and_stroke}; -/// Lay out a box as part of a paragraph. +/// Lay out a box as part of inline layout. #[typst_macros::time(name = "box", span = elem.span())] pub fn layout_box( elem: &Packed, diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 23e82c417..5a1b7b4fc 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -1,10 +1,10 @@ -use typst_library::diag::bail; +use typst_library::diag::warning; use typst_library::foundations::{Packed, Resolve}; use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::layout::{ - Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, - Spacing, + Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; +use typst_library::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, @@ -13,9 +13,10 @@ use typst_syntax::Span; use typst_utils::Numeric; use super::*; +use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify}; // The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. +// full text. const SPACING_REPLACE: &str = " "; // Space const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character @@ -26,7 +27,7 @@ const POP_EMBEDDING: &str = "\u{202C}"; const LTR_ISOLATE: &str = "\u{2066}"; const POP_ISOLATE: &str = "\u{2069}"; -/// A prepared item in a paragraph layout. +/// A prepared item in a inline layout. #[derive(Debug)] pub enum Item<'a> { /// A shaped text run with consistent style and direction. @@ -36,7 +37,7 @@ pub enum Item<'a> { /// Fractional spacing between other items. Fractional(Fr, Option<(&'a Packed, Locator<'a>, StyleChain<'a>)>), /// Layouted inline-level content. - Frame(Frame, StyleChain<'a>), + Frame(Frame), /// A tag. Tag(&'a Tag), /// An item that is invisible and needs to be skipped, e.g. a Unicode @@ -67,7 +68,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.text, Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE, - Self::Frame(_, _) => OBJ_REPLACE, + Self::Frame(_) => OBJ_REPLACE, Self::Tag(_) => "", Self::Skip(s) => s, } @@ -83,7 +84,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.width, Self::Absolute(v, _) => *v, - Self::Frame(frame, _) => frame.width(), + Self::Frame(frame) => frame.width(), Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Skip(_) => Abs::zero(), } @@ -112,38 +113,31 @@ impl Segment<'_> { } } -/// Collects all text of the paragraph into one string and a collection of -/// segments that correspond to pieces of that string. This also performs -/// string-level preprocessing like case transformations. +/// Collects all text into one string and a collection of segments that +/// correspond to pieces of that string. This also performs string-level +/// preprocessing like case transformations. #[typst_macros::time] pub fn collect<'a>( - children: &'a StyleVec, + children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: &'a StyleChain<'a>, + config: &Config, region: Size, - consecutive: bool, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); let mut quoter = SmartQuoter::new(); - let outer_dir = TextElem::dir_in(*styles); - let first_line_indent = ParElem::first_line_indent_in(*styles); - if !first_line_indent.is_zero() - && consecutive - && AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into() - { - collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false)); + if !config.first_line_indent.is_zero() { + collector.push_item(Item::Absolute(config.first_line_indent, false)); collector.spans.push(1, Span::detached()); } - let hang = ParElem::hanging_indent_in(*styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); + if !config.hanging_indent.is_zero() { + collector.push_item(Item::Absolute(-config.hanging_indent, false)); collector.spans.push(1, Span::detached()); } - for (child, styles) in children.iter(styles) { + for &(child, styles) in children { let prev_len = collector.full.len(); if child.is::() { @@ -151,7 +145,7 @@ pub fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.build_text(styles, |full| { let dir = TextElem::dir_in(styles); - if dir != outer_dir { + if dir != config.dir { // Insert "Explicit Directional Embedding". match dir { Dir::LTR => full.push_str(LTR_EMBEDDING), @@ -161,24 +155,23 @@ pub fn collect<'a>( } if let Some(case) = TextElem::case_in(styles) { - full.push_str(&case.apply(elem.text())); + full.push_str(&case.apply(&elem.text)); } else { - full.push_str(elem.text()); + full.push_str(&elem.text); } - if dir != outer_dir { + if dir != config.dir { // Insert "Pop Directional Formatting". full.push_str(POP_EMBEDDING); } }); } else if let Some(elem) = child.to_packed::() { - let amount = elem.amount(); - if amount.is_zero() { + if elem.amount.is_zero() { continue; } - collector.push_item(match amount { - Spacing::Fr(fr) => Item::Fractional(*fr, None), + collector.push_item(match elem.amount { + Spacing::Fr(fr) => Item::Fractional(fr, None), Spacing::Rel(rel) => Item::Absolute( rel.resolve(styles).relative_to(region.x), elem.weak(styles), @@ -211,8 +204,10 @@ pub fn collect<'a>( InlineItem::Space(space, weak) => { collector.push_item(Item::Absolute(space, weak)); } - InlineItem::Frame(frame) => { - collector.push_item(Item::Frame(frame, styles)); + InlineItem::Frame(mut frame) => { + frame.modify(&FrameModifiers::get_in(styles)); + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } } @@ -223,13 +218,22 @@ pub fn collect<'a>( if let Sizing::Fr(v) = elem.width(styles) { collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } else { - let frame = layout_box(elem, engine, loc, styles, region)?; - collector.push_item(Item::Frame(frame, styles)); + let mut frame = layout_and_modify(styles, |styles| { + layout_box(elem, engine, loc, styles, region) + })?; + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } else if let Some(elem) = child.to_packed::() { collector.push_item(Item::Tag(&elem.tag)); } else { - bail!(child.span(), "unexpected paragraph child"); + // Non-paragraph inline layout should never trigger this since it + // only won't be triggered if we see any non-inline content. + engine.sink.warn(warning!( + child.span(), + "{} may not occur inside of a paragraph and was ignored", + child.func().name() + )); }; let len = collector.full.len() - prev_len; diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 57044f0ec..c9de0085e 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -9,19 +9,19 @@ pub fn finalize( engine: &mut Engine, p: &Preparation, lines: &[Line], - styles: StyleChain, region: Size, expand: bool, locator: &mut SplitLocator<'_>, ) -> SourceResult { - // Determine the paragraph's width: Full width of the region if we should + // Determine the resulting width: Full width of the region if we should // expand or there's fractional spacing, fit-to-width otherwise. let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) { - region - .x - .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + region.x.min( + p.config.hanging_indent + + lines.iter().map(|line| line.width).max().unwrap_or_default(), + ) } else { region.x }; @@ -29,7 +29,7 @@ pub fn finalize( // Stack the lines into one frame per region. lines .iter() - .map(|line| commit(engine, p, line, width, region.y, locator, styles)) + .map(|line| commit(engine, p, line, width, region.y, locator)) .collect::>() .map(Fragment::frames) } diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index ef7e26c3c..659d33f4a 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -2,14 +2,14 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; use typst_library::engine::Engine; -use typst_library::foundations::NativeElement; use typst_library::introspection::{SplitLocator, Tag}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; -use typst_library::model::{ParLine, ParLineMarker}; +use typst_library::model::ParLineMarker; use typst_library::text::{Lang, TextElem}; use typst_utils::Numeric; use super::*; +use crate::modifiers::layout_and_modify; const SHY: char = '\u{ad}'; const HYPHEN: char = '-'; @@ -17,12 +17,12 @@ const EN_DASH: char = '–'; const EM_DASH: char = '—'; const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks. -/// A layouted line, consisting of a sequence of layouted paragraph items that -/// are mostly borrowed from the preparation phase. This type enables you to -/// measure the size of a line in a range before committing to building the -/// line's frame. +/// A layouted line, consisting of a sequence of layouted inline items that are +/// mostly borrowed from the preparation phase. This type enables you to measure +/// the size of a line in a range before committing to building the line's +/// frame. /// -/// At most two paragraph items must be created individually for this line: The +/// At most two inline items must be created individually for this line: The /// first and last one since they may be broken apart by the start or end of the /// line, respectively. But even those can partially reuse previous results when /// the break index is safe-to-break per rustybuzz. @@ -93,7 +93,7 @@ impl Line<'_> { pub fn has_negative_width_items(&self) -> bool { self.items.iter().any(|item| match item { Item::Absolute(amount, _) => *amount < Abs::zero(), - Item::Frame(frame, _) => frame.width() < Abs::zero(), + Item::Frame(frame) => frame.width() < Abs::zero(), _ => false, }) } @@ -134,7 +134,7 @@ pub fn line<'a>( // Whether the line is justified. let justify = full.ends_with(LINE_SEPARATOR) - || (p.justify && breakpoint != Breakpoint::Mandatory); + || (p.config.justify && breakpoint != Breakpoint::Mandatory); // Process dashes. let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { @@ -154,16 +154,16 @@ pub fn line<'a>( let mut items = collect_items(engine, p, range, trim); // Add a hyphen at the line start, if a previous dash should be repeated. - if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { + if pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) { if let Some(shaped) = items.first_text_mut() { - shaped.prepend_hyphen(engine, p.fallback); + shaped.prepend_hyphen(engine, p.config.fallback); } } // Add a hyphen at the line end, if we ended on a soft hyphen. if dash == Some(Dash::Soft) { if let Some(shaped) = items.last_text_mut() { - shaped.push_hyphen(engine, p.fallback); + shaped.push_hyphen(engine, p.config.fallback); } } @@ -233,13 +233,13 @@ where { // If there is nothing bidirectional going on, skip reordering. let Some(bidi) = &p.bidi else { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; }; // The bidi crate panics for empty lines. if range.is_empty() { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; } @@ -307,13 +307,13 @@ fn collect_range<'a>( /// punctuation marks at line start or line end. fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { if text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script)) { adjust_cj_at_line_start(p, items); } if text.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script)) { adjust_cj_at_line_end(p, items); } @@ -331,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { let shrink = glyph.shrinkability().0; glyph.shrink_left(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { + } else if p.config.cjk_latin_spacing + && glyph.is_cj_script() + && glyph.x_offset > Em::zero() + { // If the first glyph is a CJK character adjusted by // [`add_cjk_latin_spacing`], restore the original width. let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); @@ -358,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { let punct = shaped.glyphs.to_mut().last_mut().unwrap(); punct.shrink_right(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing + } else if p.config.cjk_latin_spacing && glyph.is_cj_script() && (glyph.x_advance - glyph.x_offset) > Em::one() { @@ -403,12 +406,17 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { // // See § 4.1.1.1.2.e on the "Ortografía de la lengua española" // https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea - Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()), + Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()), _ => false, } } +/// Apply the current baseline shift to a frame. +pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) { + frame.translate(Point::with_y(TextElem::baseline_in(styles))); +} + /// Commit to a line and build its frame. #[allow(clippy::too_many_arguments)] pub fn commit( @@ -418,16 +426,15 @@ pub fn commit( width: Abs, full: Abs, locator: &mut SplitLocator<'_>, - styles: StyleChain, ) -> SourceResult { - let mut remaining = width - line.width - p.hang; + let mut remaining = width - line.width - p.config.hanging_indent; let mut offset = Abs::zero(); // We always build the line from left to right. In an LTR paragraph, we must - // thus add the hanging indent to the offset. When the paragraph is RTL, the + // thus add the hanging indent to the offset. In an RTL paragraph, the // hanging indent arises naturally due to the line width. - if p.dir == Dir::LTR { - offset += p.hang; + if p.config.dir == Dir::LTR { + offset += p.config.hanging_indent; } // Handle hanging punctuation to the left. @@ -509,10 +516,11 @@ pub fn commit( let amount = v.share(fr, remaining); if let Some((elem, loc, styles)) = elem { let region = Size::new(amount, full); - let mut frame = - layout_box(elem, engine, loc.relayout(), *styles, region)?; - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + let mut frame = layout_and_modify(*styles, |styles| { + layout_box(elem, engine, loc.relayout(), styles, region) + })?; + apply_baseline_shift(&mut frame, *styles); + push(&mut offset, frame); } else { offset += amount; } @@ -524,12 +532,10 @@ pub fn commit( justification_ratio, extra_justification, ); - push(&mut offset, frame.post_processed(shaped.styles)); + push(&mut offset, frame); } - Item::Frame(frame, styles) => { - let mut frame = frame.clone(); - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + Item::Frame(frame) => { + push(&mut offset, frame.clone()); } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); @@ -549,11 +555,13 @@ pub fn commit( let mut output = Frame::soft(size); output.set_baseline(top); - add_par_line_marker(&mut output, styles, engine, locator, top); + if let Some(marker) = &p.config.numbering_marker { + add_par_line_marker(&mut output, marker, engine, locator, top); + } // Construct the line's frame. for (offset, frame) in frames { - let x = offset + p.align.position(remaining); + let x = offset + p.config.align.position(remaining); let y = top - frame.baseline(); output.push_frame(Point::new(x, y), frame); } @@ -570,26 +578,18 @@ pub fn commit( /// number in the margin, is aligned to the line's baseline. fn add_par_line_marker( output: &mut Frame, - styles: StyleChain, + marker: &Packed, engine: &mut Engine, locator: &mut SplitLocator, top: Abs, ) { - let Some(numbering) = ParLine::numbering_in(styles) else { return }; - let margin = ParLine::number_margin_in(styles); - let align = ParLine::number_align_in(styles); - - // Delay resolving the number clearance until line numbers are laid out to - // avoid inconsistent spacing depending on varying font size. - let clearance = ParLine::number_clearance_in(styles); - // Elements in tags must have a location for introspection to work. We do // the work here instead of going through all of the realization process // just for this, given we don't need to actually place the marker as we // manually search for it in the frame later (when building a root flow, // where line numbers can be displayed), so we just need it to be in a tag // and to be valid (to have a location). - let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); + let mut marker = marker.clone(); let key = typst_utils::hash128(&marker); let loc = locator.next_location(engine.introspector, key); marker.set_location(loc); @@ -601,7 +601,7 @@ fn add_par_line_marker( // line's general baseline. However, the line number will still need to // manually adjust its own 'y' position based on its own baseline. let pos = Point::with_y(top); - output.push(pos, FrameItem::Tag(Tag::Start(marker))); + output.push(pos, FrameItem::Tag(Tag::Start(marker.pack()))); output.push(pos, FrameItem::Tag(Tag::End(loc, key))); } @@ -626,7 +626,7 @@ fn overhang(c: char) -> f64 { } } -/// A collection of owned or borrowed paragraph items. +/// A collection of owned or borrowed inline items. pub struct Items<'a>(Vec>); impl<'a> Items<'a> { diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 7b66fcdb4..31512604f 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::*; -/// The cost of a line or paragraph layout. +/// The cost of a line or inline layout. type Cost = f64; // Cost parameters. @@ -104,21 +104,13 @@ impl Breakpoint { } } -/// Breaks the paragraph into lines. +/// Breaks the text into lines. pub fn linebreak<'a>( engine: &Engine, p: &'a Preparation<'a>, width: Abs, ) -> Vec> { - let linebreaks = p.linebreaks.unwrap_or_else(|| { - if p.justify { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }); - - match linebreaks { + match p.config.linebreaks { Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width), } @@ -181,13 +173,12 @@ fn linebreak_simple<'a>( /// lines with hyphens even more. /// /// To find the layout with the minimal total cost the algorithm uses dynamic -/// programming: For each possible breakpoint it determines the optimal -/// paragraph layout _up to that point_. It walks over all possible start points -/// for a line ending at that point and finds the one for which the cost of the -/// line plus the cost of the optimal paragraph up to the start point (already -/// computed and stored in dynamic programming table) is minimal. The final -/// result is simply the layout determined for the last breakpoint at the end of -/// text. +/// programming: For each possible breakpoint, it determines the optimal layout +/// _up to that point_. It walks over all possible start points for a line +/// ending at that point and finds the one for which the cost of the line plus +/// the cost of the optimal layout up to the start point (already computed and +/// stored in dynamic programming table) is minimal. The final result is simply +/// the layout determined for the last breakpoint at the end of text. #[typst_macros::time] fn linebreak_optimized<'a>( engine: &Engine, @@ -215,7 +206,7 @@ fn linebreak_optimized_bounded<'a>( metrics: &CostMetrics, upper_bound: Cost, ) -> Vec> { - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry<'a> { pred: usize, total: Cost, @@ -299,7 +290,7 @@ fn linebreak_optimized_bounded<'a>( } // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { + if best.as_ref().is_none_or(|best| best.total >= total) { best = Some(Entry { pred: pred_index, total, line: attempt, end }); } } @@ -321,7 +312,7 @@ fn linebreak_optimized_bounded<'a>( // This should only happen if our bound was faulty. Which shouldn't happen! if table[idx].end != p.text.len() { #[cfg(debug_assertions)] - panic!("bounded paragraph layout is incomplete"); + panic!("bounded inline layout is incomplete"); #[cfg(not(debug_assertions))] return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY); @@ -342,7 +333,7 @@ fn linebreak_optimized_bounded<'a>( /// (which is costly) to determine costs, it determines approximate costs using /// cumulative arrays. /// -/// This results in a likely good paragraph layouts, for which we then compute +/// This results in a likely good inline layouts, for which we then compute /// the exact cost. This cost is an upper bound for proper optimized /// linebreaking. We can use it to heavily prune the search space. #[typst_macros::time] @@ -355,7 +346,7 @@ fn linebreak_optimized_approximate( // Determine the cumulative estimation metrics. let estimates = Estimates::compute(p); - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry { pred: usize, total: Cost, @@ -385,7 +376,7 @@ fn linebreak_optimized_approximate( // Whether the line is justified. This is not 100% accurate w.r.t // to line()'s behaviour, but good enough. - let justify = p.justify && breakpoint != Breakpoint::Mandatory; + let justify = p.config.justify && breakpoint != Breakpoint::Mandatory; // We don't really know whether the line naturally ends with a dash // here, so we can miss that case, but it's ok, since all of this @@ -432,7 +423,7 @@ fn linebreak_optimized_approximate( let total = pred.total + line_cost; // If this attempt is better than what we had before, take it! - if best.as_ref().map_or(true, |best| best.total >= total) { + if best.as_ref().is_none_or(|best| best.total >= total) { best = Some(Entry { pred: pred_index, total, @@ -574,7 +565,7 @@ fn raw_ratio( // calculate the extra amount. Also, don't divide by zero. let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; // Normalize the amount by half the em size. - ratio = 1.0 + extra_stretch / (p.size / 2.0); + ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0); } // The min value must be < MIN_RATIO, but how much smaller doesn't matter @@ -664,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { return; } - let hyphenate = p.hyphenate != Some(false); + let hyphenate = p.config.hyphenate != Some(false); let lb = LINEBREAK_DATA.as_borrowed(); - let segmenter = match p.lang { + let segmenter = match p.config.lang { Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, _ => &SEGMENTER, }; @@ -831,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { /// Whether hyphenation is enabled at the given offset. fn hyphenate_at(p: &Preparation, offset: usize) -> bool { - p.hyphenate - .or_else(|| { - let (_, item) = p.get(offset); - let styles = item.text()?.styles; - Some(TextElem::hyphenate_in(styles)) - }) - .unwrap_or(false) + p.config.hyphenate.unwrap_or_else(|| { + let (_, item) = p.get(offset); + match item.text() { + Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), + None => false, + } + }) } /// The text language at the given offset. fn lang_at(p: &Preparation, offset: usize) -> Option { - let lang = p.lang.or_else(|| { + let lang = p.config.lang.or_else(|| { let (_, item) = p.get(offset); let styles = item.text()?.styles; Some(TextElem::lang_in(styles)) @@ -862,17 +853,17 @@ struct CostMetrics { } impl CostMetrics { - /// Compute shared metrics for paragraph optimization. + /// Compute shared metrics for inline layout optimization. fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. - min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, - min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, + min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 }, + min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 }, // Approximate hyphen width for estimates. - approx_hyphen_width: Em::new(0.33).at(p.size), + approx_hyphen_width: Em::new(0.33).at(p.config.font_size), // Costs. - hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), - runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), + hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(), + runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(), } } diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 658e30846..5ef820d07 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,17 +13,22 @@ pub use self::box_::layout_box; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{StyleChain, StyleVec}; -use typst_library::introspection::{Introspector, Locator, LocatorLink}; -use typst_library::layout::{Fragment, Size}; -use typst_library::model::ParElem; -use typst_library::routines::Routines; +use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; +use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; +use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size}; +use typst_library::model::{ + EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker, + TermsElem, +}; +use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::text::{Costs, Lang, TextElem}; use typst_library::World; +use typst_utils::{Numeric, SliceExt}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; use self::finalize::finalize; -use self::line::{commit, line, Line}; +use self::line::{apply_baseline_shift, commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ @@ -34,18 +39,18 @@ use self::shaping::{ /// Range of a substring of text. type Range = std::ops::Range; -/// Layouts content inline. -pub fn layout_inline( +/// Layouts the paragraph. +pub fn layout_par( + elem: &Packed, engine: &mut Engine, - children: &StyleVec, locator: Locator, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + situation: ParSituation, ) -> SourceResult { - layout_inline_impl( - children, + layout_par_impl( + elem, engine.routines, engine.world, engine.introspector, @@ -54,17 +59,17 @@ pub fn layout_inline( engine.route.track(), locator.track(), styles, - consecutive, region, expand, + situation, ) } -/// The internal, memoized implementation of `layout_inline`. +/// The internal, memoized implementation of `layout_par`. #[comemo::memoize] #[allow(clippy::too_many_arguments)] -fn layout_inline_impl( - children: &StyleVec, +fn layout_par_impl( + elem: &Packed, routines: &Routines, world: Tracked, introspector: Tracked, @@ -73,12 +78,12 @@ fn layout_inline_impl( route: Tracked, locator: Tracked, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + situation: ParSituation, ) -> SourceResult { let link = LocatorLink::new(locator); - let locator = Locator::link(&link); + let mut locator = Locator::link(&link).split(); let mut engine = Engine { routines, world, @@ -88,18 +93,227 @@ fn layout_inline_impl( route: Route::extend(route), }; - let mut locator = locator.split(); + let arenas = Arenas::default(); + let children = (engine.routines.realize)( + RealizationKind::LayoutPar, + &mut engine, + &mut locator, + &arenas, + &elem.body, + styles, + )?; + + layout_inline_impl( + &mut engine, + &children, + &mut locator, + styles, + region, + expand, + Some(situation), + &ConfigBase { + justify: elem.justify(styles), + linebreaks: elem.linebreaks(styles), + first_line_indent: elem.first_line_indent(styles), + hanging_indent: elem.hanging_indent(styles), + }, + ) +} + +/// Lays out realized content with inline layout. +pub fn layout_inline<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, + region: Size, + expand: bool, +) -> SourceResult { + layout_inline_impl( + engine, + children, + locator, + shared, + region, + expand, + None, + &ConfigBase { + justify: ParElem::justify_in(shared), + linebreaks: ParElem::linebreaks_in(shared), + first_line_indent: ParElem::first_line_indent_in(shared), + hanging_indent: ParElem::hanging_indent_in(shared), + }, + ) +} + +/// The internal implementation of [`layout_inline`]. +#[allow(clippy::too_many_arguments)] +fn layout_inline_impl<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, + region: Size, + expand: bool, + par: Option, + base: &ConfigBase, +) -> SourceResult { + // Prepare configuration that is shared across the whole inline layout. + let config = configuration(base, children, shared, par); // Collect all text into one string for BiDi analysis. - let (text, segments, spans) = - collect(children, &mut engine, &mut locator, &styles, region, consecutive)?; + let (text, segments, spans) = collect(children, engine, locator, &config, region)?; - // Perform BiDi analysis and then prepares paragraph layout. - let p = prepare(&mut engine, children, &text, segments, spans, styles)?; + // Perform BiDi analysis and performs some preparation steps before we + // proceed to line breaking. + let p = prepare(engine, &config, &text, segments, spans)?; - // Break the paragraph into lines. - let lines = linebreak(&engine, &p, region.x - p.hang); + // Break the text into lines. + let lines = linebreak(engine, &p, region.x - config.hanging_indent); // Turn the selected lines into frames. - finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator) + finalize(engine, &p, &lines, region, expand, locator) +} + +/// Determine the inline layout's configuration. +fn configuration( + base: &ConfigBase, + children: &[Pair], + shared: StyleChain, + situation: Option, +) -> Config { + let justify = base.justify; + let font_size = TextElem::size_in(shared); + let dir = TextElem::dir_in(shared); + + Config { + justify, + linebreaks: base.linebreaks.unwrap_or_else(|| { + if justify { + Linebreaks::Optimized + } else { + Linebreaks::Simple + } + }), + first_line_indent: { + let FirstLineIndent { amount, all } = base.first_line_indent; + if !amount.is_zero() + && match situation { + // First-line indent for the first paragraph after a list + // bullet just looks bad. + Some(ParSituation::First) => all && !in_list(shared), + Some(ParSituation::Consecutive) => true, + Some(ParSituation::Other) => all, + None => false, + } + && AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into() + { + amount.at(font_size) + } else { + Abs::zero() + } + }, + hanging_indent: if situation.is_some() { + base.hanging_indent + } else { + Abs::zero() + }, + numbering_marker: ParLine::numbering_in(shared).map(|numbering| { + Packed::new(ParLineMarker::new( + numbering, + ParLine::number_align_in(shared), + ParLine::number_margin_in(shared), + // Delay resolving the number clearance until line numbers are + // laid out to avoid inconsistent spacing depending on varying + // font size. + ParLine::number_clearance_in(shared), + )) + }), + align: AlignElem::alignment_in(shared).fix(dir).x, + font_size, + dir, + hyphenate: shared_get(children, shared, TextElem::hyphenate_in) + .map(|uniform| uniform.unwrap_or(justify)), + lang: shared_get(children, shared, TextElem::lang_in), + fallback: TextElem::fallback_in(shared), + cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(), + costs: TextElem::costs_in(shared), + } +} + +/// Distinguishes between a few different kinds of paragraphs. +/// +/// In the form `Option`, `None` implies that we are creating an +/// inline layout that isn't a semantic paragraph. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ParSituation { + /// The paragraph is the first thing in the flow. + First, + /// The paragraph follows another paragraph. + Consecutive, + /// Any other kind of paragraph. + Other, +} + +/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`]. +struct ConfigBase { + justify: bool, + linebreaks: Smart, + first_line_indent: FirstLineIndent, + hanging_indent: Abs, +} + +/// Shared configuration for the whole inline layout. +struct Config { + /// Whether to justify text. + justify: bool, + /// How to determine line breaks. + linebreaks: Linebreaks, + /// The indent the first line of a paragraph should have. + first_line_indent: Abs, + /// The indent that all but the first line of a paragraph should have. + hanging_indent: Abs, + /// Configuration for line numbering. + numbering_marker: Option>, + /// The resolved horizontal alignment. + align: FixedAlignment, + /// The text size. + font_size: Abs, + /// The dominant direction. + dir: Dir, + /// A uniform hyphenation setting (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + hyphenate: Option, + /// The text language (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + lang: Option, + /// Whether font fallback is enabled. + fallback: bool, + /// Whether to add spacing between CJK and Latin characters. + cjk_latin_spacing: bool, + /// Costs for various layout decisions. + costs: Costs, +} + +/// Get a style property, but only if it is the same for all of the children. +fn shared_get( + children: &[Pair], + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, +) -> Option { + let value = getter(styles); + children + .group_by_key(|&(_, s)| s) + .all(|(s, _)| getter(s) == value) + .then_some(value) +} + +/// Whether we have a list ancestor. +/// +/// When we support some kind of more general ancestry mechanism, this can +/// become more elegant. +fn in_list(styles: StyleChain) -> bool { + ListElem::depth_in(styles).0 > 0 + || !EnumElem::parents_in(styles).is_empty() + || TermsElem::within_in(styles) } diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 2dd79aecf..5d7fcd7cb 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,23 +1,23 @@ -use typst_library::foundations::{Resolve, Smart}; -use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; -use typst_library::model::Linebreaks; -use typst_library::text::{Costs, Lang, TextElem}; +use typst_library::layout::{Dir, Em}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; -/// A paragraph representation in which children are already layouted and text -/// is already preshaped. +/// A representation in which children are already layouted and text is already +/// preshaped. /// /// In many cases, we can directly reuse these results when constructing a line. /// Only when a line break falls onto a text index that is not safe-to-break per /// rustybuzz, we have to reshape that portion. pub struct Preparation<'a> { - /// The paragraph's full text. + /// The full text. pub text: &'a str, - /// Bidirectional text embedding levels for the paragraph. + /// Configuration for inline layout. + pub config: &'a Config, + /// Bidirectional text embedding levels. /// - /// This is `None` if the paragraph is BiDi-uniform (all the base direction). + /// This is `None` if all text directions are uniform (all the base + /// direction). pub bidi: Option>, /// Text runs, spacing and layouted elements. pub items: Vec<(Range, Item<'a>)>, @@ -25,28 +25,6 @@ pub struct Preparation<'a> { pub indices: Vec, /// The span mapper. pub spans: SpanMapper, - /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option, - /// Costs for various layout decisions. - pub costs: Costs, - /// The dominant direction. - pub dir: Dir, - /// The text language if it's the same for all children. - pub lang: Option, - /// The paragraph's resolved horizontal alignment. - pub align: FixedAlignment, - /// Whether to justify the paragraph. - pub justify: bool, - /// The paragraph's hanging indent. - pub hang: Abs, - /// Whether to add spacing between CJK and Latin characters. - pub cjk_latin_spacing: bool, - /// Whether font fallback is enabled for this paragraph. - pub fallback: bool, - /// How to determine line breaks. - pub linebreaks: Smart, - /// The text size. - pub size: Abs, } impl<'a> Preparation<'a> { @@ -71,20 +49,18 @@ impl<'a> Preparation<'a> { } } -/// Performs BiDi analysis and then prepares paragraph layout by building a +/// Performs BiDi analysis and then prepares further layout by building a /// representation on which we can do line breaking without layouting each and /// every line from scratch. #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &'a StyleVec, + config: &'a Config, text: &'a str, segments: Vec>, spans: SpanMapper, - styles: StyleChain<'a>, ) -> SourceResult> { - let dir = TextElem::dir_in(styles); - let default_level = match dir { + let default_level = match config.dir { Dir::RTL => BidiLevel::rtl(), _ => BidiLevel::ltr(), }; @@ -120,28 +96,17 @@ pub fn prepare<'a>( indices.extend(range.clone().map(|_| i)); } - let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); - if cjk_latin_spacing { + if config.cjk_latin_spacing { add_cjk_latin_spacing(&mut items); } Ok(Preparation { + config, text, bidi: is_bidi.then_some(bidi), items, indices, spans, - hyphenate: children.shared_get(styles, TextElem::hyphenate_in), - costs: TextElem::costs_in(styles), - dir, - lang: children.shared_get(styles, TextElem::lang_in), - align: AlignElem::alignment_in(styles).resolve(styles).x, - justify: ParElem::justify_in(styles), - hang: ParElem::hanging_indent_in(styles), - cjk_latin_spacing, - fallback: TextElem::fallback_in(styles), - linebreaks: ParElem::linebreaks_in(styles), - size: TextElem::size_in(styles), }) } diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index d6b7632b6..159619eb3 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -20,6 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use super::{decorate, Item, Range, SpanMapper}; +use crate::modifiers::{FrameModifiers, FrameModify}; /// The result of shaping text. /// @@ -28,7 +29,7 @@ use super::{decorate, Item, Range, SpanMapper}; /// frame. #[derive(Clone)] pub struct ShapedText<'a> { - /// The start of the text in the full paragraph. + /// The start of the text in the full text. pub base: usize, /// The text that was shaped. pub text: &'a str, @@ -65,9 +66,9 @@ pub struct ShapedGlyph { pub y_offset: Em, /// The adjustability of the glyph. pub adjustability: Adjustability, - /// The byte range of this glyph's cluster in the full paragraph. A cluster - /// is a sequence of one or multiple glyphs that cannot be separated and - /// must always be treated as a union. + /// The byte range of this glyph's cluster in the full inline layout. A + /// cluster is a sequence of one or multiple glyphs that cannot be separated + /// and must always be treated as a union. /// /// The range values of the glyphs in a [`ShapedText`] should not overlap /// with each other, and they should be monotonically increasing (for @@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> { offset += width; } + frame.modify(&FrameModifiers::get_in(self.styles)); frame } @@ -403,7 +405,7 @@ impl<'a> ShapedText<'a> { /// Reshape a range of the shaped text, reusing information from this /// shaping process if possible. /// - /// The text `range` is relative to the whole paragraph. + /// The text `range` is relative to the whole inline layout. pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> { let text = &self.text[text_range.start - self.base..text_range.end - self.base]; if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { @@ -463,7 +465,7 @@ impl<'a> ShapedText<'a> { None }; let mut chain = families(self.styles) - .filter(|family| family.covers().map_or(true, |c| c.is_match("-"))) + .filter(|family| family.covers().is_none_or(|c| c.is_match("-"))) .map(|family| book.select(family.as_str(), self.variant)) .chain(fallback_func.iter().map(|f| f())) .flatten(); @@ -568,7 +570,7 @@ impl<'a> ShapedText<'a> { // for the next line. let dec = if ltr { usize::checked_sub } else { usize::checked_add }; while let Some(next) = dec(idx, 1) { - if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { + if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) { break; } idx = next; @@ -810,7 +812,7 @@ fn shape_segment<'a>( .nth(1) .map(|(i, _)| offset + i) .unwrap_or(text.len()); - covers.map_or(true, |cov| cov.is_match(&text[offset..end])) + covers.is_none_or(|cov| cov.is_match(&text[offset..end])) }; // Collect the shaped glyphs, doing fallback and shaping parts again with diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 7069fc4dd..443e90d61 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -6,6 +6,7 @@ mod image; mod inline; mod lists; mod math; +mod modifiers; mod pad; mod pages; mod repeat; @@ -16,15 +17,14 @@ mod transforms; pub use self::flow::{layout_columns, layout_fragment, layout_frame}; pub use self::grid::{layout_grid, layout_table}; pub use self::image::layout_image; -pub use self::inline::{layout_box, layout_inline}; pub use self::lists::{layout_enum, layout_list}; pub use self::math::{layout_equation_block, layout_equation_inline}; pub use self::pad::layout_pad; pub use self::pages::layout_document; pub use self::repeat::layout_repeat; pub use self::shapes::{ - layout_circle, layout_ellipse, layout_line, layout_path, layout_polygon, layout_rect, - layout_square, + layout_circle, layout_curve, layout_ellipse, layout_line, layout_path, + layout_polygon, layout_rect, layout_square, }; pub use self::stack::layout_stack; pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew}; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 0d51a1e4e..f8d910abf 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -4,11 +4,12 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::introspection::Locator; +use typst_library::layout::grid::resolve::{Cell, CellGrid}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; -use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; +use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem}; use typst_library::text::TextElem; -use crate::grid::{Cell, CellGrid, GridLayouter}; +use crate::grid::GridLayouter; /// Layout the list. #[typst_macros::time(span = elem.span())] @@ -21,8 +22,9 @@ pub fn layout_list( ) -> SourceResult { let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -39,12 +41,18 @@ pub fn layout_list( let mut cells = vec![]; let mut locator = locator.split(); - for item in elem.children() { + for item in &elem.children { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(ListElem::set_depth(Depth(1))), + body.styled(ListElem::set_depth(Depth(1))), locator.next(&item.body.span()), )); } @@ -77,8 +85,9 @@ pub fn layout_enum( let reversed = elem.reversed(styles); let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -100,7 +109,7 @@ pub fn layout_enum( // relation to the item it refers to. let number_align = elem.number_align(styles); - for item in elem.children() { + for item in &elem.children { number = item.number(styles).unwrap_or(number); let context = Context::new(None, Some(styles)); @@ -123,11 +132,17 @@ pub fn layout_enum( let resolved = resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(EnumElem::set_parents(smallvec![number])), + body.styled(EnumElem::set_parents(smallvec![number])), locator.next(&item.body.span()), )); number = diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 4e26502e3..f2dfa2c45 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -1,12 +1,9 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; -use typst_library::layout::{Em, Frame, Point, Rel, Size}; +use typst_library::layout::{Em, Frame, Point, Size}; use typst_library::math::{Accent, AccentElem}; -use super::{ - scaled_font_size, style_cramped, FrameFragment, GlyphFragment, MathContext, - MathFragment, -}; +use super::{style_cramped, FrameFragment, GlyphFragment, MathContext, MathFragment}; /// How much the accent can be shorter than the base. const ACCENT_SHORT_FALL: Em = Em::new(0.5); @@ -19,7 +16,7 @@ pub fn layout_accent( styles: StyleChain, ) -> SourceResult<()> { let cramped = style_cramped(); - let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; + let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; // Try to replace a glyph with its dotless variant. if let MathFragment::Glyph(glyph) = &mut base { @@ -30,18 +27,14 @@ pub fn layout_accent( let base_class = base.class(); let base_attach = base.accent_attach(); - let width = elem - .size(styles) - .unwrap_or(Rel::one()) - .at(scaled_font_size(ctx, styles)) - .relative_to(base.width()); + let width = elem.size(styles).relative_to(base.width()); - let Accent(c) = elem.accent(); - let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); + let Accent(c) = elem.accent; + let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); // Try to replace accent glyph with flattened variant. let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); - if base.height() > flattened_base_height { + if base.ascent() > flattened_base_height { glyph.make_flattened_accent_form(ctx); } @@ -57,7 +50,7 @@ pub fn layout_accent( // minus the accent base height. Only if the base is very small, we need // a larger gap so that the accent doesn't move too low. let accent_base_height = scaled!(ctx, styles, accent_base_height); - let gap = -accent.descent() - base.height().min(accent_base_height); + let gap = -accent.descent() - base.ascent().min(accent_base_height); let size = Size::new(base.width(), accent.height() + gap + base.height()); let accent_pos = Point::with_x(base_attach - accent_attach); let base_pos = Point::with_y(accent.height() + gap); @@ -75,7 +68,7 @@ pub fn layout_accent( frame.push_frame(accent_pos, accent); frame.push_frame(base_pos, base.into_frame()); ctx.push( - FrameFragment::new(ctx, styles, frame) + FrameFragment::new(styles, frame) .with_class(base_class) .with_base_ascent(base_ascent) .with_italics_correction(base_italics_correction) diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 0f9090f77..e1d7d7c9d 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -1,10 +1,9 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, Smart, StyleChain}; -use typst_library::layout::{Abs, Axis, Corner, Frame, Length, Point, Rel, Size}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; +use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size}; use typst_library::math::{ AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, }; -use typst_library::text::TextElem; use typst_utils::OptionExt; use super::{ @@ -29,7 +28,7 @@ pub fn layout_attach( let elem = merged.as_ref().unwrap_or(elem); let stretch = stretch_size(styles, elem); - let mut base = ctx.layout_into_fragment(elem.base(), styles)?; + let mut base = ctx.layout_into_fragment(&elem.base, styles)?; let sup_style = style_for_superscript(styles); let sup_style_chain = styles.chain(&sup_style); let tl = elem.tl(sup_style_chain); @@ -95,7 +94,7 @@ pub fn layout_primes( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - match *elem.count() { + match elem.count { count @ 1..=4 => { let c = match count { 1 => '′', @@ -104,13 +103,14 @@ pub fn layout_primes( 4 => '⁗', _ => unreachable!(), }; - let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; + let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; ctx.push(f); } count => { // Custom amount of primes - let prime = - ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame(); + let prime = ctx + .layout_into_fragment(&SymbolElem::packed('′'), styles)? + .into_frame(); let width = prime.width() * (count + 1) as f64 / 2.0; let mut frame = Frame::soft(Size::new(width, prime.height())); frame.set_baseline(prime.ascent()); @@ -121,7 +121,7 @@ pub fn layout_primes( prime.clone(), ) } - ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); + ctx.push(FrameFragment::new(styles, frame).with_text_like(true)); } } Ok(()) @@ -134,7 +134,7 @@ pub fn layout_scripts( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(Limits::Never); ctx.push(fragment); Ok(()) @@ -148,21 +148,18 @@ pub fn layout_limits( styles: StyleChain, ) -> SourceResult<()> { let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(limits); ctx.push(fragment); Ok(()) } -/// Get the size to stretch the base to, if the attach argument is true. -fn stretch_size( - styles: StyleChain, - elem: &Packed, -) -> Option>> { +/// Get the size to stretch the base to. +fn stretch_size(styles: StyleChain, elem: &Packed) -> Option> { // Extract from an EquationElem. - let mut base = elem.base(); - if let Some(equation) = base.to_packed::() { - base = equation.body(); + let mut base = &elem.base; + while let Some(equation) = base.to_packed::() { + base = &equation.body; } base.to_packed::().map(|stretch| stretch.size(styles)) @@ -277,7 +274,7 @@ fn layout_attachments( layout!(b, b_x, b_y); // lower-limit // Done! Note that we retain the class of the base. - ctx.push(FrameFragment::new(ctx, styles, frame).with_class(base_class)); + ctx.push(FrameFragment::new(styles, frame).with_class(base_class)); Ok(()) } diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs index 994e0e2f7..9826397fa 100644 --- a/crates/typst-layout/src/math/cancel.rs +++ b/crates/typst-layout/src/math/cancel.rs @@ -7,7 +7,7 @@ use typst_library::text::TextElem; use typst_library::visualize::{FixedStroke, Geometry}; use typst_syntax::Span; -use super::{scaled_font_size, FrameFragment, MathContext}; +use super::{FrameFragment, MathContext}; /// Lays out a [`CancelElem`]. #[typst_macros::time(name = "math.cancel", span = elem.span())] @@ -16,7 +16,7 @@ pub fn layout_cancel( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let body = ctx.layout_into_fragment(elem.body(), styles)?; + let body = ctx.layout_into_fragment(&elem.body, styles)?; // Preserve properties of body. let body_class = body.class(); @@ -27,7 +27,7 @@ pub fn layout_cancel( let mut body = body.into_frame(); let body_size = body.size(); let span = elem.span(); - let length = elem.length(styles).at(scaled_font_size(ctx, styles)); + let length = elem.length(styles); let stroke = elem.stroke(styles).unwrap_or(FixedStroke { paint: TextElem::fill_in(styles).as_decoration(), @@ -63,7 +63,7 @@ pub fn layout_cancel( } ctx.push( - FrameFragment::new(ctx, styles, body) + FrameFragment::new(styles, body) .with_class(body_class) .with_italics_correction(body_italics) .with_accent_attach(body_attach) diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 50686333f..6d3caac45 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -1,5 +1,5 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Content, Packed, StyleChain}; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem}; use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; use typst_library::math::{BinomElem, FracElem}; use typst_library::text::TextElem; @@ -7,8 +7,8 @@ use typst_library::visualize::{FixedStroke, Geometry}; use typst_syntax::Span; use super::{ - scaled_font_size, style_for_denominator, style_for_numerator, FrameFragment, - GlyphFragment, MathContext, DELIM_SHORT_FALL, + style_for_denominator, style_for_numerator, FrameFragment, GlyphFragment, + MathContext, DELIM_SHORT_FALL, }; const FRAC_AROUND: Em = Em::new(0.1); @@ -23,8 +23,8 @@ pub fn layout_frac( layout_frac_like( ctx, styles, - elem.num(), - std::slice::from_ref(elem.denom()), + &elem.num, + std::slice::from_ref(&elem.denom), false, elem.span(), ) @@ -37,7 +37,7 @@ pub fn layout_binom( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) + layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span()) } /// Layout a fraction or binomial. @@ -49,8 +49,7 @@ fn layout_frac_like( binom: bool, span: Span, ) -> SourceResult<()> { - let font_size = scaled_font_size(ctx, styles); - let short_fall = DELIM_SHORT_FALL.at(font_size); + let short_fall = DELIM_SHORT_FALL.resolve(styles); let axis = scaled!(ctx, styles, axis_height); let thickness = scaled!(ctx, styles, fraction_rule_thickness); let shift_up = scaled!( @@ -81,12 +80,15 @@ fn layout_frac_like( let denom = ctx.layout_into_frame( &Content::sequence( // Add a comma between each element. - denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1), + denom + .iter() + .flat_map(|a| [SymbolElem::packed(','), a.clone()]) + .skip(1), ), styles.chain(&denom_style), )?; - let around = FRAC_AROUND.at(font_size); + let around = FRAC_AROUND.resolve(styles); let num_gap = (shift_up - (axis + thickness / 2.0) - num.descent()).max(num_min); let denom_gap = (shift_down + (axis - thickness / 2.0) - denom.ascent()).max(denom_min); @@ -111,7 +113,7 @@ fn layout_frac_like( .stretch_vertical(ctx, height, short_fall); left.center_on_axis(ctx); ctx.push(left); - ctx.push(FrameFragment::new(ctx, styles, frame)); + ctx.push(FrameFragment::new(styles, frame)); let mut right = GlyphFragment::new(ctx, styles, ')', span) .stretch_vertical(ctx, height, short_fall); right.center_on_axis(ctx); @@ -129,7 +131,7 @@ fn layout_frac_like( span, ), ); - ctx.push(FrameFragment::new(ctx, styles, frame)); + ctx.push(FrameFragment::new(styles, frame)); } Ok(()) diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index ac8946681..1b508a349 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,23 +1,23 @@ use std::fmt::{self, Debug, Formatter}; use rustybuzz::Feature; -use smallvec::SmallVec; use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::{GlyphId, Rect}; use typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ - Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, + Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, }; use typst_library::math::{EquationElem, MathSize}; -use typst_library::model::{Destination, LinkElem}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::visualize::Paint; use typst_syntax::Span; +use typst_utils::default_math_class; use unicode_math_class::MathClass; -use super::{scaled_font_size, stretch_glyph, MathContext, Scaled}; +use super::{stretch_glyph, MathContext, Scaled}; +use crate::modifiers::{FrameModifiers, FrameModify}; #[derive(Debug, Clone)] pub enum MathFragment { @@ -245,8 +245,7 @@ pub struct GlyphFragment { pub class: MathClass, pub math_size: MathSize, pub span: Span, - pub dests: SmallVec<[Destination; 1]>, - pub hidden: bool, + pub modifiers: FrameModifiers, pub limits: Limits, pub extended_shape: bool, } @@ -277,11 +276,7 @@ impl GlyphFragment { span: Span, ) -> Self { let class = EquationElem::class_in(styles) - .or_else(|| match c { - ':' => Some(MathClass::Relation), - '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), - _ => unicode_math_class::class(c), - }) + .or_else(|| default_math_class(c)) .unwrap_or(MathClass::Normal); let mut fragment = Self { @@ -292,7 +287,7 @@ impl GlyphFragment { region: TextElem::region_in(styles), fill: TextElem::fill_in(styles).as_decoration(), shift: TextElem::baseline_in(styles), - font_size: scaled_font_size(ctx, styles), + font_size: TextElem::size_in(styles), math_size: EquationElem::size_in(styles), width: Abs::zero(), ascent: Abs::zero(), @@ -302,8 +297,7 @@ impl GlyphFragment { accent_attach: Abs::zero(), class, span, - dests: LinkElem::dests_in(styles), - hidden: HideElem::hidden_in(styles), + modifiers: FrameModifiers::get_in(styles), extended_shape: false, }; fragment.set_id(ctx, id); @@ -390,7 +384,7 @@ impl GlyphFragment { let mut frame = Frame::soft(size); frame.set_baseline(self.ascent); frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); - frame.post_process_raw(self.dests, self.hidden); + frame.modify(&self.modifiers); frame } @@ -512,12 +506,12 @@ pub struct FrameFragment { } impl FrameFragment { - pub fn new(ctx: &MathContext, styles: StyleChain, frame: Frame) -> Self { + pub fn new(styles: StyleChain, frame: Frame) -> Self { let base_ascent = frame.ascent(); let accent_attach = frame.width() / 2.0; Self { - frame: frame.post_processed(styles), - font_size: scaled_font_size(ctx, styles), + frame: frame.modified(&FrameModifiers::get_in(styles)), + font_size: TextElem::size_in(styles), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), math_size: EquationElem::size_in(styles), limits: Limits::Never, @@ -632,7 +626,7 @@ pub enum Limits { impl Limits { /// The default limit configuration if the given character is the base. pub fn for_char(c: char) -> Self { - match unicode_math_class::class(c) { + match default_math_class(c) { Some(MathClass::Large) => { if is_integral_char(c) { Limits::Never diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index 01a7f4ccd..bf8235411 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -1,7 +1,8 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, Smart, StyleChain}; -use typst_library::layout::{Abs, Axis, Length, Rel}; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::layout::{Abs, Axis, Rel}; use typst_library::math::{EquationElem, LrElem, MidElem}; +use typst_utils::SliceExt; use unicode_math_class::MathClass; use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; @@ -13,32 +14,23 @@ pub fn layout_lr( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut body = elem.body(); - // Extract from an EquationElem. + let mut body = &elem.body; if let Some(equation) = body.to_packed::() { - body = equation.body(); + body = &equation.body; } // Extract implicit LrElem. if let Some(lr) = body.to_packed::() { - if lr.size(styles).is_auto() { - body = lr.body(); + if lr.size(styles).is_one() { + body = &lr.body; } } let mut fragments = ctx.layout_into_fragments(body, styles)?; // Ignore leading and trailing ignorant fragments. - let start_idx = fragments - .iter() - .position(|f| !f.is_ignorant()) - .unwrap_or(fragments.len()); - let end_idx = fragments - .iter() - .skip(start_idx) - .rposition(|f| !f.is_ignorant()) - .map_or(start_idx, |i| start_idx + i + 1); + let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant()); let inner_fragments = &mut fragments[start_idx..end_idx]; let axis = scaled!(ctx, styles, axis_height); @@ -100,7 +92,7 @@ pub fn layout_mid( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?; + let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?; for fragment in &mut fragments { match fragment { @@ -128,7 +120,7 @@ fn scale( styles: StyleChain, fragment: &mut MathFragment, relative_to: Abs, - height: Smart>, + height: Rel, apply: Option, ) { if matches!( diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index 24104f4ee..bf4929026 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -1,5 +1,5 @@ use typst_library::diag::{bail, SourceResult}; -use typst_library::foundations::{Content, Packed, StyleChain}; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::layout::{ Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, }; @@ -9,9 +9,8 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape}; use typst_syntax::Span; use super::{ - alignments, delimiter_alignment, scaled_font_size, stack, style_for_denominator, - AlignmentResult, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, - Scaled, DELIM_SHORT_FALL, + alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult, + FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, }; const VERTICAL_PADDING: Ratio = Ratio::new(0.1); @@ -28,9 +27,9 @@ pub fn layout_vec( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, elem.align(styles), - elem.gap(styles).at(scaled_font_size(ctx, styles)), + elem.gap(styles), LeftRightAlternator::Right, )?; @@ -45,7 +44,7 @@ pub fn layout_mat( styles: StyleChain, ) -> SourceResult<()> { let augment = elem.augment(styles); - let rows = elem.rows(); + let rows = &elem.rows; if let Some(aug) = &augment { for &offset in &aug.hline.0 { @@ -59,7 +58,7 @@ pub fn layout_mat( } } - let ncols = elem.rows().first().map_or(0, |row| row.len()); + let ncols = rows.first().map_or(0, |row| row.len()); for &offset in &aug.vline.0 { if offset == 0 || offset.unsigned_abs() >= ncols { @@ -73,9 +72,6 @@ pub fn layout_mat( } } - let font_size = scaled_font_size(ctx, styles); - let column_gap = elem.column_gap(styles).at(font_size); - let row_gap = elem.row_gap(styles).at(font_size); let delim = elem.delim(styles); let frame = layout_mat_body( ctx, @@ -83,7 +79,7 @@ pub fn layout_mat( rows, elem.align(styles), augment, - Axes::new(column_gap, row_gap), + Axes::new(elem.column_gap(styles), elem.row_gap(styles)), elem.span(), )?; @@ -101,9 +97,9 @@ pub fn layout_cases( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, FixedAlignment::Start, - elem.gap(styles).at(scaled_font_size(ctx, styles)), + elem.gap(styles), LeftRightAlternator::None, )?; @@ -162,8 +158,7 @@ fn layout_mat_body( // with font size to ensure that augmentation lines // look correct by default at all matrix sizes. // The line cap is also set to square because it looks more "correct". - let font_size = scaled_font_size(ctx, styles); - let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.at(font_size); + let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.resolve(styles); let default_stroke = FixedStroke { thickness: default_stroke_thickness, paint: TextElem::fill_in(styles).as_decoration(), @@ -308,9 +303,8 @@ fn layout_delimiters( right: Option, span: Span, ) -> SourceResult<()> { - let font_size = scaled_font_size(ctx, styles); - let short_fall = DELIM_SHORT_FALL.at(font_size); - let axis = ctx.constants.axis_height().scaled(ctx, font_size); + let short_fall = DELIM_SHORT_FALL.resolve(styles); + let axis = scaled!(ctx, styles, axis_height); let height = frame.height(); let target = height + VERTICAL_PADDING.of(height); frame.set_baseline(height / 2.0 + axis); @@ -322,7 +316,7 @@ fn layout_delimiters( ctx.push(left); } - ctx.push(FrameFragment::new(ctx, styles, frame)); + ctx.push(FrameFragment::new(styles, frame)); if let Some(right) = right { let mut right = GlyphFragment::new(ctx, styles, right, span) diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index e642f6338..708a4443d 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -17,7 +17,9 @@ use rustybuzz::Feature; use ttf_parser::Tag; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; -use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain}; +use typst_library::foundations::{ + Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem, +}; use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem}; use typst_library::layout::{ Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem, @@ -28,8 +30,7 @@ use typst_library::math::*; use typst_library::model::ParElem; use typst_library::routines::{Arenas, RealizationKind}; use typst_library::text::{ - families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, - TextElem, TextSize, + families, features, variant, Font, LinebreakElem, SpaceElem, TextEdgeBounds, TextElem, }; use typst_library::World; use typst_syntax::Span; @@ -58,12 +59,16 @@ pub fn layout_equation_inline( let mut locator = locator.split(); let mut ctx = MathContext::new(engine, &mut locator, styles, region, &font); + + let scale_style = style_for_script_scale(&ctx); + let styles = styles.chain(&scale_style); + let run = ctx.layout_into_run(&elem.body, styles)?; let mut items = if run.row_count() == 1 { run.into_par_items() } else { - vec![InlineItem::Frame(run.into_fragment(&ctx, styles).into_frame())] + vec![InlineItem::Frame(run.into_fragment(styles).into_frame())] }; // An empty equation should have a height, so we still create a frame @@ -75,13 +80,12 @@ pub fn layout_equation_inline( for item in &mut items { let InlineItem::Frame(frame) = item else { continue }; - let font_size = scaled_font_size(&ctx, styles); let slack = ParElem::leading_in(styles) * 0.7; let (t, b) = font.edges( TextElem::top_edge_in(styles), TextElem::bottom_edge_in(styles), - font_size, + TextElem::size_in(styles), TextEdgeBounds::Frame(frame), ); @@ -110,9 +114,13 @@ pub fn layout_equation_block( let mut locator = locator.split(); let mut ctx = MathContext::new(engine, &mut locator, styles, regions.base(), &font); + + let scale_style = style_for_script_scale(&ctx); + let styles = styles.chain(&scale_style); + let full_equation_builder = ctx .layout_into_run(&elem.body, styles)? - .multiline_frame_builder(&ctx, styles); + .multiline_frame_builder(styles); let width = full_equation_builder.size.x; let equation_builders = if BlockElem::breakable_in(styles) { @@ -194,8 +202,7 @@ pub fn layout_equation_block( let counter = Counter::of(EquationElem::elem()) .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .spanned(span); - let number = - (engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?; + let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?; static NUMBER_GUTTER: Em = Em::new(0.5); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); @@ -469,7 +476,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { elem: &Content, styles: StyleChain, ) -> SourceResult { - Ok(self.layout_into_run(elem, styles)?.into_fragment(self, styles)) + Ok(self.layout_into_run(elem, styles)?.into_fragment(styles)) } /// Layout the given element and return the result as a [`Frame`]. @@ -502,7 +509,7 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { // Hack because the font is fixed in math. if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { let frame = layout_external(elem, self, styles)?; - self.push(FrameFragment::new(self, styles, frame).with_spaced(true)); + self.push(FrameFragment::new(styles, frame).with_spaced(true)); continue; } @@ -522,14 +529,15 @@ fn layout_realized( if let Some(elem) = elem.to_packed::() { ctx.push(MathFragment::Tag(elem.tag.clone())); } else if elem.is::() { - let font_size = scaled_font_size(ctx, styles); - ctx.push(MathFragment::Space(ctx.space_width.at(font_size))); + ctx.push(MathFragment::Space(ctx.space_width.resolve(styles))); } else if elem.is::() { ctx.push(MathFragment::Linebreak); } else if let Some(elem) = elem.to_packed::() { layout_h(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { self::text::layout_text(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::text::layout_symbol(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { @@ -595,7 +603,7 @@ fn layout_realized( frame.set_baseline(frame.height() / 2.0 + axis); } ctx.push( - FrameFragment::new(ctx, styles, frame) + FrameFragment::new(styles, frame) .with_spaced(true) .with_ignorant(elem.is::()), ); @@ -610,15 +618,14 @@ fn layout_box( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap(); - let frame = (ctx.engine.routines.layout_box)( + let frame = crate::inline::layout_box( elem, ctx.engine, ctx.locator.next(&elem.span()), - styles.chain(&local), + styles, ctx.region.size, )?; - ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true)); + ctx.push(FrameFragment::new(styles, frame).with_spaced(true)); Ok(()) } @@ -628,29 +635,25 @@ fn layout_h( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - if let Spacing::Rel(rel) = elem.amount() { + if let Spacing::Rel(rel) = elem.amount { if rel.rel.is_zero() { - ctx.push(MathFragment::Spacing( - rel.abs.at(scaled_font_size(ctx, styles)), - elem.weak(styles), - )); + ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles))); } } Ok(()) } /// Lays out a [`ClassElem`]. -#[typst_macros::time(name = "math.op", span = elem.span())] +#[typst_macros::time(name = "math.class", span = elem.span())] fn layout_class( elem: &Packed, ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let class = *elem.class(); - let style = EquationElem::set_class(Some(class)).wrap(); - let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?; - fragment.set_class(class); - fragment.set_limits(Limits::for_class(class)); + let style = EquationElem::set_class(Some(elem.class)).wrap(); + let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?; + fragment.set_class(elem.class); + fragment.set_limits(Limits::for_class(elem.class)); ctx.push(fragment); Ok(()) } @@ -662,13 +665,13 @@ fn layout_op( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let fragment = ctx.layout_into_fragment(elem.text(), styles)?; + let fragment = ctx.layout_into_fragment(&elem.text, styles)?; let italics = fragment.italics_correction(); let accent_attach = fragment.accent_attach(); let text_like = fragment.is_text_like(); ctx.push( - FrameFragment::new(ctx, styles, fragment.into_frame()) + FrameFragment::new(styles, fragment.into_frame()) .with_class(MathClass::Large) .with_italics_correction(italics) .with_accent_attach(accent_attach) @@ -688,12 +691,11 @@ fn layout_external( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { - let local = TextElem::set_size(TextSize(scaled_font_size(ctx, styles).into())).wrap(); - (ctx.engine.routines.layout_frame)( + crate::layout_frame( ctx.engine, content, ctx.locator.next(&content.span()), - styles.chain(&local), + styles, ctx.region, ) } diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 0bb2f5393..a6b5c03d0 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -18,7 +18,6 @@ pub fn layout_root( styles: StyleChain, ) -> SourceResult<()> { let index = elem.index(styles); - let radicand = elem.radicand(); let span = elem.span(); let gap = scaled!( @@ -36,9 +35,9 @@ pub fn layout_root( let radicand = { let cramped = style_cramped(); let styles = styles.chain(&cramped); - let run = ctx.layout_into_run(radicand, styles)?; + let run = ctx.layout_into_run(&elem.radicand, styles)?; let multiline = run.is_multiline(); - let mut radicand = run.into_fragment(ctx, styles).into_frame(); + let mut radicand = run.into_fragment(styles).into_frame(); if multiline { // Align the frame center line with the math axis. radicand.set_baseline( @@ -120,7 +119,7 @@ pub fn layout_root( ); frame.push_frame(radicand_pos, radicand); - ctx.push(FrameFragment::new(ctx, styles, frame)); + ctx.push(FrameFragment::new(styles, frame)); Ok(()) } diff --git a/crates/typst-layout/src/math/run.rs b/crates/typst-layout/src/math/run.rs index b07f5893a..ae64368d6 100644 --- a/crates/typst-layout/src/math/run.rs +++ b/crates/typst-layout/src/math/run.rs @@ -6,7 +6,7 @@ use typst_library::math::{EquationElem, MathSize, MEDIUM, THICK, THIN}; use typst_library::model::ParElem; use unicode_math_class::MathClass; -use super::{alignments, scaled_font_size, FrameFragment, MathContext, MathFragment}; +use super::{alignments, FrameFragment, MathFragment}; const TIGHT_LEADING: Em = Em::new(0.25); @@ -161,15 +161,15 @@ impl MathRun { } } - pub fn into_frame(self, ctx: &MathContext, styles: StyleChain) -> Frame { + pub fn into_frame(self, styles: StyleChain) -> Frame { if !self.is_multiline() { self.into_line_frame(&[], LeftRightAlternator::Right) } else { - self.multiline_frame_builder(ctx, styles).build() + self.multiline_frame_builder(styles).build() } } - pub fn into_fragment(self, ctx: &MathContext, styles: StyleChain) -> MathFragment { + pub fn into_fragment(self, styles: StyleChain) -> MathFragment { if self.0.len() == 1 { return self.0.into_iter().next().unwrap(); } @@ -181,7 +181,7 @@ impl MathRun { .filter(|e| e.math_size().is_some()) .all(|e| e.is_text_like()); - FrameFragment::new(ctx, styles, self.into_frame(ctx, styles)) + FrameFragment::new(styles, self.into_frame(styles)) .with_text_like(text_like) .into() } @@ -189,11 +189,7 @@ impl MathRun { /// Returns a builder that lays out the [`MathFragment`]s into a possibly /// multi-row [`Frame`]. The rows are aligned using the same set of alignment /// points computed from them as a whole. - pub fn multiline_frame_builder( - self, - ctx: &MathContext, - styles: StyleChain, - ) -> MathRunFrameBuilder { + pub fn multiline_frame_builder(self, styles: StyleChain) -> MathRunFrameBuilder { let rows: Vec<_> = self.rows(); let row_count = rows.len(); let alignments = alignments(&rows); @@ -201,8 +197,7 @@ impl MathRun { let leading = if EquationElem::size_in(styles) >= MathSize::Text { ParElem::leading_in(styles) } else { - let font_size = scaled_font_size(ctx, styles); - TIGHT_LEADING.at(font_size) + TIGHT_LEADING.resolve(styles) }; let align = AlignElem::alignment_in(styles).resolve(styles).x; diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 74e62e8f0..5aebdacac 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -2,7 +2,6 @@ use ttf_parser::math::MathValue; use typst_library::foundations::{Style, StyleChain}; use typst_library::layout::{Abs, Em, FixedAlignment, Frame, Point, Size, VAlignment}; use typst_library::math::{EquationElem, MathSize}; -use typst_library::text::TextElem; use typst_utils::LazyHash; use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; @@ -18,7 +17,7 @@ macro_rules! scaled { $crate::math::Scaled::scaled( $ctx.constants.$name(), $ctx, - $crate::math::scaled_font_size($ctx, $styles), + typst_library::text::TextElem::size_in($styles), ) }; } @@ -55,16 +54,6 @@ impl Scaled for MathValue<'_> { } } -/// Get the font size scaled with the `MathSize`. -pub fn scaled_font_size(ctx: &MathContext, styles: StyleChain) -> Abs { - let factor = match EquationElem::size_in(styles) { - MathSize::Display | MathSize::Text => 1.0, - MathSize::Script => percent!(ctx, script_percent_scale_down), - MathSize::ScriptScript => percent!(ctx, script_script_percent_scale_down), - }; - factor * TextElem::size_in(styles) -} - /// Styles something as cramped. pub fn style_cramped() -> LazyHash