diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01b3e8c3a..9f0ada9f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.83.0 + - uses: dtolnay/rust-toolchain@1.85.0 - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --no-run - run: cargo test --workspace --no-fail-fast @@ -59,7 +59,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 +73,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/Cargo.lock b/Cargo.lock index caf9d063b..bc60d49ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,18 +8,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -46,9 +34,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -61,36 +49,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -104,9 +93,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -192,9 +181,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -213,9 +202,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "by_address" @@ -225,9 +214,9 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -243,9 +232,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "cc" -version = "1.1.24" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +checksum = "e4730490333d58093109dc02c23174c3f4d490998c3fed3cc8e82d57afedb9cf" dependencies = [ "jobserver", "libc", @@ -278,14 +267,14 @@ checksum = "7588475145507237ded760e52bf2f1085495245502033756d28ea72ade0e498b" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -323,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", @@ -333,9 +322,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.19" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", "clap_derive", @@ -343,9 +332,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstream", "anstyle", @@ -356,18 +345,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.32" +version = "4.5.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" +checksum = "375f9d8255adeeedd51053574fd8d4ba875ea5fa558e86617b07f09f1680c8b6" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -377,15 +366,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clap_mangen" -version = "0.2.23" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17415fd4dfbea46e3274fcd8d368284519b358654772afb700dc2e8d2b24eeb" +checksum = "724842fa9b144f9b89b3f3d371a89f3455eea660361d13a554f68f8ae5d6c13a" dependencies = [ "clap", "roff", @@ -409,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", @@ -441,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" @@ -454,7 +444,7 @@ dependencies = [ "comemo-macros", "once_cell", "parking_lot", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -486,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", ] @@ -504,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", @@ -532,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", @@ -580,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", @@ -591,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]] @@ -629,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", ] @@ -692,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]] @@ -718,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", ] @@ -745,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", @@ -765,6 +755,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "fontconfig-parser" version = "0.5.7" @@ -850,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]] @@ -885,36 +893,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", @@ -1008,6 +1010,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" @@ -1112,12 +1138,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]] @@ -1128,9 +1165,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", @@ -1170,9 +1207,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", @@ -1180,19 +1217,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", ] @@ -1233,9 +1264,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" @@ -1248,18 +1279,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", ] @@ -1296,33 +1328,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", @@ -1330,9 +1362,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" @@ -1340,7 +1372,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", ] @@ -1353,9 +1385,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" @@ -1369,9 +1401,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", ] @@ -1394,9 +1426,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" @@ -1432,9 +1464,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", @@ -1442,14 +1474,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]] @@ -1466,9 +1498,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", @@ -1493,12 +1525,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", @@ -1506,10 +1537,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" @@ -1552,18 +1590,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", @@ -1572,11 +1607,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", @@ -1598,24 +1633,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", @@ -1632,22 +1667,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", ] @@ -1695,7 +1727,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1706,17 +1738,17 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pdf-writer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be17f48d7fbbd22c6efedb58af5d409aa578e407f40b29a0bcb4e66ed84c5c98" +checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "itoa", "memchr", "ryu", @@ -1730,9 +1762,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", @@ -1740,9 +1772,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", @@ -1750,9 +1782,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", @@ -1763,11 +1795,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]] @@ -1798,7 +1830,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", @@ -1806,9 +1838,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", @@ -1819,15 +1851,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", @@ -1852,18 +1884,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", ] @@ -1874,7 +1906,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", @@ -1913,9 +1945,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", ] @@ -1973,29 +2005,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 2.0.11", ] [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2005,9 +2037,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", @@ -2070,37 +2102,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" @@ -2108,7 +2131,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", @@ -2122,9 +2145,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" @@ -2137,9 +2160,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", ] @@ -2156,7 +2179,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", @@ -2165,9 +2188,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", @@ -2186,24 +2209,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", @@ -2212,9 +2235,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", @@ -2249,7 +2272,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", @@ -2285,19 +2308,13 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -2355,12 +2372,11 @@ dependencies = [ [[package]] name = "string-interner" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6a0d765f5807e98a091107bae0a56ea3799f66a5de47b2c84c94a39c09974e" +checksum = "1a3275464d7a9f2d4cac57c89c2ef96a8524dba2864c8d6f82e3980baf136f9b" dependencies = [ - "cfg-if", - "hashbrown 0.14.5", + "hashbrown 0.15.2", "serde", ] @@ -2411,7 +2427,7 @@ dependencies = [ "once_cell", "pdf-writer", "resvg", - "siphasher 1.0.1", + "siphasher", "subsetter", "tiny-skia", "ttf-parser", @@ -2420,19 +2436,19 @@ dependencies = [ [[package]] name = "svgtypes" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794de53cc48eaabeed0ab6a3404a65f40b3e38c067e4435883a65d2aa4ca000e" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo", - "siphasher 1.0.1", + "siphasher", ] [[package]] name = "syn" -version = "2.0.79" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -2467,7 +2483,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "thiserror", + "thiserror 1.0.69", "walkdir", "yaml-rust", ] @@ -2480,9 +2496,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", @@ -2491,12 +2507,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", @@ -2513,9 +2530,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", @@ -2529,18 +2546,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", @@ -2549,9 +2586,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", @@ -2570,9 +2607,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", @@ -2629,9 +2666,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", ] @@ -2665,11 +2702,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", @@ -2687,9 +2724,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", @@ -2704,7 +2741,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "typst" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2721,12 +2758,12 @@ dependencies = [ [[package]] name = "typst-assets" -version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577" +version = "0.13.0" +source = "git+https://github.com/typst/typst-assets?rev=fa0f8a4#fa0f8a438cc4bc2113cc0aa3304cd68cdc2bc020" [[package]] name = "typst-cli" -version = "0.12.0" +version = "0.13.0" dependencies = [ "chrono", "clap", @@ -2771,12 +2808,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.0" +source = "git+https://github.com/typst/typst-dev-assets?rev=61aebe9#61aebe9575a5abff889f76d73c7b01dc8e17e340" [[package]] name = "typst-docs" -version = "0.12.0" +version = "0.13.0" dependencies = [ "clap", "ecow", @@ -2791,18 +2828,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.0" dependencies = [ "comemo", "ecow", "if_chain", - "indexmap 2.6.0", + "indexmap 2.7.1", "stacker", "toml", "typst-library", @@ -2815,7 +2854,7 @@ dependencies = [ [[package]] name = "typst-fuzz" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "libfuzzer-sys", @@ -2827,7 +2866,7 @@ dependencies = [ [[package]] name = "typst-html" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2841,7 +2880,7 @@ dependencies = [ [[package]] name = "typst-ide" -version = "0.12.0" +version = "0.13.0" dependencies = [ "comemo", "ecow", @@ -2858,7 +2897,7 @@ dependencies = [ [[package]] name = "typst-kit" -version = "0.12.0" +version = "0.13.0" dependencies = [ "dirs", "ecow", @@ -2868,6 +2907,8 @@ dependencies = [ "native-tls", "once_cell", "openssl", + "serde", + "serde_json", "tar", "typst-assets", "typst-library", @@ -2879,7 +2920,7 @@ dependencies = [ [[package]] name = "typst-layout" -version = "0.12.0" +version = "0.13.0" dependencies = [ "az", "bumpalo", @@ -2909,10 +2950,10 @@ dependencies = [ [[package]] name = "typst-library" -version = "0.12.0" +version = "0.13.0" dependencies = [ "az", - "bitflags 2.6.0", + "bitflags 2.8.0", "bumpalo", "chinese-number", "ciborium", @@ -2928,10 +2969,11 @@ dependencies = [ "icu_provider", "icu_provider_blob", "image", - "indexmap 2.6.0", + "indexmap 2.7.1", "kamadak-exif", "kurbo", "lipsum", + "memchr", "palette", "phf", "png", @@ -2945,7 +2987,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml 0.9.34+deprecated", - "siphasher 1.0.1", + "siphasher", "smallvec", "syntect", "time", @@ -2960,6 +3002,7 @@ dependencies = [ "typst-timing", "typst-utils", "unicode-math-class", + "unicode-normalization", "unicode-segmentation", "unscanny", "usvg", @@ -2969,7 +3012,7 @@ dependencies = [ [[package]] name = "typst-macros" -version = "0.12.0" +version = "0.13.0" dependencies = [ "heck", "proc-macro2", @@ -2979,7 +3022,7 @@ dependencies = [ [[package]] name = "typst-pdf" -version = "0.12.0" +version = "0.13.0" dependencies = [ "arrayvec", "base64", @@ -2987,7 +3030,7 @@ dependencies = [ "comemo", "ecow", "image", - "indexmap 2.6.0", + "indexmap 2.7.1", "miniz_oxide", "pdf-writer", "serde", @@ -3005,7 +3048,7 @@ dependencies = [ [[package]] name = "typst-realize" -version = "0.12.0" +version = "0.13.0" dependencies = [ "arrayvec", "bumpalo", @@ -3021,7 +3064,7 @@ dependencies = [ [[package]] name = "typst-render" -version = "0.12.0" +version = "0.13.0" dependencies = [ "bytemuck", "comemo", @@ -3037,12 +3080,13 @@ dependencies = [ [[package]] name = "typst-svg" -version = "0.12.0" +version = "0.13.0" dependencies = [ "base64", "comemo", "ecow", "flate2", + "image", "ttf-parser", "typst-library", "typst-macros", @@ -3054,7 +3098,7 @@ dependencies = [ [[package]] name = "typst-syntax" -version = "0.12.0" +version = "0.13.0" dependencies = [ "ecow", "serde", @@ -3070,7 +3114,7 @@ dependencies = [ [[package]] name = "typst-tests" -version = "0.12.0" +version = "0.13.0" dependencies = [ "clap", "comemo", @@ -3095,22 +3139,24 @@ dependencies = [ [[package]] name = "typst-timing" -version = "0.12.0" +version = "0.13.0" dependencies = [ "parking_lot", "serde", "serde_json", + "web-sys", ] [[package]] name = "typst-utils" -version = "0.12.0" +version = "0.13.0" dependencies = [ "once_cell", "portable-atomic", "rayon", - "siphasher 1.0.1", + "siphasher", "thin-vec", + "unicode-math-class", ] [[package]] @@ -3134,12 +3180,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-bidi" @@ -3161,9 +3204,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" @@ -3224,9 +3267,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", @@ -3240,9 +3283,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", @@ -3267,7 +3310,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -3277,6 +3320,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" @@ -3318,25 +3367,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", @@ -3345,9 +3403,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", @@ -3355,9 +3413,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", @@ -3368,15 +3426,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", @@ -3385,23 +3446,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", @@ -3409,20 +3470,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]] @@ -3446,16 +3518,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]] @@ -3464,7 +3527,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]] @@ -3473,22 +3536,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]] @@ -3497,46 +3545,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" @@ -3549,48 +3579,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" @@ -3599,13 +3605,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" @@ -3623,9 +3644,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", @@ -3646,9 +3667,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xmp-writer" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8254499146a4fd0c86e3e99cf4a9f468f595808fb49ff8f3e495f2b117bf4ebc" +checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487" [[package]] name = "xz2" @@ -3680,9 +3701,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", @@ -3692,9 +3713,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", @@ -3725,18 +3746,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", @@ -3781,18 +3802,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", ] @@ -3818,9 +3839,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 306f3ba75..e5fc8e83c 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.0" +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.0" } +typst-cli = { path = "crates/typst-cli", version = "0.13.0" } +typst-eval = { path = "crates/typst-eval", version = "0.13.0" } +typst-html = { path = "crates/typst-html", version = "0.13.0" } +typst-ide = { path = "crates/typst-ide", version = "0.13.0" } +typst-kit = { path = "crates/typst-kit", version = "0.13.0" } +typst-layout = { path = "crates/typst-layout", version = "0.13.0" } +typst-library = { path = "crates/typst-library", version = "0.13.0" } +typst-macros = { path = "crates/typst-macros", version = "0.13.0" } +typst-pdf = { path = "crates/typst-pdf", version = "0.13.0" } +typst-realize = { path = "crates/typst-realize", version = "0.13.0" } +typst-render = { path = "crates/typst-render", version = "0.13.0" } +typst-svg = { path = "crates/typst-svg", version = "0.13.0" } +typst-syntax = { path = "crates/typst-syntax", version = "0.13.0" } +typst-timing = { path = "crates/typst-timing", version = "0.13.0" } +typst-utils = { path = "crates/typst-utils", version = "0.13.0" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "fa0f8a4" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "61aebe9" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" @@ -47,19 +47,19 @@ 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.21", default-features = false } fs_extra = "1.3" glidesort = "0.1.2" -hayagriva = "0.8" +hayagriva = "0.8.1" heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } @@ -68,15 +68,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" +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 +85,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" @@ -123,21 +124,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.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.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..a5d20d2e6 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

diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 83c4c8f9e..d6855d100 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -473,6 +473,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[value(name = "a-2b")] A_2b, + /// PDF/A-3b. + #[value(name = "a-3b")] + A_3b, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index adeef0f2d..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, @@ -136,6 +137,7 @@ impl CompileConfig { .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)? @@ -187,7 +189,7 @@ pub fn compile_once( match output { // Export the PDF / PNG. - Ok(()) => { + Ok(outputs) => { let duration = start.elapsed(); if config.watching { @@ -201,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)?; } @@ -225,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); @@ -256,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()) } @@ -326,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, @@ -340,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) }) }) @@ -382,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()) @@ -391,11 +401,9 @@ fn export_image( }; export_image_page(config, page, &output, fmt)?; - Ok(()) + Ok(output) }) - .collect::, EcoString>>()?; - - Ok(()) + .collect::>>() } mod output_template { @@ -500,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. @@ -521,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 @@ -543,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. @@ -562,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 c22ea7e40..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 { @@ -525,7 +536,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool { if let Some(grand) = parent.parent(); if grand.kind() == SyntaxKind::ImportItems; if let Some(great) = grand.parent(); - if let Some(ast::Expr::Import(import)) = great.get().cast(); + if let Some(ast::Expr::ModuleImport(import)) = great.get().cast(); if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(source) = great.children().find(|child| child.is::()); then { @@ -551,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()); } } } @@ -666,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 { @@ -817,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."); @@ -838,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>, @@ -845,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, @@ -1443,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()); } @@ -1463,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); } @@ -1747,4 +1768,26 @@ mod tests { .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 31fb9e34e..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())); } } diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index ed74df226..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()); @@ -261,6 +264,11 @@ mod tests { test_click(s, point(21.0, 12.0), cursor(56)); } + #[test] + fn test_jump_from_click_math() { + test_click("$a + b$", point(28.0, 14.0), cursor(5)); + } + #[test] fn test_jump_from_cursor() { let s = "*Hello* #box[ABC] World"; @@ -268,6 +276,11 @@ mod tests { test_cursor(s, 14, pos(1, 37.55, 16.58)); } + #[test] + fn test_jump_from_cursor_math() { + test_cursor("$a + b$", -3, pos(1, 27.51, 16.83)); + } + #[test] fn test_backlink() { let s = "#footnote[Hi]"; diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index d02eb2a95..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) } @@ -269,16 +283,18 @@ mod tests { use std::borrow::Borrow; use ecow::EcoString; + use typst::foundations::Value; use typst::syntax::{LinkedNode, Side}; use super::named_items; - use crate::tests::{FilePos, WorldLike}; + use crate::tests::{FilePos, TestWorld, WorldLike}; - type Response = Vec; + 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 { @@ -286,7 +302,7 @@ mod tests { fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self { for item in includes { assert!( - self.iter().any(|v| v == item), + self.iter().any(|v| v.0 == item), "{item:?} was not contained in {self:?}", ); } @@ -297,12 +313,21 @@ mod tests { fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self { for item in excludes { assert!( - !self.iter().any(|v| v == item), + !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] @@ -314,7 +339,7 @@ mod tests { let leaf = node.leaf_at(cursor, Side::After).unwrap(); let mut items = vec![]; named_items(world, leaf, |s| { - items.push(s.name().clone()); + items.push((s.name().clone(), s.value().clone())); None::<()> }); items @@ -339,6 +364,21 @@ mod tests { #[test] fn test_named_items_import() { - test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]); + test("#import \"foo.typ\"", 2).must_include(["foo"]); + test("#import \"foo.typ\" as bar", 2) + .must_include(["bar"]) + .must_exclude(["foo"]); + } + + #[test] + fn test_named_items_import_items() { + test("#import \"foo.typ\": a; #(a);", 2) + .must_include(["a"]) + .must_exclude(["foo"]); + + let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);") + .with_source("foo.typ", "#import \"a.typ\"") + .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 f41808dac..6678ab841 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -55,7 +55,7 @@ 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); + let bytes = Bytes::new(data); Arc::make_mut(&mut self.files).assets.insert(id, bytes); self } @@ -152,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 { diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 99ae0620b..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; } }; @@ -352,6 +357,13 @@ 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\": *") 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/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 3cf66f9e3..76af8f650 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -17,7 +17,9 @@ use typst_library::model::{ use typst_syntax::Span; use typst_utils::{NonZeroExt, Numeric}; -use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; +use super::{ + distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work, +}; /// Composes the contents of a single page/region. A region can have multiple /// columns/subregions. @@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { migratable: bool, ) -> FlowResult<()> { // Footnotes are only supported at the root level. - if !self.config.root { + if self.config.mode != FlowMode::Root { return Ok(()); } diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2f0ec39a9..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,62 +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) - }; - - // 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()), - ) - }, - }), - }; + 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. @@ -225,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); @@ -247,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: @@ -318,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>, 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 77e1d0838..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::{ - Curve, Image, ImageElem, ImageFit, ImageFormat, 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( @@ -119,25 +133,23 @@ pub fn layout_image( 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 2e8c1129b..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,7 +17,6 @@ mod transforms; pub use self::flow::{layout_columns, layout_fragment, layout_frame}; pub use self::grid::{layout_grid, layout_table}; pub use self::image::layout_image; -pub use self::inline::{layout_box, layout_inline}; pub use self::lists::{layout_enum, layout_list}; pub use self::math::{layout_equation_block, layout_equation_inline}; pub use self::pad::layout_pad; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 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 0ebe785f1..f2dfa2c45 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -16,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 { @@ -29,12 +29,12 @@ pub fn layout_accent( 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); } @@ -50,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); diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 263fc5c6d..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, StyleChain}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size}; use typst_library::math::{ AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, }; -use typst_library::text::TextElem; use typst_utils::OptionExt; use super::{ @@ -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()); @@ -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,7 +148,7 @@ 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(()) @@ -157,9 +157,9 @@ pub fn layout_limits( /// 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(); + let mut base = &elem.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } base.to_packed::().map(|stretch| stretch.size(styles)) diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs index 716832fbf..9826397fa 100644 --- a/crates/typst-layout/src/math/cancel.rs +++ b/crates/typst-layout/src/math/cancel.rs @@ -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(); diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index fdc3be172..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, Resolve, StyleChain}; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem}; use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; use typst_library::math::{BinomElem, FracElem}; use typst_library::text::TextElem; @@ -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. @@ -80,7 +80,10 @@ fn layout_frac_like( let denom = ctx.layout_into_frame( &Content::sequence( // Add a comma between each element. - denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1), + denom + .iter() + .flat_map(|a| [SymbolElem::packed(','), a.clone()]) + .skip(1), ), styles.chain(&denom_style), )?; diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index a0453c14f..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::{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 { @@ -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 } @@ -516,7 +510,7 @@ impl FrameFragment { let base_ascent = frame.ascent(); let accent_attach = frame.width() / 2.0; Self { - frame: frame.post_processed(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), @@ -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 2f4556fe5..bf8235411 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -2,6 +2,7 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Abs, Axis, Rel}; use typst_library::math::{EquationElem, LrElem, MidElem}; +use typst_utils::SliceExt; use unicode_math_class::MathClass; use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; @@ -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_one() { - body = lr.body(); + 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 { diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index d28bb037d..bf4929026 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -27,7 +27,7 @@ pub fn layout_vec( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, elem.align(styles), elem.gap(styles), LeftRightAlternator::Right, @@ -44,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 { @@ -58,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 { @@ -97,7 +97,7 @@ pub fn layout_cases( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, FixedAlignment::Start, elem.gap(styles), LeftRightAlternator::None, diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 62ecd1725..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, @@ -200,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); @@ -535,6 +536,8 @@ fn layout_realized( layout_h(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { self::text::layout_text(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::text::layout_symbol(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { @@ -615,7 +618,7 @@ fn layout_box( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let frame = (ctx.engine.routines.layout_box)( + let frame = crate::inline::layout_box( elem, ctx.engine, ctx.locator.next(&elem.span()), @@ -632,7 +635,7 @@ 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.resolve(styles), elem.weak(styles))); } @@ -641,17 +644,16 @@ fn layout_h( } /// Lays out a [`ClassElem`]. -#[typst_macros::time(name = "math.op", span = elem.span())] +#[typst_macros::time(name = "math.class", span = elem.span())] fn layout_class( elem: &Packed, ctx: &mut MathContext, 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(()) } @@ -663,7 +665,7 @@ 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(); @@ -689,7 +691,7 @@ fn layout_external( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { - (ctx.engine.routines.layout_frame)( + crate::layout_frame( ctx.engine, content, ctx.locator.next(&content.span()), diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 4e5d844f2..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,7 +35,7 @@ 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(styles).into_frame(); if multiline { diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 4bc5a9262..dafa8cbe8 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -10,6 +10,7 @@ use super::{ delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled, VariantFragment, }; +use crate::modifiers::FrameModify; /// Maximum number of times extenders can be repeated. const MAX_REPEATS: usize = 1024; @@ -21,7 +22,7 @@ pub fn layout_stretch( 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)?; stretch_fragment( ctx, styles, @@ -265,7 +266,7 @@ fn assemble( let mut frame = Frame::soft(size); let mut offset = Abs::zero(); frame.set_baseline(baseline); - frame.post_process_raw(base.dests, base.hidden); + frame.modify(&base.modifiers); for (fragment, advance) in selected { let pos = match axis { diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index eb30373dd..59ac5b089 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -1,8 +1,8 @@ use std::f64::consts::SQRT_2; -use ecow::{eco_vec, EcoString}; +use ecow::EcoString; use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, StyleVec}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -20,56 +20,68 @@ pub fn layout_text( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let text = elem.text(); + let text = &elem.text; let span = elem.span(); - let mut chars = text.chars(); - let math_size = EquationElem::size_in(styles); - let mut dtls = ctx.dtls_table.is_some(); - let fragment: MathFragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| dtls_char(c, &mut dtls)) - .map(|c| styled_char(styles, c, true)) - .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span)) - { - // A single letter that is available in the math font. - if dtls { - glyph.make_dotless_form(ctx); - } + let fragment = if text.contains(is_newline) { + layout_text_lines(text.split(is_newline), span, ctx, styles)? + } else { + layout_inline_text(text, span, ctx, styles)? + }; + ctx.push(fragment); + Ok(()) +} - match math_size { - MathSize::Script => { - glyph.make_script_size(ctx); - } - MathSize::ScriptScript => { - glyph.make_script_script_size(ctx); - } - _ => (), +/// Layout multiple lines of text. +fn layout_text_lines<'a>( + lines: impl Iterator, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + let mut fragments = vec![]; + for (i, line) in lines.enumerate() { + if i != 0 { + fragments.push(MathFragment::Linebreak); } + if !line.is_empty() { + fragments.push(layout_inline_text(line, span, ctx, styles)?.into()); + } + } + let mut frame = MathRun::new(fragments).into_frame(styles); + let axis = scaled!(ctx, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + Ok(FrameFragment::new(styles, frame)) +} - if glyph.class == MathClass::Large { - let mut variant = if math_size == MathSize::Display { - let height = scaled!(ctx, styles, display_operator_min_height) - .max(SQRT_2 * glyph.height()); - glyph.stretch_vertical(ctx, height, Abs::zero()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(ctx); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. +/// Layout the given text string into a [`FrameFragment`] after styling all +/// characters for the math font (without auto-italics). +fn layout_inline_text( + text: &str, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + if text.chars().all(|c| c.is_ascii_digit() || c == '.') { + // Small optimization for numbers. Note that this lays out slightly + // differently to normal text and is worth re-evaluating in the future. let mut fragments = vec![]; - for c in text.chars() { - let c = styled_char(styles, c, false); - fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); + let is_single = text.chars().count() == 1; + for unstyled_c in text.chars() { + let c = styled_char(styles, unstyled_c, false); + let mut glyph = GlyphFragment::new(ctx, styles, c, span); + if is_single { + // Duplicate what `layout_glyph` does exactly even if it's + // probably incorrect here. + match EquationElem::size_in(styles) { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} + } + } + fragments.push(glyph.into()); } let frame = MathRun::new(fragments).into_frame(styles); - FrameFragment::new(styles, frame).with_text_like(true).into() + Ok(FrameFragment::new(styles, frame).with_text_like(true)) } else { let local = [ TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), @@ -77,64 +89,96 @@ pub fn layout_text( ] .map(|p| p.wrap()); - // Anything else is handled by Typst's standard text layout. let styles = styles.chain(&local); - let text: EcoString = + let styled_text: EcoString = text.chars().map(|c| styled_char(styles, c, false)).collect(); - if text.contains(is_newline) { - let mut fragments = vec![]; - for (i, piece) in text.split(is_newline).enumerate() { - if i != 0 { - fragments.push(MathFragment::Linebreak); - } - if !piece.is_empty() { - fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); - } - } - let mut frame = MathRun::new(fragments).into_frame(styles); - let axis = scaled!(ctx, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(styles, frame).into() - } else { - layout_complex_text(&text, ctx, span, styles)?.into() + + let spaced = styled_text.graphemes(true).nth(1).is_some(); + let elem = TextElem::packed(styled_text).spanned(span); + + // There isn't a natural width for a paragraph in a math environment; + // because it will be placed somewhere probably not at the left margin + // it will overflow. So emulate an `hbox` instead and allow the + // paragraph to extend as far as needed. + let frame = crate::inline::layout_inline( + ctx.engine, + &[(&elem, styles)], + &mut ctx.locator.next(&span).split(), + styles, + Size::splat(Abs::inf()), + false, + )? + .into_frame(); + + Ok(FrameFragment::new(styles, frame) + .with_class(MathClass::Alphabetic) + .with_text_like(true) + .with_spaced(spaced)) + } +} + +/// Layout a single character in the math font with the correct styling applied +/// (includes auto-italics). +pub fn layout_symbol( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + // Switch dotless char to normal when we have the dtls OpenType feature. + // This should happen before the main styling pass. + let (unstyled_c, dtls) = match try_dotless(elem.text) { + Some(c) if ctx.dtls_table.is_some() => (c, true), + _ => (elem.text, false), + }; + let c = styled_char(styles, unstyled_c, true); + let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) { + Some(glyph) => layout_glyph(glyph, dtls, ctx, styles), + None => { + // Not in the math font, fallback to normal inline text layout. + layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? + .into() } }; - ctx.push(fragment); Ok(()) } -/// Layout the given text string into a [`FrameFragment`]. -fn layout_complex_text( - text: &str, +/// Layout a [`GlyphFragment`]. +fn layout_glyph( + mut glyph: GlyphFragment, + dtls: bool, ctx: &mut MathContext, - span: Span, styles: StyleChain, -) -> SourceResult { - // There isn't a natural width for a paragraph in a math environment; - // because it will be placed somewhere probably not at the left margin - // it will overflow. So emulate an `hbox` instead and allow the paragraph - // to extend as far as needed. - let spaced = text.graphemes(true).nth(1).is_some(); - let elem = TextElem::packed(text).spanned(span); - let frame = (ctx.engine.routines.layout_inline)( - ctx.engine, - &StyleVec::wrap(eco_vec![elem]), - ctx.locator.next(&span), - styles, - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); +) -> MathFragment { + if dtls { + glyph.make_dotless_form(ctx); + } + let math_size = EquationElem::size_in(styles); + match math_size { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} + } - Ok(FrameFragment::new(styles, frame) - .with_class(MathClass::Alphabetic) - .with_text_like(true) - .with_spaced(spaced)) + if glyph.class == MathClass::Large { + let mut variant = if math_size == MathSize::Display { + let height = scaled!(ctx, styles, display_operator_min_height) + .max(SQRT_2 * glyph.height()); + glyph.stretch_vertical(ctx, height, Abs::zero()) + } else { + glyph.into_variant() + }; + // TeXbook p 155. Large operators are always vertically centered on the + // axis. + variant.center_on_axis(ctx); + variant.into() + } else { + glyph.into() + } } -/// Select the correct styled math letter. +/// Style the character by selecting the unicode codepoint for italic, bold, +/// caligraphic, etc. /// /// /// @@ -353,15 +397,12 @@ fn greek_exception( }) } -/// Switch dotless character to non dotless character for use of the dtls -/// OpenType feature. -pub fn dtls_char(c: char, dtls: &mut bool) -> char { - match (c, *dtls) { - ('ı', true) => 'i', - ('ȷ', true) => 'j', - _ => { - *dtls = false; - c - } +/// The non-dotless version of a dotless character that can be used with the +/// `dtls` OpenType feature. +pub fn try_dotless(c: char) -> Option { + match c { + 'ı' => Some('i'), + 'ȷ' => Some('j'), + _ => None, } } diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index e55996389..7b3617c3e 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -32,7 +32,7 @@ pub fn layout_underline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under) } /// Lays out an [`OverlineElem`]. @@ -42,7 +42,7 @@ pub fn layout_overline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over) } /// Lays out an [`UnderbraceElem`]. @@ -55,7 +55,7 @@ pub fn layout_underbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏟', BRACE_GAP, @@ -74,7 +74,7 @@ pub fn layout_overbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏞', BRACE_GAP, @@ -93,7 +93,7 @@ pub fn layout_underbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎵', BRACKET_GAP, @@ -112,7 +112,7 @@ pub fn layout_overbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎴', BRACKET_GAP, @@ -131,7 +131,7 @@ pub fn layout_underparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏝', PAREN_GAP, @@ -150,7 +150,7 @@ pub fn layout_overparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏜', PAREN_GAP, @@ -169,7 +169,7 @@ pub fn layout_undershell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏡', SHELL_GAP, @@ -188,7 +188,7 @@ pub fn layout_overshell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏠', SHELL_GAP, diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs new file mode 100644 index 000000000..ac5f40b04 --- /dev/null +++ b/crates/typst-layout/src/modifiers.rs @@ -0,0 +1,110 @@ +use typst_library::foundations::StyleChain; +use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; +use typst_library::model::{Destination, LinkElem}; + +/// Frame-level modifications resulting from styles that do not impose any +/// layout structure. +/// +/// These are always applied at the highest level of style uniformity. +/// Consequently, they must be applied by all layouters that manually manage +/// styles of their children (because they can produce children with varying +/// styles). This currently includes flow, inline, and math layout. +/// +/// Other layouters don't manually need to handle it because their parents that +/// result from realization will take care of it and the styles can only apply +/// to them as a whole, not part of it (since they don't manage styles). +/// +/// Currently existing frame modifiers are: +/// - `HideElem::hidden` +/// - `LinkElem::dests` +#[derive(Debug, Clone)] +pub struct FrameModifiers { + /// A destination to link to. + dest: Option, + /// Whether the contents of the frame should be hidden. + hidden: bool, +} + +impl FrameModifiers { + /// Retrieve all modifications that should be applied per-frame. + pub fn get_in(styles: StyleChain) -> Self { + Self { + dest: LinkElem::current_in(styles), + hidden: HideElem::hidden_in(styles), + } + } +} + +/// Applies [`FrameModifiers`]. +pub trait FrameModify { + /// Apply the modifiers in-place. + fn modify(&mut self, modifiers: &FrameModifiers); + + /// Apply the modifiers, and return the modified result. + fn modified(mut self, modifiers: &FrameModifiers) -> Self + where + Self: Sized, + { + self.modify(modifiers); + self + } +} + +impl FrameModify for Frame { + fn modify(&mut self, modifiers: &FrameModifiers) { + if let Some(dest) = &modifiers.dest { + let size = self.size(); + self.push(Point::zero(), FrameItem::Link(dest.clone(), size)); + } + + if modifiers.hidden { + self.hide(); + } + } +} + +impl FrameModify for Fragment { + fn modify(&mut self, modifiers: &FrameModifiers) { + for frame in self.iter_mut() { + frame.modify(modifiers); + } + } +} + +impl FrameModify for Result +where + T: FrameModify, +{ + fn modify(&mut self, props: &FrameModifiers) { + if let Ok(inner) = self { + inner.modify(props); + } + } +} + +/// Performs layout and modification in one step. +/// +/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, +/// but with the additional step that redundant modifiers (which are already +/// applied here) are removed from the `styles` passed to `layout`. This is used +/// for the layout of containers like `block`. +pub fn layout_and_modify(styles: StyleChain, layout: F) -> R +where + F: FnOnce(StyleChain) -> R, + R: FrameModify, +{ + let modifiers = FrameModifiers::get_in(styles); + + // Disable the current link internally since it's already applied at this + // level of layout. This means we don't generate redundant nested links, + // which may bloat the output considerably. + let reset; + let outer = styles; + let mut styles = styles; + if modifiers.dest.is_some() { + reset = LinkElem::set_current(None).wrap(); + styles = outer.chain(&reset); + } + + layout(styles).modified(&modifiers) +} diff --git a/crates/typst-layout/src/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs index 0bbae9f4c..8eab18a62 100644 --- a/crates/typst-layout/src/pages/collect.rs +++ b/crates/typst-layout/src/pages/collect.rs @@ -23,7 +23,7 @@ pub enum Item<'a> { /// things like tags and weak pagebreaks. pub fn collect<'a>( mut children: &'a mut [Pair<'a>], - mut locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, mut initial: StyleChain<'a>, ) -> Vec> { // The collected page-level items. diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 27002a6c9..14dc0f3fb 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -83,7 +83,7 @@ fn layout_document_impl( styles, )?; - let pages = layout_pages(&mut engine, &mut children, locator, styles)?; + let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?; let introspector = Introspector::paged(&pages); Ok(PagedDocument { pages, info, introspector }) @@ -93,7 +93,7 @@ fn layout_document_impl( fn layout_pages<'a>( engine: &mut Engine, children: &'a mut [Pair<'a>], - locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, styles: StyleChain<'a>, ) -> SourceResult> { // Slice up the children into logical parts. diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs index 79ff5ab05..6d2d29da5 100644 --- a/crates/typst-layout/src/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -19,7 +19,7 @@ use typst_library::visualize::Paint; use typst_library::World; use typst_utils::Numeric; -use crate::flow::layout_flow; +use crate::flow::{layout_flow, FlowMode}; /// A mostly finished layout for one page. Needs only knowledge of its exact /// page number to be finalized into a `Page`. (Because the margins can depend @@ -181,7 +181,7 @@ fn layout_page_run_impl( Regions::repeat(area, area.map(Abs::is_finite)), PageElem::columns_in(styles), ColumnsElem::gutter_in(styles), - true, + FlowMode::Root, )?; // Layouts a single marginal. diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 7c56bf763..7ab41e9d4 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -62,7 +62,7 @@ pub fn layout_path( axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() }; - let vertices = elem.vertices(); + let vertices = &elem.vertices; let points: Vec = vertices.iter().map(|c| resolve(c.vertex())).collect(); let mut size = Size::zero(); @@ -150,7 +150,7 @@ pub fn layout_curve( ) -> SourceResult { let mut builder = CurveBuilder::new(region, styles); - for item in elem.components() { + for item in &elem.components { match item { CurveComponent::Move(element) => { let relative = element.relative(styles); @@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> { self.last_point = point; self.last_control_from = point; self.is_started = true; + self.is_empty = true; } /// Add a line segment. @@ -399,7 +400,7 @@ pub fn layout_polygon( region: Region, ) -> SourceResult { let points: Vec = elem - .vertices() + .vertices .iter() .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) .collect(); @@ -1281,7 +1282,7 @@ impl ControlPoints { } } -/// Helper to draw arcs with bezier curves. +/// Helper to draw arcs with Bézier curves. trait CurveExt { fn arc(&mut self, start: Point, center: Point, end: Point); fn arc_move(&mut self, start: Point, center: Point, end: Point); @@ -1305,7 +1306,7 @@ impl CurveExt for Curve { } } -/// Get the control points for a bezier curve that approximates a circular arc for +/// Get the control points for a Bézier curve that approximates a circular arc for /// a start point, an end point and a center of the circle whose arc connects /// the two. fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { diff --git a/crates/typst-layout/src/stack.rs b/crates/typst-layout/src/stack.rs index a3ebc9f36..c468945eb 100644 --- a/crates/typst-layout/src/stack.rs +++ b/crates/typst-layout/src/stack.rs @@ -27,7 +27,7 @@ pub fn layout_stack( let spacing = elem.spacing(styles); let mut deferred = None; - for child in elem.children() { + for child in &elem.children { match child { StackChild::Spacing(kind) => { layouter.layout_spacing(*kind); @@ -36,14 +36,14 @@ pub fn layout_stack( StackChild::Block(block) => { // Transparently handle `h`. if let (Axis::X, Some(h)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*h.amount()); + layouter.layout_spacing(h.amount); deferred = None; continue; } // Transparently handle `v`. if let (Axis::Y, Some(v)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*v.amount()); + layouter.layout_spacing(v.amount); deferred = None; continue; } diff --git a/crates/typst-layout/src/transforms.rs b/crates/typst-layout/src/transforms.rs index e0f29c4c2..f4526dd09 100644 --- a/crates/typst-layout/src/transforms.rs +++ b/crates/typst-layout/src/transforms.rs @@ -52,7 +52,7 @@ pub fn layout_rotate( region, size, styles, - elem.body(), + &elem.body, Transform::rotate(angle), align, elem.reflow(styles), @@ -81,7 +81,7 @@ pub fn layout_scale( region, size, styles, - elem.body(), + &elem.body, Transform::scale(scale.x, scale.y), elem.origin(styles).resolve(styles), elem.reflow(styles), @@ -169,7 +169,7 @@ pub fn layout_skew( region, size, styles, - elem.body(), + &elem.body, Transform::skew(ax, ay), align, elem.reflow(styles), diff --git a/crates/typst-library/Cargo.toml b/crates/typst-library/Cargo.toml index c6331bced..b210637a8 100644 --- a/crates/typst-library/Cargo.toml +++ b/crates/typst-library/Cargo.toml @@ -39,6 +39,7 @@ indexmap = { workspace = true } kamadak-exif = { workspace = true } kurbo = { workspace = true } lipsum = { workspace = true } +memchr = { workspace = true } palette = { workspace = true } phf = { workspace = true } png = { workspace = true } @@ -61,6 +62,7 @@ ttf-parser = { workspace = true } two-face = { workspace = true } typed-arena = { workspace = true } unicode-math-class = { workspace = true } +unicode-normalization = { workspace = true } unicode-segmentation = { workspace = true } unscanny = { workspace = true } usvg = { workspace = true } diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index bd4c90a15..49cbd02c6 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -11,6 +11,7 @@ use ecow::{eco_vec, EcoVec}; use typst_syntax::package::{PackageSpec, PackageVersion}; use typst_syntax::{Span, Spanned, SyntaxError}; +use crate::engine::Engine; use crate::{World, WorldExt}; /// Early-return with a [`StrResult`] or [`SourceResult`]. @@ -228,6 +229,23 @@ impl From for SourceDiagnostic { } } +/// Destination for a deprecation message when accessing a deprecated value. +pub trait DeprecationSink { + /// Emits the given deprecation message into this sink. + fn emit(self, message: &str); +} + +impl DeprecationSink for () { + fn emit(self, _: &str) {} +} + +impl DeprecationSink for (&mut Engine<'_>, Span) { + /// Emits the deprecation message as a warning. + fn emit(self, message: &str) { + self.0.sink.warn(SourceDiagnostic::warning(self.1, message)); + } +} + /// A part of a diagnostic's [trace](SourceDiagnostic::trace). #[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum Tracepoint { diff --git a/crates/typst-library/src/engine.rs b/crates/typst-library/src/engine.rs index 80aaef224..43a7b4671 100644 --- a/crates/typst-library/src/engine.rs +++ b/crates/typst-library/src/engine.rs @@ -312,7 +312,8 @@ impl Route<'_> { if !self.within(Route::MAX_SHOW_RULE_DEPTH) { bail!( "maximum show rule depth exceeded"; - hint: "check whether the show rule matches its own output" + hint: "maybe a show rule matches its own output"; + hint: "maybe there are too deeply nested elements" ); } Ok(()) diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index a60e6d7f2..430c4e9ad 100644 --- a/crates/typst-library/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -1,4 +1,5 @@ use std::fmt::{self, Debug, Formatter}; +use std::ops::Add; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use typst_syntax::{Span, Spanned}; @@ -304,8 +305,6 @@ impl Args { /// ``` #[func(constructor)] pub fn construct( - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to construct. #[external] @@ -366,7 +365,7 @@ impl Debug for Args { impl Repr for Args { fn repr(&self) -> EcoString { let pieces = self.items.iter().map(Arg::repr).collect::>(); - repr::pretty_array_like(&pieces, false).into() + eco_format!("arguments{}", repr::pretty_array_like(&pieces, false)) } } @@ -376,6 +375,21 @@ impl PartialEq for Args { } } +impl Add for Args { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self.items.retain(|item| { + !item.name.as_ref().is_some_and(|name| { + rhs.items.iter().any(|a| a.name.as_ref() == Some(name)) + }) + }); + self.items.extend(rhs.items); + self.span = Span::detached(); + self + } +} + /// An argument to a function call: `12` or `draw: false`. #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index 30481cd7f..18020e4cb 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -301,9 +301,7 @@ impl Array { #[func] pub fn find( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -325,9 +323,7 @@ impl Array { #[func] pub fn position( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -363,8 +359,6 @@ impl Array { /// ``` #[func] pub fn range( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The start of the range (inclusive). #[external] @@ -402,9 +396,7 @@ impl Array { #[func] pub fn filter( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -427,9 +419,7 @@ impl Array { #[func] pub fn map( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. mapper: Func, @@ -481,8 +471,6 @@ impl Array { #[func] pub fn zip( self, - /// The real arguments (the `others` arguments are just for the docs, this - /// function is a bit involved, so we parse the positional arguments manually). args: &mut Args, /// Whether all arrays have to have the same length. /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an @@ -569,9 +557,7 @@ impl Array { #[func] pub fn fold( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The initial value to start with. init: Value, @@ -631,9 +617,7 @@ impl Array { #[func] pub fn any( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -651,9 +635,7 @@ impl Array { #[func] pub fn all( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -831,11 +813,8 @@ impl Array { #[func] pub fn sorted( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// If given, applies this function to the elements in the array to /// determine the keys to sort by. @@ -911,9 +890,7 @@ impl Array { #[func(title = "Deduplicate")] pub fn dedup( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// If given, applies this function to the elements in the array to /// determine the keys to deduplicate by. @@ -997,9 +974,7 @@ impl Array { #[func] pub fn reduce( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The reducing function. Must have two parameters: One for the /// accumulated value and one for an item. @@ -1154,6 +1129,53 @@ impl FromValue for SmallVec<[T; N]> { } } +/// One element, or multiple provided as an array. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct OneOrMultiple(pub Vec); + +impl Reflect for OneOrMultiple { + fn input() -> CastInfo { + T::input() + Array::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || T::castable(value) + } +} + +impl IntoValue for OneOrMultiple { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for OneOrMultiple { + fn from_value(value: Value) -> HintedStrResult { + if T::castable(&value) { + return Ok(Self(vec![T::from_value(value)?])); + } + if Array::castable(&value) { + return Ok(Self( + Array::from_value(value)? + .into_iter() + .map(|value| T::from_value(value)) + .collect::>()?, + )); + } + Err(Self::error(&value)) + } +} + +impl Default for OneOrMultiple { + fn default() -> Self { + Self(vec![]) + } +} + /// The error message when the array is empty. #[cold] fn array_is_empty() -> EcoString { diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index 05fe4763a..d633c99ad 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -1,6 +1,8 @@ -use std::borrow::Cow; +use std::any::Any; use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref}; +use std::str::Utf8Error; use std::sync::Arc; use ecow::{eco_format, EcoString}; @@ -39,28 +41,75 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value /// #str(data.slice(1, 4)) /// ``` #[ty(scope, cast)] -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Bytes(Arc>>); +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Bytes(Arc>); impl Bytes { - /// Create a buffer from a static byte slice. - pub fn from_static(slice: &'static [u8]) -> Self { - Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) + /// Create `Bytes` from anything byte-like. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be + /// allocated. + /// + /// If the type is `Vec` and the `Bytes` are unique (i.e. not cloned), + /// the vector will be reused when mutating to the `Bytes`. + /// + /// If your source type is a string, prefer [`Bytes::from_string`] to + /// directly use the UTF-8 encoded string data without any copying. + pub fn new(data: T) -> Self + where + T: AsRef<[u8]> + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(data))) + } + + /// Create `Bytes` from anything string-like, implicitly viewing the UTF-8 + /// representation. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `String` or `EcoString` without any copying. + pub fn from_string(data: T) -> Self + where + T: AsRef + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(StrWrapper(data)))) } /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.as_slice().is_empty() } - /// Return a view into the buffer. + /// Return a view into the bytes. pub fn as_slice(&self) -> &[u8] { self } - /// Return a copy of the buffer as a vector. + /// Try to view the bytes as an UTF-8 string. + /// + /// If these bytes were created via `Bytes::from_string`, UTF-8 validation + /// is skipped. + pub fn as_str(&self) -> Result<&str, Utf8Error> { + self.inner().as_str() + } + + /// Return a copy of the bytes as a vector. pub fn to_vec(&self) -> Vec { - self.0.to_vec() + self.as_slice().to_vec() + } + + /// Try to turn the bytes into a `Str`. + /// + /// - If these bytes were created via `Bytes::from_string::`, the + /// string is cloned directly. + /// - If these bytes were created via `Bytes::from_string`, but from a + /// different type of string, UTF-8 validation is still skipped. + pub fn to_str(&self) -> Result { + match self.inner().as_any().downcast_ref::() { + Some(string) => Ok(string.clone()), + None => self.as_str().map(Into::into), + } } /// Resolve an index or throw an out of bounds error. @@ -72,12 +121,15 @@ impl Bytes { /// /// `index == len` is considered in bounds. fn locate_opt(&self, index: i64) -> Option { + let len = self.as_slice().len(); let wrapped = - if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + if index >= 0 { Some(index) } else { (len as i64).checked_add(index) }; + wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len) + } - wrapped - .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v <= self.0.len()) + /// Access the inner `dyn Bytelike`. + fn inner(&self) -> &dyn Bytelike { + &**self.0 } } @@ -106,7 +158,7 @@ impl Bytes { /// The length in bytes. #[func(title = "Length")] pub fn len(&self) -> usize { - self.0.len() + self.as_slice().len() } /// Returns the byte at the specified index. Returns the default value if @@ -122,13 +174,13 @@ impl Bytes { default: Option, ) -> StrResult { self.locate_opt(index) - .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into()))) .or(default) .ok_or_else(|| out_of_bounds_no_default(index, self.len())) } - /// Extracts a subslice of the bytes. Fails with an error if the start or end - /// index is out of bounds. + /// Extracts a subslice of the bytes. Fails with an error if the start or + /// end index is out of bounds. #[func] pub fn slice( &self, @@ -148,9 +200,17 @@ impl Bytes { if end.is_none() { end = count.map(|c: i64| start + c); } + let start = self.locate(start)?; let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); - Ok(self.0[start..end].into()) + let slice = &self.as_slice()[start..end]; + + // We could hold a view into the original bytes here instead of + // making a copy, but it's unclear when that's worth it. Java + // originally did that for strings, but went back on it because a + // very small view into a very large buffer would be a sort of + // memory leak. + Ok(Bytes::new(slice.to_vec())) } } @@ -170,7 +230,15 @@ impl Deref for Bytes { type Target = [u8]; fn deref(&self) -> &Self::Target { - &self.0 + self.inner().as_bytes() + } +} + +impl Eq for Bytes {} + +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) } } @@ -180,18 +248,6 @@ impl AsRef<[u8]> for Bytes { } } -impl From<&[u8]> for Bytes { - fn from(slice: &[u8]) -> Self { - Self(Arc::new(LazyHash::new(slice.to_vec().into()))) - } -} - -impl From> for Bytes { - fn from(vec: Vec) -> Self { - Self(Arc::new(LazyHash::new(vec.into()))) - } -} - impl Add for Bytes { type Output = Self; @@ -207,10 +263,12 @@ impl AddAssign for Bytes { // Nothing to do } else if self.is_empty() { *self = rhs; - } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { - Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); + } else if let Some(vec) = Arc::get_mut(&mut self.0) + .and_then(|unique| unique.as_any_mut().downcast_mut::>()) + { + vec.extend_from_slice(&rhs); } else { - *self = Self::from([self.as_slice(), rhs.as_slice()].concat()); + *self = Self::new([self.as_slice(), rhs.as_slice()].concat()); } } } @@ -228,20 +286,79 @@ impl Serialize for Bytes { } } +/// Any type that can back a byte buffer. +trait Bytelike: Send + Sync { + fn as_bytes(&self) -> &[u8]; + fn as_str(&self) -> Result<&str, Utf8Error>; + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl Bytelike for T +where + T: AsRef<[u8]> + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.as_ref() + } + + fn as_str(&self) -> Result<&str, Utf8Error> { + std::str::from_utf8(self.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Hash for dyn Bytelike { + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +/// Makes string-like objects usable with `Bytes`. +struct StrWrapper(T); + +impl Bytelike for StrWrapper +where + T: AsRef + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.0.as_ref().as_bytes() + } + + fn as_str(&self) -> Result<&str, Utf8Error> { + Ok(self.0.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + /// A value that can be cast to bytes. pub struct ToBytes(Bytes); cast! { ToBytes, - v: Str => Self(v.as_bytes().into()), + v: Str => Self(Bytes::from_string(v)), v: Array => Self(v.iter() .map(|item| match item { Value::Int(byte @ 0..=255) => Ok(*byte as u8), Value::Int(_) => bail!("number must be between 0 and 255"), value => Err(::error(value)), }) - .collect::, _>>()? - .into() + .collect::, _>>() + .map(Bytes::new)? ), v: Bytes => Self(v), } diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index fd4498e07..a8e0eaeb3 100644 --- a/crates/typst-library/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -97,7 +97,6 @@ cast! { /// ``` #[func(title = "Power")] pub fn pow( - /// The callsite span. span: Span, /// The base of the power. /// @@ -159,7 +158,6 @@ pub fn pow( /// ``` #[func(title = "Exponential")] pub fn exp( - /// The callsite span. span: Span, /// The exponent of the power. exponent: Spanned, @@ -412,7 +410,6 @@ pub fn tanh( /// ``` #[func(title = "Logarithm")] pub fn log( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -454,7 +451,6 @@ pub fn log( /// ``` #[func(title = "Natural Logarithm")] pub fn ln( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -782,7 +778,6 @@ pub fn round( /// ``` #[func] pub fn clamp( - /// The callsite span. span: Span, /// The number to clamp. value: DecNum, @@ -815,7 +810,6 @@ pub fn clamp( /// ``` #[func(title = "Minimum")] pub fn min( - /// The callsite span. span: Span, /// The sequence of values from which to extract the minimum. /// Must not be empty. @@ -833,7 +827,6 @@ pub fn min( /// ``` #[func(title = "Maximum")] pub fn max( - /// The callsite span. span: Span, /// The sequence of values from which to extract the maximum. /// Must not be empty. @@ -911,7 +904,6 @@ pub fn odd( /// ``` #[func(title = "Remainder")] pub fn rem( - /// The span of the function call. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -950,7 +942,6 @@ pub fn rem( /// ``` #[func(title = "Euclidean Division")] pub fn div_euclid( - /// The callsite span. span: Span, /// The dividend of the division. dividend: DecNum, @@ -994,7 +985,6 @@ pub fn div_euclid( /// ``` #[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])] pub fn rem_euclid( - /// The callsite span. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -1031,7 +1021,6 @@ pub fn rem_euclid( /// ``` #[func(title = "Quotient")] pub fn quo( - /// The span of the function call. span: Span, /// The dividend of the quotient. dividend: DecNum, diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs index 84f38f36e..38f409c67 100644 --- a/crates/typst-library/src/foundations/cast.rs +++ b/crates/typst-library/src/foundations/cast.rs @@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned}; use unicode_math_class::MathClass; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; -use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; +use crate::foundations::{ + array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value, +}; /// Determine details of a type. /// @@ -497,3 +499,58 @@ cast! { /// An operator that can be both unary or binary like `+`. "vary" => MathClass::Vary, } + +/// A type that contains a user-visible source portion and something that is +/// derived from it, but not user-visible. +/// +/// An example usage would be `source` being a `DataSource` and `derived` a +/// TextMate theme parsed from it. With `Derived`, we can store both parts in +/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue` +/// impls. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Derived { + /// The source portion. + pub source: S, + /// The derived portion. + pub derived: D, +} + +impl Derived { + /// Create a new instance from the `source` and the `derived` data. + pub fn new(source: S, derived: D) -> Self { + Self { source, derived } + } +} + +impl Reflect for Derived { + fn input() -> CastInfo { + S::input() + } + + fn output() -> CastInfo { + S::output() + } + + fn castable(value: &Value) -> bool { + S::castable(value) + } + + fn error(found: &Value) -> HintedString { + S::error(found) + } +} + +impl IntoValue for Derived { + fn into_value(self) -> Value { + self.source.into_value() + } +} + +impl Fold for Derived { + fn fold(self, outer: Self) -> Self { + Self { + source: self.source.fold(outer.source), + derived: self.derived.fold(outer.derived), + } + } +} diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index ab2f68ac2..76cd6a222 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use comemo::Tracked; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; -use smallvec::smallvec; use typst_syntax::Span; use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; @@ -500,7 +499,7 @@ impl Content { /// Link the content somewhere. pub fn linked(self, dest: Destination) -> Self { - self.styled(LinkElem::set_dests(smallvec![dest])) + self.styled(LinkElem::set_current(Some(dest))) } /// Set alignments for this content. diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs index d15cd417a..2fc48a521 100644 --- a/crates/typst-library/src/foundations/datetime.rs +++ b/crates/typst-library/src/foundations/datetime.rs @@ -318,7 +318,6 @@ impl Datetime { /// ``` #[func] pub fn today( - /// The engine. engine: &mut Engine, /// An offset to apply to the current UTC date. If set to `{auto}`, the /// offset will be the local offset. diff --git a/crates/typst-library/src/foundations/dict.rs b/crates/typst-library/src/foundations/dict.rs index e4ab54e72..c93670c1d 100644 --- a/crates/typst-library/src/foundations/dict.rs +++ b/crates/typst-library/src/foundations/dict.rs @@ -261,7 +261,12 @@ pub struct ToDict(Dict); cast! { ToDict, - v: Module => Self(v.scope().iter().map(|(k, v, _)| (Str::from(k.clone()), v.clone())).collect()), + v: Module => Self(v + .scope() + .iter() + .map(|(k, b)| (Str::from(k.clone()), b.read().clone())) + .collect() + ), } impl Debug for Dict { diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index c3d4e0e73..21d0a8d81 100644 --- a/crates/typst-library/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -110,7 +110,7 @@ impl f64 { f64::signum(self) } - /// Converts bytes to a float. + /// Interprets bytes as a float. /// /// ```example /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ @@ -120,8 +120,10 @@ impl f64 { pub fn from_bytes( /// The bytes that should be converted to a float. /// - /// Must be of length exactly 8 so that the result fits into a 64-bit - /// float. + /// Must have a length of either 4 or 8. The bytes are then + /// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s + /// binary32 (single-precision) or binary64 (double-precision) format + /// depending on the length of the bytes. bytes: Bytes, /// The endianness of the conversion. #[named] @@ -158,23 +160,26 @@ impl f64 { #[named] #[default(Endianness::Little)] endian: Endianness, + /// The size of the resulting bytes. + /// + /// This must be either 4 or 8. The call will return the + /// representation of this float in either + /// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32 + /// (single-precision) or binary64 (double-precision) format + /// depending on the provided size. #[named] #[default(8)] size: u32, ) -> StrResult { Ok(match size { - 8 => match endian { + 8 => Bytes::new(match endian { Endianness::Little => self.to_le_bytes(), Endianness::Big => self.to_be_bytes(), - } - .as_slice() - .into(), - 4 => match endian { + }), + 4 => Bytes::new(match endian { Endianness::Little => (self as f32).to_le_bytes(), Endianness::Big => (self as f32).to_be_bytes(), - } - .as_slice() - .into(), + }), _ => bail!("size must be either 4 or 8"), }) } diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 40c826df9..66c6b70a5 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -9,11 +9,11 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{ast, Span, SyntaxNode}; use typst_utils::{singleton, LazyHash, Static}; -use crate::diag::{bail, SourceResult, StrResult}; +use crate::diag::{bail, At, DeprecationSink, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, - Selector, Type, Value, + cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs, + PluginFunc, Scope, Selector, Type, Value, }; /// A mapping from argument values to a return value. @@ -151,6 +151,8 @@ enum Repr { Element(Element), /// A user-defined closure. Closure(Arc>), + /// A plugin WebAssembly function. + Plugin(Arc), /// A nested function with pre-applied arguments. With(Arc<(Func, Args)>), } @@ -164,6 +166,7 @@ impl Func { Repr::Native(native) => Some(native.name), Repr::Element(elem) => Some(elem.name()), Repr::Closure(closure) => closure.name(), + Repr::Plugin(func) => Some(func.name()), Repr::With(with) => with.0.name(), } } @@ -176,6 +179,7 @@ impl Func { Repr::Native(native) => Some(native.title), Repr::Element(elem) => Some(elem.title()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.title(), } } @@ -186,6 +190,7 @@ impl Func { Repr::Native(native) => Some(native.docs), Repr::Element(elem) => Some(elem.docs()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.docs(), } } @@ -204,6 +209,7 @@ impl Func { Repr::Native(native) => Some(&native.0.params), Repr::Element(elem) => Some(elem.params()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.params(), } } @@ -221,6 +227,7 @@ impl Func { Some(singleton!(CastInfo, CastInfo::Type(Type::of::()))) } Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.returns(), } } @@ -231,6 +238,7 @@ impl Func { Repr::Native(native) => native.keywords, Repr::Element(elem) => elem.keywords(), Repr::Closure(_) => &[], + Repr::Plugin(_) => &[], Repr::With(with) => with.0.keywords(), } } @@ -241,16 +249,21 @@ impl Func { Repr::Native(native) => Some(&native.0.scope), Repr::Element(elem) => Some(elem.scope()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.scope(), } } /// Get a field from this function's scope, if possible. - pub fn field(&self, field: &str) -> StrResult<&'static Value> { + pub fn field( + &self, + field: &str, + sink: impl DeprecationSink, + ) -> StrResult<&'static Value> { let scope = self.scope().ok_or("cannot access fields on user-defined functions")?; match scope.get(field) { - Some(field) => Ok(field), + Some(binding) => Ok(binding.read_checked(sink)), None => match self.name() { Some(name) => bail!("function `{name}` does not contain field `{field}`"), None => bail!("function does not contain field `{field}`"), @@ -266,6 +279,14 @@ impl Func { } } + /// Extract the plugin function, if it is one. + pub fn to_plugin(&self) -> Option<&PluginFunc> { + match &self.repr { + Repr::Plugin(func) => Some(func), + _ => None, + } + } + /// Call the function with the given context and arguments. pub fn call( &self, @@ -307,6 +328,12 @@ impl Func { context, args, ), + Repr::Plugin(func) => { + let inputs = args.all::()?; + let output = func.call(inputs).at(args.span)?; + args.finish()?; + Ok(Value::Bytes(output)) + } Repr::With(with) => { args.items = with.1.items.iter().cloned().chain(args.items).collect(); with.0.call(engine, context, args) @@ -334,8 +361,6 @@ impl Func { #[func] pub fn with( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to apply to the function. #[external] @@ -361,8 +386,6 @@ impl Func { #[func] pub fn where_( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The fields to filter for. #[variadic] @@ -414,10 +437,10 @@ impl PartialEq for Func { } } -impl PartialEq<&NativeFuncData> for Func { - fn eq(&self, other: &&NativeFuncData) -> bool { +impl PartialEq<&'static NativeFuncData> for Func { + fn eq(&self, other: &&'static NativeFuncData) -> bool { match &self.repr { - Repr::Native(native) => native.function == other.function, + Repr::Native(native) => *native == Static(*other), _ => false, } } @@ -429,12 +452,30 @@ impl From for Func { } } +impl From<&'static NativeFuncData> for Func { + fn from(data: &'static NativeFuncData) -> Self { + Repr::Native(Static(data)).into() + } +} + impl From for Func { fn from(func: Element) -> Self { Repr::Element(func).into() } } +impl From for Func { + fn from(closure: Closure) -> Self { + Repr::Closure(Arc::new(LazyHash::new(closure))).into() + } +} + +impl From for Func { + fn from(func: PluginFunc) -> Self { + Repr::Plugin(Arc::new(func)).into() + } +} + /// A Typst function that is defined by a native Rust type that shadows a /// native Rust function. pub trait NativeFunc { @@ -470,12 +511,6 @@ pub struct NativeFuncData { pub returns: LazyLock, } -impl From<&'static NativeFuncData> for Func { - fn from(data: &'static NativeFuncData) -> Self { - Repr::Native(Static(data)).into() - } -} - cast! { &'static NativeFuncData, self => Func::from(self).into_value(), @@ -529,12 +564,6 @@ impl Closure { } } -impl From for Func { - fn from(closure: Closure) -> Self { - Repr::Closure(Arc::new(LazyHash::new(closure))).into() - } -} - cast! { Closure, self => Value::Func(self.into()), diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index bddffada3..83a89bf8a 100644 --- a/crates/typst-library/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -1,6 +1,7 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use ecow::{eco_format, EcoString}; +use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{ @@ -322,7 +323,7 @@ impl i64 { Endianness::Little => self.to_le_bytes(), }; - let mut buf = vec![0u8; size]; + let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size); match endian { Endianness::Big => { // Copy the bytes from the array to the buffer, starting from @@ -339,7 +340,7 @@ impl i64 { } } - Bytes::from(buf) + Bytes::new(buf) } } diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index d960a666c..8e3aa060d 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -25,7 +25,8 @@ mod int; mod label; mod module; mod none; -mod plugin; +#[path = "plugin.rs"] +mod plugin_; mod scope; mod selector; mod str; @@ -56,7 +57,7 @@ pub use self::int::*; pub use self::label::*; pub use self::module::*; pub use self::none::*; -pub use self::plugin::*; +pub use self::plugin_::*; pub use self::repr::Repr; pub use self::scope::*; pub use self::selector::*; @@ -84,16 +85,9 @@ use crate::engine::Engine; use crate::routines::EvalMode; use crate::{Feature, Features}; -/// Foundational types and functions. -/// -/// Here, you'll find documentation for basic data types like [integers]($int) -/// and [strings]($str) as well as details about core computational functions. -#[category] -pub static FOUNDATIONS: Category; - /// Hook up all `foundations` definitions. pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { - global.category(FOUNDATIONS); + global.start_category(crate::Category::Foundations); global.define_type::(); global.define_type::(); global.define_type::(); @@ -114,16 +108,17 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { global.define_type::(); global.define_type::(); global.define_type::(); - global.define_type::(); global.define_func::(); global.define_func::(); global.define_func::(); global.define_func::(); + global.define_func::(); if features.is_enabled(Feature::Html) { global.define_func::(); } - global.define_module(calc::module()); - global.define_module(sys::module(inputs)); + global.define("calc", calc::module()); + global.define("sys", sys::module(inputs)); + global.reset_category(); } /// Fails with an error. @@ -266,7 +261,6 @@ impl assert { /// ``` #[func(title = "Evaluate")] pub fn eval( - /// The engine. engine: &mut Engine, /// A string of Typst code to evaluate. source: Spanned, @@ -301,7 +295,7 @@ pub fn eval( let dict = scope; let mut scope = Scope::new(); for (key, value) in dict { - scope.define_spanned(key, value, span); + scope.bind(key.into(), Binding::new(value, span)); } (engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope) } diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index a476d6af1..8d9626a1a 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -4,17 +4,23 @@ use std::sync::Arc; use ecow::{eco_format, EcoString}; use typst_syntax::FileId; -use crate::diag::StrResult; +use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{repr, ty, Content, Scope, Value}; -/// An evaluated module, either built-in or resulting from a file. +/// An module of definitions. /// -/// You can access definitions from the module using -/// [field access notation]($scripting/#fields) and interact with it using the -/// [import and include syntaxes]($scripting/#modules). Alternatively, it is -/// possible to convert a module to a dictionary, and therefore access its -/// contents dynamically, using the -/// [dictionary constructor]($dictionary/#constructor). +/// A module +/// - be built-in +/// - stem from a [file import]($scripting/#modules) +/// - stem from a [package import]($scripting/#packages) (and thus indirectly +/// its entrypoint file) +/// - result from a call to the [plugin]($plugin) function +/// +/// You can access definitions from the module using [field access +/// notation]($scripting/#fields) and interact with it using the [import and +/// include syntaxes]($scripting/#modules). Alternatively, it is possible to +/// convert a module to a dictionary, and therefore access its contents +/// dynamically, using the [dictionary constructor]($dictionary/#constructor). /// /// # Example /// ```example @@ -32,7 +38,7 @@ use crate::foundations::{repr, ty, Content, Scope, Value}; #[allow(clippy::derived_hash_with_manual_eq)] pub struct Module { /// The module's name. - name: EcoString, + name: Option, /// The reference-counted inner fields. inner: Arc, } @@ -52,14 +58,22 @@ impl Module { /// Create a new module. pub fn new(name: impl Into, scope: Scope) -> Self { Self { - name: name.into(), + name: Some(name.into()), + inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), + } + } + + /// Create a new anonymous module without a name. + pub fn anonymous(scope: Scope) -> Self { + Self { + name: None, inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), } } /// Update the module's name. pub fn with_name(mut self, name: impl Into) -> Self { - self.name = name.into(); + self.name = Some(name.into()); self } @@ -82,8 +96,8 @@ impl Module { } /// Get the module's name. - pub fn name(&self) -> &EcoString { - &self.name + pub fn name(&self) -> Option<&EcoString> { + self.name.as_ref() } /// Access the module's scope. @@ -104,10 +118,14 @@ impl Module { } /// Try to access a definition in the module. - pub fn field(&self, name: &str) -> StrResult<&Value> { - self.scope().get(name).ok_or_else(|| { - eco_format!("module `{}` does not contain `{name}`", self.name()) - }) + pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<&Value> { + match self.scope().get(field) { + Some(binding) => Ok(binding.read_checked(sink)), + None => match &self.name { + Some(name) => bail!("module `{name}` does not contain `{field}`"), + None => bail!("module does not contain `{field}`"), + }, + } } /// Extract the module's content. @@ -131,7 +149,10 @@ impl Debug for Module { impl repr::Repr for Module { fn repr(&self) -> EcoString { - eco_format!("", self.name()) + match &self.name { + Some(module) => eco_format!(""), + None => "".into(), + } } } diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 8d12966bf..6c2408446 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -6,7 +6,9 @@ use ecow::eco_format; use typst_utils::Numeric; use crate::diag::{bail, HintedStrResult, StrResult}; -use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value}; +use crate::foundations::{ + format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value, +}; use crate::layout::{Alignment, Length, Rel}; use crate::text::TextElem; use crate::visualize::Stroke; @@ -30,12 +32,13 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())), (Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Str(a), Content(b)) => Content(TextElem::packed(a) + b), - (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), + (Args(a), Args(b)) => Args(a + b), (a, b) => mismatch!("cannot join {} with {}", a, b), }) } @@ -129,13 +132,14 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())), (Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Str(a), Content(b)) => Content(TextElem::packed(a) + b), - (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), + (Args(a), Args(b)) => Args(a + b), (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { Stroke::from_pair(color, thickness).into_value() @@ -443,7 +447,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { (Args(a), Args(b)) => a == b, (Type(a), Type(b)) => a == b, (Module(a), Module(b)) => a == b, - (Plugin(a), Plugin(b)) => a == b, (Datetime(a), Datetime(b)) => a == b, (Duration(a), Duration(b)) => a == b, (Dyn(a), Dyn(b)) => a == b, diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index f57257a45..31f8cd732 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -4,43 +4,27 @@ use std::sync::{Arc, Mutex}; use ecow::{eco_format, EcoString}; use typst_syntax::Spanned; -use wasmi::{AsContext, AsContextMut}; +use wasmi::Memory; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; -use crate::foundations::{func, repr, scope, ty, Bytes}; -use crate::World; +use crate::foundations::{cast, func, scope, Binding, Bytes, Func, Module, Scope, Value}; +use crate::loading::{DataSource, Load}; -/// A WebAssembly plugin. +/// Loads a WebAssembly module. /// -/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin -/// functions may accept multiple [byte buffers]($bytes) as arguments and return -/// a single byte buffer. They should typically be wrapped in idiomatic Typst -/// functions that perform the necessary conversions between native Typst types -/// and bytes. +/// The resulting [module] will contain one Typst [function] for each function +/// export of the loaded WebAssembly module. /// -/// Plugins run in isolation from your system, which means that printing, -/// reading files, or anything like that will not be supported for security -/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit -/// shared WebAssembly library. Many compilers will use the -/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. -/// emscripten), which allows printing, reading files, etc. This ABI will not -/// directly work with Typst. You will either need to compile to a different -/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub). +/// Typst WebAssembly plugins need to follow a specific +/// [protocol]($plugin/#protocol). To run as a plugin, a program needs to be +/// compiled to a 32-bit shared WebAssembly library. Plugin functions may accept +/// multiple [byte buffers]($bytes) as arguments and return a single byte +/// buffer. They should typically be wrapped in idiomatic Typst functions that +/// perform the necessary conversions between native Typst types and bytes. /// -/// # Plugins and Packages -/// Plugins are distributed as packages. A package can make use of a plugin -/// simply by including a WebAssembly file and loading it. Because the -/// byte-based plugin interface is quite low-level, plugins are typically -/// exposed through wrapper functions, that also live in the same package. -/// -/// # Purity -/// Plugin functions must be pure: Given the same arguments, they must always -/// return the same value. The reason for this is that Typst functions must be -/// pure (which is quite fundamental to the language design) and, since Typst -/// function can call plugin functions, this requirement is inherited. In -/// particular, if a plugin function is called twice with the same arguments, -/// Typst might cache the results and call your function only once. +/// For security reasons, plugins run in isolation from your system. This means +/// that printing, reading files, or similar things are not supported. /// /// # Example /// ```example @@ -55,6 +39,50 @@ use crate::World; /// #concat("hello", "world") /// ``` /// +/// Since the plugin function returns a module, it can be used with import +/// syntax: +/// ```typ +/// #import plugin("hello.wasm"): concatenate +/// ``` +/// +/// # Purity +/// Plugin functions **must be pure:** A plugin function call most not have any +/// observable side effects on future plugin calls and given the same arguments, +/// it must always return the same value. +/// +/// The reason for this is that Typst functions must be pure (which is quite +/// fundamental to the language design) and, since Typst function can call +/// plugin functions, this requirement is inherited. In particular, if a plugin +/// function is called twice with the same arguments, Typst might cache the +/// results and call your function only once. Moreover, Typst may run multiple +/// instances of your plugin in multiple threads, with no state shared between +/// them. +/// +/// Typst does not enforce plugin function purity (for efficiency reasons), but +/// calling an impure function will lead to unpredictable and irreproducible +/// results and must be avoided. +/// +/// That said, mutable operations _can be_ useful for plugins that require +/// costly runtime initialization. Due to the purity requirement, such +/// initialization cannot be performed through a normal function call. Instead, +/// Typst exposes a [plugin transition API]($plugin.transition), which executes +/// a function call and then creates a derived module with new functions which +/// will observe the side effects produced by the transition call. The original +/// plugin remains unaffected. +/// +/// # Plugins and Packages +/// Any Typst code can make use of a plugin simply by including a WebAssembly +/// file and loading it. However, because the byte-based plugin interface is +/// quite low-level, plugins are typically exposed through a package containing +/// the plugin and idiomatic wrapper functions. +/// +/// # WASI +/// Many compilers will use the [WASI ABI](https://wasi.dev/) by default or as +/// their only option (e.g. emscripten), which allows printing, reading files, +/// etc. This ABI will not directly work with Typst. You will either need to +/// compile to a different target or [stub all +/// functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub). +/// /// # Protocol /// To be used as a plugin, a WebAssembly module must conform to the following /// protocol: @@ -67,8 +95,8 @@ use crate::World; /// lengths, so `usize/size_t` may be preferable), and return one 32-bit /// integer. /// -/// - The function should first allocate a buffer `buf` of length -/// `a_1 + a_2 + ... + a_n`, and then call +/// - The function should first allocate a buffer `buf` of length `a_1 + a_2 + +/// ... + a_n`, and then call /// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. /// /// - The `a_1` first bytes of the buffer now constitute the first argument, the @@ -85,19 +113,21 @@ use crate::World; /// then interpreted as an UTF-8 encoded error message. /// /// ## Imports -/// Plugin modules need to import two functions that are provided by the runtime. -/// (Types and functions are described using WAT syntax.) +/// Plugin modules need to import two functions that are provided by the +/// runtime. (Types and functions are described using WAT syntax.) /// -/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` +/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func +/// (param i32)))` /// /// Writes the arguments for the current function into a plugin-allocated -/// buffer. When a plugin function is called, it -/// [receives the lengths](#exports) of its input buffers as arguments. It -/// should then allocate a buffer whose capacity is at least the sum of these -/// lengths. It should then call this function with a `ptr` to the buffer to -/// fill it with the arguments, one after another. +/// buffer. When a plugin function is called, it [receives the +/// lengths](#exports) of its input buffers as arguments. It should then +/// allocate a buffer whose capacity is at least the sum of these lengths. It +/// should then call this function with a `ptr` to the buffer to fill it with +/// the arguments, one after another. /// -/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` +/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func +/// (param i32 i32)))` /// /// Sends the output of the current function to the host (Typst). The first /// parameter shall be a pointer to a buffer (`ptr`), while the second is the @@ -106,75 +136,145 @@ use crate::World; /// interpreted as an error message, it should be encoded as UTF-8. /// /// # Resources -/// For more resources, check out the -/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). -/// It contains: +/// For more resources, check out the [wasm-minimal-protocol +/// repository](https://github.com/astrale-sharp/wasm-minimal-protocol). It +/// contains: /// /// - A list of example plugin implementations and a test runner for these /// examples /// - Wrappers to help you write your plugin in Rust (Zig wrapper in /// development) /// - A stubber for WASI -#[ty(scope, cast)] -#[derive(Clone)] -pub struct Plugin(Arc); - -/// The internal representation of a plugin. -struct Repr { - /// The raw WebAssembly bytes. - bytes: Bytes, - /// The function defined by the WebAssembly module. - functions: Vec<(EcoString, wasmi::Func)>, - /// Owns all data associated with the WebAssembly module. - store: Mutex, -} - -/// Owns all data associated with the WebAssembly module. -type Store = wasmi::Store; - -/// If there was an error reading/writing memory, keep the offset + length to -/// display an error message. -struct MemoryError { - offset: u32, - length: u32, - write: bool, -} -/// The persistent store data used for communication between store and host. -#[derive(Default)] -struct StoreData { - args: Vec, - output: Vec, - memory_error: Option, +#[func(scope)] +pub fn plugin( + engine: &mut Engine, + /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes. + source: Spanned, +) -> SourceResult { + let data = source.load(engine.world)?; + Plugin::module(data).at(source.span) } #[scope] -impl Plugin { - /// Creates a new plugin from a WebAssembly file. - #[func(constructor)] - pub fn construct( - /// The engine. - engine: &mut Engine, - /// Path to a WebAssembly file. - /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, - ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - Plugin::new(data).at(span) +impl plugin { + /// Calls a plugin function that has side effects and returns a new module + /// with plugin functions that are guaranteed to have observed the results + /// of the mutable call. + /// + /// Note that calling an impure function through a normal function call + /// (without use of the transition API) is forbidden and leads to + /// unpredictable behaviour. Read the [section on purity]($plugin/#purity) + /// for more details. + /// + /// In the example below, we load the plugin `hello-mut.wasm` which exports + /// two functions: The `get()` function retrieves a global array as a + /// string. The `add(value)` function adds a value to the global array. + /// + /// We call `add` via the transition API. The call `mutated.get()` on the + /// derived module will observe the addition. Meanwhile the original module + /// remains untouched as demonstrated by the `base.get()` call. + /// + /// _Note:_ Due to limitations in the internal WebAssembly implementation, + /// the transition API can only guarantee to reflect changes in the plugin's + /// memory, not in WebAssembly globals. If your plugin relies on changes to + /// globals being visible after transition, you might want to avoid use of + /// the transition API for now. We hope to lift this limitation in the + /// future. + /// + /// ```typ + /// #let base = plugin("hello-mut.wasm") + /// #assert.eq(base.get(), "[]") + /// + /// #let mutated = plugin.transition(base.add, "hello") + /// #assert.eq(base.get(), "[]") + /// #assert.eq(mutated.get(), "[hello]") + /// ``` + #[func] + pub fn transition( + /// The plugin function to call. + func: PluginFunc, + /// The byte buffers to call the function with. + #[variadic] + arguments: Vec, + ) -> StrResult { + func.transition(arguments) } } +/// A function loaded from a WebAssembly plugin. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct PluginFunc { + /// The underlying plugin, shared by this and the other functions. + plugin: Arc, + /// The name of the plugin function. + name: EcoString, +} + +impl PluginFunc { + /// The name of the plugin function. + pub fn name(&self) -> &str { + &self.name + } + + /// Call the WebAssembly function with the given arguments. + #[comemo::memoize] + #[typst_macros::time(name = "call plugin")] + pub fn call(&self, args: Vec) -> StrResult { + self.plugin.call(&self.name, args) + } + + /// Transition a plugin and turn the result into a module. + #[comemo::memoize] + #[typst_macros::time(name = "transition plugin")] + pub fn transition(&self, args: Vec) -> StrResult { + self.plugin.transition(&self.name, args).map(Plugin::into_module) + } +} + +cast! { + PluginFunc, + self => Value::Func(self.into()), + v: Func => v.to_plugin().ok_or("expected plugin function")?.clone(), +} + +/// A plugin with potentially multiple instances for multi-threaded +/// execution. +struct Plugin { + /// Shared by all variants of the plugin. + base: Arc, + /// A pool of plugin instances. + /// + /// When multiple plugin calls run concurrently due to multi-threading, we + /// create new instances whenever we run out of ones. + pool: Mutex>, + /// A snapshot that new instances should be restored to. + snapshot: Option, + /// A combined hash that incorporates all function names and arguments used + /// in transitions of this plugin, such that this plugin has a deterministic + /// hash and equality check that can differentiate it from "siblings" (same + /// base, different transitions). + fingerprint: u128, +} + impl Plugin { - /// Create a new plugin from raw WebAssembly bytes. + /// Create a plugin and turn it into a module. #[comemo::memoize] #[typst_macros::time(name = "load plugin")] - pub fn new(bytes: Bytes) -> StrResult { + fn module(bytes: Bytes) -> StrResult { + Self::new(bytes).map(Self::into_module) + } + + /// Create a new plugin from raw WebAssembly bytes. + fn new(bytes: Bytes) -> StrResult { let engine = wasmi::Engine::default(); let module = wasmi::Module::new(&engine, bytes.as_slice()) .map_err(|err| format!("failed to load WebAssembly module ({err})"))?; + // Ensure that the plugin exports its memory. + if !matches!(module.get_export("memory"), Some(wasmi::ExternType::Memory(_))) { + bail!("plugin does not export its memory"); + } + let mut linker = wasmi::Linker::new(&engine); linker .func_wrap( @@ -191,58 +291,174 @@ impl Plugin { ) .unwrap(); - let mut store = Store::new(&engine, StoreData::default()); - let instance = linker - .instantiate(&mut store, &module) + let base = Arc::new(PluginBase { bytes, linker, module }); + let instance = PluginInstance::new(&base, None)?; + + Ok(Self { + base, + snapshot: None, + fingerprint: 0, + pool: Mutex::new(vec![instance]), + }) + } + + /// Execute a function with access to an instsance. + fn call(&self, func: &str, args: Vec) -> StrResult { + // Acquire an instance from the pool (potentially creating a new one). + let mut instance = self.acquire()?; + + // Execute the call on an instance from the pool. If the call fails, we + // return early and _don't_ return the instance to the pool as it might + // be irrecoverably damaged. + let output = instance.call(func, args)?; + + // Return the instance to the pool. + self.pool.lock().unwrap().push(instance); + + Ok(output) + } + + /// Call a mutable plugin function, producing a new mutable whose functions + /// are guaranteed to be able to observe the mutation. + fn transition(&self, func: &str, args: Vec) -> StrResult { + // Derive a new transition hash from the old one and the function and arguments. + let fingerprint = typst_utils::hash128(&(self.fingerprint, func, &args)); + + // Execute the mutable call on an instance. + let mut instance = self.acquire()?; + + // Call the function. If the call fails, we return early and _don't_ + // return the instance to the pool as it might be irrecoverably damaged. + instance.call(func, args)?; + + // Snapshot the instance after the mutable call. + let snapshot = instance.snapshot(); + + // Create a new plugin and move (this is important!) the used instance + // into it, so that the old plugin won't observe the mutation. Also + // save the snapshot so that instances that are initialized for the + // transitioned plugin's pool observe the mutation. + Ok(Self { + base: self.base.clone(), + snapshot: Some(snapshot), + fingerprint, + pool: Mutex::new(vec![instance]), + }) + } + + /// Acquire an instance from the pool (or create a new one). + fn acquire(&self) -> StrResult { + // Don't use match to ensure that the lock is released before we create + // a new instance. + if let Some(instance) = self.pool.lock().unwrap().pop() { + return Ok(instance); + } + + PluginInstance::new(&self.base, self.snapshot.as_ref()) + } + + /// Turn a plugin into a Typst module containing plugin functions. + fn into_module(self) -> Module { + let shared = Arc::new(self); + + // Build a scope from the collected functions. + let mut scope = Scope::new(); + for export in shared.base.module.exports() { + if matches!(export.ty(), wasmi::ExternType::Func(_)) { + let name = EcoString::from(export.name()); + let func = PluginFunc { plugin: shared.clone(), name: name.clone() }; + scope.bind(name, Binding::detached(Func::from(func))); + } + } + + Module::anonymous(scope) + } +} + +impl Debug for Plugin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Plugin(..)") + } +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.base.bytes == other.base.bytes && self.fingerprint == other.fingerprint + } +} + +impl Hash for Plugin { + fn hash(&self, state: &mut H) { + self.base.bytes.hash(state); + self.fingerprint.hash(state); + } +} + +/// Shared by all pooled & transitioned variants of the plugin. +struct PluginBase { + /// The raw WebAssembly bytes. + bytes: Bytes, + /// The compiled WebAssembly module. + module: wasmi::Module, + /// A linker used to create a `Store` for execution. + linker: wasmi::Linker, +} + +/// An single plugin instance for single-threaded execution. +struct PluginInstance { + /// The underlying wasmi instance. + instance: wasmi::Instance, + /// The execution store of this concrete plugin instance. + store: wasmi::Store, +} + +/// A snapshot of a plugin instance. +struct Snapshot { + /// The number of pages in the main memory. + mem_pages: u32, + /// The data in the main memory. + mem_data: Vec, +} + +impl PluginInstance { + /// Create a new execution instance of a plugin, potentially restoring + /// a snapshot. + #[typst_macros::time(name = "create plugin instance")] + fn new(base: &PluginBase, snapshot: Option<&Snapshot>) -> StrResult { + let mut store = wasmi::Store::new(base.linker.engine(), CallData::default()); + let instance = base + .linker + .instantiate(&mut store, &base.module) .and_then(|pre_instance| pre_instance.start(&mut store)) .map_err(|e| eco_format!("{e}"))?; - // Ensure that the plugin exports its memory. - if !matches!( - instance.get_export(&store, "memory"), - Some(wasmi::Extern::Memory(_)) - ) { - bail!("plugin does not export its memory"); + let mut instance = PluginInstance { instance, store }; + if let Some(snapshot) = snapshot { + instance.restore(snapshot); } - - // Collect exported functions. - let functions = instance - .exports(&store) - .filter_map(|export| { - let name = export.name().into(); - export.into_func().map(|func| (name, func)) - }) - .collect(); - - Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) }))) + Ok(instance) } - /// Call the plugin function with the given `name`. - #[comemo::memoize] - #[typst_macros::time(name = "call plugin")] - pub fn call(&self, name: &str, args: Vec) -> StrResult { - // Find the function with the given name. - let func = self - .0 - .functions - .iter() - .find(|(v, _)| v == name) - .map(|&(_, func)| func) - .ok_or_else(|| { - eco_format!("plugin does not contain a function called {name}") - })?; + /// Call a plugin function with byte arguments. + fn call(&mut self, func: &str, args: Vec) -> StrResult { + let handle = self + .instance + .get_export(&self.store, func) + .unwrap() + .into_func() + .unwrap(); + let ty = handle.ty(&self.store); - let mut store = self.0.store.lock().unwrap(); - let ty = func.ty(store.as_context()); - - // Check function signature. + // Check function signature. Do this lazily only when a function is called + // because there might be exported functions like `_initialize` that don't + // match the schema. if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) { bail!( - "plugin function `{name}` has a parameter that is not a 32-bit integer" + "plugin function `{func}` has a parameter that is not a 32-bit integer" ); } if ty.results() != [wasmi::core::ValType::I32] { - bail!("plugin function `{name}` does not return exactly one 32-bit integer"); + bail!("plugin function `{func}` does not return exactly one 32-bit integer"); } // Check inputs. @@ -263,23 +479,26 @@ impl Plugin { .collect::>(); // Store the input data. - store.data_mut().args = args; + self.store.data_mut().args = args; // Call the function. let mut code = wasmi::Val::I32(-1); - func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) + handle + .call(&mut self.store, &lengths, std::slice::from_mut(&mut code)) .map_err(|err| eco_format!("plugin panicked: {err}"))?; + if let Some(MemoryError { offset, length, write }) = - store.data_mut().memory_error.take() + self.store.data_mut().memory_error.take() { return Err(eco_format!( - "plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}", + "plugin tried to {kind} out of bounds: \ + pointer {offset:#x} is out of bounds for {kind} of length {length}", kind = if write { "write" } else { "read" } )); } // Extract the returned data. - let output = std::mem::take(&mut store.data_mut().output); + let output = std::mem::take(&mut self.store.data_mut().output); // Parse the functions return value. match code { @@ -293,42 +512,66 @@ impl Plugin { _ => bail!("plugin did not respect the protocol"), }; - Ok(output.into()) + Ok(Bytes::new(output)) } - /// An iterator over all the function names defined by the plugin. - pub fn iter(&self) -> impl Iterator { - self.0.functions.as_slice().iter().map(|(func_name, _)| func_name) + /// Creates a snapshot of this instance from which another one can be + /// initialized. + #[typst_macros::time(name = "save snapshot")] + fn snapshot(&self) -> Snapshot { + let memory = self.memory(); + let mem_pages = memory.size(&self.store); + let mem_data = memory.data(&self.store).to_vec(); + Snapshot { mem_pages, mem_data } + } + + /// Restores the instance to a snapshot. + #[typst_macros::time(name = "restore snapshot")] + fn restore(&mut self, snapshot: &Snapshot) { + let memory = self.memory(); + let current_size = memory.size(&self.store); + if current_size < snapshot.mem_pages { + memory + .grow(&mut self.store, snapshot.mem_pages - current_size) + .unwrap(); + } + + memory.data_mut(&mut self.store)[..snapshot.mem_data.len()] + .copy_from_slice(&snapshot.mem_data); + } + + /// Retrieves a handle to the plugin's main memory. + fn memory(&self) -> Memory { + self.instance + .get_export(&self.store, "memory") + .unwrap() + .into_memory() + .unwrap() } } -impl Debug for Plugin { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("Plugin(..)") - } +/// The persistent store data used for communication between store and host. +#[derive(Default)] +struct CallData { + /// Arguments for a current call. + args: Vec, + /// The results of the current call. + output: Vec, + /// A memory error that occured during execution of the current call. + memory_error: Option, } -impl repr::Repr for Plugin { - fn repr(&self) -> EcoString { - "plugin(..)".into() - } -} - -impl PartialEq for Plugin { - fn eq(&self, other: &Self) -> bool { - self.0.bytes == other.0.bytes - } -} - -impl Hash for Plugin { - fn hash(&self, state: &mut H) { - self.0.bytes.hash(state); - } +/// If there was an error reading/writing memory, keep the offset + length to +/// display an error message. +struct MemoryError { + offset: u32, + length: u32, + write: bool, } /// Write the arguments to the plugin function into the plugin's memory. fn wasm_minimal_protocol_write_args_to_buffer( - mut caller: wasmi::Caller, + mut caller: wasmi::Caller, ptr: u32, ) { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); @@ -349,7 +592,7 @@ fn wasm_minimal_protocol_write_args_to_buffer( /// Extracts the output of the plugin function from the plugin's memory. fn wasm_minimal_protocol_send_result_to_host( - mut caller: wasmi::Caller, + mut caller: wasmi::Caller, ptr: u32, len: u32, ) { diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index b51f8caaf..e1ce61b8a 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -1,21 +1,17 @@ -#[doc(inline)] -pub use typst_macros::category; - use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use ecow::{eco_format, EcoString}; +use indexmap::map::Entry; use indexmap::IndexMap; -use typst_syntax::ast::{self, AstNode}; use typst_syntax::Span; -use typst_utils::Static; -use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; +use crate::diag::{bail, DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ - Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, - NativeType, Type, Value, + Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, + Type, Value, }; -use crate::Library; +use crate::{Category, Library}; /// A stack of scopes. #[derive(Debug, Default, Clone)] @@ -46,14 +42,14 @@ impl<'a> Scopes<'a> { self.top = self.scopes.pop().expect("no pushed scope"); } - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> HintedStrResult<&Value> { + /// Try to access a binding immutably. + pub fn get(&self, var: &str) -> HintedStrResult<&Binding> { std::iter::once(&self.top) .chain(self.scopes.iter().rev()) .find_map(|scope| scope.get(var)) .or_else(|| { self.base.and_then(|base| match base.global.scope().get(var) { - Some(value) => Some(value), + Some(binding) => Some(binding), None if var == "std" => Some(&base.std), None => None, }) @@ -61,14 +57,28 @@ impl<'a> Scopes<'a> { .ok_or_else(|| unknown_variable(var)) } - /// Try to access a variable immutably in math. - pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Value> { + /// Try to access a binding mutably. + pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Binding> { + std::iter::once(&mut self.top) + .chain(&mut self.scopes.iter_mut().rev()) + .find_map(|scope| scope.get_mut(var)) + .ok_or_else(|| { + match self.base.and_then(|base| base.global.scope().get(var)) { + Some(_) => cannot_mutate_constant(var), + _ if var == "std" => cannot_mutate_constant(var), + _ => unknown_variable(var), + } + }) + } + + /// Try to access a binding immutably in math. + pub fn get_in_math(&self, var: &str) -> HintedStrResult<&Binding> { std::iter::once(&self.top) .chain(self.scopes.iter().rev()) .find_map(|scope| scope.get(var)) .or_else(|| { self.base.and_then(|base| match base.math.scope().get(var) { - Some(value) => Some(value), + Some(binding) => Some(binding), None if var == "std" => Some(&base.std), None => None, }) @@ -81,20 +91,6 @@ impl<'a> Scopes<'a> { }) } - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> HintedStrResult<&mut Value> { - std::iter::once(&mut self.top) - .chain(&mut self.scopes.iter_mut().rev()) - .find_map(|scope| scope.get_mut(var)) - .ok_or_else(|| { - match self.base.and_then(|base| base.global.scope().get(var)) { - Some(_) => cannot_mutate_constant(var), - _ if var == "std" => cannot_mutate_constant(var), - _ => unknown_variable(var), - } - })? - } - /// Check if an std variable is shadowed. pub fn check_std_shadowed(&self, var: &str) -> bool { self.base.is_some_and(|base| base.global.scope().get(var).is_some()) @@ -104,63 +100,15 @@ impl<'a> Scopes<'a> { } } -#[cold] -fn cannot_mutate_constant(var: &str) -> HintedString { - eco_format!("cannot mutate a constant: {}", var).into() -} - -/// The error message when a variable is not found. -#[cold] -fn unknown_variable(var: &str) -> HintedString { - let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); - - if var.contains('-') { - res.hint(eco_format!( - "if you meant to use subtraction, try adding spaces around the minus sign{}: `{}`", - if var.matches('-').count() > 1 { "s" } else { "" }, - var.replace('-', " - ") - )); - } - - res -} - -#[cold] -fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { - let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); - - if matches!(var, "none" | "auto" | "false" | "true") { - res.hint(eco_format!( - "if you meant to use a literal, try adding a hash before it: `#{var}`", - )); - } else if in_global { - res.hint(eco_format!( - "`{var}` is not available directly in math, try adding a hash before it: `#{var}`", - )); - } else { - res.hint(eco_format!( - "if you meant to display multiple letters as is, try adding spaces between each letter: `{}`", - var.chars() - .flat_map(|c| [' ', c]) - .skip(1) - .collect::() - )); - res.hint(eco_format!( - "or if you meant to display this as text, try placing it in quotes: `\"{var}\"`" - )); - } - - res -} - /// A map from binding names to values. #[derive(Default, Clone)] pub struct Scope { - map: IndexMap, + map: IndexMap, deduplicate: bool, category: Option, } +/// Scope construction. impl Scope { /// Create a new empty scope. pub fn new() -> Self { @@ -173,7 +121,7 @@ impl Scope { } /// Enter a new category. - pub fn category(&mut self, category: Category) { + pub fn start_category(&mut self, category: Category) { self.category = Some(category); } @@ -182,107 +130,87 @@ impl Scope { self.category = None; } - /// Bind a value to a name. - #[track_caller] - pub fn define(&mut self, name: impl Into, value: impl IntoValue) { - self.define_spanned(name, value, Span::detached()) - } - - /// Bind a value to a name defined by an identifier. - #[track_caller] - pub fn define_ident(&mut self, ident: ast::Ident, value: impl IntoValue) { - self.define_spanned(ident.get().clone(), value, ident.span()) - } - - /// Bind a value to a name. - #[track_caller] - pub fn define_spanned( - &mut self, - name: impl Into, - value: impl IntoValue, - span: Span, - ) { - let name = name.into(); - - #[cfg(debug_assertions)] - if self.deduplicate && self.map.contains_key(&name) { - panic!("duplicate definition: {name}"); - } - - self.map.insert( - name, - Slot::new(value.into_value(), span, Kind::Normal, self.category), - ); - } - - /// Define a captured, immutable binding. - pub fn define_captured( - &mut self, - name: EcoString, - value: Value, - capturer: Capturer, - span: Span, - ) { - self.map.insert( - name, - Slot::new(value.into_value(), span, Kind::Captured(capturer), self.category), - ); - } - /// Define a native function through a Rust type that shadows the function. - pub fn define_func(&mut self) { + #[track_caller] + pub fn define_func(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Func::from(data)); + self.define(data.name, Func::from(data)) } /// Define a native function with raw function data. - pub fn define_func_with_data(&mut self, data: &'static NativeFuncData) { - self.define(data.name, Func::from(data)); + #[track_caller] + pub fn define_func_with_data( + &mut self, + data: &'static NativeFuncData, + ) -> &mut Binding { + self.define(data.name, Func::from(data)) } /// Define a native type. - pub fn define_type(&mut self) { + #[track_caller] + pub fn define_type(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Type::from(data)); + self.define(data.name, Type::from(data)) } /// Define a native element. - pub fn define_elem(&mut self) { + #[track_caller] + pub fn define_elem(&mut self) -> &mut Binding { let data = T::data(); - self.define(data.name, Element::from(data)); + self.define(data.name, Element::from(data)) } - /// Define a module. - pub fn define_module(&mut self, module: Module) { - self.define(module.name().clone(), module); + /// Define a built-in with compile-time known name and returns a mutable + /// reference to it. + /// + /// When the name isn't compile-time known, you should instead use: + /// - `Vm::bind` if you already have [`Binding`] + /// - `Vm::define` if you only have a [`Value`] + /// - [`Scope::bind`](Self::bind) if you are not operating in the context of + /// a `Vm` or if you are binding to something that is not an AST + /// identifier (e.g. when constructing a dynamic + /// [`Module`](super::Module)) + #[track_caller] + pub fn define(&mut self, name: &'static str, value: impl IntoValue) -> &mut Binding { + #[cfg(debug_assertions)] + if self.deduplicate && self.map.contains_key(name) { + panic!("duplicate definition: {name}"); + } + + let mut binding = Binding::detached(value); + binding.category = self.category; + self.bind(name.into(), binding) + } +} + +/// Scope manipulation and access. +impl Scope { + /// Inserts a binding into this scope and returns a mutable reference to it. + /// + /// Prefer `Vm::bind` if you are operating in the context of a `Vm`. + pub fn bind(&mut self, name: EcoString, binding: Binding) -> &mut Binding { + match self.map.entry(name) { + Entry::Occupied(mut entry) => { + entry.insert(binding); + entry.into_mut() + } + Entry::Vacant(entry) => entry.insert(binding), + } } - /// Try to access a variable immutably. - pub fn get(&self, var: &str) -> Option<&Value> { - self.map.get(var).map(Slot::read) + /// Try to access a binding immutably. + pub fn get(&self, var: &str) -> Option<&Binding> { + self.map.get(var) } - /// Try to access a variable mutably. - pub fn get_mut(&mut self, var: &str) -> Option> { - self.map - .get_mut(var) - .map(Slot::write) - .map(|res| res.map_err(HintedString::from)) - } - - /// Get the span of a definition. - pub fn get_span(&self, var: &str) -> Option { - Some(self.map.get(var)?.span) - } - - /// Get the category of a definition. - pub fn get_category(&self, var: &str) -> Option { - self.map.get(var)?.category + /// Try to access a binding mutably. + pub fn get_mut(&mut self, var: &str) -> Option<&mut Binding> { + self.map.get_mut(var) } /// Iterate over all definitions. - pub fn iter(&self) -> impl Iterator { - self.map.iter().map(|(k, v)| (k, v.read(), v.span)) + pub fn iter(&self) -> impl Iterator { + self.map.iter() } } @@ -315,28 +243,111 @@ pub trait NativeScope { fn scope() -> Scope; } -/// A slot where a value is stored. -#[derive(Clone, Hash)] -struct Slot { - /// The stored value. +/// A bound value with metadata. +#[derive(Debug, Clone, Hash)] +pub struct Binding { + /// The bound value. value: Value, - /// The kind of slot, determines how the value can be accessed. - kind: Kind, - /// A span associated with the stored value. + /// The kind of binding, determines how the value can be accessed. + kind: BindingKind, + /// A span associated with the binding. span: Span, - /// The category of the slot. + /// The category of the binding. category: Option, + /// A deprecation message for the definition. + deprecation: Option<&'static str>, } /// The different kinds of slots. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum Kind { +enum BindingKind { /// A normal, mutable binding. Normal, /// A captured copy of another variable. Captured(Capturer), } +impl Binding { + /// Create a new binding with a span marking its definition site. + pub fn new(value: impl IntoValue, span: Span) -> Self { + Self { + value: value.into_value(), + span, + kind: BindingKind::Normal, + category: None, + deprecation: None, + } + } + + /// Create a binding without a span. + pub fn detached(value: impl IntoValue) -> Self { + Self::new(value, Span::detached()) + } + + /// Marks this binding as deprecated, with the given `message`. + pub fn deprecated(&mut self, message: &'static str) -> &mut Self { + self.deprecation = Some(message); + self + } + + /// Read the value. + pub fn read(&self) -> &Value { + &self.value + } + + /// Read the value, checking for deprecation. + /// + /// As the `sink` + /// - pass `()` to ignore the message. + /// - pass `(&mut engine, span)` to emit a warning into the engine. + pub fn read_checked(&self, sink: impl DeprecationSink) -> &Value { + if let Some(message) = self.deprecation { + sink.emit(message); + } + &self.value + } + + /// Try to write to the value. + /// + /// This fails if the value is a read-only closure capture. + pub fn write(&mut self) -> StrResult<&mut Value> { + match self.kind { + BindingKind::Normal => Ok(&mut self.value), + BindingKind::Captured(capturer) => bail!( + "variables from outside the {} are \ + read-only and cannot be modified", + match capturer { + Capturer::Function => "function", + Capturer::Context => "context expression", + } + ), + } + } + + /// Create a copy of the binding for closure capturing. + pub fn capture(&self, capturer: Capturer) -> Self { + Self { + kind: BindingKind::Captured(capturer), + ..self.clone() + } + } + + /// A span associated with the stored value. + pub fn span(&self) -> Span { + self.span + } + + /// A deprecation message for the value, if any. + pub fn deprecation(&self) -> Option<&'static str> { + self.deprecation + } + + /// The category of the value, if any. + pub fn category(&self) -> Option { + self.category + } +} + /// What the variable was captured by. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Capturer { @@ -346,71 +357,56 @@ pub enum Capturer { Context, } -impl Slot { - /// Create a new slot. - fn new(value: Value, span: Span, kind: Kind, category: Option) -> Self { - Self { value, span, kind, category } - } - - /// Read the value. - fn read(&self) -> &Value { - &self.value - } - - /// Try to write to the value. - fn write(&mut self) -> StrResult<&mut Value> { - match self.kind { - Kind::Normal => Ok(&mut self.value), - Kind::Captured(capturer) => { - bail!( - "variables from outside the {} are \ - read-only and cannot be modified", - match capturer { - Capturer::Function => "function", - Capturer::Context => "context expression", - } - ) - } - } - } +/// The error message when trying to mutate a variable from the standard +/// library. +#[cold] +fn cannot_mutate_constant(var: &str) -> HintedString { + eco_format!("cannot mutate a constant: {}", var).into() } -/// A group of related definitions. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct Category(Static); +/// The error message when a variable wasn't found. +#[cold] +fn unknown_variable(var: &str) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); -impl Category { - /// Create a new category from raw data. - pub const fn from_data(data: &'static CategoryData) -> Self { - Self(Static(data)) + if var.contains('-') { + res.hint(eco_format!( + "if you meant to use subtraction, \ + try adding spaces around the minus sign{}: `{}`", + if var.matches('-').count() > 1 { "s" } else { "" }, + var.replace('-', " - ") + )); } - /// The category's name. - pub fn name(&self) -> &'static str { - self.0.name - } - - /// The type's title case name, for use in documentation (e.g. `String`). - pub fn title(&self) -> &'static str { - self.0.title - } - - /// Documentation for the category. - pub fn docs(&self) -> &'static str { - self.0.docs - } + res } -impl Debug for Category { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Category({})", self.name()) - } -} +/// The error message when a variable wasn't found it math. +#[cold] +fn unknown_variable_math(var: &str, in_global: bool) -> HintedString { + let mut res = HintedString::new(eco_format!("unknown variable: {}", var)); -/// Defines a category. -#[derive(Debug)] -pub struct CategoryData { - pub name: &'static str, - pub title: &'static str, - pub docs: &'static str, + if matches!(var, "none" | "auto" | "false" | "true") { + res.hint(eco_format!( + "if you meant to use a literal, \ + try adding a hash before it: `#{var}`", + )); + } else if in_global { + res.hint(eco_format!( + "`{var}` is not available directly in math, \ + try adding a hash before it: `#{var}`", + )); + } else { + res.hint(eco_format!( + "if you meant to display multiple letters as is, \ + try adding spaces between each letter: `{}`", + var.chars().flat_map(|c| [' ', c]).skip(1).collect::() + )); + res.hint(eco_format!( + "or if you meant to display this as text, \ + try placing it in quotes: `\"{var}\"`" + )); + } + + res } diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 8ac99ac63..23a1bd4cf 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -7,12 +7,13 @@ use comemo::Tracked; use ecow::EcoString; use serde::{Deserialize, Serialize}; use typst_syntax::{Span, Spanned}; +use unicode_normalization::UnicodeNormalization; use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func, + cast, dict, func, repr, scope, ty, Array, Bytes, Cast, Context, Decimal, Dict, Func, IntoValue, Label, Repr, Type, Value, Version, }; use crate::layout::Alignment; @@ -286,6 +287,30 @@ impl Str { Ok(c.into()) } + /// Normalizes the string to the given Unicode normal form. + /// + /// This is useful when manipulating strings containing Unicode combining + /// characters. + /// + /// ```typ + /// #assert.eq("é".normalize(form: "nfd"), "e\u{0301}") + /// #assert.eq("ſ́".normalize(form: "nfkc"), "ś") + /// ``` + #[func] + pub fn normalize( + &self, + #[named] + #[default(UnicodeNormalForm::Nfc)] + form: UnicodeNormalForm, + ) -> Str { + match form { + UnicodeNormalForm::Nfc => self.nfc().collect(), + UnicodeNormalForm::Nfd => self.nfd().collect(), + UnicodeNormalForm::Nfkc => self.nfkc().collect(), + UnicodeNormalForm::Nfkd => self.nfkd().collect(), + } + } + /// Whether the string contains the specified pattern. /// /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` @@ -425,9 +450,7 @@ impl Str { #[func] pub fn replace( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The pattern to search for. pattern: StrPattern, @@ -577,9 +600,10 @@ impl Str { /// of the resulting parts. /// /// When the empty string is used as a separator, it separates every - /// character in the string, along with the beginning and end of the - /// string. In practice, this means that the resulting list of parts - /// will contain the empty string at the start and end of the list. + /// character (i.e., Unicode code point) in the string, along with the + /// beginning and end of the string. In practice, this means that the + /// resulting list of parts will contain the empty string at the start + /// and end of the list. #[func] pub fn split( &self, @@ -783,16 +807,31 @@ cast! { v: f64 => Self::Str(repr::display_float(v).into()), v: Decimal => Self::Str(format_str!("{}", v)), v: Version => Self::Str(format_str!("{}", v)), - v: Bytes => Self::Str( - std::str::from_utf8(&v) - .map_err(|_| "bytes are not valid utf-8")? - .into() - ), + v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?), v: Label => Self::Str(v.resolve().as_str().into()), v: Type => Self::Str(v.long_name().into()), v: Str => Self::Str(v), } +/// A Unicode normalization form. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum UnicodeNormalForm { + /// Canonical composition where e.g. accented letters are turned into a + /// single Unicode codepoint. + #[string("nfc")] + Nfc, + /// Canonical decomposition where e.g. accented letters are split into a + /// separate base and diacritic. + #[string("nfd")] + Nfd, + /// Like NFC, but using the Unicode compatibility decompositions. + #[string("nfkc")] + Nfkc, + /// Like NFD, but using the Unicode compatibility decompositions. + #[string("nfkd")] + Nfkd, +} + /// Convert an item of std's `match_indices` to a dictionary. fn match_to_dict((start, text): (usize, &str)) -> Dict { dict! { diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 7354719e9..983803300 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -12,7 +12,8 @@ use typst_utils::LazyHash; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ - cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector, + cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr, + Selector, }; use crate::text::{FontFamily, FontList, TextElem}; @@ -775,107 +776,6 @@ impl<'a> Iterator for Links<'a> { } } -/// A sequence of elements with associated styles. -#[derive(Clone, PartialEq, Hash)] -pub struct StyleVec { - /// The elements themselves. - elements: EcoVec, - /// A run-length encoded list of style lists. - /// - /// Each element is a (styles, count) pair. Any elements whose - /// style falls after the end of this list is considered to - /// have an empty style list. - styles: EcoVec<(Styles, usize)>, -} - -impl StyleVec { - /// Create a style vector from an unstyled vector content. - pub fn wrap(elements: EcoVec) -> Self { - Self { elements, styles: EcoVec::new() } - } - - /// Create a `StyleVec` from a list of content with style chains. - pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { - let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); - let depth = trunk.links().count(); - - let mut elements = EcoVec::with_capacity(buf.len()); - let mut styles = EcoVec::<(Styles, usize)>::new(); - let mut last: Option<(StyleChain<'a>, usize)> = None; - - for &(element, chain) in buf { - elements.push(element.clone()); - - if let Some((prev, run)) = &mut last { - if chain == *prev { - *run += 1; - } else { - styles.push((prev.suffix(depth), *run)); - last = Some((chain, 1)); - } - } else { - last = Some((chain, 1)); - } - } - - if let Some((last, run)) = last { - let skippable = styles.is_empty() && last == trunk; - if !skippable { - styles.push((last.suffix(depth), run)); - } - } - - (StyleVec { elements, styles }, trunk) - } - - /// Whether there are no elements. - pub fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - /// The number of elements. - pub fn len(&self) -> usize { - self.elements.len() - } - - /// Iterate over the contained content and style chains. - pub fn iter<'a>( - &'a self, - outer: &'a StyleChain<'_>, - ) -> impl Iterator)> { - static EMPTY: Styles = Styles::new(); - self.elements - .iter() - .zip( - self.styles - .iter() - .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) - .chain(std::iter::repeat(&EMPTY)), - ) - .map(|(element, local)| (element, outer.chain(local))) - } - - /// Get a style property, but only if it is the same for all children of the - /// style vector. - pub fn shared_get( - &self, - styles: StyleChain<'_>, - getter: fn(StyleChain) -> T, - ) -> Option { - let value = getter(styles); - self.styles - .iter() - .all(|(local, _)| getter(styles.chain(local)) == value) - .then_some(value) - } -} - -impl Debug for StyleVec { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - f.debug_list().entries(&self.elements).finish() - } -} - /// A property that is resolved with other properties from the style chain. pub trait Resolve { /// The type of the resolved output. @@ -939,6 +839,13 @@ impl Fold for SmallVec<[T; N]> { } } +impl Fold for OneOrMultiple { + fn fold(self, mut outer: Self) -> Self { + outer.0.extend(self.0); + outer + } +} + /// A variant of fold for foldable optional (`Option`) values where an inner /// `None` value isn't respected (contrary to `Option`'s usual `Fold` /// implementation, with which folding with an inner `None` always returns diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 72800f311..50fcfb403 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -9,7 +9,10 @@ use typst_syntax::{is_ident, Span, Spanned}; use typst_utils::hash128; use crate::diag::{bail, SourceResult, StrResult}; -use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _}; +use crate::foundations::{ + cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed, + PlainText, Repr as _, +}; /// A Unicode symbol. /// @@ -18,6 +21,7 @@ use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as /// be accessed using [field access notation]($scripting/#fields): /// /// - General symbols are defined in the [`sym` module]($category/symbols/sym) +/// and are accessible without the `sym.` prefix in math mode. /// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) /// /// Moreover, you can define custom symbols with this type's constructor @@ -187,7 +191,6 @@ impl Symbol { /// ``` #[func(constructor)] pub fn construct( - /// The callsite span. span: Span, /// The variants of the symbol. /// @@ -408,7 +411,7 @@ fn find<'a>( } let score = (matching, Reverse(total)); - if best_score.map_or(true, |b| score > b) { + if best_score.is_none_or(|b| score > b) { best = Some(candidate.1); best_score = Some(score); } @@ -426,3 +429,31 @@ fn parts(modifiers: &str) -> impl Iterator { fn contained(modifiers: &str, m: &str) -> bool { parts(modifiers).any(|part| part == m) } + +/// A single character. +#[elem(Repr, PlainText)] +pub struct SymbolElem { + /// The symbol's character. + #[required] + pub text: char, // This is called `text` for consistency with `TextElem`. +} + +impl SymbolElem { + /// Create a new packed symbol element. + pub fn packed(text: impl Into) -> Content { + Self::new(text.into()).pack() + } +} + +impl PlainText for Packed { + fn plain_text(&self, text: &mut EcoString) { + text.push(self.text); + } +} + +impl crate::foundations::Repr for SymbolElem { + /// Use a custom repr that matches normal content. + fn repr(&self) -> EcoString { + eco_format!("[{}]", self.text) + } +} diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index b743ea1ab..2a21fd42b 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -3,7 +3,7 @@ use comemo::Tracked; use crate::diag::HintedStrResult; use crate::foundations::{elem, func, Cast, Context}; -/// The compilation target. +/// The export target. #[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)] pub enum Target { /// The target that is used for paged, fully laid-out content. @@ -28,11 +28,50 @@ pub struct TargetElem { pub target: Target, } -/// Returns the current compilation target. +/// Returns the current export target. +/// +/// This function returns either +/// - `{"paged"}` (for PDF, PNG, and SVG export), or +/// - `{"html"}` (for HTML export). +/// +/// The design of this function is not yet finalized and for this reason it is +/// guarded behind the `html` feature. Visit the [HTML documentation +/// page]($html) for more details. +/// +/// # When to use it +/// This function allows you to format your document properly across both HTML +/// and paged export targets. It should primarily be used in templates and show +/// rules, rather than directly in content. This way, the document's contents +/// can be fully agnostic to the export target and content can be shared between +/// PDF and HTML export. +/// +/// # Varying targets +/// This function is [contextual]($context) as the target can vary within a +/// single compilation: When exporting to HTML, the target will be `{"paged"}` +/// while within an [`html.frame`]. +/// +/// # Example +/// ```example +/// #let kbd(it) = context { +/// if target() == "html" { +/// html.elem("kbd", it) +/// } else { +/// set text(fill: rgb("#1f2328")) +/// let r = 3pt +/// box( +/// fill: rgb("#f6f8fa"), +/// stroke: rgb("#d1d9e0b3"), +/// outset: (y: r), +/// inset: (x: r), +/// radius: r, +/// raw(it) +/// ) +/// } +/// } +/// +/// Press #kbd("F1") for help. +/// ``` #[func(contextual)] -pub fn target( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn target(context: Tracked) -> HintedStrResult { Ok(TargetElem::target_in(context.styles()?)) } diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index a2395f2a7..40f7003c3 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -8,7 +8,7 @@ use std::sync::LazyLock; use ecow::{eco_format, EcoString}; use typst_utils::Static; -use crate::diag::StrResult; +use crate::diag::{bail, DeprecationSink, StrResult}; use crate::foundations::{ cast, func, AutoValue, Func, NativeFuncData, NoneValue, Repr, Scope, Value, }; @@ -94,10 +94,15 @@ impl Type { } /// Get a field from this type's scope, if possible. - pub fn field(&self, field: &str) -> StrResult<&'static Value> { - self.scope() - .get(field) - .ok_or_else(|| eco_format!("type {self} does not contain field `{field}`")) + pub fn field( + &self, + field: &str, + sink: impl DeprecationSink, + ) -> StrResult<&'static Value> { + match self.scope().get(field) { + Some(binding) => Ok(binding.read_checked(sink)), + None => bail!("type {self} does not contain field `{field}`"), + } } } @@ -136,7 +141,7 @@ impl Repr for Type { } else if *self == Type::of::() { "type(none)" } else { - self.long_name() + self.short_name() } .into() } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index eb0d6eedc..854c2486e 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -11,12 +11,12 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use typst_syntax::{ast, Span}; use typst_utils::ArcExt; -use crate::diag::{HintedStrResult, HintedString, StrResult}; +use crate::diag::{DeprecationSink, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, - NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, - Styles, Symbol, Type, Version, + NativeElement, NativeType, NoneValue, Reflect, Repr, Resolve, Scope, Str, Styles, + Symbol, SymbolElem, Type, Version, }; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; use crate::text::{RawContent, RawElem, TextElem}; @@ -84,8 +84,6 @@ pub enum Value { Type(Type), /// A module. Module(Module), - /// A WebAssembly plugin. - Plugin(Plugin), /// A dynamic value. Dyn(Dynamic), } @@ -147,7 +145,6 @@ impl Value { Self::Args(_) => Type::of::(), Self::Type(_) => Type::of::(), Self::Module(_) => Type::of::(), - Self::Plugin(_) => Type::of::(), Self::Dyn(v) => v.ty(), } } @@ -158,15 +155,15 @@ impl Value { } /// Try to access a field on the value. - pub fn field(&self, field: &str) -> StrResult { + pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult { match self { Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), Self::Version(version) => version.component(field).map(Self::Int), Self::Dict(dict) => dict.get(field).cloned(), Self::Content(content) => content.field_by_name(field), - Self::Type(ty) => ty.field(field).cloned(), - Self::Func(func) => func.field(field).cloned(), - Self::Module(module) => module.field(field).cloned(), + Self::Type(ty) => ty.field(field, sink).cloned(), + Self::Func(func) => func.field(field, sink).cloned(), + Self::Module(module) => module.field(field, sink).cloned(), _ => fields::field(self, field), } } @@ -181,16 +178,6 @@ impl Value { } } - /// The name, if this is a function, type, or module. - pub fn name(&self) -> Option<&str> { - match self { - Self::Func(func) => func.name(), - Self::Type(ty) => Some(ty.short_name()), - Self::Module(module) => Some(module.name()), - _ => None, - } - } - /// Try to extract documentation for the value. pub fn docs(&self) -> Option<&'static str> { match self { @@ -209,7 +196,7 @@ impl Value { Self::Decimal(v) => TextElem::packed(eco_format!("{v}")), Self::Str(v) => TextElem::packed(v), Self::Version(v) => TextElem::packed(eco_format!("{v}")), - Self::Symbol(v) => TextElem::packed(v.get()), + Self::Symbol(v) => SymbolElem::packed(v.get()), Self::Content(v) => v, Self::Module(module) => module.content(), _ => RawElem::new(RawContent::Text(self.repr())) @@ -261,7 +248,6 @@ impl Debug for Value { Self::Args(v) => Debug::fmt(v, f), Self::Type(v) => Debug::fmt(v, f), Self::Module(v) => Debug::fmt(v, f), - Self::Plugin(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f), } } @@ -299,7 +285,6 @@ impl Repr for Value { Self::Args(v) => v.repr(), Self::Type(v) => v.repr(), Self::Module(v) => v.repr(), - Self::Plugin(v) => v.repr(), Self::Dyn(v) => v.repr(), } } @@ -350,7 +335,6 @@ impl Hash for Value { Self::Args(v) => v.hash(state), Self::Type(v) => v.hash(state), Self::Module(v) => v.hash(state), - Self::Plugin(v) => v.hash(state), Self::Dyn(v) => v.hash(state), } } @@ -459,15 +443,15 @@ impl<'de> Visitor<'de> for ValueVisitor { } fn visit_bytes(self, v: &[u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_byte_buf(self, v: Vec) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v).into_value()) } fn visit_none(self) -> Result { @@ -656,7 +640,7 @@ primitive! { Duration: "duration", Duration } primitive! { Content: "content", Content, None => Content::empty(), - Symbol(v) => TextElem::packed(v.get()), + Symbol(v) => SymbolElem::packed(v.get()), Str(v) => TextElem::packed(v) } primitive! { Styles: "styles", Styles } @@ -671,7 +655,6 @@ primitive! { primitive! { Args: "arguments", Args } primitive! { Type: "type", Type } primitive! { Module: "module", Module } -primitive! { Plugin: "plugin", Plugin } impl Reflect for Arc { fn input() -> CastInfo { @@ -730,6 +713,11 @@ mod tests { assert_eq!(value.into_value().repr(), exp); } + #[test] + fn test_value_size() { + assert!(std::mem::size_of::() <= 32); + } + #[test] fn test_value_debug() { // Primitives. diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 5b6eab4d6..1b725d543 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -210,7 +210,10 @@ impl HtmlAttr { /// Creates a compile-time constant `HtmlAttr`. /// - /// Should only be used in const contexts because it can panic. + /// Must only be used in const contexts (in a constant definition or + /// explicit `const { .. }` block) because otherwise a panic for a malformed + /// attribute or not auto-internible constant will only be caught at + /// runtime. #[track_caller] pub const fn constant(string: &'static str) -> Self { if string.is_empty() { @@ -472,17 +475,55 @@ pub mod tag { wbr } + /// Whether this is a void tag whose associated element may not have a + /// children. + pub fn is_void(tag: HtmlTag) -> bool { + matches!( + tag, + self::area + | self::base + | self::br + | self::col + | self::embed + | self::hr + | self::img + | self::input + | self::link + | self::meta + | self::param + | self::source + | self::track + | self::wbr + ) + } + + /// Whether this is a tag containing raw text. + pub fn is_raw(tag: HtmlTag) -> bool { + matches!(tag, self::script | self::style) + } + + /// Whether this is a tag containing escapable raw text. + pub fn is_escapable_raw(tag: HtmlTag) -> bool { + matches!(tag, self::textarea | self::title) + } + + /// Whether an element is considered metadata. + pub fn is_metadata(tag: HtmlTag) -> bool { + matches!( + tag, + self::base + | self::link + | self::meta + | self::noscript + | self::script + | self::style + | self::template + | self::title + ) + } + /// Whether nodes with the tag have the CSS property `display: block` by /// default. - /// - /// If this is true, then pretty-printing can insert spaces around such - /// nodes and around the contents of such nodes. - /// - /// However, when users change the properties of such tags via CSS, the - /// insertion of whitespace may actually impact the visual output; for - /// example, shows how - /// adding CSS rules to `

` can make it sensitive to whitespace. In such - /// cases, users should disable pretty-printing. pub fn is_block_by_default(tag: HtmlTag) -> bool { matches!( tag, @@ -569,42 +610,29 @@ pub mod tag { ) } - /// Whether this is a void tag whose associated element may not have a - /// children. - pub fn is_void(tag: HtmlTag) -> bool { + /// Whether nodes with the tag have the CSS property `display: table(-.*)?` + /// by default. + pub fn is_tabular_by_default(tag: HtmlTag) -> bool { matches!( tag, - self::area - | self::base - | self::br + self::table + | self::thead + | self::tbody + | self::tfoot + | self::tr + | self::th + | self::td + | self::caption | self::col - | self::embed - | self::hr - | self::img - | self::input - | self::link - | self::meta - | self::param - | self::source - | self::track - | self::wbr + | self::colgroup ) } - - /// Whether this is a tag containing raw text. - pub fn is_raw(tag: HtmlTag) -> bool { - matches!(tag, self::script | self::style) - } - - /// Whether this is a tag containing escapable raw text. - pub fn is_escapable_raw(tag: HtmlTag) -> bool { - matches!(tag, self::textarea | self::title) - } } /// Predefined constants for HTML attributes. /// /// Note: These are very incomplete. +#[allow(non_upper_case_globals)] pub mod attr { use super::HtmlAttr; @@ -619,13 +647,18 @@ pub mod attr { attrs! { charset + cite + colspan content href name - value + reversed role + rowspan + start + style + value } - #[allow(non_upper_case_globals)] pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); } diff --git a/crates/typst-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs index ea248172a..1d88781c1 100644 --- a/crates/typst-library/src/html/mod.rs +++ b/crates/typst-library/src/html/mod.rs @@ -6,53 +6,77 @@ pub use self::dom::*; use ecow::EcoString; -use crate::foundations::{category, elem, Category, Content, Module, Scope}; - -/// HTML output. -#[category] -pub static HTML: Category; +use crate::foundations::{elem, Content, Module, Scope}; /// Create a module with all HTML definitions. pub fn module() -> Module { let mut html = Scope::deduplicating(); - html.category(HTML); + html.start_category(crate::Category::Html); html.define_elem::(); html.define_elem::(); Module::new("html", html) } -/// A HTML element that can contain Typst content. +/// An HTML element that can contain Typst content. +/// +/// Typst's HTML export automatically generates the appropriate tags for most +/// elements. However, sometimes, it is desirable to retain more control. For +/// example, when using Typst to generate your blog, you could use this function +/// to wrap each article in an `

` tag. +/// +/// Typst is aware of what is valid HTML. A tag and its attributes must form +/// syntactically valid HTML. Some tags, like `meta` do not accept content. +/// Hence, you must not provide a body for them. We may add more checks in the +/// future, so be sure that you are generating valid HTML when using this +/// function. +/// +/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If +/// you instead create them with this function, Typst will omit its own tags. +/// +/// ```typ +/// #html.elem("div", attrs: (style: "background: aqua"))[ +/// A div with _Typst content_ inside! +/// ] +/// ``` #[elem(name = "elem")] pub struct HtmlElem { /// The element's tag. #[required] pub tag: HtmlTag, - /// The element's attributes. + /// The element's HTML attributes. #[borrowed] pub attrs: HtmlAttrs, /// The contents of the HTML element. + /// + /// The body can be arbitrary Typst content. #[positional] #[borrowed] pub body: Option, } impl HtmlElem { - /// Add an atribute to the element. + /// Add an attribute to the element. pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into) -> Self { self.attrs.get_or_insert_with(Default::default).push(attr, value); self } } -/// An element that forces its contents to be laid out. +/// An element that lays out its content as an inline SVG. /// -/// Integrates content that requires layout (e.g. a plot) into HTML output -/// by turning it into an inline SVG. +/// Sometimes, converting Typst content to HTML is not desirable. This can be +/// the case for plots and other content that relies on positioning and styling +/// to convey its message. +/// +/// This function allows you to use the Typst layout engine that would also be +/// used for PDF, SVG, and PNG export to render a part of your document exactly +/// how it would appear when exported in one of these formats. It embeds the +/// content as an inline SVG. #[elem] pub struct FrameElem { - /// The contents that shall be laid out. + /// The content that shall be laid out. #[positional] #[required] pub body: Content, diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index e189103d9..5432df238 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -428,11 +428,8 @@ impl Counter { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -444,11 +441,8 @@ impl Counter { #[func(contextual)] pub fn display( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The call span of the display. span: Span, /// A [numbering pattern or a function]($numbering), which specifies how /// to display the counter. If given a function, that function receives @@ -482,11 +476,8 @@ impl Counter { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the counter's value should be retrieved. selector: LocatableSelector, @@ -500,11 +491,8 @@ impl Counter { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -528,7 +516,6 @@ impl Counter { #[func] pub fn step( self, - /// The call span of the update. span: Span, /// The depth at which to step the counter. Defaults to `{1}`. #[named] @@ -545,7 +532,6 @@ impl Counter { #[func] pub fn update( self, - /// The call span of the update. span: Span, /// If given an integer or array of integers, sets the counter to that /// value. If given a function, that function receives the previous @@ -800,7 +786,7 @@ impl ManualPageCounter { let Some(elem) = elem.to_packed::() else { continue; }; - if *elem.key() == CounterKey::Page { + if elem.key == CounterKey::Page { let mut state = CounterState(smallvec![self.logical]); state.update(engine, elem.update.clone())?; self.logical = state.first(); diff --git a/crates/typst-library/src/introspection/here.rs b/crates/typst-library/src/introspection/here.rs index 9d6133816..510093247 100644 --- a/crates/typst-library/src/introspection/here.rs +++ b/crates/typst-library/src/introspection/here.rs @@ -44,9 +44,6 @@ use crate::introspection::Location; /// ``` /// Refer to the [`selector`] type for more details on before/after selectors. #[func(contextual)] -pub fn here( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn here(context: Tracked) -> HintedStrResult { context.location() } diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index 8cbaea891..9751dfcb8 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -10,7 +10,7 @@ use typst_utils::NonZeroExt; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::html::{HtmlElement, HtmlNode}; +use crate::html::HtmlNode; use crate::introspection::{Location, Tag}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::model::Numbering; @@ -55,8 +55,8 @@ impl Introspector { /// Creates an introspector for HTML. #[typst_macros::time(name = "introspect html")] - pub fn html(root: &HtmlElement) -> Self { - IntrospectorBuilder::new().build_html(root) + pub fn html(output: &[HtmlNode]) -> Self { + IntrospectorBuilder::new().build_html(output) } /// Iterates over all locatable elements. @@ -392,9 +392,9 @@ impl IntrospectorBuilder { } /// Build an introspector for an HTML document. - fn build_html(mut self, root: &HtmlElement) -> Introspector { + fn build_html(mut self, output: &[HtmlNode]) -> Introspector { let mut elems = Vec::new(); - self.discover_in_html(&mut elems, root); + self.discover_in_html(&mut elems, output); self.finalize(elems) } @@ -434,16 +434,16 @@ impl IntrospectorBuilder { } /// Processes the tags in the HTML element. - fn discover_in_html(&mut self, sink: &mut Vec, elem: &HtmlElement) { - for child in &elem.children { - match child { + fn discover_in_html(&mut self, sink: &mut Vec, nodes: &[HtmlNode]) { + for node in nodes { + match node { HtmlNode::Tag(tag) => self.discover_in_tag( sink, tag, Position { page: NonZeroUsize::ONE, point: Point::zero() }, ), HtmlNode::Text(_, _) => {} - HtmlNode::Element(elem) => self.discover_in_html(sink, elem), + HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), HtmlNode::Frame(frame) => self.discover_in_frame( sink, frame, diff --git a/crates/typst-library/src/introspection/locate.rs b/crates/typst-library/src/introspection/locate.rs index f6631b021..50f217851 100644 --- a/crates/typst-library/src/introspection/locate.rs +++ b/crates/typst-library/src/introspection/locate.rs @@ -24,9 +24,7 @@ use crate::introspection::Location; /// ``` #[func(contextual)] pub fn locate( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// A selector that should match exactly one element. This element will be /// located. diff --git a/crates/typst-library/src/introspection/mod.rs b/crates/typst-library/src/introspection/mod.rs index b1ff2e080..995fbd7b5 100644 --- a/crates/typst-library/src/introspection/mod.rs +++ b/crates/typst-library/src/introspection/mod.rs @@ -25,24 +25,11 @@ pub use self::query_::*; pub use self::state::*; pub use self::tag::*; -use crate::foundations::{category, Category, Scope}; - -/// Interactions between document parts. -/// -/// This category is home to Typst's introspection capabilities: With the -/// `counter` function, you can access and manipulate page, section, figure, and -/// equation counters or create custom ones. Meanwhile, the `query` function -/// lets you search for elements in the document to construct things like a list -/// of figures or headers which show the current chapter title. -/// -/// Most of the functions are _contextual._ It is recommended to read the chapter -/// on [context] before continuing here. -#[category] -pub static INTROSPECTION: Category; +use crate::foundations::Scope; /// Hook up all `introspection` definitions. pub fn define(global: &mut Scope) { - global.category(INTROSPECTION); + global.start_category(crate::Category::Introspection); global.define_type::(); global.define_type::(); global.define_type::(); @@ -50,4 +37,5 @@ pub fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/introspection/query.rs b/crates/typst-library/src/introspection/query.rs index f616208c5..b742ac010 100644 --- a/crates/typst-library/src/introspection/query.rs +++ b/crates/typst-library/src/introspection/query.rs @@ -136,9 +136,7 @@ use crate::foundations::{func, Array, Context, LocatableSelector, Value}; /// ``` #[func(contextual)] pub fn query( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Can be /// - an element function like a `heading` or `figure`, diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index 7e019e6c7..cc3f566b5 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -245,7 +245,7 @@ impl State { for elem in introspector.query(&self.selector()) { let elem = elem.to_packed::().unwrap(); - match elem.update() { + match &elem.update { StateUpdate::Set(value) => state = value.clone(), StateUpdate::Func(func) => { state = func.call(&mut engine, Context::none().track(), [state])? @@ -289,11 +289,8 @@ impl State { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -309,11 +306,8 @@ impl State { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the state's value should be retrieved. selector: LocatableSelector, @@ -326,11 +320,8 @@ impl State { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -349,7 +340,6 @@ impl State { #[func] pub fn update( self, - /// The span of the `update` call. span: Span, /// If given a non function-value, sets the state to that value. If /// given a function, that function receives the previous state and has diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index e8ba4d7c3..5604d6831 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -100,7 +100,7 @@ pub struct AlignElem { impl Show for Packed { #[typst_macros::time(name = "align", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().aligned(self.alignment(styles))) + Ok(self.body.clone().aligned(self.alignment(styles))) } } diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index 266d1d88f..725f177b7 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst-library/src/layout/container.rs @@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke}; /// An inline-level container that sizes content. /// /// All elements except inline math, text, and boxes are block-level and cannot -/// occur inside of a paragraph. The box function can be used to integrate such -/// elements into a paragraph. Boxes take the size of their contents by default -/// but can also be sized explicitly. +/// occur inside of a [paragraph]($par). The box function can be used to +/// integrate such elements into a paragraph. Boxes take the size of their +/// contents by default but can also be sized explicitly. /// /// # Example /// ```example @@ -166,7 +166,7 @@ impl Packed { styles: StyleChain, region: Size, ) -> SourceResult> { - self.body().call(engine, locator, styles, region) + self.body.call(engine, locator, styles, region) } } @@ -184,6 +184,10 @@ pub enum InlineItem { /// Such a container can be used to separate content, size it, and give it a /// background or border. /// +/// Blocks are also the primary way to control whether text becomes part of a +/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph) +/// for more details. +/// /// # Examples /// With a block, you can give a background to content while still allowing it /// to break across multiple pages. diff --git a/crates/typst-library/src/layout/dir.rs b/crates/typst-library/src/layout/dir.rs index 9a2e77105..699c8c481 100644 --- a/crates/typst-library/src/layout/dir.rs +++ b/crates/typst-library/src/layout/dir.rs @@ -50,6 +50,42 @@ impl Dir { pub const TTB: Self = Self::TTB; pub const BTT: Self = Self::BTT; + /// Returns a direction from a starting point. + /// + /// ```example + /// direction.from(left) \ + /// direction.from(right) \ + /// direction.from(top) \ + /// direction.from(bottom) + /// ``` + #[func] + pub const fn from(side: Side) -> Dir { + match side { + Side::Left => Self::LTR, + Side::Right => Self::RTL, + Side::Top => Self::TTB, + Side::Bottom => Self::BTT, + } + } + + /// Returns a direction from an end point. + /// + /// ```example + /// direction.to(left) \ + /// direction.to(right) \ + /// direction.to(top) \ + /// direction.to(bottom) + /// ``` + #[func] + pub const fn to(side: Side) -> Dir { + match side { + Side::Right => Self::LTR, + Side::Left => Self::RTL, + Side::Bottom => Self::TTB, + Side::Top => Self::BTT, + } + } + /// The axis this direction belongs to, either `{"horizontal"}` or /// `{"vertical"}`. /// @@ -65,6 +101,22 @@ impl Dir { } } + /// The corresponding sign, for use in calculations. + /// + /// ```example + /// #ltr.sign() \ + /// #rtl.sign() \ + /// #ttb.sign() \ + /// #btt.sign() + /// ``` + #[func] + pub const fn sign(self) -> i64 { + match self { + Self::LTR | Self::TTB => 1, + Self::RTL | Self::BTT => -1, + } + } + /// The start point of this direction, as an alignment. /// /// ```example diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs index e57eb27e8..a26a7d0ef 100644 --- a/crates/typst-library/src/layout/frame.rs +++ b/crates/typst-library/src/layout/frame.rs @@ -4,16 +4,13 @@ use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; use std::sync::Arc; -use smallvec::SmallVec; use typst_syntax::Span; use typst_utils::{LazyHash, Numeric}; -use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; +use crate::foundations::{cast, dict, Dict, Label, Value}; use crate::introspection::{Location, Tag}; -use crate::layout::{ - Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform, -}; -use crate::model::{Destination, LinkElem}; +use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform}; +use crate::model::Destination; use crate::text::TextItem; use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape}; @@ -304,49 +301,6 @@ impl Frame { } } - /// Apply late-stage properties from the style chain to this frame. This - /// includes: - /// - `HideElem::hidden` - /// - `LinkElem::dests` - /// - /// This must be called on all frames produced by elements - /// that manually handle styles (because their children can have varying - /// styles). This currently includes flow, par, and equation. - /// - /// Other elements don't manually need to handle it because their parents - /// that result from realization will take care of it and the styles can - /// only apply to them as a whole, not part of it (because they don't manage - /// styles). - pub fn post_processed(mut self, styles: StyleChain) -> Self { - self.post_process(styles); - self - } - - /// Post process in place. - pub fn post_process(&mut self, styles: StyleChain) { - if !self.is_empty() { - self.post_process_raw( - LinkElem::dests_in(styles), - HideElem::hidden_in(styles), - ); - } - } - - /// Apply raw late-stage properties from the raw data. - pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) { - if !self.is_empty() { - let size = self.size; - self.push_multiple( - dests - .into_iter() - .map(|dest| (Point::zero(), FrameItem::Link(dest, size))), - ); - if hide { - self.hide(); - } - } - } - /// Hide all content in the frame, but keep metadata. pub fn hide(&mut self) { Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item { diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid/mod.rs similarity index 99% rename from crates/typst-library/src/layout/grid.rs rename to crates/typst-library/src/layout/grid/mod.rs index 2e1e9abc4..6616c3311 100644 --- a/crates/typst-library/src/layout/grid.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -1,3 +1,5 @@ +pub mod resolve; + use std::num::NonZeroUsize; use std::sync::Arc; @@ -747,7 +749,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-layout/src/grid/cells.rs b/crates/typst-library/src/layout/grid/resolve.rs similarity index 77% rename from crates/typst-layout/src/grid/cells.rs rename to crates/typst-library/src/layout/grid/resolve.rs index 175e21833..08d0130da 100644 --- a/crates/typst-layout/src/grid/cells.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -2,19 +2,463 @@ use std::num::NonZeroUsize; use std::sync::Arc; use ecow::eco_format; -use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; +use typst_library::diag::{ + bail, At, Hint, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint, +}; use typst_library::engine::Engine; -use typst_library::foundations::{Content, Smart, StyleChain}; +use typst_library::foundations::{Content, Fold, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ - Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides, - Sizing, + Abs, Alignment, Axes, Celled, GridCell, GridChild, GridElem, GridItem, Length, + OuterHAlignment, OuterVAlignment, Rel, ResolvedCelled, Sides, Sizing, }; +use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; +use typst_library::text::TextElem; use typst_library::visualize::{Paint, Stroke}; +use typst_library::Dir; + use typst_syntax::Span; use typst_utils::NonZeroExt; -use super::{Footer, Header, Line, Repeatable}; +/// Convert a grid to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn grid_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> 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)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +/// Convert a table to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn table_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> 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)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +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) + } +} + +/// 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, +} + +/// 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, + } + } +} /// Used for cell-like elements which are aware of their final properties in /// the table, and may have property overrides. @@ -131,26 +575,6 @@ impl<'a> Cell<'a> { breakable: true, } } - - /// 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( - &self, - engine: &mut Engine, - disambiguator: usize, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - let mut locator = self.locator.relayout(); - if disambiguator > 0 { - locator = locator.split().next_inner(disambiguator as u128); - } - crate::layout_fragment(engine, &self.body, locator, styles, regions) - } } /// Indicates whether the line should be drawn before or after the track with @@ -178,7 +602,7 @@ pub enum Entry<'a> { impl<'a> Entry<'a> { /// Obtains the cell inside this entry, if this is not a merged cell. - fn as_cell(&self) -> Option<&Cell<'a>> { + pub fn as_cell(&self) -> Option<&Cell<'a>> { match self { Self::Cell(cell) => Some(cell), Self::Merged { .. } => None, @@ -963,7 +1387,7 @@ impl<'a> CellGrid<'a> { // Include the gutter right before the footer, unless there is // none, or the gutter is already included in the header (no // rows between the header and the footer). - if header_end.map_or(true, |header_end| header_end != footer.start) { + if header_end != Some(footer.start) { footer.start = footer.start.saturating_sub(1); } } @@ -1102,11 +1526,7 @@ impl<'a> CellGrid<'a> { self.entry(x, y).map(|entry| match entry { Entry::Cell(_) => Axes::new(x, y), Entry::Merged { parent } => { - let c = if self.has_gutter { - 1 + self.cols.len() / 2 - } else { - self.cols.len() - }; + let c = self.non_gutter_column_count(); let factor = if self.has_gutter { 2 } else { 1 }; Axes::new(factor * (*parent % c), factor * (*parent / c)) } @@ -1178,6 +1598,21 @@ impl<'a> CellGrid<'a> { cell.rowspan.get() } } + + #[inline] + pub fn non_gutter_column_count(&self) -> usize { + if self.has_gutter { + // Calculation: With gutters, we have + // 'cols = 2 * (non-gutter cols) - 1', since there is a gutter + // column between each regular column. Therefore, + // 'floor(cols / 2)' will be equal to + // 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1', + // so 'non-gutter cols = 1 + floor(cols / 2)'. + 1 + self.cols.len() / 2 + } else { + self.cols.len() + } + } } /// Given a cell's requested x and y, the vector with the resolved cell diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index 1b8b9bd57..eca33471a 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs @@ -29,6 +29,6 @@ pub struct HideElem { impl Show for Packed { #[typst_macros::time(name = "hide", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(HideElem::set_hidden(true))) + Ok(self.body.clone().styled(HideElem::set_hidden(true))) } } diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index c3d112e16..cde3187d3 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -54,7 +54,6 @@ use crate::layout::{BlockElem, Size}; /// corresponding page dimension is set to `{auto}`. #[func] pub fn layout( - /// The call span of this function. span: Span, /// A function to call with the outer container's size. Its return value is /// displayed in the document. @@ -89,7 +88,7 @@ impl Show for Packed { let loc = elem.location().unwrap(); let context = Context::new(Some(loc), Some(styles)); let result = elem - .func() + .func .call( engine, context.track(), diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs index 0c6071eb0..93c48ad40 100644 --- a/crates/typst-library/src/layout/measure.rs +++ b/crates/typst-library/src/layout/measure.rs @@ -43,11 +43,8 @@ use crate::layout::{Abs, Axes, Length, Region, Size}; /// `height`, both of type [`length`]. #[func(contextual)] pub fn measure( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The width available to layout the content. /// diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index b54d6906e..ef1ecdb36 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -12,7 +12,7 @@ mod em; mod fr; mod fragment; mod frame; -mod grid; +pub mod grid; mod hide; #[path = "layout.rs"] mod layout_; @@ -64,17 +64,11 @@ pub use self::spacing::*; pub use self::stack::*; pub use self::transform::*; -use crate::foundations::{category, Category, Scope}; - -/// Arranging elements on the page in different ways. -/// -/// By combining layout functions, you can create complex and automatic layouts. -#[category] -pub static LAYOUT: Category; +use crate::foundations::Scope; /// Hook up all `layout` definitions. pub fn define(global: &mut Scope) { - global.category(LAYOUT); + global.start_category(crate::Category::Layout); global.define_type::(); global.define_type::(); global.define_type::(); @@ -103,4 +97,5 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_func::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index 68fd89745..0964dccd2 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -270,7 +270,7 @@ pub struct PageElem { /// margin: (top: 32pt, bottom: 20pt), /// header: [ /// #set text(8pt) - /// #smallcaps[Typst Academcy] + /// #smallcaps[Typst Academy] /// #h(1fr) _Exercise Sheet 3_ /// ], /// ) diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs index e423410ab..9579f1856 100644 --- a/crates/typst-library/src/layout/repeat.rs +++ b/crates/typst-library/src/layout/repeat.rs @@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length}; /// Space may be inserted between the instances of the body parameter, so be /// sure to adjust the [`justify`]($repeat.justify) parameter accordingly. /// -/// Errors if there no bounds on the available space, as it would create +/// Errors if there are no bounds on the available space, as it would create /// infinite content. /// /// # Example diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 87b2fcb44..c39024f71 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -21,6 +21,7 @@ pub mod layout; pub mod loading; pub mod math; pub mod model; +pub mod pdf; pub mod routines; pub mod symbols; pub mod text; @@ -28,11 +29,12 @@ pub mod visualize; use std::ops::{Deref, Range}; +use serde::{Deserialize, Serialize}; use typst_syntax::{FileId, Source, Span}; use typst_utils::{LazyHash, SmallBitSet}; use crate::diag::FileResult; -use crate::foundations::{Array, Bytes, Datetime, Dict, Module, Scope, Styles, Value}; +use crate::foundations::{Array, Binding, Bytes, Datetime, Dict, Module, Scope, Styles}; use crate::layout::{Alignment, Dir}; use crate::text::{Font, FontBook}; use crate::visualize::Color; @@ -147,7 +149,7 @@ pub struct Library { /// everything else configurable via set and show rules). pub styles: Styles, /// The standard library as a value. Used to provide the `std` variable. - pub std: Value, + pub std: Binding, /// In-development features that were enabled. pub features: Features, } @@ -195,12 +197,11 @@ impl LibraryBuilder { let math = math::module(); let inputs = self.inputs.unwrap_or_default(); let global = global(math.clone(), inputs, &self.features); - let std = Value::Module(global.clone()); Library { - global, + global: global.clone(), math, styles: Styles::new(), - std, + std: Binding::detached(global), features: self.features, } } @@ -236,30 +237,72 @@ pub enum Feature { Html, } +/// A group of related standard library definitions. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum Category { + Foundations, + Introspection, + Layout, + DataLoading, + Math, + Model, + Symbols, + Text, + Visualize, + Pdf, + Html, + Svg, + Png, +} + +impl Category { + /// The kebab-case name of the category. + pub fn name(&self) -> &'static str { + match self { + Self::Foundations => "foundations", + Self::Introspection => "introspection", + Self::Layout => "layout", + Self::DataLoading => "data-loading", + Self::Math => "math", + Self::Model => "model", + Self::Symbols => "symbols", + Self::Text => "text", + Self::Visualize => "visualize", + Self::Pdf => "pdf", + Self::Html => "html", + Self::Svg => "svg", + Self::Png => "png", + } + } +} + /// Construct the module with global definitions. fn global(math: Module, inputs: Dict, features: &Features) -> Module { let mut global = Scope::deduplicating(); + self::foundations::define(&mut global, inputs, features); self::model::define(&mut global); self::text::define(&mut global); - global.reset_category(); - global.define_module(math); self::layout::define(&mut global); self::visualize::define(&mut global); self::introspection::define(&mut global); self::loading::define(&mut global); self::symbols::define(&mut global); - global.reset_category(); + + global.define("math", math); + global.define("pdf", self::pdf::module()); if features.is_enabled(Feature::Html) { - global.define_module(self::html::module()); + global.define("html", self::html::module()); } + prelude(&mut global); + Module::new("global", global) } /// Defines scoped values that are globally available, too. fn prelude(global: &mut Scope) { - global.reset_category(); global.define("black", Color::BLACK); global.define("gray", Color::GRAY); global.define("silver", Color::SILVER); diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 977059c3d..aa14c5c77 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -1,10 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Bytes, Value}; -use crate::World; +use crate::loading::{DataSource, Load}; /// Reads structured data from a CBOR file. /// @@ -19,31 +19,27 @@ use crate::World; /// floating point numbers, which may result in an approximative value. #[func(scope, title = "CBOR")] pub fn cbor( - /// The engine. engine: &mut Engine, - /// Path to a CBOR file. - /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes. + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - cbor::decode(Spanned::new(data, span)) + let data = source.load(engine.world)?; + ciborium::from_reader(data.as_slice()) + .map_err(|err| eco_format!("failed to parse CBOR ({err})")) + .at(source.span) } #[scope] impl cbor { /// Reads structured data from CBOR bytes. #[func(title = "Decode CBOR")] + #[deprecated = "`cbor.decode` is deprecated, directly pass bytes to `cbor` instead"] pub fn decode( - /// cbor data. + engine: &mut Engine, + /// CBOR data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - ciborium::from_reader(data.as_slice()) - .map_err(|err| eco_format!("failed to parse CBOR ({err})")) - .at(span) + cbor(engine, data.map(DataSource::Bytes)) } /// Encode structured data into CBOR bytes. @@ -55,7 +51,7 @@ impl cbor { let Spanned { v: value, span } = value; let mut res = Vec::new(); ciborium::into_writer(&value, &mut res) - .map(|_| res.into()) + .map(|_| Bytes::new(res)) .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) .at(span) } diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6822505d3..6afb5baeb 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -4,8 +4,7 @@ use typst_syntax::Spanned; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a CSV file. /// @@ -26,12 +25,9 @@ use crate::World; /// ``` #[func(scope, title = "CSV")] pub fn csv( - /// The engine. engine: &mut Engine, - /// Path to a CSV file. - /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// A [path]($syntax/#paths) to a CSV file or raw CSV bytes. + source: Spanned, /// The delimiter that separates columns in the CSV file. /// Must be a single ASCII character. #[named] @@ -48,17 +44,60 @@ pub fn csv( #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type) + let data = source.load(engine.world)?; + + let mut builder = ::csv::ReaderBuilder::new(); + let has_headers = row_type == RowType::Dict; + builder.has_headers(has_headers); + builder.delimiter(delimiter.0 as u8); + + // Counting lines from 1 by default. + let mut line_offset: usize = 1; + let mut reader = builder.from_reader(data.as_slice()); + let mut headers: Option<::csv::StringRecord> = None; + + if has_headers { + // Counting lines from 2 because we have a header. + line_offset += 1; + headers = Some( + reader + .headers() + .map_err(|err| format_csv_error(err, 1)) + .at(source.span)? + .clone(), + ); + } + + let mut array = Array::new(); + for (line, result) in reader.records().enumerate() { + // Original solution was to use line from error, but that is + // incorrect with `has_headers` set to `false`. See issue: + // https://github.com/BurntSushi/rust-csv/issues/184 + let line = line + line_offset; + let row = result.map_err(|err| format_csv_error(err, line)).at(source.span)?; + let item = if let Some(headers) = &headers { + let mut dict = Dict::new(); + for (field, value) in headers.iter().zip(&row) { + dict.insert(field.into(), value.into_value()); + } + dict.into_value() + } else { + let sub = row.into_iter().map(|field| field.into_value()).collect(); + Value::Array(sub) + }; + array.push(item); + } + + Ok(array) } #[scope] impl csv { /// Reads structured data from a CSV string/bytes. #[func(title = "Decode CSV")] + #[deprecated = "`csv.decode` is deprecated, directly pass bytes to `csv` instead"] pub fn decode( + engine: &mut Engine, /// CSV data. data: Spanned, /// The delimiter that separates columns in the CSV file. @@ -77,51 +116,7 @@ impl csv { #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: data, span } = data; - let has_headers = row_type == RowType::Dict; - - let mut builder = ::csv::ReaderBuilder::new(); - builder.has_headers(has_headers); - builder.delimiter(delimiter.0 as u8); - - // Counting lines from 1 by default. - let mut line_offset: usize = 1; - let mut reader = builder.from_reader(data.as_slice()); - let mut headers: Option<::csv::StringRecord> = None; - - if has_headers { - // Counting lines from 2 because we have a header. - line_offset += 1; - headers = Some( - reader - .headers() - .map_err(|err| format_csv_error(err, 1)) - .at(span)? - .clone(), - ); - } - - let mut array = Array::new(); - for (line, result) in reader.records().enumerate() { - // Original solution was to use line from error, but that is - // incorrect with `has_headers` set to `false`. See issue: - // https://github.com/BurntSushi/rust-csv/issues/184 - let line = line + line_offset; - let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; - let item = if let Some(headers) = &headers { - let mut dict = Dict::new(); - for (field, value) in headers.iter().zip(&row) { - dict.insert(field.into(), value.into_value()); - } - dict.into_value() - } else { - let sub = row.into_iter().map(|field| field.into_value()).collect(); - Value::Array(sub) - }; - array.push(item); - } - - Ok(array) + csv(engine, data.map(Readable::into_source), delimiter, row_type) } } @@ -137,18 +132,10 @@ impl Default for Delimiter { cast! { Delimiter, self => self.0.into_value(), - v: EcoString => { - let mut chars = v.chars(); - let first = chars.next().ok_or("delimiter must not be empty")?; - if chars.next().is_some() { - bail!("delimiter must be a single character"); - } - - if !first.is_ascii() { - bail!("delimiter must be an ASCII character"); - } - - Self(first) + c: char => if c.is_ascii() { + Self(c) + } else { + bail!("delimiter must be an ASCII character") }, } diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 597cf4cc6..aa908cca4 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a JSON file. /// @@ -51,31 +50,27 @@ use crate::World; /// ``` #[func(scope, title = "JSON")] pub fn json( - /// The engine. engine: &mut Engine, - /// Path to a JSON file. - /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// A [path]($syntax/#paths) to a JSON file or raw JSON bytes. + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - json::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + serde_json::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse JSON ({err})")) + .at(source.span) } #[scope] impl json { /// Reads structured data from a JSON string/bytes. #[func(title = "Decode JSON")] + #[deprecated = "`json.decode` is deprecated, directly pass bytes to `json` instead"] pub fn decode( + engine: &mut Engine, /// JSON data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(span) + json(engine, data.map(Readable::into_source)) } /// Encodes structured data into a JSON string. diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index ae74df864..c57e02888 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -15,6 +15,10 @@ mod xml_; #[path = "yaml.rs"] mod yaml_; +use comemo::Tracked; +use ecow::EcoString; +use typst_syntax::Spanned; + pub use self::cbor_::*; pub use self::csv_::*; pub use self::json_::*; @@ -23,18 +27,14 @@ pub use self::toml_::*; pub use self::xml_::*; pub use self::yaml_::*; -use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; - -/// Data loading from external files. -/// -/// These functions help you with loading and embedding data, for example from -/// the results of an experiment. -#[category] -pub static DATA_LOADING: Category; +use crate::diag::{At, SourceResult}; +use crate::foundations::OneOrMultiple; +use crate::foundations::{cast, Bytes, Scope, Str}; +use crate::World; /// Hook up all `data-loading` definitions. pub(super) fn define(global: &mut Scope) { - global.category(DATA_LOADING); + global.start_category(crate::Category::DataLoading); global.define_func::(); global.define_func::(); global.define_func::(); @@ -42,6 +42,77 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); +} + +/// Something we can retrieve byte data from. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum DataSource { + /// A path to a file. + Path(EcoString), + /// Raw bytes. + Bytes(Bytes), +} + +cast! { + DataSource, + self => match self { + Self::Path(v) => v.into_value(), + Self::Bytes(v) => v.into_value(), + }, + v: EcoString => Self::Path(v), + v: Bytes => Self::Bytes(v), +} + +/// Loads data from a path or provided bytes. +pub trait Load { + /// Bytes or a list of bytes (if there are multiple sources). + type Output; + + /// Load the bytes. + fn load(&self, world: Tracked) -> SourceResult; +} + +impl Load for Spanned { + type Output = Bytes; + + fn load(&self, world: Tracked) -> SourceResult { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&DataSource> { + type Output = Bytes; + + fn load(&self, world: Tracked) -> SourceResult { + match &self.v { + DataSource::Path(path) => { + let file_id = self.span.resolve_path(path).at(self.span)?; + world.file(file_id).at(self.span) + } + DataSource::Bytes(bytes) => Ok(bytes.clone()), + } + } +} + +impl Load for Spanned> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&OneOrMultiple> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.v + .0 + .iter() + .map(|source| Spanned::new(source, self.span).load(world)) + .collect() + } } /// A value that can be read from a file. @@ -54,18 +125,15 @@ pub enum Readable { } impl Readable { - pub fn as_slice(&self) -> &[u8] { + pub fn into_bytes(self) -> Bytes { match self { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes(), + Self::Bytes(v) => v, + Self::Str(v) => Bytes::from_string(v), } } - pub fn as_str(&self) -> Option<&str> { - match self { - Readable::Str(v) => Some(v.as_str()), - Readable::Bytes(v) => std::str::from_utf8(v).ok(), - } + pub fn into_source(self) -> DataSource { + DataSource::Bytes(self.into_bytes()) } } @@ -78,12 +146,3 @@ cast! { v: Str => Self::Str(v), v: Bytes => Self::Bytes(v), } - -impl From for Bytes { - fn from(value: Readable) -> Self { - match value { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes().into(), - } - } -} diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index 23e6e27e7..32dadc799 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, Cast}; use crate::loading::Readable; @@ -24,7 +24,6 @@ use crate::World; /// ``` #[func] pub fn read( - /// The engine. engine: &mut Engine, /// Path to a file. /// @@ -42,12 +41,9 @@ pub fn read( let data = engine.world.file(id).at(span)?; Ok(match encoding { None => Readable::Bytes(data), - Some(Encoding::Utf8) => Readable::Str( - std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)? - .into(), - ), + Some(Encoding::Utf8) => { + Readable::Str(data.to_str().map_err(FileError::from).at(span)?) + } }) } diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 5167703ef..f04b2e746 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -1,11 +1,10 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{is_newline, Spanned}; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a TOML file. /// @@ -29,34 +28,28 @@ use crate::World; /// ``` #[func(scope, title = "TOML")] pub fn toml( - /// The engine. engine: &mut Engine, - /// Path to a TOML file. - /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// A [path]($syntax/#paths) to a TOML file or raw TOML bytes. + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - toml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + let raw = data.as_str().map_err(FileError::from).at(source.span)?; + ::toml::from_str(raw) + .map_err(|err| format_toml_error(err, raw)) + .at(source.span) } #[scope] impl toml { /// Reads structured data from a TOML string/bytes. #[func(title = "Decode TOML")] + #[deprecated = "`toml.decode` is deprecated, directly pass bytes to `toml` instead"] pub fn decode( + engine: &mut Engine, /// TOML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let raw = std::str::from_utf8(data.as_slice()) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(span) + toml(engine, data.map(Readable::into_source)) } /// Encodes structured data into a TOML string. diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 3b1a9674b..daccd02fc 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -5,8 +5,7 @@ use typst_syntax::Spanned; use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from an XML file. /// @@ -58,38 +57,32 @@ use crate::World; /// ``` #[func(scope, title = "XML")] pub fn xml( - /// The engine. engine: &mut Engine, - /// Path to an XML file. - /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// A [path]($syntax/#paths) to an XML file or raw XML bytes. + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - xml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + let text = data.as_str().map_err(FileError::from).at(source.span)?; + let document = roxmltree::Document::parse_with_options( + text, + ParsingOptions { allow_dtd: true, ..Default::default() }, + ) + .map_err(format_xml_error) + .at(source.span)?; + Ok(convert_xml(document.root())) } #[scope] impl xml { /// Reads structured data from an XML string/bytes. #[func(title = "Decode XML")] + #[deprecated = "`xml.decode` is deprecated, directly pass bytes to `xml` instead"] pub fn decode( + engine: &mut Engine, /// XML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let text = std::str::from_utf8(data.as_slice()) - .map_err(FileError::from) - .at(span)?; - let document = roxmltree::Document::parse_with_options( - text, - ParsingOptions { allow_dtd: true, ..Default::default() }, - ) - .map_err(format_xml_error) - .at(span)?; - Ok(convert_xml(document.root())) + xml(engine, data.map(Readable::into_source)) } } diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 0e8ca3fb0..3f48113e8 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a YAML file. /// @@ -41,31 +40,27 @@ use crate::World; /// ``` #[func(scope, title = "YAML")] pub fn yaml( - /// The engine. engine: &mut Engine, - /// Path to a YAML file. - /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes. + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - yaml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + serde_yaml::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse YAML ({err})")) + .at(source.span) } #[scope] impl yaml { /// Reads structured data from a YAML string/bytes. #[func(title = "Decode YAML")] + #[deprecated = "`yaml.decode` is deprecated, directly pass bytes to `yaml` instead"] pub fn decode( + engine: &mut Engine, /// YAML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(span) + yaml(engine, data.map(Readable::into_source)) } /// Encode structured data into a YAML string. diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index fee705ee4..b162c52b1 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -1,8 +1,7 @@ use crate::diag::bail; -use crate::foundations::{cast, elem, func, Content, NativeElement, Value}; +use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem}; use crate::layout::{Length, Rel}; use crate::math::Mathy; -use crate::text::TextElem; /// Attaches an accent to a base. /// @@ -142,8 +141,8 @@ cast! { Accent, self => self.0.into_value(), v: char => Self::new(v), - v: Content => match v.to_packed::() { - Some(elem) => Value::Str(elem.text().clone().into()).cast()?, - None => bail!("expected text"), + v: Content => match v.to_packed::() { + Some(elem) => Self::new(elem.text), + None => bail!("expected a symbol"), }, } diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs index e1f577272..d526aba57 100644 --- a/crates/typst-library/src/math/attach.rs +++ b/crates/typst-library/src/math/attach.rs @@ -47,9 +47,9 @@ impl Packed { /// base AttachElem where possible. pub fn merge_base(&self) -> Option { // Extract from an EquationElem. - let mut base = self.base(); + let mut base = &self.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } // Move attachments from elem into base where possible. diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index a9173c433..32be216a4 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; /// A mathematical equation. /// -/// Can be displayed inline with text or as a separate block. +/// Can be displayed inline with text or as a separate block. An equation +/// becomes block-level through the presence of at least one space after the +/// opening dollar sign and one space before the closing dollar sign. /// /// # Example /// ```example @@ -229,35 +231,20 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.block(StyleChain::default()) { - return Ok(None); - } - let Some(numbering) = self.numbering() else { - return Ok(None); - }; - - // After synthesis, this should always be custom content. - let mut supplement = match (**self).supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - }; + fn outlined(&self) -> bool { + self.block(StyleChain::default()) && self.numbering().is_some() + } + fn prefix(&self, numbers: Content) -> Content { + let supplement = self.supplement(); if !supplement.is_empty() { - supplement += TextElem::packed("\u{a0}"); + supplement + TextElem::packed('\u{a0}') + numbers + } else { + numbers } + } - let numbers = self.counter().display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - Ok(Some(supplement + numbers)) + fn body(&self) -> Content { + Content::empty() } } diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs index 965f53516..7558717af 100644 --- a/crates/typst-library/src/math/lr.rs +++ b/crates/typst-library/src/math/lr.rs @@ -1,7 +1,6 @@ -use crate::foundations::{elem, func, Content, NativeElement}; +use crate::foundations::{elem, func, Content, NativeElement, SymbolElem}; use crate::layout::{Length, Rel}; use crate::math::Mathy; -use crate::text::TextElem; /// Scales delimiters. /// @@ -19,7 +18,7 @@ pub struct LrElem { #[parse( let mut arguments = args.all::()?.into_iter(); let mut body = arguments.next().unwrap_or_default(); - arguments.for_each(|arg| body += TextElem::packed(',') + arg); + arguments.for_each(|arg| body += SymbolElem::packed(',') + arg); body )] pub body: Content, @@ -125,9 +124,9 @@ fn delimited( ) -> Content { let span = body.span(); let mut elem = LrElem::new(Content::sequence([ - TextElem::packed(left), + SymbolElem::packed(left), body, - TextElem::packed(right), + SymbolElem::packed(right), ])); // Push size only if size is provided if let Some(size) = size { diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs index c74eb8fad..b6c4654ed 100644 --- a/crates/typst-library/src/math/matrix.rs +++ b/crates/typst-library/src/math/matrix.rs @@ -1,6 +1,6 @@ use smallvec::{smallvec, SmallVec}; use typst_syntax::Spanned; -use typst_utils::Numeric; +use typst_utils::{default_math_class, Numeric}; use unicode_math_class::MathClass; use crate::diag::{bail, At, HintedStrResult, StrResult}; @@ -292,7 +292,7 @@ impl Delimiter { pub fn char(c: char) -> StrResult { if !matches!( - unicode_math_class::class(c), + default_math_class(c), Some(MathClass::Opening | MathClass::Closing | MathClass::Fence), ) { bail!("invalid delimiter: \"{}\"", c) @@ -311,7 +311,7 @@ impl Delimiter { Some(']') => Self(Some('[')), Some('{') => Self(Some('}')), Some('}') => Self(Some('{')), - Some(c) => match unicode_math_class::class(c) { + Some(c) => match default_math_class(c) { Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)), Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)), _ => Self(Some(c)), diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 5a83c854f..2e6d42b13 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -27,117 +27,10 @@ pub use self::underover::*; use typst_utils::singleton; use unicode_math_class::MathClass; -use crate::foundations::{ - category, elem, Category, Content, Module, NativeElement, Scope, -}; +use crate::foundations::{elem, Content, Module, NativeElement, Scope}; use crate::layout::{Em, HElem}; use crate::text::TextElem; -/// Typst has special [syntax]($syntax/#math) and library functions to typeset -/// mathematical formulas. Math formulas can be displayed inline with text or as -/// separate blocks. They will be typeset into their own block if they start and -/// end with at least one space (e.g. `[$ x^2 $]`). -/// -/// # Variables -/// In math, single letters are always displayed as is. Multiple letters, -/// however, are interpreted as variables and functions. To display multiple -/// letters verbatim, you can place them into quotes and to access single letter -/// variables, you can use the [hash syntax]($scripting/#expressions). -/// -/// ```example -/// $ A = pi r^2 $ -/// $ "area" = pi dot "radius"^2 $ -/// $ cal(A) := -/// { x in RR | x "is natural" } $ -/// #let x = 5 -/// $ #x < 17 $ -/// ``` -/// -/// # Symbols -/// Math mode makes a wide selection of [symbols]($category/symbols/sym) like -/// `pi`, `dot`, or `RR` available. Many mathematical symbols are available in -/// different variants. You can select between different variants by applying -/// [modifiers]($symbol) to the symbol. Typst further recognizes a number of -/// shorthand sequences like `=>` that approximate a symbol. When such a -/// shorthand exists, the symbol's documentation lists it. -/// -/// ```example -/// $ x < y => x gt.eq.not y $ -/// ``` -/// -/// # Line Breaks -/// Formulas can also contain line breaks. Each line can contain one or multiple -/// _alignment points_ (`&`) which are then aligned. -/// -/// ```example -/// $ sum_(k=0)^n k -/// &= 1 + ... + n \ -/// &= (n(n+1)) / 2 $ -/// ``` -/// -/// # Function calls -/// Math mode supports special function calls without the hash prefix. In these -/// "math calls", the argument list works a little differently than in code: -/// -/// - Within them, Typst is still in "math mode". Thus, you can write math -/// directly into them, but need to use hash syntax to pass code expressions -/// (except for strings, which are available in the math syntax). -/// - They support positional and named arguments, but don't support trailing -/// content blocks and argument spreading. -/// - They provide additional syntax for 2-dimensional argument lists. The -/// semicolon (`;`) merges preceding arguments separated by commas into an -/// array argument. -/// -/// ```example -/// $ frac(a^2, 2) $ -/// $ vec(1, 2, delim: "[") $ -/// $ mat(1, 2; 3, 4) $ -/// $ lim_x = -/// op("lim", limits: #true)_x $ -/// ``` -/// -/// To write a verbatim comma or semicolon in a math call, escape it with a -/// backslash. The colon on the other hand is only recognized in a special way -/// if directly preceded by an identifier, so to display it verbatim in those -/// cases, you can just insert a space before it. -/// -/// Functions calls preceded by a hash are normal code function calls and not -/// affected by these rules. -/// -/// # Alignment -/// When equations include multiple _alignment points_ (`&`), this creates -/// blocks of alternatingly right- and left-aligned columns. In the example -/// below, the expression `(3x + y) / 7` is right-aligned and `= 9` is -/// left-aligned. The word "given" is also left-aligned because `&&` creates two -/// alignment points in a row, alternating the alignment twice. `& &` and `&&` -/// behave exactly the same way. Meanwhile, "multiply by 7" is right-aligned -/// because just one `&` precedes it. Each alignment point simply alternates -/// between right-aligned/left-aligned. -/// -/// ```example -/// $ (3x + y) / 7 &= 9 && "given" \ -/// 3x + y &= 63 & "multiply by 7" \ -/// 3x &= 63 - y && "subtract y" \ -/// x &= 21 - y/3 & "divide by 3" $ -/// ``` -/// -/// # Math fonts -/// You can set the math font by with a [show-set rule]($styling/#show-rules) as -/// demonstrated below. Note that only special OpenType math fonts are suitable -/// for typesetting maths. -/// -/// ```example -/// #show math.equation: set text(font: "Fira Math") -/// $ sum_(i in NN) 1 + i $ -/// ``` -/// -/// # Math module -/// All math functions are part of the `math` [module]($scripting/#modules), -/// which is available by default in equations. Outside of equations, they can -/// be accessed with the `math.` prefix. -#[category] -pub static MATH: Category; - // Spacings. pub const THIN: Em = Em::new(1.0 / 6.0); pub const MEDIUM: Em = Em::new(2.0 / 9.0); @@ -148,7 +41,7 @@ pub const WIDE: Em = Em::new(2.0); /// Create a module with all math definitions. pub fn module() -> Module { let mut math = Scope::deduplicating(); - math.category(MATH); + math.start_category(crate::Category::Math); math.define_elem::(); math.define_elem::(); math.define_elem::(); diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs index ef24705a7..55696e534 100644 --- a/crates/typst-library/src/math/op.rs +++ b/crates/typst-library/src/math/op.rs @@ -1,6 +1,6 @@ use ecow::EcoString; -use crate::foundations::{elem, Content, NativeElement, Scope}; +use crate::foundations::{elem, Content, NativeElement, Scope, SymbolElem}; use crate::layout::HElem; use crate::math::{upright, Mathy, THIN}; use crate::text::TextElem; @@ -17,9 +17,9 @@ use crate::text::TextElem; /// # Predefined Operators { #predefined } /// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, /// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`, -/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, -/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, -/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. +/// `gcd`, `lcm`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, +/// `limsup`, `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, +/// `sinc`, `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. #[elem(title = "Text Operator", Mathy)] pub struct OpElem { /// The operator's text. @@ -38,6 +38,7 @@ macro_rules! ops { let operator = EcoString::from(ops!(@name $name $(: $value)?)); math.define( stringify!($name), + // Latex also uses their equivalent of `TextElem` here. OpElem::new(TextElem::new(operator).into()) .with_limits(ops!(@limit $($tts)*)) .pack() @@ -46,7 +47,7 @@ macro_rules! ops { let dif = |d| { HElem::new(THIN.into()).with_weak(true).pack() - + upright(TextElem::packed(d)) + + upright(SymbolElem::packed(d)) }; math.define("dif", dif('d')); math.define("Dif", dif('D')); @@ -75,6 +76,7 @@ ops! { dim, exp, gcd (limits), + lcm (limits), hom, id, im, diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs index e25c6d423..ad111700b 100644 --- a/crates/typst-library/src/math/root.rs +++ b/crates/typst-library/src/math/root.rs @@ -10,7 +10,6 @@ use crate::math::Mathy; /// ``` #[func(title = "Square Root")] pub fn sqrt( - /// The call span of this function. span: Span, /// The expression to take the square root of. radicand: Content, diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 280ac4a42..a391e5804 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -1,7 +1,7 @@ +use std::any::TypeId; use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; @@ -12,33 +12,34 @@ use hayagriva::archive::ArchivedStyle; use hayagriva::io::BibLaTeXError; use hayagriva::{ citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, - SpecificLocator, + Library, SpecificLocator, }; use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; -use typed_arena::Arena; use typst_syntax::{Span, Spanned}; -use typst_utils::{LazyHash, NonZeroExt, PicoStr}; +use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, - NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, - Styles, Synthesize, Type, Value, + elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, + OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, + Synthesize, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, - Sizing, TrackSizings, VElem, + Sides, Sizing, TrackSizings, }; +use crate::loading::{DataSource, Load}; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, Url, }; use crate::routines::{EvalMode, Routines}; use crate::text::{ - FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta, + FontStyle, Lang, LocalName, Region, Smallcaps, SubElem, SuperElem, TextElem, + WeightDelta, }; use crate::World; @@ -86,13 +87,20 @@ use crate::World; /// ``` #[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] pub struct BibliographyElem { - /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. + /// One or multiple paths to or raw bytes for Hayagriva `.yml` and/or + /// BibLaTeX `.bib` files. + /// + /// This can be a: + /// - A path string to load a bibliography file from the given path. For + /// more details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the bibliography should be decoded. + /// - An array where each item is one the above. #[required] #[parse( - let (paths, bibliography) = Bibliography::parse(engine, args)?; - paths + let sources = args.expect("sources")?; + Bibliography::load(engine.world, sources)? )] - pub path: BibliographyPaths, + pub sources: Derived, Bibliography>, /// The title of the bibliography. /// @@ -116,19 +124,22 @@ pub struct BibliographyElem { /// The bibliography style. /// - /// Should be either one of the built-in styles (see below) or a path to - /// a [CSL file](https://citationstyles.org/). Some of the styles listed - /// below appear twice, once with their full name and once with a short - /// alias. - #[parse(CslStyle::parse(engine, args)?)] - #[default(CslStyle::from_name("ieee").unwrap())] - pub style: CslStyle, - - /// The loaded bibliography. - #[internal] - #[required] - #[parse(bibliography)] - pub bibliography: Bibliography, + /// This can be: + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>("style")? { + Some(source) => Some(CslStyle::load(engine.world, source)?), + None => None, + })] + #[default({ + let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers; + Derived::new(CslSource::Named(default), CslStyle::from_archived(default)) + })] + pub style: Derived, /// The language setting where the bibliography is. #[internal] @@ -141,17 +152,6 @@ pub struct BibliographyElem { pub region: Option, } -/// A list of bibliography file paths. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct BibliographyPaths(Vec); - -cast! { - BibliographyPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked) -> StrResult> { @@ -169,13 +169,12 @@ impl BibliographyElem { } /// Whether the bibliography contains the given key. - pub fn has(engine: &Engine, key: impl Into) -> bool { - let key = key.into(); + pub fn has(engine: &Engine, key: Label) -> bool { engine .introspector .query(&Self::elem().select()) .iter() - .any(|elem| elem.to_packed::().unwrap().bibliography().has(key)) + .any(|elem| elem.to_packed::().unwrap().sources.derived.has(key)) } /// Find all bibliography keys. @@ -183,9 +182,9 @@ impl BibliographyElem { let mut vec = vec![]; for elem in introspector.query(&Self::elem().select()).iter() { let this = elem.to_packed::().unwrap(); - for (key, entry) in this.bibliography().iter() { + for (key, entry) in this.sources.derived.iter() { let detail = entry.title().map(|title| title.value.to_str().into()); - vec.push((Label::new(key), detail)) + vec.push((key, detail)) } } vec @@ -207,19 +206,20 @@ impl Show for Packed { const COLUMN_GUTTER: Em = Em::new(0.65); const INDENT: Em = Em::new(1.5); + let span = self.span(); + let mut seq = vec![]; if let Some(title) = self.title(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) }) { seq.push( HeadingElem::new(title) .with_depth(NonZeroUsize::ONE) .pack() - .spanned(self.span()), + .spanned(span), ); } - let span = self.span(); let works = Works::generate(engine).at(span)?; let references = works .references @@ -227,10 +227,9 @@ impl Show for Packed { .ok_or("CSL style is not suitable for bibliographies") .at(span)?; - let row_gutter = ParElem::spacing_in(styles); - let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack(); - if references.iter().any(|(prefix, _)| prefix.is_some()) { + let row_gutter = ParElem::spacing_in(styles); + let mut cells = vec![]; for (prefix, reference) in references { cells.push(GridChild::Item(GridItem::Cell( @@ -247,23 +246,27 @@ impl Show for Packed { .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) .pack() - .spanned(self.span()), + .spanned(span), ); } else { - for (i, (_, reference)) in references.iter().enumerate() { - if i > 0 { - seq.push(row_gutter_elem.clone()); - } - seq.push(reference.clone()); + for (_, reference) in references { + let realized = reference.clone(); + let block = if works.hanging_indent { + let body = HElem::new((-INDENT).into()).pack() + realized; + let inset = Sides::default() + .with(TextElem::dir_in(styles).start(), Some(INDENT.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + + seq.push(block.pack().spanned(span)); } } - let mut content = Content::sequence(seq); - if works.hanging_indent { - content = content.styled(ParElem::set_hanging_indent(INDENT.into())); - } - - Ok(content) + Ok(Content::sequence(seq)) } } @@ -282,63 +285,35 @@ impl LocalName for Packed { } /// A loaded bibliography. -#[derive(Clone, PartialEq)] -pub struct Bibliography { - map: Arc>, - hash: u128, -} +#[derive(Clone, PartialEq, Hash)] +pub struct Bibliography(Arc>>); impl Bibliography { - /// Parse the bibliography argument. - fn parse( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult<(BibliographyPaths, Bibliography)> { - let Spanned { v: paths, span } = - args.expect::>("path to bibliography file")?; - - // Load bibliography files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Parse. - let bibliography = Self::load(&paths, &data).at(span)?; - - Ok((paths, bibliography)) + /// Load a bibliography from data sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Self>> { + let data = sources.load(world)?; + let bibliography = Self::decode(&sources.v, &data).at(sources.span)?; + Ok(Derived::new(sources.v, bibliography)) } - /// Load bibliography entries from paths. + /// Decode a bibliography from loaded data sources. #[comemo::memoize] #[typst_macros::time(name = "load bibliography")] - fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult { + fn decode( + sources: &OneOrMultiple, + data: &[Bytes], + ) -> StrResult { let mut map = IndexMap::new(); let mut duplicates = Vec::::new(); // We might have multiple bib/yaml files - for (path, bytes) in paths.0.iter().zip(data) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - - let ext = Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - let library = match ext.to_lowercase().as_str() { - "yml" | "yaml" => hayagriva::io::from_yaml_str(src) - .map_err(|err| eco_format!("failed to parse YAML ({err})"))?, - "bib" => hayagriva::io::from_biblatex_str(src) - .map_err(|errors| format_biblatex_error(path, src, errors))?, - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), - }; - + for (source, data) in sources.0.iter().zip(data) { + let library = decode_library(source, data)?; for entry in library { - match map.entry(PicoStr::intern(entry.key())) { + match map.entry(Label::new(PicoStr::intern(entry.key()))) { indexmap::map::Entry::Vacant(vacant) => { vacant.insert(entry); } @@ -353,182 +328,210 @@ impl Bibliography { bail!("duplicate bibliography keys: {}", duplicates.join(", ")); } - Ok(Bibliography { - map: Arc::new(map), - hash: typst_utils::hash128(data), - }) + Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data))))) } - fn has(&self, key: impl Into) -> bool { - self.map.contains_key(&key.into()) + fn has(&self, key: Label) -> bool { + self.0.contains_key(&key) } - fn iter(&self) -> impl Iterator { - self.map.iter().map(|(&k, v)| (k, v)) + fn get(&self, key: Label) -> Option<&hayagriva::Entry> { + self.0.get(&key) + } + + fn iter(&self) -> impl Iterator { + self.0.iter().map(|(&k, v)| (k, v)) } } impl Debug for Bibliography { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_set().entries(self.map.keys()).finish() + f.debug_set().entries(self.0.keys()).finish() } } -impl Hash for Bibliography { - fn hash(&self, state: &mut H) { - self.hash.hash(state); +/// Decode on library from one data source. +fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { + let src = data.as_str().map_err(FileError::from)?; + + if let DataSource::Path(path) = source { + // If we got a path, use the extension to determine whether it is + // YAML or BibLaTeX. + let ext = Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + match ext.to_lowercase().as_str() { + "yml" | "yaml" => hayagriva::io::from_yaml_str(src) + .map_err(|err| eco_format!("failed to parse YAML ({err})")), + "bib" => hayagriva::io::from_biblatex_str(src) + .map_err(|errors| format_biblatex_error(src, Some(path), errors)), + _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + } + } else { + // If we just got bytes, we need to guess. If it can be decoded as + // hayagriva YAML, we'll use that. + let haya_err = match hayagriva::io::from_yaml_str(src) { + Ok(library) => return Ok(library), + Err(err) => err, + }; + + // If it can be decoded as BibLaTeX, we use that isntead. + let bib_errs = match hayagriva::io::from_biblatex_str(src) { + Ok(library) => return Ok(library), + Err(err) => err, + }; + + // If neither decoded correctly, check whether `:` or `{` appears + // more often to guess whether it's more likely to be YAML or BibLaTeX + // and emit the more appropriate error. + let mut yaml = 0; + let mut biblatex = 0; + for c in src.chars() { + match c { + ':' => yaml += 1, + '{' => biblatex += 1, + _ => {} + } + } + + if yaml > biblatex { + bail!("failed to parse YAML ({haya_err})") + } else { + Err(format_biblatex_error(src, None, bib_errs)) + } } } /// Format a BibLaTeX loading error. -fn format_biblatex_error(path: &str, src: &str, errors: Vec) -> EcoString { +fn format_biblatex_error( + src: &str, + path: Option<&str>, + errors: Vec, +) -> EcoString { let Some(error) = errors.first() else { - return eco_format!("failed to parse BibLaTeX file ({path})"); + return match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path})"), + None => eco_format!("failed to parse BibLaTeX"), + }; }; let (span, msg) = match error { BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), }; + let line = src.get(..span.start).unwrap_or_default().lines().count(); - eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") + match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})"), + None => eco_format!("failed to parse BibLaTeX ({line}: {msg})"), + } } /// A loaded CSL style. -#[ty(cast)] #[derive(Debug, Clone, PartialEq, Hash)] -pub struct CslStyle { - name: Option, - style: Arc>, -} +pub struct CslStyle(Arc>); impl CslStyle { - /// Parse the style argument. - pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult> { - let Some(Spanned { v: string, span }) = - args.named::>("style")? - else { - return Ok(None); - }; - - Ok(Some(Self::parse_impl(engine, &string, span).at(span)?)) - } - - /// Parse the style argument with `Smart`. - pub fn parse_smart( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult>> { - let Some(Spanned { v: smart, span }) = - args.named::>>("style")? - else { - return Ok(None); - }; - - Ok(Some(match smart { - Smart::Auto => Smart::Auto, - Smart::Custom(string) => { - Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?) + /// Load a CSL style from a data source. + pub fn load( + world: Tracked, + Spanned { v: source, span }: Spanned, + ) -> SourceResult> { + let style = match &source { + CslSource::Named(style) => Self::from_archived(*style), + CslSource::Normal(source) => { + let data = Spanned::new(source, span).load(world)?; + Self::from_data(data).at(span)? } - })) - } - - /// Parse internally. - fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult { - let ext = Path::new(string) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - if ext == "csl" { - let id = span.resolve_path(string)?; - let data = engine.world.file(id)?; - CslStyle::from_data(&data) - } else { - CslStyle::from_name(string) - } + }; + Ok(Derived::new(source, style)) } /// Load a built-in CSL style. #[comemo::memoize] - pub fn from_name(name: &str) -> StrResult { - match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) { - Some(citationberg::Style::Independent(style)) => Ok(Self { - name: Some(name.into()), - style: Arc::new(LazyHash::new(style)), - }), - _ => bail!("unknown style: `{name}`"), + pub fn from_archived(archived: ArchivedStyle) -> CslStyle { + match archived.get() { + citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), archived)), + ))), + // Ensured by `test_bibliography_load_builtin_styles`. + _ => unreachable!("archive should not contain dependant styles"), } } /// Load a CSL style from file contents. #[comemo::memoize] - pub fn from_data(data: &Bytes) -> StrResult { - let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; + pub fn from_data(data: Bytes) -> StrResult { + let text = data.as_str().map_err(FileError::from)?; citationberg::IndependentStyle::from_xml(text) - .map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) }) + .map(|style| { + Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), data)), + ))) + }) .map_err(|err| eco_format!("failed to load CSL style ({err})")) } /// Get the underlying independent style. pub fn get(&self) -> &citationberg::IndependentStyle { - self.style.as_ref() + self.0.as_ref() } } -// This Reflect impl is technically a bit wrong because it doesn't say what -// FromValue and IntoValue really do. Instead, it says what the `style` argument -// on `bibliography` and `cite` expect (through manual parsing). -impl Reflect for CslStyle { +/// Source for a CSL style. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CslSource { + /// A predefined named style. + Named(ArchivedStyle), + /// A normal data source. + Normal(DataSource), +} + +impl Reflect for CslSource { #[comemo::memoize] fn input() -> CastInfo { - let ty = std::iter::once(CastInfo::Type(Type::of::())); - let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| { + let source = std::iter::once(DataSource::input()); + let names = ArchivedStyle::all().iter().map(|name| { CastInfo::Value(name.names()[0].into_value(), name.display_name()) }); - CastInfo::Union(ty.chain(options).collect()) + CastInfo::Union(source.into_iter().chain(names).collect()) } fn output() -> CastInfo { - EcoString::output() + DataSource::output() } fn castable(value: &Value) -> bool { - if let Value::Dyn(dynamic) = &value { - if dynamic.is::() { - return true; - } - } - - false + DataSource::castable(value) } } -impl FromValue for CslStyle { +impl FromValue for CslSource { fn from_value(value: Value) -> HintedStrResult { - if let Value::Dyn(dynamic) = &value { - if let Some(concrete) = dynamic.downcast::() { - return Ok(concrete.clone()); + if EcoString::castable(&value) { + let string = EcoString::from_value(value.clone())?; + if Path::new(string.as_str()).extension().is_none() { + let style = ArchivedStyle::by_name(&string) + .ok_or_else(|| eco_format!("unknown style: {}", string))?; + return Ok(CslSource::Named(style)); } } - Err(::error(&value)) + DataSource::from_value(value).map(CslSource::Normal) } } -impl IntoValue for CslStyle { +impl IntoValue for CslSource { fn into_value(self) -> Value { - Value::dynamic(self) - } -} - -impl Repr for CslStyle { - fn repr(&self) -> EcoString { - self.name - .as_ref() - .map(|name| name.repr()) - .unwrap_or_else(|| "..".into()) + match self { + // We prefer the shorter names which are at the back of the array. + Self::Named(v) => v.names().last().unwrap().into_value(), + Self::Normal(v) => v.into_value(), + } } } @@ -632,16 +635,15 @@ impl<'a> Generator<'a> { static LOCALES: LazyLock> = LazyLock::new(hayagriva::archive::locales); - let database = self.bibliography.bibliography(); - let bibliography_style = self.bibliography.style(StyleChain::default()); - let styles = Arena::new(); + let database = &self.bibliography.sources.derived; + let bibliography_style = &self.bibliography.style(StyleChain::default()).derived; // Process all citation groups. let mut driver = BibliographyDriver::new(); for elem in &self.groups { let group = elem.to_packed::().unwrap(); let location = elem.location().unwrap(); - let children = group.children(); + let children = &group.children; // Groups should never be empty. let Some(first) = children.first() else { continue }; @@ -653,12 +655,11 @@ impl<'a> Generator<'a> { // Create infos and items for each child in the group. for child in children { - let key = *child.key(); - let Some(entry) = database.map.get(&key.into_inner()) else { + let Some(entry) = database.get(child.key) else { errors.push(error!( child.span(), "key `{}` does not exist in the bibliography", - key.resolve() + child.key.resolve() )); continue; }; @@ -685,7 +686,7 @@ impl<'a> Generator<'a> { }; normal &= special_form.is_none(); - subinfos.push(CiteInfo { key, supplement, hidden }); + subinfos.push(CiteInfo { key: child.key, supplement, hidden }); items.push(CitationItem::new(entry, locator, None, hidden, special_form)); } @@ -695,8 +696,8 @@ impl<'a> Generator<'a> { } let style = match first.style(StyleChain::default()) { - Smart::Auto => &bibliography_style.style, - Smart::Custom(style) => styles.alloc(style.style), + Smart::Auto => bibliography_style.get(), + Smart::Custom(style) => style.derived.get(), }; self.infos.push(GroupInfo { @@ -727,7 +728,7 @@ impl<'a> Generator<'a> { // Add hidden items for everything if we should print the whole // bibliography. if self.bibliography.full(StyleChain::default()) { - for entry in database.map.values() { + for (_, entry) in database.iter() { driver.citation(CitationRequest::new( vec![CitationItem::new(entry, None, None, true, None)], bibliography_style.get(), @@ -1050,7 +1051,8 @@ fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Con match format.font_variant { citationberg::FontVariant::Normal => {} citationberg::FontVariant::SmallCaps => { - content = content.styled(TextElem::set_smallcaps(true)); + content = + content.styled(TextElem::set_smallcaps(Some(Smallcaps::Minuscules))); } } @@ -1097,3 +1099,15 @@ fn locale(lang: Lang, region: Option) -> citationberg::LocaleCode { } citationberg::LocaleCode(value) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bibliography_load_builtin_styles() { + for &archived in ArchivedStyle::all() { + let _ = CslStyle::from_archived(archived); + } + } +} diff --git a/crates/typst-library/src/model/cite.rs b/crates/typst-library/src/model/cite.rs index ac0cfa790..29497993d 100644 --- a/crates/typst-library/src/model/cite.rs +++ b/crates/typst-library/src/model/cite.rs @@ -1,11 +1,14 @@ +use typst_syntax::Spanned; + use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Cast, Content, Label, Packed, Show, Smart, StyleChain, Synthesize, + cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain, + Synthesize, }; use crate::introspection::Locatable; use crate::model::bibliography::Works; -use crate::model::CslStyle; +use crate::model::{CslSource, CslStyle}; use crate::text::{Lang, Region, TextElem}; /// Cite a work from the bibliography. @@ -87,15 +90,24 @@ pub struct CiteElem { /// The citation style. /// - /// Should be either `{auto}`, one of the built-in styles (see below) or a - /// path to a [CSL file](https://citationstyles.org/). Some of the styles - /// listed below appear twice, once with their full name and once with a - /// short alias. - /// - /// When set to `{auto}`, automatically use the - /// [bibliography's style]($bibliography.style) for the citations. - #[parse(CslStyle::parse_smart(engine, args)?)] - pub style: Smart, + /// This can be: + /// - `{auto}` to automatically use the + /// [bibliography's style]($bibliography.style) for citations. + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>>("style")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom( + CslStyle::load(engine.world, Spanned::new(source, span))? + )), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub style: Smart>, /// The text language setting where the citation is. #[internal] diff --git a/crates/typst-library/src/model/document.rs b/crates/typst-library/src/model/document.rs index 5124b2487..1bce6b357 100644 --- a/crates/typst-library/src/model/document.rs +++ b/crates/typst-library/src/model/document.rs @@ -3,8 +3,8 @@ use ecow::EcoString; use crate::diag::{bail, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain, - Styles, Value, + cast, elem, Args, Array, Construct, Content, Datetime, Fields, OneOrMultiple, Smart, + StyleChain, Styles, Value, }; /// The root element of a document and its metadata. @@ -35,7 +35,7 @@ pub struct DocumentElem { /// The document's authors. #[ghost] - pub author: Author, + pub author: OneOrMultiple, /// The document's description. #[ghost] @@ -43,7 +43,7 @@ pub struct DocumentElem { /// The document's keywords. #[ghost] - pub keywords: Keywords, + pub keywords: OneOrMultiple, /// The document's creation date. /// @@ -93,7 +93,7 @@ cast! { pub struct DocumentInfo { /// The document's title. pub title: Option, - /// The document's author. + /// The document's author(s). pub author: Vec, /// The document's description. pub description: Option, diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index eb3c2ea45..a4126e72c 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -9,9 +9,11 @@ use crate::foundations::{ cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, Styles, TargetElem, }; -use crate::html::{attr, tag, HtmlAttr, HtmlElem}; +use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; -use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; +use crate::model::{ + ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem, +}; /// A numbered list. /// @@ -226,22 +228,29 @@ impl EnumElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { let mut elem = HtmlElem::new(tag::ol); if self.reversed(styles) { - elem = - elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed"); + elem = elem.with_attr(attr::reversed, "reversed"); } - return Ok(elem - .with_body(Some(Content::sequence(self.children.iter().map(|item| { - let mut li = HtmlElem::new(tag::li); - if let Some(nr) = item.number(styles) { - li = li.with_attr(attr::value, eco_format!("{nr}")); - } - li.with_body(Some(item.body.clone())).pack().spanned(item.span()) - })))) - .pack() - .spanned(self.span())); + if let Some(n) = self.start(styles).custom() { + elem = elem.with_attr(attr::start, eco_format!("{n}")); + } + let body = Content::sequence(self.children.iter().map(|item| { + let mut li = HtmlElem::new(tag::li); + if let Some(nr) = item.number(styles) { + li = li.with_attr(attr::value, eco_format!("{nr}")); + } + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + li.with_body(Some(body)).pack().spanned(item.span()) + })); + return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); } let mut realized = @@ -249,7 +258,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index fd843ee53..78a79a8e2 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -19,7 +19,9 @@ use crate::layout::{ AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem, PlacementScope, VAlignment, VElem, }; -use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; +use crate::model::{ + Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement, +}; use crate::text::{Lang, Region, TextElem}; use crate::visualize::ImageElem; @@ -156,6 +158,7 @@ pub struct FigureElem { pub scope: PlacementScope, /// The figure's caption. + #[borrowed] pub caption: Option>, /// The kind of figure this is. @@ -257,7 +260,7 @@ impl Synthesize for Packed { // Determine the figure's kind. let kind = elem.kind(styles).unwrap_or_else(|| { - elem.body() + elem.body .query_first(&Selector::can::()) .map(|elem| FigureKind::Elem(elem.func())) .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) @@ -288,14 +291,13 @@ impl Synthesize for Packed { // Resolve the supplement with the first descendant of the kind or // just the body, if none was found. let descendant = match kind { - FigureKind::Elem(func) => elem - .body() - .query_first(&Selector::Elem(func, None)) - .map(Cow::Owned), + FigureKind::Elem(func) => { + elem.body.query_first(&Selector::Elem(func, None)).map(Cow::Owned) + } FigureKind::Name(_) => None, }; - let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body())); + let target = descendant.unwrap_or_else(|| Cow::Borrowed(&elem.body)); Some(supplement.resolve(engine, styles, [target])?) } }; @@ -306,7 +308,7 @@ impl Synthesize for Packed { )); // Fill the figure's caption. - let mut caption = elem.caption(styles); + let mut caption = elem.caption(styles).clone(); if let Some(caption) = &mut caption { caption.synthesize(engine, styles)?; caption.push_kind(kind.clone()); @@ -328,11 +330,12 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "figure", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); let target = TargetElem::target_in(styles); let mut realized = self.body.clone(); // Build the caption, if any. - if let Some(caption) = self.caption(styles) { + if let Some(caption) = self.caption(styles).clone() { let (first, second) = match caption.position(styles) { OuterVAlignment::Top => (caption.pack(), realized), OuterVAlignment::Bottom => (realized, caption.pack()), @@ -341,24 +344,27 @@ impl Show for Packed { seq.push(first); if !target.is_html() { let v = VElem::new(self.gap(styles).into()).with_weak(true); - seq.push(v.pack().spanned(self.span())) + seq.push(v.pack().spanned(span)) } seq.push(second); realized = Content::sequence(seq) } + // Ensure that the body is considered a paragraph. + realized += ParbreakElem::shared().clone().spanned(span); + if target.is_html() { return Ok(HtmlElem::new(tag::figure) .with_body(Some(realized)) .pack() - .spanned(self.span())); + .spanned(span)); } // Wrap the contents in a block. realized = BlockElem::new() .with_body(Some(BlockBody::Content(realized))) .pack() - .spanned(self.span()); + .spanned(span); // Wrap in a float. if let Some(align) = self.placement(styles) { @@ -367,10 +373,10 @@ impl Show for Packed { .with_scope(self.scope(styles)) .with_float(true) .pack() - .spanned(self.span()); + .spanned(span); } else if self.scope(styles) == PlacementScope::Parent { bail!( - self.span(), + span, "parent-scoped placement is only available for floating figures"; hint: "you can enable floating placement with `figure(placement: auto, ..)`" ); @@ -424,46 +430,26 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.outlined(StyleChain::default()) { - return Ok(None); + fn outlined(&self) -> bool { + (**self).outlined(StyleChain::default()) + && (self.caption(StyleChain::default()).is_some() + || self.numbering().is_some()) + } + + fn prefix(&self, numbers: Content) -> Content { + let supplement = self.supplement(); + if !supplement.is_empty() { + supplement + TextElem::packed('\u{a0}') + numbers + } else { + numbers } + } - let Some(caption) = self.caption(StyleChain::default()) else { - return Ok(None); - }; - - let mut realized = caption.body().clone(); - if let ( - Smart::Custom(Some(Supplement::Content(mut supplement))), - Some(Some(counter)), - Some(numbering), - ) = ( - (**self).supplement(StyleChain::default()).clone(), - (**self).counter(), - self.numbering(), - ) { - let numbers = counter.display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - if !supplement.is_empty() { - supplement += TextElem::packed('\u{a0}'); - } - - let separator = caption.get_separator(StyleChain::default()); - - realized = supplement + numbers + separator + caption.body(); - } - - Ok(Some(realized)) + fn body(&self) -> Content { + self.caption(StyleChain::default()) + .as_ref() + .map(|caption| caption.body.clone()) + .unwrap_or_default() } } @@ -604,7 +590,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "figure.caption", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); if let ( Some(Some(mut supplement)), @@ -624,14 +610,17 @@ impl Show for Packed { realized = supplement + numbers + self.get_separator(styles) + realized; } - if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::figcaption) + Ok(if TargetElem::target_in(styles).is_html() { + HtmlElem::new(tag::figcaption) .with_body(Some(realized)) .pack() - .spanned(self.span())); - } - - Ok(realized) + .spanned(self.span()) + } else { + BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()) + }) } } diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index ffc78ea05..dfa3933bb 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -105,12 +105,12 @@ impl FootnoteElem { /// Tests if this footnote is a reference to another footnote. pub fn is_ref(&self) -> bool { - matches!(self.body(), FootnoteBody::Reference(_)) + matches!(self.body, FootnoteBody::Reference(_)) } /// Returns the content of the body of this footnote if it is not a ref. pub fn body_content(&self) -> Option<&Content> { - match self.body() { + match &self.body { FootnoteBody::Content(content) => Some(content), _ => None, } @@ -120,9 +120,9 @@ impl FootnoteElem { impl Packed { /// Returns the location of the definition of this footnote. pub fn declaration_location(&self, engine: &Engine) -> StrResult { - match self.body() { + match self.body { FootnoteBody::Reference(label) => { - let element = engine.introspector.query_label(*label)?; + let element = engine.introspector.query_label(label)?; let footnote = element .to_packed::() .ok_or("referenced element should be a footnote")?; @@ -281,12 +281,11 @@ impl Show for Packed { #[typst_macros::time(name = "footnote.entry", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let span = self.span(); - let note = self.note(); let number_gap = Em::new(0.05); let default = StyleChain::default(); - let numbering = note.numbering(default); + let numbering = self.note.numbering(default); let counter = Counter::of(FootnoteElem::elem()); - let Some(loc) = note.location() else { + let Some(loc) = self.note.location() else { bail!( span, "footnote entry must have a location"; hint: "try using a query or a show rule to customize the footnote instead" @@ -304,18 +303,16 @@ impl Show for Packed { HElem::new(self.indent(styles).into()).pack(), sup, HElem::new(number_gap.into()).with_weak(true).pack(), - note.body_content().unwrap().clone(), + self.note.body_content().unwrap().clone(), ])) } } impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { - let text_size = Em::new(0.85); - let leading = Em::new(0.5); let mut out = Styles::new(); - out.set(ParElem::set_leading(leading.into())); - out.set(TextElem::set_size(TextSize(text_size.into()))); + out.set(ParElem::set_leading(Em::new(0.5).into())); + out.set(TextElem::set_size(TextSize(Em::new(0.85).into()))); out } } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index ec9cf4e99..00931c815 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,7 +1,7 @@ use std::num::NonZeroUsize; use ecow::eco_format; -use typst_utils::NonZeroExt; +use typst_utils::{Get, NonZeroExt}; use crate::diag::{warning, SourceResult}; use crate::engine::Engine; @@ -13,8 +13,8 @@ use crate::html::{attr, tag, HtmlElem}; use crate::introspection::{ Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, }; -use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region}; -use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; +use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides}; +use crate::model::{Numbering, Outlinable, Refable, Supplement}; use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; /// A section heading. @@ -223,7 +223,7 @@ impl Show for Packed { const SPACING_TO_NUMBERING: Em = Em::new(0.3); let span = self.span(); - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); let hanging_indent = self.hanging_indent(styles); let mut indent = match hanging_indent { @@ -264,10 +264,6 @@ impl Show for Packed { realized = numbering + spacing + realized; } - if indent != Abs::zero() && !html { - realized = realized.styled(ParElem::set_hanging_indent(indent.into())); - } - Ok(if html { // HTML's h1 is closer to a title element. There should only be one. // Meanwhile, a level 1 Typst heading is a section heading. For this @@ -294,8 +290,17 @@ impl Show for Packed { HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) } } else { - let realized = BlockBody::Content(realized); - BlockElem::new().with_body(Some(realized)).pack().spanned(span) + let block = if indent != Abs::zero() { + let body = HElem::new((-indent).into()).pack() + realized; + let inset = Sides::default() + .with(TextElem::dir_in(styles).start(), Some(indent.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + block.pack().spanned(span) }) } } @@ -351,32 +356,21 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.outlined(StyleChain::default()) { - return Ok(None); - } - - let mut content = self.body().clone(); - if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { - let numbers = Counter::of(HeadingElem::elem()).display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - content = numbers + SpaceElem::shared().clone() + content; - }; - - Ok(Some(content)) + fn outlined(&self) -> bool { + (**self).outlined(StyleChain::default()) } fn level(&self) -> NonZeroUsize { (**self).resolve_level(StyleChain::default()) } + + fn prefix(&self, numbers: Content) -> Content { + numbers + } + + fn body(&self) -> Content { + self.body.clone() + } } impl LocalName for Packed { diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 8ab129fdd..ea85aa945 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -1,22 +1,21 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; -use smallvec::SmallVec; use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Label, NativeElement, Packed, Repr, Show, Smart, StyleChain, - TargetElem, + cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart, + StyleChain, Styles, TargetElem, }; use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; use crate::layout::Position; -use crate::text::{Hyphenate, TextElem}; +use crate::text::TextElem; /// Links to a URL or a location in the document. /// -/// By default, links are not styled any different from normal text. However, +/// By default, links do not look any different from normal text. However, /// you can easily apply a style of your choice with a show rule. /// /// # Example @@ -31,6 +30,11 @@ use crate::text::{Hyphenate, TextElem}; /// ] /// ``` /// +/// # Hyphenation +/// If you enable hyphenation or justification, by default, it will not apply to +/// links to prevent unwanted hyphenation in URLs. You can opt out of this +/// default via `{show link: set text(hyphenate: true)}`. +/// /// # Syntax /// This function also has dedicated syntax: Text that starts with `http://` or /// `https://` is automatically turned into a link. @@ -85,10 +89,10 @@ pub struct LinkElem { })] pub body: Content, - /// This style is set on the content contained in the `link` element. + /// A destination style that should be applied to elements. #[internal] #[ghost] - pub dests: SmallVec<[Destination; 1]>, + pub current: Option, } impl LinkElem { @@ -102,11 +106,10 @@ impl LinkElem { impl Show for Packed { #[typst_macros::time(name = "link", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); - let dest = self.dest(); + let body = self.body.clone(); Ok(if TargetElem::target_in(styles).is_html() { - if let LinkTarget::Dest(Destination::Url(url)) = dest { + if let LinkTarget::Dest(Destination::Url(url)) = &self.dest { HtmlElem::new(tag::a) .with_attr(attr::href, url.clone().into_inner()) .with_body(Some(body)) @@ -120,25 +123,31 @@ impl Show for Packed { body } } else { - let linked = match self.dest() { + match &self.dest { LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Label(label) => { let elem = engine.introspector.query_label(*label).at(self.span())?; let dest = Destination::Location(elem.location().unwrap()); body.clone().linked(dest) } - }; - - linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))) + } }) } } -fn body_from_url(url: &Url) -> Content { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); +impl ShowSet for Packed { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(TextElem::set_hyphenate(Smart::Custom(false))); + out } +} + +fn body_from_url(url: &Url) -> Content { + let text = ["mailto:", "tel:"] + .into_iter() + .find_map(|prefix| url.strip_prefix(prefix)) + .unwrap_or(url); let shorter = text.len() < url.len(); TextElem::packed(if shorter { text.into() } else { (**url).clone() }) } diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index 1e369d541..d93ec9172 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -8,7 +8,7 @@ use crate::foundations::{ }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockElem, Em, Length, VElem}; -use crate::model::ParElem; +use crate::model::{ParElem, ParbreakElem}; use crate::text::TextElem; /// A bullet list. @@ -141,11 +141,18 @@ impl ListElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { return Ok(HtmlElem::new(tag::ul) .with_body(Some(Content::sequence(self.children.iter().map(|item| { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } HtmlElem::new(tag::li) - .with_body(Some(item.body.clone())) + .with_body(Some(body)) .pack() .spanned(item.span()) })))) @@ -158,7 +165,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 7dad51c39..9bdbf0013 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -40,19 +40,11 @@ pub use self::strong::*; pub use self::table::*; pub use self::terms::*; -use crate::foundations::{category, Category, Scope}; - -/// Document structuring. -/// -/// Here, you can find functions to structure your document and interact with -/// that structure. This includes section headings, figures, bibliography -/// management, cross-referencing and more. -#[category] -pub static MODEL: Category; +use crate::foundations::Scope; /// Hook up all `model` definitions. pub fn define(global: &mut Scope) { - global.category(MODEL); + global.start_category(crate::Category::Model); global.define_elem::(); global.define_elem::(); global.define_elem::(); @@ -72,4 +64,5 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_func::(); + global.reset_category(); } diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 4e2fe4579..150506758 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -53,9 +53,7 @@ use crate::text::Case; /// ``` #[func] pub fn numbering( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Defines how the numbering works. /// diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index e8d32a540..7ceb530f8 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -1,50 +1,61 @@ use std::num::NonZeroUsize; use std::str::FromStr; -use comemo::Track; +use comemo::{Track, Tracked}; +use smallvec::SmallVec; use typst_syntax::Span; -use typst_utils::NonZeroExt; +use typst_utils::{Get, NonZeroExt}; -use crate::diag::{bail, At, SourceResult}; +use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, select_where, Content, Context, Func, LocatableSelector, - NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, + cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func, + LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, + Styles, +}; +use crate::introspection::{ + Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink, }; -use crate::introspection::{Counter, CounterKey, Locatable}; use crate::layout::{ - BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing, + Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel, + RepeatElem, Sides, }; -use crate::model::{ - Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, -}; -use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; +use crate::math::EquationElem; +use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable}; +use crate::text::{LocalName, SpaceElem, TextElem}; /// A table of contents, figures, or other elements. /// /// This function generates a list of all occurrences of an element in the -/// document, up to a given depth. The element's numbering and page number will -/// be displayed in the outline alongside its title or caption. By default this -/// generates a table of contents. +/// document, up to a given [`depth`]($outline.depth). The element's numbering +/// and page number will be displayed in the outline alongside its title or +/// caption. /// /// # Example /// ```example +/// #set heading(numbering: "1.") /// #outline() /// /// = Introduction /// #lorem(5) /// -/// = Prior work +/// = Methods +/// == Setup /// #lorem(10) /// ``` /// /// # Alternative outlines +/// In its default configuration, this function generates a table of contents. /// By setting the `target` parameter, the outline can be used to generate a -/// list of other kinds of elements than headings. In the example below, we list -/// all figures containing images by setting `target` to `{figure.where(kind: -/// image)}`. We could have also set it to just `figure`, but then the list -/// would also include figures containing tables or other material. For more -/// details on the `where` selector, [see here]($function.where). +/// list of other kinds of elements than headings. +/// +/// In the example below, we list all figures containing images by setting +/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set +/// it to `{figure.where(kind: table)}` to generate a list of tables. +/// +/// We could also set it to just `figure`, without using a [`where`]($function.where) +/// selector, but then the list would contain _all_ figures, be it ones +/// containing images, tables, or other material. /// /// ```example /// #outline( @@ -59,16 +70,89 @@ use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; /// ``` /// /// # Styling the outline -/// The outline element has several options for customization, such as its -/// `title` and `indent` parameters. If desired, however, it is possible to have -/// more control over the outline's look and style through the -/// [`outline.entry`]($outline.entry) element. -#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)] +/// At the most basic level, you can style the outline by setting properties on +/// it and its entries. This way, you can customize the outline's +/// [title]($outline.title), how outline entries are +/// [indented]($outline.indent), and how the space between an entry's text and +/// its page number should be [filled]($outline.entry.fill). +/// +/// Richer customization is possible through configuration of the outline's +/// [entries]($outline.entry). The outline generates one entry for each outlined +/// element. +/// +/// ## Spacing the entries { #entry-spacing } +/// Outline entries are [blocks]($block), so you can adjust the spacing between +/// them with normal block-spacing rules: +/// +/// ```example +/// #show outline.entry.where( +/// level: 1 +/// ): set block(above: 1.2em) +/// +/// #outline() +/// +/// = About ACME Corp. +/// == History +/// === Origins +/// = Products +/// == ACME Tools +/// ``` +/// +/// ## Building an outline entry from its parts { #building-an-entry } +/// For full control, you can also write a transformational show rule on +/// `outline.entry`. However, the logic for properly formatting and indenting +/// outline entries is quite complex and the outline entry itself only contains +/// two fields: The level and the outlined element. +/// +/// For this reason, various helper functions are provided. You can mix and +/// match these to compose an entry from just the parts you like. +/// +/// The default show rule for an outline entry looks like this[^1]: +/// ```typ +/// #show outline.entry: it => link( +/// it.element.location(), +/// it.indented(it.prefix(), it.inner()), +/// ) +/// ``` +/// +/// - The [`indented`]($outline.entry.indented) function takes an optional +/// prefix and inner content and automatically applies the proper indentation +/// to it, such that different entries align nicely and long headings wrap +/// properly. +/// +/// - The [`prefix`]($outline.entry.prefix) function formats the element's +/// numbering (if any). It also appends a supplement for certain elements. +/// +/// - The [`inner`]($outline.entry.inner) function combines the element's +/// [`body`]($outline.entry.body), the filler, and the +/// [`page` number]($outline.entry.page). +/// +/// You can use these individual functions to format the outline entry in +/// different ways. Let's say, you'd like to fully remove the filler and page +/// numbers. To achieve this, you could write a show rule like this: +/// +/// ```example +/// #show outline.entry: it => link( +/// it.element.location(), +/// // Keep just the body, dropping +/// // the fill and the page. +/// it.indented(it.prefix(), it.body()), +/// ) +/// +/// #outline() +/// +/// = About ACME Corp. +/// == History +/// ``` +/// +/// [^1]: The outline of equations is the exception to this rule as it does not +/// have a body and thus does not use indented layout. +#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)] pub struct OutlineElem { /// The title of the outline. /// /// - When set to `{auto}`, an appropriate title for the - /// [text language]($text.lang) will be used. This is the default. + /// [text language]($text.lang) will be used. /// - When set to `{none}`, the outline will not have a title. /// - A custom title can be set by passing content. /// @@ -79,8 +163,10 @@ pub struct OutlineElem { /// The type of element to include in the outline. /// - /// To list figures containing a specific kind of element, like a table, you - /// can write `{figure.where(kind: table)}`. + /// To list figures containing a specific kind of element, like an image or + /// a table, you can specify the desired kind in a [`where`]($function.where) + /// selector. See the section on [alternative outlines]($outline/#alternative-outlines) + /// for more details. /// /// ```example /// #outline( @@ -97,7 +183,7 @@ pub struct OutlineElem { /// caption: [Experiment results], /// ) /// ``` - #[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))] + #[default(LocatableSelector(HeadingElem::elem().select()))] #[borrowed] pub target: LocatableSelector, @@ -121,21 +207,22 @@ pub struct OutlineElem { /// How to indent the outline's entries. /// - /// - `{none}`: No indent - /// - `{auto}`: Indents the numbering of the nested entry with the title of - /// its parent entry. This only has an effect if the entries are numbered - /// (e.g., via [heading numbering]($heading.numbering)). - /// - [Relative length]($relative): Indents the item by this length - /// multiplied by its nesting level. Specifying `{2em}`, for instance, - /// would indent top-level headings (not nested) by `{0em}`, second level + /// - `{auto}`: Indents the numbering/prefix of a nested entry with the + /// title of its parent entry. If the entries are not numbered (e.g., via + /// [heading numbering]($heading.numbering)), this instead simply inserts + /// a fixed amount of `{1.2em}` indent per level. + /// + /// - [Relative length]($relative): Indents the entry by the specified + /// length per nesting level. Specifying `{2em}`, for instance, would + /// indent top-level headings by `{0em}` (not nested), second level /// headings by `{2em}` (nested once), third-level headings by `{4em}` /// (nested twice) and so on. - /// - [Function]($function): You can completely customize this setting with - /// a function. That function receives the nesting level as a parameter - /// (starting at 0 for top-level headings/elements) and can return a - /// relative length or content making up the indent. For example, - /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while - /// `{n => [→ ] * n}` would indent with one arrow per nesting level. + /// + /// - [Function]($function): You can further customize this setting with a + /// function. That function receives the nesting level as a parameter + /// (starting at 0 for top-level headings/elements) and should return a + /// (relative) length. For example, `{n => n * 2em}` would be equivalent + /// to just specifying `{2em}`. /// /// ```example /// #set heading(numbering: "1.a.") @@ -150,11 +237,6 @@ pub struct OutlineElem { /// indent: 2em, /// ) /// - /// #outline( - /// title: [Contents (Function)], - /// indent: n => [→ ] * n, - /// ) - /// /// = About ACME Corp. /// == History /// === Origins @@ -163,20 +245,7 @@ pub struct OutlineElem { /// == Products /// #lorem(10) /// ``` - #[default(None)] - #[borrowed] - pub indent: Option>, - - /// Content to fill the space between the title and the page number. Can be - /// set to `{none}` to disable filling. - /// - /// ```example - /// #outline(fill: line(length: 100%)) - /// - /// = A New Beginning - /// ``` - #[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))] - pub fill: Option, + pub indent: Smart, } #[scope] @@ -188,80 +257,51 @@ impl OutlineElem { impl Show for Packed { #[typst_macros::time(name = "outline", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut seq = vec![ParbreakElem::shared().clone()]; + let span = self.span(); + // Build the outline title. + let mut seq = vec![]; if let Some(title) = self.title(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) }) { seq.push( HeadingElem::new(title) .with_depth(NonZeroUsize::ONE) .pack() - .spanned(self.span()), + .spanned(span), ); } - let indent = self.indent(styles); - let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap()); - - let mut ancestors: Vec<&Content> = vec![]; let elems = engine.introspector.query(&self.target(styles).0); + let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX); - for elem in &elems { - let Some(entry) = OutlineEntry::from_outlinable( - engine, - self.span(), - elem.clone(), - self.fill(styles), - styles, - )? - else { - continue; + // Build the outline entries. + for elem in elems { + let Some(outlinable) = elem.with::() else { + bail!(span, "cannot outline {}", elem.func().name()); }; - let level = entry.level(); - if depth < *level { - continue; + let level = outlinable.level(); + if outlinable.outlined() && level <= depth { + let entry = OutlineEntry::new(level, elem); + seq.push(entry.pack().spanned(span)); } - - // Deals with the ancestors of the current element. - // This is only applicable for elements with a hierarchy/level. - while ancestors - .last() - .and_then(|ancestor| ancestor.with::()) - .is_some_and(|last| last.level() >= *level) - { - ancestors.pop(); - } - - OutlineIndent::apply( - indent, - engine, - &ancestors, - &mut seq, - styles, - self.span(), - )?; - - // Add the overridable outline entry, followed by a line break. - seq.push(entry.pack().spanned(self.span())); - seq.push(LinebreakElem::shared().clone()); - - ancestors.push(elem); } - seq.push(ParbreakElem::shared().clone()); - Ok(Content::sequence(seq)) } } impl ShowSet for Packed { - fn show_set(&self, _: StyleChain) -> Styles { + fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); out.set(HeadingElem::set_outlined(false)); out.set(HeadingElem::set_numbering(None)); - out.set(ParElem::set_first_line_indent(Em::new(0.0).into())); + out.set(ParElem::set_justify(false)); + out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into()))); + // Makes the outline itself available to its entries. Should be + // superseded by a proper ancestry mechanism in the future. + out.set(OutlineEntry::set_parent(Some(self.clone()))); out } } @@ -270,93 +310,29 @@ impl LocalName for Packed { const KEY: &'static str = "outline"; } -/// Marks an element as being able to be outlined. This is used to implement the -/// `#outline()` element. -pub trait Outlinable: Refable { - /// Produce an outline item for this element. - fn outline( - &self, - engine: &mut Engine, - - styles: StyleChain, - ) -> SourceResult>; - - /// Returns the nesting level of this element. - fn level(&self) -> NonZeroUsize { - NonZeroUsize::ONE - } -} - /// Defines how an outline is indented. #[derive(Debug, Clone, PartialEq, Hash)] pub enum OutlineIndent { - Rel(Rel), + /// Indents by the specified length per level. + Rel(Rel), + /// Resolve the indent for a specific level through the given function. Func(Func), } impl OutlineIndent { - fn apply( - indent: &Option>, + /// Resolve the indent for an entry with the given level. + fn resolve( + &self, engine: &mut Engine, - ancestors: &Vec<&Content>, - seq: &mut Vec, - styles: StyleChain, + context: Tracked, + level: NonZeroUsize, span: Span, - ) -> SourceResult<()> { - match indent { - // 'none' | 'false' => no indenting - None => {} - - // 'auto' | 'true' => use numbering alignment for indenting - Some(Smart::Auto) => { - // Add hidden ancestors numberings to realize the indent. - let mut hidden = Content::empty(); - for ancestor in ancestors { - let ancestor_outlinable = ancestor.with::().unwrap(); - - if let Some(numbering) = ancestor_outlinable.numbering() { - let numbers = ancestor_outlinable.counter().display_at_loc( - engine, - ancestor.location().unwrap(), - styles, - numbering, - )?; - - hidden += numbers + SpaceElem::shared().clone(); - }; - } - - if !ancestors.is_empty() { - seq.push(HideElem::new(hidden).pack().spanned(span)); - seq.push(SpaceElem::shared().clone().spanned(span)); - } - } - - // Length => indent with some fixed spacing per level - Some(Smart::Custom(OutlineIndent::Rel(length))) => { - seq.push( - HElem::new(Spacing::Rel(*length)) - .pack() - .spanned(span) - .repeat(ancestors.len()), - ); - } - - // Function => call function with the current depth and take - // the returned content - Some(Smart::Custom(OutlineIndent::Func(func))) => { - let depth = ancestors.len(); - let LengthOrContent(content) = func - .call(engine, Context::new(None, Some(styles)).track(), [depth])? - .cast() - .at(span)?; - if !content.is_empty() { - seq.push(content); - } - } - }; - - Ok(()) + ) -> SourceResult { + let depth = level.get() - 1; + match self { + Self::Rel(length) => Ok(*length * depth as f64), + Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span), + } } } @@ -366,46 +342,33 @@ cast! { Self::Rel(v) => v.into_value(), Self::Func(v) => v.into_value() }, - v: Rel => OutlineIndent::Rel(v), - v: Func => OutlineIndent::Func(v), + v: Rel => Self::Rel(v), + v: Func => Self::Func(v), } -struct LengthOrContent(Content); +/// Marks an element as being able to be outlined. +pub trait Outlinable: Refable { + /// Whether this element should be included in the outline. + fn outlined(&self) -> bool; -cast! { - LengthOrContent, - v: Rel => Self(HElem::new(Spacing::Rel(v)).pack()), - v: Content => Self(v), + /// The nesting level of this element. + fn level(&self) -> NonZeroUsize { + NonZeroUsize::ONE + } + + /// Constructs the default prefix given the formatted numbering. + fn prefix(&self, numbers: Content) -> Content; + + /// The body of the entry. + fn body(&self) -> Content; } -/// Represents each entry line in an outline, including the reference to the -/// outlined element, its page number, and the filler content between both. +/// Represents an entry line in an outline. /// -/// This element is intended for use with show rules to control the appearance -/// of outlines. To customize an entry's line, you can build it from scratch by -/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the -/// entry. -/// -/// ```example -/// #set heading(numbering: "1.") -/// -/// #show outline.entry.where( -/// level: 1 -/// ): it => { -/// v(12pt, weak: true) -/// strong(it) -/// } -/// -/// #outline(indent: auto) -/// -/// = Introduction -/// = Background -/// == History -/// == State of the Art -/// = Analysis -/// == Setup -/// ``` -#[elem(name = "entry", title = "Outline Entry", Show)] +/// With show-set and show rules on outline entries, you can richly customize +/// the outline's appearance. See the +/// [section on styling the outline]($outline/#styling-the-outline) for details. +#[elem(scope, name = "entry", title = "Outline Entry", Show)] pub struct OutlineEntry { /// The nesting level of this outline entry. Starts at `{1}` for top-level /// entries. @@ -413,90 +376,206 @@ pub struct OutlineEntry { pub level: NonZeroUsize, /// The element this entry refers to. Its location will be available - /// through the [`location`]($content.location) method on content + /// through the [`location`]($content.location) method on the content /// and can be [linked]($link) to. #[required] pub element: Content, - /// The content which is displayed in place of the referred element at its - /// entry in the outline. For a heading, this would be its number followed - /// by the heading's title, for example. - #[required] - pub body: Content, - - /// The content used to fill the space between the element's outline and - /// its page number, as defined by the outline element this entry is - /// located in. When `{none}`, empty space is inserted in that gap instead. + /// Content to fill the space between the title and the page number. Can be + /// set to `{none}` to disable filling. /// - /// Note that, when using show rules to override outline entries, it is - /// recommended to wrap the filling content in a [`box`] with fractional - /// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely - /// as many `-` characters as necessary to fill a particular gap. - #[required] + /// The `fill` will be placed into a fractionally sized box that spans the + /// space between the entry's body and the page number. When using show + /// rules to override outline entries, it is thus recommended to wrap the + /// fill in a [`box`] with fractional width, i.e. + /// `{box(width: 1fr, it.fill}`. + /// + /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful + /// to tweak the visual weight of the fill. + /// + /// ```example + /// #set outline.entry(fill: line(length: 100%)) + /// #outline() + /// + /// = A New Beginning + /// ``` + #[borrowed] + #[default(Some( + RepeatElem::new(TextElem::packed(".")) + .with_gap(Em::new(0.15).into()) + .pack() + ))] pub fill: Option, - /// The page number of the element this entry links to, formatted with the - /// numbering set for the referenced page. - #[required] - pub page: Content, -} - -impl OutlineEntry { - /// Generates an OutlineEntry from the given element, if possible (errors if - /// the element does not implement `Outlinable`). If the element should not - /// be outlined (e.g. heading with 'outlined: false'), does not generate an - /// entry instance (returns `Ok(None)`). - fn from_outlinable( - engine: &mut Engine, - span: Span, - elem: Content, - fill: Option, - styles: StyleChain, - ) -> SourceResult> { - let Some(outlinable) = elem.with::() else { - bail!(span, "cannot outline {}", elem.func().name()); - }; - - let Some(body) = outlinable.outline(engine, styles)? else { - return Ok(None); - }; - - let location = elem.location().unwrap(); - let page_numbering = engine - .introspector - .page_numbering(location) - .cloned() - .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); - - let page = Counter::new(CounterKey::Page).display_at_loc( - engine, - location, - styles, - &page_numbering, - )?; - - Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) - } + /// Lets outline entries access the outline they are part of. This is a bit + /// of a hack and should be superseded by a proper ancestry mechanism. + #[ghost] + #[internal] + pub parent: Option>, } impl Show for Packed { #[typst_macros::time(name = "outline.entry", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut seq = vec![]; - let elem = self.element(); + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); + let context = Context::new(None, Some(styles)); + let context = context.track(); - // In case a user constructs an outline entry with an arbitrary element. - let Some(location) = elem.location() else { - if elem.can::() && elem.can::() { - bail!( - self.span(), "{} must have a location", elem.func().name(); - hint: "try using a query or a show rule to customize the outline.entry instead", - ) - } else { - bail!(self.span(), "cannot outline {}", elem.func().name()) + let prefix = self.prefix(engine, context, span)?; + let inner = self.inner(engine, context, span)?; + let block = if self.element.is::() { + let body = prefix.unwrap_or_default() + inner; + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .pack() + .spanned(span) + } else { + self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())? + }; + + let loc = self.element_location().at(span)?; + Ok(block.linked(Destination::Location(loc))) + } +} + +#[scope] +impl OutlineEntry { + /// A helper function for producing an indented entry layout: Lays out a + /// prefix and the rest of the entry in an indent-aware way. + /// + /// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the + /// inner content of all entries at level `N` is aligned with the prefix of + /// all entries at level `N + 1`, leaving at least `gap` space between the + /// prefix and inner parts. Furthermore, the `inner` contents of all entries + /// at the same level are aligned. + /// + /// If the outline's indent is a fixed value or a function, the prefixes are + /// indented, but the inner contents are simply inset from the prefix by the + /// specified `gap`, rather than aligning outline-wide. + #[func(contextual)] + pub fn indented( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + /// The `prefix` is aligned with the `inner` content of entries that + /// have level one less. + /// + /// In the default show rule, this is just `it.prefix()`, but it can be + /// freely customized. + prefix: Option, + /// The formatted inner content of the entry. + /// + /// In the default show rule, this is just `it.inner()`, but it can be + /// freely customized. + inner: Content, + /// The gap between the prefix and the inner content. + #[named] + #[default(Em::new(0.5).into())] + gap: Length, + ) -> SourceResult { + let styles = context.styles().at(span)?; + let outline = Self::parent_in(styles) + .ok_or("must be called within the context of an outline") + .at(span)?; + let outline_loc = outline.location().unwrap(); + + let prefix_width = prefix + .as_ref() + .map(|prefix| measure_prefix(engine, prefix, outline_loc, styles)) + .transpose()?; + let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles)); + + let indent = outline.indent(styles); + let (base_indent, hanging_indent) = match &indent { + Smart::Auto => compute_auto_indents( + engine.introspector, + outline_loc, + styles, + self.level, + prefix_inset, + ), + Smart::Custom(amount) => { + let base = amount.resolve(engine, context, self.level, span)?; + (base, prefix_inset) } }; + let body = if let ( + Some(prefix), + Some(prefix_width), + Some(prefix_inset), + Some(hanging_indent), + ) = (prefix, prefix_width, prefix_inset, hanging_indent) + { + // Save information about our prefix that other outline entries + // can query for (within `compute_auto_indent`) to align + // themselves). + let mut seq = Vec::with_capacity(5); + if indent.is_auto() { + seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack()); + } + + // Dedent the prefix by the amount of hanging indent and then skip + // ahead so that the inner contents are aligned. + seq.extend([ + HElem::new((-hanging_indent).into()).pack(), + prefix, + HElem::new((hanging_indent - prefix_width).into()).pack(), + inner, + ]); + Content::sequence(seq) + } else { + inner + }; + + let inset = Sides::default().with( + TextElem::dir_in(styles).start(), + Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())), + ); + + Ok(BlockElem::new() + .with_inset(inset) + .with_body(Some(BlockBody::Content(body))) + .pack() + .spanned(span)) + } + + /// Formats the element's numbering (if any). + /// + /// This also appends the element's supplement in case of figures or + /// equations. For instance, it would output `1.1` for a heading, but + /// `Figure 1` for a figure, as is usual for outlines. + #[func(contextual)] + pub fn prefix( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult> { + let outlinable = self.outlinable().at(span)?; + let Some(numbering) = outlinable.numbering() else { return Ok(None) }; + let loc = self.element_location().at(span)?; + let styles = context.styles().at(span)?; + let numbers = + outlinable.counter().display_at_loc(engine, loc, styles, numbering)?; + Ok(Some(outlinable.prefix(numbers))) + } + + /// Creates the default inner content of the entry. + /// + /// This includes the body, the fill, and page number. + #[func(contextual)] + pub fn inner( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult { + let styles = context.styles().at(span)?; + + let mut seq = vec![]; + // Isolate the entry body in RTL because the page number is typically // LTR. I'm not sure whether LTR should conceptually also be isolated, // but in any case we don't do it for now because the text shaping @@ -512,32 +591,174 @@ impl Show for Packed { seq.push(TextElem::packed("\u{202B}")); } - seq.push(self.body().clone().linked(Destination::Location(location))); + seq.push(self.body().at(span)?); if rtl { // "Pop Directional Formatting" seq.push(TextElem::packed("\u{202C}")); } - // Add filler symbols between the section name and page number. - if let Some(filler) = self.fill() { + // Add the filler between the section name and page number. + if let Some(filler) = self.fill(styles) { seq.push(SpaceElem::shared().clone()); seq.push( BoxElem::new() .with_body(Some(filler.clone())) .with_width(Fr::one().into()) .pack() - .spanned(self.span()), + .spanned(span), ); seq.push(SpaceElem::shared().clone()); } else { - seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span())); + seq.push(HElem::new(Fr::one().into()).pack().spanned(span)); } - // Add the page number. - let page = self.page().clone().linked(Destination::Location(location)); - seq.push(page); + // Add the page number. The word joiner in front ensures that the page + // number doesn't stand alone in its line. + seq.push(TextElem::packed("\u{2060}")); + seq.push(self.page(engine, context, span)?); Ok(Content::sequence(seq)) } + + /// The content which is displayed in place of the referred element at its + /// entry in the outline. For a heading, this is its + /// [`body`]($heading.body); for a figure a caption and for equations, it is + /// empty. + #[func] + pub fn body(&self) -> StrResult { + Ok(self.outlinable()?.body()) + } + + /// The page number of this entry's element, formatted with the numbering + /// set for the referenced page. + #[func(contextual)] + pub fn page( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult { + let loc = self.element_location().at(span)?; + let styles = context.styles().at(span)?; + let numbering = engine + .introspector + .page_numbering(loc) + .cloned() + .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); + Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering) + } +} + +impl OutlineEntry { + fn outlinable(&self) -> StrResult<&dyn Outlinable> { + self.element + .with::() + .ok_or_else(|| error!("cannot outline {}", self.element.func().name())) + } + + fn element_location(&self) -> HintedStrResult { + let elem = &self.element; + elem.location().ok_or_else(|| { + if elem.can::() && elem.can::() { + error!( + "{} must have a location", elem.func().name(); + hint: "try using a show rule to customize the outline.entry instead", + ) + } else { + error!("cannot outline {}", elem.func().name()) + } + }) + } +} + +cast! { + OutlineEntry, + v: Content => v.unpack::().map_err(|_| "expected outline entry")? +} + +/// Measures the width of a prefix. +fn measure_prefix( + engine: &mut Engine, + prefix: &Content, + loc: Location, + styles: StyleChain, +) -> SourceResult { + let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); + let link = LocatorLink::measure(loc); + Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)? + .width()) +} + +/// Compute the base indent and hanging indent for an auto-indented outline +/// entry of the given level, with the given prefix inset. +fn compute_auto_indents( + introspector: Tracked, + outline_loc: Location, + styles: StyleChain, + level: NonZeroUsize, + prefix_inset: Option, +) -> (Rel, Option) { + let indents = query_prefix_widths(introspector, outline_loc); + + let fallback = Em::new(1.2).resolve(styles); + let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback); + + let last = level.get() - 1; + let base: Abs = (0..last).map(get).sum(); + let hang = prefix_inset.map(|p| p.max(get(last))); + + (base.into(), hang) +} + +/// Determines the maximum prefix inset (prefix width + gap) at each outline +/// level, for the outline with the given `loc`. Levels for which there is no +/// information available yield `None`. +#[comemo::memoize] +fn query_prefix_widths( + introspector: Tracked, + outline_loc: Location, +) -> SmallVec<[Option; 4]> { + let mut widths = SmallVec::<[Option; 4]>::new(); + let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc)); + for elem in &elems { + let info = elem.to_packed::().unwrap(); + let level = info.level.get(); + if widths.len() < level { + widths.resize(level, None); + } + widths[level - 1].get_or_insert(info.inset).set_max(info.inset); + } + widths +} + +/// Helper type for introspection-based prefix alignment. +#[elem(Construct, Locatable, Show)] +struct PrefixInfo { + /// The location of the outline this prefix is part of. This is used to + /// scope prefix computations to a specific outline. + #[required] + key: Location, + + /// The level of this prefix's entry. + #[required] + #[internal] + level: NonZeroUsize, + + /// The width of the prefix, including the gap. + #[required] + #[internal] + inset: Abs, +} + +impl Construct for PrefixInfo { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { + bail!(args.span, "cannot be constructed manually"); + } +} + +impl Show for Packed { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(Content::empty()) + } } diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs index 8b82abdf7..cf31b5195 100644 --- a/crates/typst-library/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -1,22 +1,78 @@ -use std::fmt::{self, Debug, Formatter}; - use typst_utils::singleton; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, - StyleVec, Unlabellable, + cast, dict, elem, scope, Args, Cast, Construct, Content, Dict, NativeElement, Packed, + Smart, Unlabellable, Value, }; use crate::introspection::{Count, CounterUpdate, Locatable}; use crate::layout::{Em, HAlignment, Length, OuterHAlignment}; use crate::model::Numbering; -/// Arranges text, spacing and inline-level elements into a paragraph. +/// A logical subdivison of textual content. /// -/// Although this function is primarily used in set rules to affect paragraph -/// properties, it can also be used to explicitly render its argument onto a -/// paragraph of its own. +/// Typst automatically collects _inline-level_ elements into paragraphs. +/// Inline-level elements include [text], [horizontal spacing]($h), +/// [boxes]($box), and [inline equations]($math.equation). +/// +/// To separate paragraphs, use a blank line (or an explicit [`parbreak`]). +/// Paragraphs are also automatically interrupted by any block-level element +/// (like [`block`], [`place`], or anything that shows itself as one of these). +/// +/// The `par` element is primarily used in set rules to affect paragraph +/// properties, but it can also be used to explicitly display its argument as a +/// paragraph of its own. Then, the paragraph's body may not contain any +/// block-level content. +/// +/// # Boxes and blocks +/// As explained above, usually paragraphs only contain inline-level content. +/// However, you can integrate any kind of block-level content into a paragraph +/// by wrapping it in a [`box`]. +/// +/// Conversely, you can separate inline-level content from a paragraph by +/// wrapping it in a [`block`]. In this case, it will not become part of any +/// paragraph at all. Read the following section for an explanation of why that +/// matters and how it differs from just adding paragraph breaks around the +/// content. +/// +/// # What becomes a paragraph? +/// When you add inline-level content to your document, Typst will automatically +/// wrap it in paragraphs. However, a typical document also contains some text +/// that is not semantically part of a paragraph, for example in a heading or +/// caption. +/// +/// The rules for when Typst wraps inline-level content in a paragraph are as +/// follows: +/// +/// - All text at the root of a document is wrapped in paragraphs. +/// +/// - Text in a container (like a `block`) is only wrapped in a paragraph if the +/// container holds any block-level content. If all of the contents are +/// inline-level, no paragraph is created. +/// +/// In the laid-out document, it's not immediately visible whether text became +/// part of a paragraph. However, it is still important for various reasons: +/// +/// - Certain paragraph styling like `first-line-indent` will only apply to +/// proper paragraphs, not any text. Similarly, `par` show rules of course +/// only trigger on paragraphs. +/// +/// - A proper distinction between paragraphs and other text helps people who +/// rely on assistive technologies (such as screen readers) navigate and +/// understand the document properly. Currently, this only applies to HTML +/// export since Typst does not yet output accessible PDFs, but support for +/// this is planned for the near future. +/// +/// - HTML export will generate a `

` tag only for paragraphs. +/// +/// When creating custom reusable components, you can and should take charge +/// over whether Typst creates paragraphs. By wrapping text in a [`block`] +/// instead of just adding paragraph breaks around it, you can force the absence +/// of a paragraph. Conversely, by adding a [`parbreak`] after some content in a +/// container, you can force it to become a paragraph even if it's just one +/// word. This is, for example, what [non-`tight`]($list.tight) lists do to +/// force their items to become paragraphs. /// /// # Example /// ```example @@ -37,7 +93,7 @@ use crate::model::Numbering; /// let $a$ be the smallest of the /// three integers. Then, we ... /// ``` -#[elem(scope, title = "Paragraph", Debug, Construct)] +#[elem(scope, title = "Paragraph")] pub struct ParElem { /// The spacing between lines. /// @@ -53,7 +109,6 @@ pub struct ParElem { /// distribution of the top- and bottom-edge values affects the bounds of /// the first and last line. #[resolve] - #[ghost] #[default(Em::new(0.65).into())] pub leading: Length, @@ -68,7 +123,6 @@ pub struct ParElem { /// takes precedence over the paragraph spacing. Headings, for instance, /// reduce the spacing below them by default for a better look. #[resolve] - #[ghost] #[default(Em::new(1.2).into())] pub spacing: Length, @@ -81,7 +135,6 @@ pub struct ParElem { /// Note that the current [alignment]($align.alignment) still has an effect /// on the placement of the last line except if it ends with a /// [justified line break]($linebreak.justify). - #[ghost] #[default(false)] pub justify: bool, @@ -106,35 +159,66 @@ pub struct ParElem { /// challenging to break in a visually /// pleasing way. /// ``` - #[ghost] pub linebreaks: Smart, /// The indent the first line of a paragraph should have. /// - /// Only the first line of a consecutive paragraph will be indented (not - /// the first one in a block or on the page). + /// By default, only the first line of a consecutive paragraph will be + /// indented (not the first one in the document or container, and not + /// paragraphs immediately following other block-level elements). + /// + /// If you want to indent all paragraphs instead, you can pass a dictionary + /// containing the `amount` of indent as a length and the pair + /// `{all: true}`. When `all` is omitted from the dictionary, it defaults to + /// `{false}`. /// /// By typographic convention, paragraph breaks are indicated either by some - /// space between paragraphs or by indented first lines. Consider reducing - /// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading) - /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`). - #[ghost] - pub first_line_indent: Length, + /// space between paragraphs or by indented first lines. Consider + /// - reducing the [paragraph `spacing`]($par.spacing) to the + /// [`leading`]($par.leading) using `{set par(spacing: 0.65em)}` + /// - increasing the [block `spacing`]($block.spacing) (which inherits the + /// paragraph spacing by default) to the original paragraph spacing using + /// `{set block(spacing: 1.2em)}` + /// + /// ```example + /// #set block(spacing: 1.2em) + /// #set par( + /// first-line-indent: 1.5em, + /// spacing: 0.65em, + /// ) + /// + /// The first paragraph is not affected + /// by the indent. + /// + /// But the second paragraph is. + /// + /// #line(length: 100%) + /// + /// #set par(first-line-indent: ( + /// amount: 1.5em, + /// all: true, + /// )) + /// + /// Now all paragraphs are affected + /// by the first line indent. + /// + /// Even the first one. + /// ``` + pub first_line_indent: FirstLineIndent, - /// The indent all but the first line of a paragraph should have. - #[ghost] + /// The indent that all but the first line of a paragraph should have. + /// + /// ```example + /// #set par(hanging-indent: 1em) + /// + /// #lorem(15) + /// ``` #[resolve] pub hanging_indent: Length, /// The contents of the paragraph. - #[external] #[required] pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: StyleVec, } #[scope] @@ -143,28 +227,6 @@ impl ParElem { type ParLine; } -impl Construct for ParElem { - fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult { - // The paragraph constructor is special: It doesn't create a paragraph - // element. Instead, it just ensures that the passed content lives in a - // separate paragraph and styles it. - let styles = Self::set(engine, args)?; - let body = args.expect::("body")?; - Ok(Content::sequence([ - ParbreakElem::shared().clone(), - body.styled_with_map(styles), - ParbreakElem::shared().clone(), - ])) - } -} - -impl Debug for ParElem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Par ")?; - self.children.fmt(f) - } -} - /// How to determine line breaks in a paragraph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum Linebreaks { @@ -177,6 +239,36 @@ pub enum Linebreaks { Optimized, } +/// Configuration for first line indent. +#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] +pub struct FirstLineIndent { + /// The amount of indent. + pub amount: Length, + /// Whether to indent all paragraphs, not just consecutive ones. + pub all: bool, +} + +cast! { + FirstLineIndent, + self => Value::Dict(self.into()), + amount: Length => Self { amount, all: false }, + mut dict: Dict => { + let amount = dict.take("amount")?.cast()?; + let all = dict.take("all").ok().map(|v| v.cast()).transpose()?.unwrap_or(false); + dict.finish(&["amount", "all"])?; + Self { amount, all } + }, +} + +impl From for Dict { + fn from(indent: FirstLineIndent) -> Self { + dict! { + "amount" => indent.amount, + "all" => indent.all, + } + } +} + /// A paragraph break. /// /// This starts a new paragraph. Especially useful when used within code like diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 110825f13..919ab12c7 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -2,13 +2,14 @@ use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, - StyleChain, Styles, + StyleChain, Styles, TargetElem, }; +use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Locatable; use crate::layout::{ Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, }; -use crate::model::{CitationForm, CiteElem}; +use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget}; use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// Displays a quote alongside an optional attribution. @@ -156,8 +157,9 @@ cast! { impl Show for Packed { #[typst_macros::time(name = "quote", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); let block = self.block(styles); + let html = TargetElem::target_in(styles).is_html(); if self.quotes(styles) == Smart::Custom(true) || !block { let quotes = SmartQuotes::get( @@ -171,50 +173,69 @@ impl Show for Packed { let Depth(depth) = QuoteElem::depth_in(styles); let double = depth % 2 == 0; - // Add zero-width weak spacing to make the quotes "sticky". - let hole = HElem::hole().pack(); + if !html { + // Add zero-width weak spacing to make the quotes "sticky". + let hole = HElem::hole().pack(); + realized = Content::sequence([hole.clone(), realized, hole]); + } realized = Content::sequence([ TextElem::packed(quotes.open(double)), - hole.clone(), realized, - hole, TextElem::packed(quotes.close(double)), ]) .styled(QuoteElem::set_depth(Depth(1))); } + let attribution = self.attribution(styles); + if block { - realized = BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(self.span()); - - if let Some(attribution) = self.attribution(styles).as_ref() { - let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()]; - - match attribution { - Attribution::Content(content) => { - seq.push(content.clone()); - } - Attribution::Label(label) => { - seq.push( - CiteElem::new(*label) - .with_form(Some(CitationForm::Prose)) - .pack() - .spanned(self.span()), - ); + realized = if html { + let mut elem = HtmlElem::new(tag::blockquote).with_body(Some(realized)); + if let Some(Attribution::Content(attribution)) = attribution { + if let Some(link) = attribution.to_packed::() { + if let LinkTarget::Dest(Destination::Url(url)) = &link.dest { + elem = elem.with_attr(attr::cite, url.clone().into_inner()); + } } } + elem.pack() + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack() + } + .spanned(self.span()); - // Use v(0.9em, weak: true) bring the attribution closer to the - // quote. - let gap = Spacing::Rel(Em::new(0.9).into()); - let v = VElem::new(gap).with_weak(true).pack(); - realized += v + Content::sequence(seq).aligned(Alignment::END); + if let Some(attribution) = attribution.as_ref() { + let attribution = match attribution { + Attribution::Content(content) => content.clone(), + Attribution::Label(label) => CiteElem::new(*label) + .with_form(Some(CitationForm::Prose)) + .pack() + .spanned(self.span()), + }; + let attribution = Content::sequence([ + TextElem::packed('—'), + SpaceElem::shared().clone(), + attribution, + ]); + + if html { + realized += attribution; + } else { + // Bring the attribution a bit closer to the quote. + let gap = Spacing::Rel(Em::new(0.9).into()); + let v = VElem::new(gap).with_weak(true).pack(); + realized += v; + realized += BlockElem::new() + .with_body(Some(BlockBody::Content(attribution))) + .pack() + .aligned(Alignment::END); + } } - realized = PadElem::new(realized).pack(); - } else if let Some(Attribution::Label(label)) = self.attribution(styles) { + if !html { + realized = PadElem::new(realized).pack(); + } + } else if let Some(Attribution::Label(label)) = attribution { realized += SpaceElem::shared().clone() + CiteElem::new(*label).pack().spanned(self.span()); } diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 96aa2117d..316617688 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -182,9 +182,8 @@ impl Synthesize for Packed { elem.push_citation(Some(citation)); elem.push_element(None); - let target = *elem.target(); - if !BibliographyElem::has(engine, target) { - if let Ok(found) = engine.introspector.query_label(target).cloned() { + if !BibliographyElem::has(engine, elem.target) { + if let Ok(found) = engine.introspector.query_label(elem.target).cloned() { elem.push_element(Some(found)); return Ok(()); } @@ -197,8 +196,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "ref", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let target = *self.target(); - let elem = engine.introspector.query_label(target); + let elem = engine.introspector.query_label(self.target); let span = self.span(); let form = self.form(styles); @@ -229,7 +227,7 @@ impl Show for Packed { } // RefForm::Normal - if BibliographyElem::has(engine, target) { + if BibliographyElem::has(engine, self.target) { if elem.is_ok() { bail!(span, "label occurs in the document and its bibliography"); } @@ -240,7 +238,7 @@ impl Show for Packed { let elem = elem.at(span)?; if let Some(footnote) = elem.to_packed::() { - return Ok(footnote.into_ref(target).pack().spanned(span)); + return Ok(footnote.into_ref(self.target).pack().spanned(span)); } let elem = elem.clone(); @@ -319,7 +317,7 @@ fn to_citation( engine: &mut Engine, styles: StyleChain, ) -> SourceResult> { - let mut elem = Packed::new(CiteElem::new(*reference.target()).with_supplement( + let mut elem = Packed::new(CiteElem::new(reference.target).with_supplement( match reference.supplement(styles).clone() { Smart::Custom(Some(Supplement::Content(content))) => Some(content), _ => None, diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 7dfaf45d7..6f4461bd4 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -7,7 +7,11 @@ use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, + TargetElem, }; +use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; +use crate::introspection::Locator; +use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use crate::layout::{ show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, @@ -258,11 +262,65 @@ impl TableElem { type TableFooter; } +fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { + let cell = cell.body.clone(); + let Some(cell) = cell.to_packed::() else { return cell }; + let mut attrs = HtmlAttrs::default(); + let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string()); + if let Some(colspan) = span(cell.colspan(styles)) { + attrs.push(attr::colspan, colspan); + } + if let Some(rowspan) = span(cell.rowspan(styles)) { + attrs.push(attr::rowspan, rowspan); + } + HtmlElem::new(tag) + .with_body(Some(cell.body.clone())) + .with_attrs(attrs) + .pack() + .spanned(cell.span()) +} + +fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { + let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack(); + let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect(); + + let tr = |tag, row: &[Entry]| { + let row = row + .iter() + .flat_map(|entry| entry.as_cell()) + .map(|cell| show_cell_html(tag, cell, styles)); + elem(tag::tr, Content::sequence(row)) + }; + + let footer = grid.footer.map(|ft| { + let rows = rows.drain(ft.unwrap().start..); + elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) + }); + let header = grid.header.map(|hd| { + let rows = rows.drain(..hd.unwrap().end); + elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row)))) + }); + + let mut body = Content::sequence(rows.into_iter().map(|row| tr(tag::td, row))); + if header.is_some() || footer.is_some() { + body = elem(tag::tbody, body); + } + + let content = header.into_iter().chain(core::iter::once(body)).chain(footer); + elem(tag::table, Content::sequence(content)) +} + impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table) - .pack() - .spanned(self.span())) + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + Ok(if TargetElem::target_in(styles).is_html() { + // TODO: This is a hack, it is not clear whether the locator is actually used by HTML. + // How can we find out whether locator is actually used? + let locator = Locator::root(); + show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles) + } else { + BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack() + } + .spanned(self.span())) } } @@ -706,7 +764,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 13aa8c6d5..e197ff318 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -1,4 +1,4 @@ -use typst_utils::Numeric; +use typst_utils::{Get, Numeric}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; @@ -7,8 +7,8 @@ use crate::foundations::{ Styles, TargetElem, }; use crate::html::{tag, HtmlElem}; -use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::{ListItemLike, ListLike, ParElem}; +use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; +use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; use crate::text::TextElem; /// A list of terms and their descriptions. @@ -105,6 +105,11 @@ pub struct TermsElem { /// ``` #[variadic] pub children: Vec>, + + /// Whether we are currently within a term list. + #[internal] + #[ghost] + pub within: bool, } #[scope] @@ -116,17 +121,25 @@ impl TermsElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let span = self.span(); + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { return Ok(HtmlElem::new(tag::dl) .with_body(Some(Content::sequence(self.children.iter().flat_map( |item| { + // Text in wide term lists shall always turn into paragraphs. + let mut description = item.description.clone(); + if !tight { + description += ParbreakElem::shared(); + } + [ HtmlElem::new(tag::dt) .with_body(Some(item.term.clone())) .pack() .spanned(item.term.span()), HtmlElem::new(tag::dd) - .with_body(Some(item.description.clone())) + .with_body(Some(description)) .pack() .spanned(item.description.span()), ] @@ -139,7 +152,7 @@ impl Show for Packed { let indent = self.indent(styles); let hanging_indent = self.hanging_indent(styles); let gutter = self.spacing(styles).unwrap_or_else(|| { - if self.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -151,29 +164,31 @@ impl Show for Packed { .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); let mut children = vec![]; - for child in self.children().iter() { + for child in self.children.iter() { let mut seq = vec![]; seq.extend(unpad.clone()); - seq.push(child.term().clone().strong()); + seq.push(child.term.clone().strong()); seq.push((*separator).clone()); - seq.push(child.description().clone()); + seq.push(child.description.clone()); + + // Text in wide term lists shall always turn into paragraphs. + if !tight { + seq.push(ParbreakElem::shared().clone()); + } + children.push(StackChild::Block(Content::sequence(seq))); } - let mut padding = Sides::default(); - if TextElem::dir_in(styles) == Dir::LTR { - padding.left = pad.into(); - } else { - padding.right = pad.into(); - } + let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into()); let mut realized = StackElem::new(children) .with_spacing(Some(gutter.into())) .pack() .spanned(span) - .padded(padding); + .padded(padding) + .styled(TermsElem::set_within(true)); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()) .with_weak(true) diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs new file mode 100644 index 000000000..f902e7f14 --- /dev/null +++ b/crates/typst-library/src/pdf/embed.rs @@ -0,0 +1,105 @@ +use ecow::EcoString; +use typst_library::foundations::Target; +use typst_syntax::Spanned; + +use crate::diag::{warning, At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem, +}; +use crate::introspection::Locatable; +use crate::World; + +/// A file that will be embedded into the output PDF. +/// +/// This can be used to distribute additional files that are related to the PDF +/// within it. PDF readers will display the files in a file listing. +/// +/// Some international standards use this mechanism to embed machine-readable +/// data (e.g., ZUGFeRD/Factur-X for invoices) that mirrors the visual content +/// of the PDF. +/// +/// # Example +/// ```typ +/// #pdf.embed( +/// "experiment.csv", +/// relationship: "supplement", +/// mime-type: "text/csv", +/// description: "Raw Oxygen readings from the Arctic experiment", +/// ) +/// ``` +/// +/// # Notes +/// - This element is ignored if exporting to a format other than PDF. +/// - File embeddings are not currently supported for PDF/A-2, even if the +/// embedded file conforms to PDF/A-1 or PDF/A-2. +#[elem(Show, Locatable)] +pub struct EmbedElem { + /// The [path]($syntax/#paths) of the file to be embedded. + /// + /// Must always be specified, but is only read from if no data is provided + /// in the following argument. + #[required] + #[parse( + let Spanned { v: path, span } = + args.expect::>("path")?; + let id = span.resolve_path(&path).at(span)?; + // The derived part is the project-relative resolved path. + let resolved = id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into(); + Derived::new(path.clone(), resolved) + )] + #[borrowed] + pub path: Derived, + + /// Raw file data, optionally. + /// + /// If omitted, the data is read from the specified path. + #[positional] + // Not actually required as an argument, but always present as a field. + // We can't distinguish between the two at the moment. + #[required] + #[parse( + match args.find::()? { + Some(data) => data, + None => engine.world.file(id).at(span)?, + } + )] + pub data: Bytes, + + /// The relationship of the embedded file to the document. + /// + /// Ignored if export doesn't target PDF/A-3. + pub relationship: Option, + + /// The MIME type of the embedded file. + #[borrowed] + pub mime_type: Option, + + /// A description for the embedded file. + #[borrowed] + pub description: Option, +} + +impl Show for Packed { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles) == Target::Html { + engine + .sink + .warn(warning!(self.span(), "embed was ignored during HTML export")); + } + Ok(Content::empty()) + } +} + +/// The relationship of an embedded file with the document. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum EmbeddedFileRelationship { + /// The PDF document was created from the source file. + Source, + /// The file was used to derive a visual presentation in the PDF. + Data, + /// An alternative representation of the document. + Alternative, + /// Additional resources for the document. + Supplement, +} diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs new file mode 100644 index 000000000..786a36372 --- /dev/null +++ b/crates/typst-library/src/pdf/mod.rs @@ -0,0 +1,15 @@ +//! PDF-specific functionality. + +mod embed; + +pub use self::embed::*; + +use crate::foundations::{Module, Scope}; + +/// Hook up all `pdf` definitions. +pub fn module() -> Module { + let mut pdf = Scope::deduplicating(); + pdf.start_category(crate::Category::Pdf); + pdf.define_elem::(); + Module::new("pdf", pdf) +} diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index a11268604..b283052a4 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -10,8 +10,7 @@ use typst_utils::LazyHash; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, StyleVec, - Styles, Value, + Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::layout::{ @@ -104,26 +103,6 @@ routines! { region: Region, ) -> SourceResult - /// Lays out inline content. - fn layout_inline( - engine: &mut Engine, - children: &StyleVec, - locator: Locator, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult - - /// Lays out a [`BoxElem`]. - fn layout_box( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult - /// Lays out a [`ListElem`]. fn layout_list( elem: &Packed, @@ -348,17 +327,62 @@ pub enum RealizationKind<'a> { /// This the root realization for layout. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. LayoutDocument(&'a mut DocumentInfo), - /// A nested realization in a container (e.g. a `block`). - LayoutFragment, + /// A nested realization in a container (e.g. a `block`). Requires a mutable + /// reference to an enum that will be set to `FragmentKind::Inline` if the + /// fragment's content was fully inline. + LayoutFragment(&'a mut FragmentKind), + /// A nested realization in a paragraph (i.e. a `par`) + LayoutPar, /// This the root realization for HTML. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. HtmlDocument(&'a mut DocumentInfo), - /// A nested realization in a container (e.g. a `block`). - HtmlFragment, + /// A nested realization in a container (e.g. a `block`). Requires a mutable + /// reference to an enum that will be set to `FragmentKind::Inline` if the + /// fragment's content was fully inline. + HtmlFragment(&'a mut FragmentKind), /// A realization within math. Math, } +impl RealizationKind<'_> { + /// It this a realization for HTML export? + pub fn is_html(&self) -> bool { + matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_)) + } + + /// It this a realization for a container? + pub fn is_fragment(&self) -> bool { + matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_)) + } + + /// If this is a document-level realization, accesses the document info. + pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> { + match self { + Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info), + _ => None, + } + } + + /// If this is a container-level realization, accesses the fragment kind. + pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> { + match self { + Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind), + _ => None, + } + } +} + +/// The kind of fragment output that realization produced. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum FragmentKind { + /// The fragment's contents were fully inline, and as a result, the output + /// elements are too. + Inline, + /// The fragment contained non-inline content, so inline content was forced + /// into paragraphs, and as a result, the output elements are not inline. + Block, +} + /// Temporary storage arenas for lifetime extension during realization. /// /// Must be kept live while the content returned from realization is processed. diff --git a/crates/typst-library/src/symbols.rs b/crates/typst-library/src/symbols.rs index 1617d3aa8..0588ace95 100644 --- a/crates/typst-library/src/symbols.rs +++ b/crates/typst-library/src/symbols.rs @@ -1,14 +1,32 @@ //! Modifiable symbols. -use crate::foundations::{category, Category, Module, Scope, Symbol, Value}; +use crate::foundations::{Module, Scope, Symbol, Value}; -/// These two modules give names to symbols and emoji to make them easy to -/// insert with a normal keyboard. Alternatively, you can also always directly -/// enter Unicode symbols into your text and formulas. In addition to the -/// symbols listed below, math mode defines `dif` and `Dif`. These are not -/// normal symbol values because they also affect spacing and font style. -#[category] -pub static SYMBOLS: Category; +/// Hook up all `symbol` definitions. +pub(super) fn define(global: &mut Scope) { + global.start_category(crate::Category::Symbols); + extend_scope_from_codex_module(global, codex::ROOT); + global.reset_category(); +} + +/// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. +pub(super) fn define_math(math: &mut Scope) { + extend_scope_from_codex_module(math, codex::SYM); +} + +fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { + for (name, binding) in module.iter() { + let value = match binding.def { + codex::Def::Symbol(s) => Value::Symbol(s.into()), + codex::Def::Module(m) => Value::Module(Module::new(name, m.into())), + }; + + let scope_binding = scope.define(name, value); + if let Some(message) = binding.deprecation { + scope_binding.deprecated(message); + } + } +} impl From for Scope { fn from(module: codex::Module) -> Scope { @@ -26,24 +44,3 @@ impl From for Symbol { } } } - -fn extend_scope_from_codex_module(scope: &mut Scope, module: codex::Module) { - for (name, definition) in module.iter() { - let value = match definition { - codex::Def::Symbol(s) => Value::Symbol(s.into()), - codex::Def::Module(m) => Value::Module(Module::new(name, m.into())), - }; - scope.define(name, value); - } -} - -/// Hook up all `symbol` definitions. -pub(super) fn define(global: &mut Scope) { - global.category(SYMBOLS); - extend_scope_from_codex_module(global, codex::ROOT); -} - -/// Hook up all math `symbol` definitions, i.e., elements of the `sym` module. -pub(super) fn define_math(math: &mut Scope) { - extend_scope_from_codex_module(math, codex::SYM); -} diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 5da7ecec4..485d0edcf 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -81,7 +81,7 @@ pub struct UnderlineElem { impl Show for Packed { #[typst_macros::time(name = "underline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Underline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -173,7 +173,7 @@ pub struct OverlineElem { impl Show for Packed { #[typst_macros::time(name = "overline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Overline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -250,7 +250,7 @@ pub struct StrikeElem { impl Show for Packed { #[typst_macros::time(name = "strike", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { // Note that we do not support evade option for strikethrough. line: DecoLine::Strikethrough { stroke: self.stroke(styles).unwrap_or_default(), @@ -345,7 +345,7 @@ pub struct HighlightElem { impl Show for Packed { #[typst_macros::time(name = "highlight", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Highlight { fill: self.fill(styles), stroke: self diff --git a/crates/typst-library/src/text/font/book.rs b/crates/typst-library/src/text/font/book.rs index 23e27f64c..9f8acce87 100644 --- a/crates/typst-library/src/text/font/book.rs +++ b/crates/typst-library/src/text/font/book.rs @@ -160,7 +160,7 @@ impl FontBook { current.variant.weight.distance(variant.weight), ); - if best_key.map_or(true, |b| key < b) { + if best_key.is_none_or(|b| key < b) { best = Some(id); best_key = Some(key); } diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index 08f6fe0a3..0a7b13c97 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -7,9 +7,12 @@ use typst_syntax::Span; use usvg::tiny_skia_path; use xmlwriter::XmlWriter; +use crate::foundations::Bytes; use crate::layout::{Abs, Frame, FrameItem, Point, Size}; use crate::text::{Font, Glyph}; -use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat}; +use crate::visualize::{ + ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, SvgImage, +}; /// Whether this glyph should be rendered via simple outlining instead of via /// `glyph_frame`. @@ -101,8 +104,8 @@ fn draw_raster_glyph( upem: Abs, raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { - let image = - Image::new(raster_image.data.into(), RasterFormat::Png.into(), None).ok()?; + let data = Bytes::new(raster_image.data.to_vec()); + let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?); // Apple Color emoji doesn't provide offset information (or at least // not in a way ttf-parser understands), so we artificially shift their @@ -173,9 +176,8 @@ fn draw_colr_glyph( ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?; svg.end_element(); - let data = svg.end_document().into_bytes(); - - let image = Image::new(data.into(), VectorFormat::Svg.into(), None).ok()?; + let data = Bytes::from_string(svg.end_document()); + let image = Image::plain(SvgImage::new(data).ok()?); let y_shift = Abs::pt(upem.to_pt() - y_max); let position = Point::new(Abs::pt(x_min), y_shift); @@ -250,9 +252,8 @@ fn draw_svg_glyph( ty = -top, ); - let image = - Image::new(wrapper_svg.into_bytes().into(), VectorFormat::Svg.into(), None) - .ok()?; + let data = Bytes::from_string(wrapper_svg); + let image = Image::plain(SvgImage::new(data).ok()?); let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); let size = Size::new(Abs::pt(width), Abs::pt(height)); diff --git a/crates/typst-library/src/text/font/exceptions.rs b/crates/typst-library/src/text/font/exceptions.rs index 465ec510c..00038c50c 100644 --- a/crates/typst-library/src/text/font/exceptions.rs +++ b/crates/typst-library/src/text/font/exceptions.rs @@ -228,6 +228,8 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { .style(FontStyle::Oblique), "NewCMSans10-Regular" => Exception::new() .family("New Computer Modern Sans"), + "NewCMSansMath-Regular" => Exception::new() + .family("New Computer Modern Sans Math"), "NewCMUncial08-Bold" => Exception::new() .family("New Computer Modern Uncial 08"), "NewCMUncial08-Book" => Exception::new() diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 25ed009e9..3aac15ba5 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -45,25 +45,18 @@ use typst_utils::singleton; use crate::diag::{bail, warning, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, category, dict, elem, Args, Array, Cast, Category, Construct, Content, Dict, - Fold, IntoValue, NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr, - Resolve, Scope, Set, Smart, StyleChain, + cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue, + NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr, Resolve, Scope, Set, + Smart, StyleChain, }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::math::{EquationElem, MathSize}; -use crate::model::ParElem; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; -/// Text styling. -/// -/// The [text function]($text) is of particular interest. -#[category] -pub static TEXT: Category; - /// Hook up all `text` definitions. pub(super) fn define(global: &mut Scope) { - global.category(TEXT); + global.start_category(crate::Category::Text); global.define_elem::(); global.define_elem::(); global.define_elem::(); @@ -78,6 +71,7 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); global.define_func::(); global.define_func::(); + global.reset_category(); } /// Customizes the look and layout of text in a variety of ways. @@ -509,9 +503,8 @@ pub struct TextElem { /// enabling hyphenation can /// improve justification. /// ``` - #[resolve] #[ghost] - pub hyphenate: Hyphenate, + pub hyphenate: Smart, /// The "cost" of various choices when laying out text. A higher cost means /// the layout engine will make the choice less often. Costs are specified @@ -555,6 +548,7 @@ pub struct TextElem { /// #lorem(10) /// ``` #[fold] + #[ghost] pub costs: Costs, /// Whether to apply kerning. @@ -754,11 +748,10 @@ pub struct TextElem { #[ghost] pub case: Option, - /// Whether small capital glyphs should be used. ("smcp") + /// Whether small capital glyphs should be used. ("smcp", "c2sc") #[internal] - #[default(false)] #[ghost] - pub smallcaps: bool, + pub smallcaps: Option, } impl TextElem { @@ -793,7 +786,7 @@ impl Construct for TextElem { impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } @@ -1115,27 +1108,6 @@ impl Resolve for TextDir { } } -/// Whether to hyphenate text. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub Smart); - -cast! { - Hyphenate, - self => self.0.into_value(), - v: Smart => Self(v), -} - -impl Resolve for Hyphenate { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => ParElem::justify_in(styles), - Smart::Custom(v) => v, - } - } -} - /// A set of stylistic sets to enable. #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] pub struct StylisticSets(u32); @@ -1248,8 +1220,11 @@ pub fn features(styles: StyleChain) -> Vec { } // Features that are off by default in Harfbuzz are only added if enabled. - if TextElem::smallcaps_in(styles) { + if let Some(sc) = TextElem::smallcaps_in(styles) { feat(b"smcp", 1); + if sc == Smallcaps::All { + feat(b"c2sc", 1); + } } if TextElem::alternates_in(styles) { @@ -1405,24 +1380,7 @@ pub fn is_default_ignorable(c: char) -> bool { fn check_font_list(engine: &mut Engine, list: &Spanned) { let book = engine.world.book(); for family in &list.v { - let found = book.contains_family(family.as_str()); - if family.as_str() == "linux libertine" { - let mut warning = warning!( - list.span, - "Typst's default font has changed from Linux Libertine to its successor Libertinus Serif"; - hint: "please set the font to `\"Libertinus Serif\"` instead" - ); - - if found { - warning.hint( - "Linux Libertine is available on your system - \ - you can ignore this warning if you are sure you want to use it", - ); - warning.hint("this warning will be removed in Typst 0.13"); - } - - engine.sink.warn(warning); - } else if !found { + if !book.contains_family(family.as_str()) { engine.sink.warn(warning!( list.span, "unknown font family: {}", @@ -1431,3 +1389,13 @@ fn check_font_list(engine: &mut Engine, list: &Spanned) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_text_elem_size() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + } +} diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 10a7cfee1..1ce8bfc61 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -1,36 +1,30 @@ use std::cell::LazyCell; -use std::hash::Hash; use std::ops::Range; use std::sync::{Arc, LazyLock}; +use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; -use syntect::highlighting::{self as synt, Theme}; +use syntect::highlighting as synt; use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; use typst_syntax::{split_newlines, LinkedNode, Span, Spanned}; +use typst_utils::ManuallyHash; use unicode_segmentation::UnicodeSegmentation; use super::Lang; -use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::diag::{At, FileError, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, - PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, Value, + cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, + PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; +use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; -use crate::text::{ - FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, -}; +use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize}; use crate::visualize::Color; use crate::World; -// Shorthand for highlighter closures. -type StyleFn<'a> = - &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; -type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); -type ThemeArgType = Smart>; - /// Raw text with optional syntax highlighting. /// /// Displays the text verbatim and in a monospace font. This is typically used @@ -186,9 +180,15 @@ pub struct RawElem { #[default(HAlignment::Start)] pub align: HAlignment, - /// One or multiple additional syntax definitions to load. The syntax - /// definitions should be in the - /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// Additional syntax definitions to load. The syntax definitions should be + /// in the [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// + /// You can pass any of the following values: + /// + /// - A path string to load a syntax file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the syntax should be decoded. + /// - An array where each item is one the above. /// /// ````example /// #set raw(syntaxes: "SExpressions.sublime-syntax") @@ -201,22 +201,24 @@ pub struct RawElem { /// (* x (factorial (- x 1))))) /// ``` /// ```` - #[parse( - let (syntaxes, syntaxes_data) = parse_syntaxes(engine, args)?; - syntaxes - )] + #[parse(match args.named("syntaxes")? { + Some(sources) => Some(RawSyntax::load(engine.world, sources)?), + None => None, + })] #[fold] - pub syntaxes: SyntaxPaths, + pub syntaxes: Derived, Vec>, - /// The raw file buffers of syntax definition files. - #[internal] - #[parse(syntaxes_data)] - #[fold] - pub syntaxes_data: Vec, - - /// The theme to use for syntax highlighting. Theme files should be in the + /// The theme to use for syntax highlighting. Themes should be in the /// [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). /// + /// You can pass any of the following values: + /// + /// - `{none}`: Disables syntax highlighting. + /// - `{auto}`: Highlights with Typst's default theme. + /// - A path string to load a theme file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the theme should be decoded. + /// /// Applying a theme only affects the color of specifically highlighted /// text. It does not consider the theme's foreground and background /// properties, so that you retain control over the color of raw text. You @@ -224,8 +226,6 @@ pub struct RawElem { /// the background with a [filled block]($block.fill). You could also use /// the [`xml`] function to extract these properties from the theme. /// - /// Additionally, you can set the theme to `{none}` to disable highlighting. - /// /// ````example /// #set raw(theme: "halcyon.tmTheme") /// #show raw: it => block( @@ -240,18 +240,16 @@ pub struct RawElem { /// #let hi = "Hello World" /// ``` /// ```` - #[parse( - let (theme_path, theme_data) = parse_theme(engine, args)?; - theme_path - )] + #[parse(match args.named::>>>("theme")? { + Some(Spanned { v: Smart::Custom(Some(source)), span }) => Some(Smart::Custom( + Some(RawTheme::load(engine.world, Spanned::new(source, span))?) + )), + Some(Spanned { v: Smart::Custom(None), .. }) => Some(Smart::Custom(None)), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] #[borrowed] - pub theme: ThemeArgType, - - /// The raw file buffer of syntax theme file. - #[internal] - #[parse(theme_data.map(Some))] - #[borrowed] - pub theme_data: Option, + pub theme: Smart>>, /// The size for a tab stop in spaces. A tab is replaced with enough spaces to /// align with the next multiple of the size. @@ -315,7 +313,7 @@ impl Packed { #[comemo::memoize] fn highlight(&self, styles: StyleChain) -> Vec> { let elem = self.as_ref(); - let lines = preprocess(elem.text(), styles, self.span()); + let lines = preprocess(&elem.text, styles, self.span()); let count = lines.len() as i64; let lang = elem @@ -325,9 +323,6 @@ impl Packed { .map(|s| s.to_lowercase()) .or(Some("txt".into())); - let extra_syntaxes = LazyCell::new(|| { - load_syntaxes(&elem.syntaxes(styles), &elem.syntaxes_data(styles)).unwrap() - }); let non_highlighted_result = |lines: EcoVec<(EcoString, Span)>| { lines.into_iter().enumerate().map(|(i, (line, line_span))| { Packed::new(RawLine::new( @@ -340,17 +335,13 @@ impl Packed { }) }; - let theme = elem.theme(styles).as_ref().as_ref().map(|theme_path| { - theme_path.as_ref().map(|path| { - load_theme(path, elem.theme_data(styles).as_ref().as_ref().unwrap()) - .unwrap() - }) - }); - let theme: &Theme = match theme { + let syntaxes = LazyCell::new(|| elem.syntaxes(styles)); + let theme: &synt::Theme = match elem.theme(styles) { Smart::Auto => &RAW_THEME, - Smart::Custom(Some(ref theme)) => theme, + Smart::Custom(Some(theme)) => theme.derived.get(), Smart::Custom(None) => return non_highlighted_result(lines).collect(), }; + let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK); let mut seq = vec![]; @@ -391,13 +382,14 @@ impl Packed { ) .highlight(); } else if let Some((syntax_set, syntax)) = lang.and_then(|token| { - RAW_SYNTAXES - .find_syntax_by_token(&token) - .map(|syntax| (&*RAW_SYNTAXES, syntax)) - .or_else(|| { - extra_syntaxes - .find_syntax_by_token(&token) - .map(|syntax| (&**extra_syntaxes, syntax)) + // Prefer user-provided syntaxes over built-in ones. + syntaxes + .derived + .iter() + .map(|syntax| syntax.get()) + .chain(std::iter::once(&*RAW_SYNTAXES)) + .find_map(|set| { + set.find_syntax_by_token(&token).map(|syntax| (set, syntax)) }) }) { let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme); @@ -454,10 +446,14 @@ impl Show for Packed { let mut realized = Content::sequence(seq); if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::pre) - .with_body(Some(realized)) - .pack() - .spanned(self.span())); + return Ok(HtmlElem::new(if self.block(styles) { + tag::pre + } else { + tag::code + }) + .with_body(Some(realized)) + .pack() + .spanned(self.span())); } if self.block(styles) { @@ -478,9 +474,10 @@ impl ShowSet for Packed { let mut out = Styles::new(); out.set(TextElem::set_overhang(false)); out.set(TextElem::set_lang(Lang::ENGLISH)); - out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out.set(TextElem::set_hyphenate(Smart::Custom(false))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); + out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); if self.block(styles) { out.set(ParElem::set_justify(false)); } @@ -496,7 +493,7 @@ impl Figurable for Packed {} impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(&self.text().get()); + text.push_str(&self.text.get()); } } @@ -532,6 +529,89 @@ cast! { v: EcoString => Self::Text(v), } +/// A loaded syntax. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawSyntax(Arc>); + +impl RawSyntax { + /// Load syntaxes from sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Vec>> { + let data = sources.load(world)?; + let list = sources + .v + .0 + .iter() + .zip(&data) + .map(|(source, data)| Self::decode(source, data)) + .collect::>() + .at(sources.span)?; + Ok(Derived::new(sources.v, list)) + } + + /// Decode a syntax from a loaded source. + #[comemo::memoize] + #[typst_macros::time(name = "load syntaxes")] + fn decode(source: &DataSource, data: &Bytes) -> StrResult { + let src = data.as_str().map_err(FileError::from)?; + let syntax = SyntaxDefinition::load_from_str(src, false, None).map_err( + |err| match source { + DataSource::Path(path) => { + eco_format!("failed to parse syntax file `{path}` ({err})") + } + DataSource::Bytes(_) => { + eco_format!("failed to parse syntax ({err})") + } + }, + )?; + + let mut builder = SyntaxSetBuilder::new(); + builder.add(syntax); + + Ok(RawSyntax(Arc::new(ManuallyHash::new( + builder.build(), + typst_utils::hash128(data), + )))) + } + + /// Return the underlying syntax set. + fn get(&self) -> &SyntaxSet { + self.0.as_ref() + } +} + +/// A loaded syntect theme. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawTheme(Arc>); + +impl RawTheme { + /// Load a theme from a data source. + fn load( + world: Tracked, + source: Spanned, + ) -> SourceResult> { + let data = source.load(world)?; + let theme = Self::decode(&data).at(source.span)?; + Ok(Derived::new(source.v, theme)) + } + + /// Decode a theme from bytes. + #[comemo::memoize] + fn decode(data: &Bytes) -> StrResult { + let mut cursor = std::io::Cursor::new(data.as_slice()); + let theme = synt::ThemeSet::load_from_reader(&mut cursor) + .map_err(|err| eco_format!("failed to parse theme ({err})"))?; + Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(data))))) + } + + /// Get the underlying syntect theme. + pub fn get(&self) -> &synt::Theme { + self.0.as_ref() + } +} + /// A highlighted line of raw text. /// /// This is a helper element that is synthesized by [`raw`] elements. @@ -561,13 +641,13 @@ pub struct RawLine { impl Show for Packed { #[typst_macros::time(name = "raw.line", span = self.span())] fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult { - Ok(self.body().clone()) + Ok(self.body.clone()) } } impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } @@ -593,6 +673,11 @@ struct ThemedHighlighter<'a> { line_fn: LineFn<'a>, } +// Shorthands for highlighter closures. +type StyleFn<'a> = + &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; +type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); + impl<'a> ThemedHighlighter<'a> { pub fn new( code: &'a str, @@ -738,108 +823,50 @@ fn to_syn(color: Color) -> synt::Color { synt::Color { r, g, b, a } } -/// A list of raw syntax file paths. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct SyntaxPaths(Vec); - -cast! { - SyntaxPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - -impl Fold for SyntaxPaths { - fn fold(self, outer: Self) -> Self { - Self(self.0.fold(outer.0)) +/// Create a syntect theme item. +fn item( + scope: &str, + color: Option<&str>, + font_style: Option, +) -> synt::ThemeItem { + synt::ThemeItem { + scope: scope.parse().unwrap(), + style: synt::StyleModifier { + foreground: color.map(|s| to_syn(s.parse::().unwrap())), + background: None, + font_style, + }, } } -/// Load a syntax set from a list of syntax file paths. -#[comemo::memoize] -#[typst_macros::time(name = "load syntaxes")] -fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult> { - let mut out = SyntaxSetBuilder::new(); +/// Replace tabs with spaces to align with multiples of `tab_size`. +fn align_tabs(text: &str, tab_size: usize) -> EcoString { + let replacement = " ".repeat(tab_size); + let divisor = tab_size.max(1); + let amount = text.chars().filter(|&c| c == '\t').count(); - // We might have multiple sublime-syntax/yaml files - for (path, bytes) in paths.0.iter().zip(bytes.iter()) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| { - eco_format!("failed to parse syntax file `{path}` ({err})") - })?); + let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); + let mut column = 0; + + for grapheme in text.graphemes(true) { + match grapheme { + "\t" => { + let required = tab_size - column % divisor; + res.push_str(&replacement[..required]); + column += required; + } + "\n" => { + res.push_str(grapheme); + column = 0; + } + _ => { + res.push_str(grapheme); + column += 1; + } + } } - Ok(Arc::new(out.build())) -} - -/// Function to parse the syntaxes argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_syntaxes( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option>)> { - let Some(Spanned { v: paths, span }) = - args.named::>("syntaxes")? - else { - return Ok((None, None)); - }; - - // Load syntax files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Check that parsing works. - let _ = load_syntaxes(&paths, &data).at(span)?; - - Ok((Some(paths), Some(data))) -} - -#[comemo::memoize] -#[typst_macros::time(name = "load theme")] -fn load_theme(path: &str, bytes: &Bytes) -> StrResult> { - let mut cursor = std::io::Cursor::new(bytes.as_slice()); - - synt::ThemeSet::load_from_reader(&mut cursor) - .map(Arc::new) - .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})")) -} - -/// Function to parse the theme argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_theme( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option)> { - let Some(Spanned { v: path, span }) = args.named::>("theme")? - else { - // Argument `theme` not found. - return Ok((None, None)); - }; - - let Smart::Custom(path) = path else { - // Argument `theme` is `auto`. - return Ok((Some(Smart::Auto), None)); - }; - - let Some(path) = path else { - // Argument `theme` is `none`. - return Ok((Some(Smart::Custom(None)), None)); - }; - - // Load theme file. - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - - // Check that parsing works. - let _ = load_theme(&path, &data).at(span)?; - - Ok((Some(Smart::Custom(Some(path))), Some(data))) + res } /// The syntect syntax definitions. @@ -886,49 +913,3 @@ pub static RAW_THEME: LazyLock = LazyLock::new(|| synt::Theme { item("markup.deleted, meta.diff.header.from-file", Some("#d73a49"), None), ], }); - -/// Create a syntect theme item. -fn item( - scope: &str, - color: Option<&str>, - font_style: Option, -) -> synt::ThemeItem { - synt::ThemeItem { - scope: scope.parse().unwrap(), - style: synt::StyleModifier { - foreground: color.map(|s| to_syn(s.parse::().unwrap())), - background: None, - font_style, - }, - } -} - -/// Replace tabs with spaces to align with multiples of `tab_size`. -fn align_tabs(text: &str, tab_size: usize) -> EcoString { - let replacement = " ".repeat(tab_size); - let divisor = tab_size.max(1); - let amount = text.chars().filter(|&c| c == '\t').count(); - - let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); - let mut column = 0; - - for grapheme in text.graphemes(true) { - match grapheme { - "\t" => { - let required = tab_size - column % divisor; - res.push_str(&replacement[..required]); - column += required; - } - "\n" => { - res.push_str(grapheme); - column = 0; - } - _ => { - res.push_str(grapheme); - column += 1; - } - } - } - - res -} diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 9723bbf0c..dbf1be8a1 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -50,7 +50,7 @@ pub struct SubElem { impl Show for Packed { #[typst_macros::time(name = "sub", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, true) { @@ -109,7 +109,7 @@ pub struct SuperElem { impl Show for Packed { #[typst_macros::time(name = "super", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, false) { @@ -132,9 +132,9 @@ fn convert_script(content: &Content, sub: bool) -> Option { Some(' '.into()) } else if let Some(elem) = content.to_packed::() { if sub { - elem.text().chars().map(to_subscript_codepoint).collect() + elem.text.chars().map(to_subscript_codepoint).collect() } else { - elem.text().chars().map(to_superscript_codepoint).collect() + elem.text.chars().map(to_superscript_codepoint).collect() } } else if let Some(sequence) = content.to_packed::() { sequence @@ -159,7 +159,7 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool { { let covers = family.covers(); return text.chars().all(|c| { - covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) + covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) && font.ttf().glyph_index(c).is_some() }); } diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index bf003bd1c..924a45e8c 100644 --- a/crates/typst-library/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs @@ -12,11 +12,11 @@ use crate::text::TextElem; /// ``` /// /// # Smallcaps fonts -/// By default, this enables the OpenType `smcp` feature for the font. Not all -/// fonts support this feature. Sometimes smallcaps are part of a dedicated -/// font. This is, for example, the case for the _Latin Modern_ family of fonts. -/// In those cases, you can use a show-set rule to customize the appearance of -/// the text in smallcaps: +/// By default, this uses the `smcp` and `c2sc` OpenType features on the font. +/// Not all fonts support these features. Sometimes, smallcaps are part of a +/// dedicated font. This is, for example, the case for the _Latin Modern_ family +/// of fonts. In those cases, you can use a show-set rule to customize the +/// appearance of the text in smallcaps: /// /// ```typ /// #show smallcaps: set text(font: "Latin Modern Roman Caps") @@ -45,6 +45,17 @@ use crate::text::TextElem; /// ``` #[elem(title = "Small Capitals", Show)] pub struct SmallcapsElem { + /// Whether to turn uppercase letters into small capitals as well. + /// + /// Unless overridden by a show rule, this enables the `c2sc` OpenType + /// feature. + /// + /// ```example + /// #smallcaps(all: true)[UNICEF] is an + /// agency of #smallcaps(all: true)[UN]. + /// ``` + #[default(false)] + pub all: bool, /// The content to display in small capitals. #[required] pub body: Content, @@ -52,7 +63,17 @@ pub struct SmallcapsElem { impl Show for Packed { #[typst_macros::time(name = "smallcaps", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_smallcaps(true))) + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let sc = if self.all(styles) { Smallcaps::All } else { Smallcaps::Minuscules }; + Ok(self.body.clone().styled(TextElem::set_smallcaps(Some(sc)))) } } + +/// What becomes small capitals. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Smallcaps { + /// Minuscules become small capitals. + Minuscules, + /// All letters become small capitals. + All, +} diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 2f89fe298..f457a6371 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> { "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), + "bg" => ("’", "’", "„", "“"), _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), _ => default, }; diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index 8ff8dbdbc..20b0f5719 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -130,7 +130,7 @@ static TO_SRGB: LazyLock = LazyLock::new(|| { /// /// # Predefined color maps /// Typst also includes a number of preset color maps that can be used for -/// [gradients]($gradient.linear). These are simply arrays of colors defined in +/// [gradients]($gradient/#stops). These are simply arrays of colors defined in /// the module `color.map`. /// /// ```example @@ -248,8 +248,6 @@ impl Color { /// ``` #[func] pub fn luma( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -300,8 +298,6 @@ impl Color { /// ``` #[func] pub fn oklab( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -358,8 +354,6 @@ impl Color { /// ``` #[func] pub fn oklch( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -420,8 +414,6 @@ impl Color { /// ``` #[func(title = "Linear RGB")] pub fn linear_rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -477,8 +469,6 @@ impl Color { /// ``` #[func(title = "RGB")] pub fn rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -555,8 +545,6 @@ impl Color { /// ``` #[func(title = "CMYK")] pub fn cmyk( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The cyan component. #[external] @@ -614,8 +602,6 @@ impl Color { /// ``` #[func(title = "HSL")] pub fn hsl( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -673,8 +659,6 @@ impl Color { /// ``` #[func(title = "HSV")] pub fn hsv( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -898,7 +882,6 @@ impl Color { #[func] pub fn saturate( self, - /// The call span span: Span, /// The factor to saturate the color by. factor: Ratio, @@ -924,7 +907,6 @@ impl Color { #[func] pub fn desaturate( self, - /// The call span span: Span, /// The factor to desaturate the color by. factor: Ratio, @@ -1001,7 +983,6 @@ impl Color { #[func] pub fn rotate( self, - /// The call span span: Span, /// The angle to rotate the hue by. angle: Angle, diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index 607d92ab1..fb5151e8f 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -10,12 +10,12 @@ use crate::foundations::{ use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; -/// A curve consisting of movements, lines, and Beziér segments. +/// A curve consisting of movements, lines, and Bézier segments. /// /// At any point in time, there is a conceptual pen or cursor. /// - Move elements move the cursor without drawing. /// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new -/// position, potentially with control point for a Beziér curve. +/// position, potentially with control point for a Bézier curve. /// - Close elements draw a straight or smooth line back to the start of the /// curve or the latest preceding move segment. /// @@ -26,7 +26,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// or relative to the current pen/cursor position, that is, the position where /// the previous segment ended. /// -/// Beziér curve control points can be skipped by passing `{none}` or +/// Bézier curve control points can be skipped by passing `{none}` or /// automatically mirrored from the preceding segment by passing `{auto}`. /// /// # Example @@ -88,7 +88,7 @@ pub struct CurveElem { #[fold] pub stroke: Smart>, - /// The components of the curve, in the form of moves, line and Beziér + /// The components of the curve, in the form of moves, line and Bézier /// segment, and closes. #[variadic] pub components: Vec, @@ -225,7 +225,7 @@ pub struct CurveLine { pub relative: bool, } -/// Adds a quadratic Beziér curve segment from the last point to `end`, using +/// Adds a quadratic Bézier curve segment from the last point to `end`, using /// `control` as the control point. /// /// ```example @@ -245,9 +245,9 @@ pub struct CurveLine { /// ``` #[elem(name = "quad", title = "Curve Quadratic Segment")] pub struct CurveQuad { - /// The control point of the quadratic Beziér curve. + /// The control point of the quadratic Bézier curve. /// - /// - If `{auto}` and this segment follows another quadratic Beziér curve, + /// - If `{auto}` and this segment follows another quadratic Bézier curve, /// the previous control point will be mirrored. /// - If `{none}`, the control point defaults to `end`, and the curve will /// be a straight line. @@ -272,7 +272,7 @@ pub struct CurveQuad { pub relative: bool, } -/// Adds a cubic Beziér curve segment from the last point to `end`, using +/// Adds a cubic Bézier curve segment from the last point to `end`, using /// `control-start` and `control-end` as the control points. /// /// ```example @@ -388,7 +388,7 @@ pub enum CloseMode { Straight, } -/// A curve consisting of movements, lines, and Beziér segments. +/// A curve consisting of movements, lines, and Bézier segments. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct Curve(pub Vec); diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index e16e5d88a..1a723a9f5 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -70,6 +70,9 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// the offsets when defining a gradient. In this case, Typst will space all /// stops evenly. /// +/// Typst predefines color maps that you can use as stops. See the +/// [`color`]($color/#predefined-color-maps) documentation for more details. +/// /// # Relativeness /// The location of the `{0%}` and `{100%}` stops depends on the dimensions /// of a container. This container can either be the shape that it is being @@ -157,10 +160,6 @@ use crate::visualize::{Color, ColorSpace, WeightedColor}; /// ) /// ``` /// -/// # Presets -/// Typst predefines color maps that you can use with your gradients. See the -/// [`color`]($color/#predefined-color-maps) documentation for more details. -/// /// # Note on file sizes /// /// Gradients can be quite large, especially if they have many stops. This is @@ -200,9 +199,7 @@ impl Gradient { /// ``` #[func(title = "Linear Gradient")] pub fn linear( - /// The args of this function. args: &mut Args, - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -290,9 +287,8 @@ impl Gradient { /// )), /// ) /// ``` - #[func] + #[func(title = "Radial Gradient")] fn radial( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -405,9 +401,8 @@ impl Gradient { /// )), /// ) /// ``` - #[func] + #[func(title = "Conic Gradient")] pub fn conic( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -586,12 +581,11 @@ impl Gradient { let mut stops = stops .iter() .map(move |&(color, offset)| { - let t = i as f64 / n as f64; let r = offset.get(); if i % 2 == 1 && mirror { - (color, Ratio::new(t + (1.0 - r) / n as f64)) + (color, Ratio::new((i as f64 + 1.0 - r) / n as f64)) } else { - (color, Ratio::new(t + r / n as f64)) + (color, Ratio::new((i as f64 + r) / n as f64)) } }) .collect::>(); @@ -1234,7 +1228,7 @@ fn process_stops(stops: &[Spanned]) -> SourceResult>("path to image file")?; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - path + let source = args.expect::>("source")?; + let data = source.load(engine.world)?; + Derived::new(source.v, data) )] - #[borrowed] - pub path: EcoString, + pub source: Derived, - /// The raw file data. - #[internal] - #[required] - #[parse(Readable::Bytes(data))] - pub data: Readable, - - /// The image's format. Detected automatically by default. + /// The image's format. /// - /// Supported formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image - /// is [not currently supported](https://github.com/typst/typst/issues/145). + /// By default, the format is detected automatically. Typically, you thus + /// only need to specify this when providing raw bytes as the + /// [`source`]($image.source) (even then, Typst will try to figure out the + /// format automatically, but that's not always possible). + /// + /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well + /// as raw pixel data. Embedding PDFs as images is + /// [not currently supported](https://github.com/typst/typst/issues/145). + /// + /// When providing raw pixel data as the `source`, you must specify a + /// dictionary with the following keys as the `format`: + /// - `encoding` ([str]): The encoding of the pixel data. One of: + /// - `{"rgb8"}` (three 8-bit channels: red, green, blue) + /// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha) + /// - `{"luma8"}` (one 8-bit channel) + /// - `{"lumaa8"}` (two 8-bit channels: luma and alpha) + /// - `width` ([int]): The pixel width of the image. + /// - `height` ([int]): The pixel height of the image. + /// + /// The pixel width multiplied by the height multiplied by the channel count + /// for the specified encoding must then match the `source` data. + /// + /// ```example + /// #image( + /// read( + /// "tetrahedron.svg", + /// encoding: none, + /// ), + /// format: "svg", + /// width: 2cm, + /// ) + /// + /// #image( + /// bytes(range(16).map(x => x * 16)), + /// format: ( + /// encoding: "luma8", + /// width: 4, + /// height: 4, + /// ), + /// width: 2cm, + /// ) + /// ``` pub format: Smart, /// The width of the image. @@ -95,30 +139,38 @@ pub struct ImageElem { #[default(ImageFit::Cover)] pub fit: ImageFit, - /// Whether text in SVG images should be converted into curves before - /// embedding. This will result in the text becoming unselectable in the - /// output. - #[default(false)] - pub flatten_text: bool, + /// A hint to viewers how they should scale the image. + /// + /// When set to `{auto}`, the default is left up to the viewer. For PNG + /// export, Typst will default to smooth scaling, like most PDF and SVG + /// viewers. + /// + /// _Note:_ The exact look may differ across PDF viewers. + pub scaling: Smart, + + /// An ICC profile for the image. + /// + /// ICC profiles define how to interpret the colors in an image. When set + /// to `{auto}`, Typst will try to extract an ICC profile from the image. + #[parse(match args.named::>>("icc")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({ + let data = Spanned::new(&source, span).load(engine.world)?; + Derived::new(source, data) + })), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub icc: Smart>, } #[scope] +#[allow(clippy::too_many_arguments)] impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. - /// - /// ```example - /// #let original = read("diagram.svg") - /// #let changed = original.replace( - /// "#2B80FF", // blue - /// green.to-hex(), - /// ) - /// - /// #image.decode(original) - /// #image.decode(changed) - /// ``` #[func(title = "Decode Image")] + #[deprecated = "`image.decode` is deprecated, directly pass bytes to `image` instead"] pub fn decode( - /// The call span of this function. span: Span, /// The data to decode as an image. Can be a string for SVGs. data: Readable, @@ -137,8 +189,13 @@ impl ImageElem { /// How the image should adjust itself to a given area. #[named] fit: Option, + /// A hint to viewers how they should scale the image. + #[named] + scaling: Option>, ) -> StrResult { - let mut elem = ImageElem::new(EcoString::new(), data); + let bytes = data.into_bytes(); + let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); + let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); } @@ -154,6 +211,9 @@ impl ImageElem { if let Some(fit) = fit { elem.push_fit(fit); } + if let Some(scaling) = scaling { + elem.push_scaling(scaling); + } Ok(elem.pack().spanned(span)) } } @@ -204,15 +264,8 @@ struct Repr { kind: ImageKind, /// A text describing the image. alt: Option, -} - -/// A kind of image. -#[derive(Hash)] -pub enum ImageKind { - /// A raster image. - Raster(RasterImage), - /// An SVG image. - Svg(SvgImage), + /// The scaling algorithm to use. + scaling: Smart, } impl Image { @@ -223,55 +276,29 @@ impl Image { /// Should always be the same as the default DPI used by usvg. pub const USVG_DEFAULT_DPI: f64 = 96.0; - /// Create an image from a buffer and a format. - #[comemo::memoize] - #[typst_macros::time(name = "load image")] + /// Create an image from a `RasterImage` or `SvgImage`. pub fn new( - data: Bytes, - format: ImageFormat, + kind: impl Into, alt: Option, - ) -> StrResult { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::new(data)?) - } - }; - - Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) + scaling: Smart, + ) -> Self { + Self::new_impl(kind.into(), alt, scaling) } - /// Create a possibly font-dependent image from a buffer and a format. + /// Create an image with optional properties set to the default. + pub fn plain(kind: impl Into) -> Self { + Self::new(kind, None, Smart::Auto) + } + + /// The internal, non-generic implementation. This is memoized to reuse + /// the `Arc` and `LazyHash`. #[comemo::memoize] - #[typst_macros::time(name = "load image")] - pub fn with_fonts( - data: Bytes, - format: ImageFormat, + fn new_impl( + kind: ImageKind, alt: Option, - world: Tracked, - families: &[&str], - flatten_text: bool, - ) -> StrResult { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::with_fonts(data, world, flatten_text, families)?) - } - }; - - Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) - } - - /// The raw image data. - pub fn data(&self) -> &Bytes { - match &self.0.kind { - ImageKind::Raster(raster) => raster.data(), - ImageKind::Svg(svg) => svg.data(), - } + scaling: Smart, + ) -> Image { + Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling }))) } /// The format of the image. @@ -311,6 +338,11 @@ impl Image { self.0.alt.as_deref() } + /// The image scaling algorithm to use for this image. + pub fn scaling(&self) -> Smart { + self.0.scaling + } + /// The decoded image. pub fn kind(&self) -> &ImageKind { &self.0.kind @@ -324,10 +356,32 @@ impl Debug for Image { .field("width", &self.width()) .field("height", &self.height()) .field("alt", &self.alt()) + .field("scaling", &self.scaling()) .finish() } } +/// A kind of image. +#[derive(Clone, Hash)] +pub enum ImageKind { + /// A raster image. + Raster(RasterImage), + /// An SVG image. + Svg(SvgImage), +} + +impl From for ImageKind { + fn from(image: RasterImage) -> Self { + Self::Raster(image) + } +} + +impl From for ImageKind { + fn from(image: SvgImage) -> Self { + Self::Svg(image) + } +} + /// A raster or vector image format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ImageFormat { @@ -337,6 +391,36 @@ pub enum ImageFormat { Vector(VectorFormat), } +impl ImageFormat { + /// Try to detect the format of an image from data. + pub fn detect(data: &[u8]) -> Option { + if let Some(format) = ExchangeFormat::detect(data) { + return Some(Self::Raster(RasterFormat::Exchange(format))); + } + + if is_svg(data) { + return Some(Self::Vector(VectorFormat::Svg)); + } + + None + } +} + +/// Checks whether the data looks like an SVG or a compressed SVG. +fn is_svg(data: &[u8]) -> bool { + // Check for the gzip magic bytes. This check is perhaps a bit too + // permissive as other formats than SVGZ could use gzip. + if data.starts_with(&[0x1f, 0x8b]) { + return true; + } + + // If the first 2048 bytes contain the SVG namespace declaration, we assume + // that it's an SVG. Note that, if the SVG does not contain a namespace + // declaration, usvg will reject it. + let head = &data[..data.len().min(2048)]; + memchr::memmem::find(head, b"http://www.w3.org/2000/svg").is_some() +} + /// A vector graphics format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum VectorFormat { @@ -344,9 +428,12 @@ pub enum VectorFormat { Svg, } -impl From for ImageFormat { - fn from(format: RasterFormat) -> Self { - Self::Raster(format) +impl From for ImageFormat +where + R: Into, +{ + fn from(format: R) -> Self { + Self::Raster(format.into()) } } @@ -360,8 +447,18 @@ cast! { ImageFormat, self => match self { Self::Raster(v) => v.into_value(), - Self::Vector(v) => v.into_value() + Self::Vector(v) => v.into_value(), }, v: RasterFormat => Self::Raster(v), v: VectorFormat => Self::Vector(v), } + +/// The image scaling algorithm a viewer should use. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum ImageScaling { + /// Scale with a smoothing algorithm such as bilinear interpolation. + Smooth, + /// Scale with nearest neighbor or a similar algorithm to preserve the + /// pixelated look of the image. + Pixelated, +} diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 829826c75..0883fe71d 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -7,10 +7,12 @@ use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; -use image::{guess_format, DynamicImage, ImageDecoder, ImageResult, Limits}; +use image::{ + guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, +}; use crate::diag::{bail, StrResult}; -use crate::foundations::{Bytes, Cast}; +use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; /// A decoded raster image. #[derive(Clone, Hash)] @@ -21,43 +23,118 @@ struct Repr { data: Bytes, format: RasterFormat, dynamic: image::DynamicImage, - icc: Option>, + icc: Option, dpi: Option, } impl RasterImage { /// Decode a raster image. + pub fn new( + data: Bytes, + format: impl Into, + icc: Smart, + ) -> StrResult { + Self::new_impl(data, format.into(), icc) + } + + /// Create a raster image with optional properties set to the default. + pub fn plain(data: Bytes, format: impl Into) -> StrResult { + Self::new(data, format, Smart::Auto) + } + + /// The internal, non-generic implementation. #[comemo::memoize] - pub fn new(data: Bytes, format: RasterFormat) -> StrResult { - fn decode_with( - decoder: ImageResult, - ) -> ImageResult<(image::DynamicImage, Option>)> { - let mut decoder = decoder?; - let icc = decoder.icc_profile().ok().flatten().filter(|icc| !icc.is_empty()); - decoder.set_limits(Limits::default())?; - let dynamic = image::DynamicImage::from_decoder(decoder)?; - Ok((dynamic, icc)) - } + #[typst_macros::time(name = "load raster image")] + fn new_impl( + data: Bytes, + format: RasterFormat, + icc: Smart, + ) -> StrResult { + let (dynamic, icc, dpi) = match format { + RasterFormat::Exchange(format) => { + fn decode( + decoder: ImageResult, + icc: Smart, + ) -> ImageResult<(image::DynamicImage, Option)> { + let mut decoder = decoder?; + let icc = icc.custom().or_else(|| { + decoder + .icc_profile() + .ok() + .flatten() + .filter(|icc| !icc.is_empty()) + .map(Bytes::new) + }); + decoder.set_limits(Limits::default())?; + let dynamic = image::DynamicImage::from_decoder(decoder)?; + Ok((dynamic, icc)) + } - let cursor = io::Cursor::new(&data); - let (mut dynamic, icc) = match format { - RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)), - RasterFormat::Png => decode_with(PngDecoder::new(cursor)), - RasterFormat::Gif => decode_with(GifDecoder::new(cursor)), - } - .map_err(format_image_error)?; + let cursor = io::Cursor::new(&data); + let (mut dynamic, icc) = match format { + ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc), + ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc), + ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc), + } + .map_err(format_image_error)?; - let exif = exif::Reader::new() - .read_from_container(&mut std::io::Cursor::new(&data)) - .ok(); + let exif = exif::Reader::new() + .read_from_container(&mut std::io::Cursor::new(&data)) + .ok(); - // Apply rotation from EXIF metadata. - if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { - apply_rotation(&mut dynamic, rotation); - } + // Apply rotation from EXIF metadata. + if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { + apply_rotation(&mut dynamic, rotation); + } - // Extract pixel density. - let dpi = determine_dpi(&data, exif.as_ref()); + // Extract pixel density. + let dpi = determine_dpi(&data, exif.as_ref()); + + (dynamic, icc, dpi) + } + + RasterFormat::Pixel(format) => { + if format.width == 0 || format.height == 0 { + bail!("zero-sized images are not allowed"); + } + + let channels = match format.encoding { + PixelEncoding::Rgb8 => 3, + PixelEncoding::Rgba8 => 4, + PixelEncoding::Luma8 => 1, + PixelEncoding::Lumaa8 => 2, + }; + + let Some(expected_size) = format + .width + .checked_mul(format.height) + .and_then(|size| size.checked_mul(channels)) + else { + bail!("pixel dimensions are too large"); + }; + + if expected_size as usize != data.len() { + bail!("pixel dimensions and pixel data do not match"); + } + + fn to>( + data: &Bytes, + format: PixelFormat, + ) -> ImageBuffer> { + ImageBuffer::from_raw(format.width, format.height, data.to_vec()) + .unwrap() + } + + let dynamic = match format.encoding { + PixelEncoding::Rgb8 => to::>(&data, format).into(), + PixelEncoding::Rgba8 => to::>(&data, format).into(), + PixelEncoding::Luma8 => to::>(&data, format).into(), + PixelEncoding::Lumaa8 => to::>(&data, format).into(), + }; + + (dynamic, icc.custom(), None) + } + }; Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi }))) } @@ -83,6 +160,8 @@ impl RasterImage { } /// The image's pixel density in pixels per inch, if known. + /// + /// This is guaranteed to be positive. pub fn dpi(&self) -> Option { self.0.dpi } @@ -93,60 +172,141 @@ impl RasterImage { } /// Access the ICC profile, if any. - pub fn icc(&self) -> Option<&[u8]> { - self.0.icc.as_deref() + pub fn icc(&self) -> Option<&Bytes> { + self.0.icc.as_ref() } } impl Hash for Repr { fn hash(&self, state: &mut H) { - // The image is fully defined by data and format. + // The image is fully defined by data, format, and ICC profile. self.data.hash(state); self.format.hash(state); + self.icc.hash(state); } } /// A raster graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum RasterFormat { + /// A format typically used in image exchange. + Exchange(ExchangeFormat), + /// A format of raw pixel data. + Pixel(PixelFormat), +} + +impl From for RasterFormat { + fn from(format: ExchangeFormat) -> Self { + Self::Exchange(format) + } +} + +impl From for RasterFormat { + fn from(format: PixelFormat) -> Self { + Self::Pixel(format) + } +} + +cast! { + RasterFormat, + self => match self { + Self::Exchange(v) => v.into_value(), + Self::Pixel(v) => v.into_value(), + }, + v: ExchangeFormat => Self::Exchange(v), + v: PixelFormat => Self::Pixel(v), +} + +/// A raster format typically used in image exchange, with efficient encoding. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum ExchangeFormat { /// Raster format for illustrations and transparent graphics. Png, /// Lossy raster format suitable for photos. Jpg, - /// Raster format that is typically used for short animated clips. + /// Raster format that is typically used for short animated clips. Typst can + /// load GIFs, but they will become static. Gif, } -impl RasterFormat { +impl ExchangeFormat { /// Try to detect the format of data in a buffer. pub fn detect(data: &[u8]) -> Option { guess_format(data).ok().and_then(|format| format.try_into().ok()) } } -impl From for image::ImageFormat { - fn from(format: RasterFormat) -> Self { +impl From for image::ImageFormat { + fn from(format: ExchangeFormat) -> Self { match format { - RasterFormat::Png => image::ImageFormat::Png, - RasterFormat::Jpg => image::ImageFormat::Jpeg, - RasterFormat::Gif => image::ImageFormat::Gif, + ExchangeFormat::Png => image::ImageFormat::Png, + ExchangeFormat::Jpg => image::ImageFormat::Jpeg, + ExchangeFormat::Gif => image::ImageFormat::Gif, } } } -impl TryFrom for RasterFormat { +impl TryFrom for ExchangeFormat { type Error = EcoString; fn try_from(format: image::ImageFormat) -> StrResult { Ok(match format { - image::ImageFormat::Png => RasterFormat::Png, - image::ImageFormat::Jpeg => RasterFormat::Jpg, - image::ImageFormat::Gif => RasterFormat::Gif, - _ => bail!("Format not yet supported."), + image::ImageFormat::Png => ExchangeFormat::Png, + image::ImageFormat::Jpeg => ExchangeFormat::Jpg, + image::ImageFormat::Gif => ExchangeFormat::Gif, + _ => bail!("format not yet supported"), }) } } +/// Information that is needed to understand a pixmap buffer. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct PixelFormat { + /// The channel encoding. + encoding: PixelEncoding, + /// The pixel width. + width: u32, + /// The pixel height. + height: u32, +} + +/// Determines the channel encoding of raw pixel data. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum PixelEncoding { + /// Three 8-bit channels: Red, green, blue. + Rgb8, + /// Four 8-bit channels: Red, green, blue, alpha. + Rgba8, + /// One 8-bit channel. + Luma8, + /// Two 8-bit channels: Luma and alpha. + Lumaa8, +} + +cast! { + PixelFormat, + self => Value::Dict(self.into()), + mut dict: Dict => { + let format = Self { + encoding: dict.take("encoding")?.cast()?, + width: dict.take("width")?.cast()?, + height: dict.take("height")?.cast()?, + }; + dict.finish(&["encoding", "width", "height"])?; + format + } +} + +impl From for Dict { + fn from(format: PixelFormat) -> Self { + dict! { + "encoding" => format.encoding, + "width" => format.width, + "height" => format.height, + } + } +} + /// Try to get the rotation from the EXIF metadata. fn exif_rotation(exif: &exif::Exif) -> Option { exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)? @@ -176,6 +336,9 @@ fn apply_rotation(image: &mut DynamicImage, rotation: u32) { } /// Try to determine the DPI (dots per inch) of the image. +/// +/// This is guaranteed to be a positive value, or `None` if invalid or +/// unspecified. fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { // Try to extract the DPI from the EXIF metadata. If that doesn't yield // anything, fall back to specialized procedures for extracting JPEG or PNG @@ -183,6 +346,7 @@ fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { exif.and_then(exif_dpi) .or_else(|| jpeg_dpi(data)) .or_else(|| png_dpi(data)) + .filter(|&dpi| dpi > 0.0) } /// Try to get the DPI from the EXIF metadata. @@ -266,21 +430,20 @@ fn format_image_error(error: image::ImageError) -> EcoString { #[cfg(test)] mod tests { - use super::{RasterFormat, RasterImage}; - use crate::foundations::Bytes; + use super::*; #[test] fn test_image_dpi() { #[track_caller] - fn test(path: &str, format: RasterFormat, dpi: f64) { + fn test(path: &str, format: ExchangeFormat, dpi: f64) { let data = typst_dev_assets::get(path).unwrap(); - let bytes = Bytes::from_static(data); - let image = RasterImage::new(bytes, format).unwrap(); + let bytes = Bytes::new(data); + let image = RasterImage::plain(bytes, format).unwrap(); assert_eq!(image.dpi().map(f64::round), Some(dpi)); } - test("images/f2t.jpg", RasterFormat::Jpg, 220.0); - test("images/tiger.jpg", RasterFormat::Jpg, 72.0); - test("images/graph.png", RasterFormat::Png, 144.0); + test("images/f2t.jpg", ExchangeFormat::Jpg, 220.0); + test("images/tiger.jpg", ExchangeFormat::Jpg, 72.0); + test("images/graph.png", ExchangeFormat::Png, 144.0); } } diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index 6b6a1b6b2..9bf1ead0d 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -22,7 +22,6 @@ pub struct SvgImage(Arc); struct Repr { data: Bytes, size: Axes, - flatten_text: bool, font_hash: u128, tree: usvg::Tree, } @@ -30,24 +29,19 @@ struct Repr { impl SvgImage { /// Decode an SVG image without fonts. #[comemo::memoize] + #[typst_macros::time(name = "load svg")] pub fn new(data: Bytes) -> StrResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash: 0, - flatten_text: false, - tree, - }))) + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree }))) } /// Decode an SVG image with access to fonts. #[comemo::memoize] + #[typst_macros::time(name = "load svg")] pub fn with_fonts( data: Bytes, world: Tracked, - flatten_text: bool, families: &[&str], ) -> StrResult { let book = world.book(); @@ -68,13 +62,7 @@ impl SvgImage { ) .map_err(format_usvg_error)?; let font_hash = resolver.into_inner().unwrap().finish(); - Ok(Self(Arc::new(Repr { - data, - size: tree_size(&tree), - font_hash, - flatten_text, - tree, - }))) + Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree }))) } /// The raw image data. @@ -87,11 +75,6 @@ impl SvgImage { self.0.size.x } - /// Whether the SVG's text should be flattened. - pub fn flatten_text(&self) -> bool { - self.0.flatten_text - } - /// The SVG's height in pixels. pub fn height(&self) -> f64 { self.0.size.y diff --git a/crates/typst-library/src/visualize/mod.rs b/crates/typst-library/src/visualize/mod.rs index 431191491..72a420657 100644 --- a/crates/typst-library/src/visualize/mod.rs +++ b/crates/typst-library/src/visualize/mod.rs @@ -24,19 +24,11 @@ pub use self::shape::*; pub use self::stroke::*; pub use self::tiling::*; -use crate::foundations::{category, Category, Scope, Type}; - -/// Drawing and data visualization. -/// -/// If you want to create more advanced drawings or plots, also have a look at -/// the [CetZ](https://github.com/johannes-wolf/cetz) package as well as more -/// specialized [packages]($universe) for your use case. -#[category] -pub static VISUALIZE: Category; +use crate::foundations::{Element, Scope, Type}; /// Hook up all visualize definitions. pub(super) fn define(global: &mut Scope) { - global.category(VISUALIZE); + global.start_category(crate::Category::Visualize); global.define_type::(); global.define_type::(); global.define_type::(); @@ -49,8 +41,11 @@ pub(super) fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); - - // Compatibility. - global.define("pattern", Type::of::()); + global + .define("path", Element::of::()) + .deprecated("the `path` function is deprecated, use `curve` instead"); + global + .define("pattern", Type::of::()) + .deprecated("the name `pattern` is deprecated, use `tiling` instead"); + global.reset_category(); } diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index 6aacb3198..968146cda 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -8,7 +8,7 @@ use crate::foundations::{ use crate::layout::{Axes, BlockElem, Length, Rel}; use crate::visualize::{FillRule, Paint, Stroke}; -/// A path through a list of points, connected by Bezier curves. +/// A path through a list of points, connected by Bézier curves. /// /// # Example /// ```example @@ -21,9 +21,6 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// ((50%, 0pt), (40pt, 0pt)), /// ) /// ``` -/// -/// # Deprecation -/// This element is deprecated. The [`curve`] element should be used instead. #[elem(Show)] pub struct PathElem { /// How to fill the path. @@ -62,8 +59,8 @@ pub struct PathElem { #[fold] pub stroke: Smart>, - /// Whether to close this path with one last bezier curve. This curve will - /// takes into account the adjacent control points. If you want to close + /// Whether to close this path with one last Bézier curve. This curve will + /// take into account the adjacent control points. If you want to close /// with a straight line, simply add one last point that's the same as the /// start point. #[default(false)] diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index 465f2c1a7..42b083431 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -67,8 +67,8 @@ impl PolygonElem { /// ``` #[func(title = "Regular Polygon")] pub fn regular( - /// The call span of this function. span: Span, + /// How to fill the polygon. See the general /// [polygon's documentation]($polygon.fill) for more details. #[named] diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index 3c62b210f..439b4cd98 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -412,7 +412,7 @@ pub enum Geometry { Line(Point), /// A rectangle with its origin in the topleft corner. Rect(Size), - /// A curve consisting of movements, lines, and Bezier segments. + /// A curve consisting of movements, lines, and Bézier segments. Curve(Curve), } diff --git a/crates/typst-library/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs index 97a1535db..a0830cf19 100644 --- a/crates/typst-library/src/visualize/stroke.rs +++ b/crates/typst-library/src/visualize/stroke.rs @@ -97,8 +97,6 @@ impl Stroke { /// ``` #[func(constructor)] pub fn construct( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The color or gradient to use for the stroke. diff --git a/crates/typst-library/src/visualize/tiling.rs b/crates/typst-library/src/visualize/tiling.rs index d699d3b6d..98a71f927 100644 --- a/crates/typst-library/src/visualize/tiling.rs +++ b/crates/typst-library/src/visualize/tiling.rs @@ -138,7 +138,6 @@ impl Tiling { #[func(constructor)] pub fn construct( engine: &mut Engine, - /// The callsite span. span: Span, /// The bounding box of each cell of the tiling. #[named] diff --git a/crates/typst-macros/src/category.rs b/crates/typst-macros/src/category.rs deleted file mode 100644 index 26ec879cc..000000000 --- a/crates/typst-macros/src/category.rs +++ /dev/null @@ -1,59 +0,0 @@ -use heck::{ToKebabCase, ToTitleCase}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::parse::{Parse, ParseStream}; -use syn::{Attribute, Ident, Result, Token, Type, Visibility}; - -use crate::util::{documentation, foundations}; - -/// Expand the `#[category]` macro. -pub fn category(_: TokenStream, item: syn::Item) -> Result { - let syn::Item::Verbatim(stream) = item else { - bail!(item, "expected bare static"); - }; - - let BareStatic { attrs, vis, ident, ty, .. } = syn::parse2(stream)?; - - let name = ident.to_string().to_kebab_case(); - let title = name.to_title_case(); - let docs = documentation(&attrs); - - Ok(quote! { - #(#attrs)* - #[allow(rustdoc::broken_intra_doc_links)] - #vis static #ident: #ty = { - static DATA: #foundations::CategoryData = #foundations::CategoryData { - name: #name, - title: #title, - docs: #docs, - }; - #foundations::Category::from_data(&DATA) - }; - }) -} - -/// Parse a bare `pub static CATEGORY: Category;` item. -#[allow(dead_code)] -pub struct BareStatic { - pub attrs: Vec, - pub vis: Visibility, - pub static_token: Token![static], - pub ident: Ident, - pub colon_token: Token![:], - pub ty: Type, - pub semi_token: Token![;], -} - -impl Parse for BareStatic { - fn parse(input: ParseStream) -> Result { - Ok(Self { - attrs: input.call(Attribute::parse_outer)?, - vis: input.parse()?, - static_token: input.parse()?, - ident: input.parse()?, - colon_token: input.parse()?, - ty: input.parse()?, - semi_token: input.parse()?, - }) - } -} diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index 78a3c1800..67fe7ed6a 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -63,6 +63,11 @@ impl Elem { self.real_fields().filter(|field| !field.ghost) } + /// Fields that get accessor, with, and push methods. + fn accessor_fields(&self) -> impl Iterator + Clone { + self.struct_fields().filter(|field| !field.required) + } + /// Fields that are relevant for equality. /// /// Synthesized fields are excluded to ensure equality before and after @@ -442,9 +447,9 @@ fn create_inherent_impl(element: &Elem) -> TokenStream { let Elem { ident, .. } = element; let new_func = create_new_func(element); - let with_field_methods = element.struct_fields().map(create_with_field_method); - let push_field_methods = element.struct_fields().map(create_push_field_method); - let field_methods = element.struct_fields().map(create_field_method); + let with_field_methods = element.accessor_fields().map(create_with_field_method); + let push_field_methods = element.accessor_fields().map(create_push_field_method); + let field_methods = element.accessor_fields().map(create_field_method); let field_in_methods = element.style_fields().map(create_field_in_method); let set_field_methods = element.style_fields().map(create_set_field_method); diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs index 578389c7f..82e63ddc8 100644 --- a/crates/typst-macros/src/lib.rs +++ b/crates/typst-macros/src/lib.rs @@ -5,7 +5,6 @@ extern crate proc_macro; #[macro_use] mod util; mod cast; -mod category; mod elem; mod func; mod scope; @@ -266,15 +265,6 @@ pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { .into() } -/// Defines a category of definitions. -#[proc_macro_attribute] -pub fn category(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { - let item = syn::parse_macro_input!(item as syn::Item); - category::category(stream.into(), item) - .unwrap_or_else(|err| err.to_compile_error()) - .into() -} - /// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. /// /// - `Reflect` makes Typst's runtime aware of the type's characteristics. diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs index 8a2f1ce61..392ab1a53 100644 --- a/crates/typst-macros/src/scope.rs +++ b/crates/typst-macros/src/scope.rs @@ -31,18 +31,37 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result { let mut definitions = vec![]; let mut constructor = quote! { None }; for child in &mut item.items { - let def = match child { - syn::ImplItem::Const(item) => handle_const(&self_ty_expr, item)?, - syn::ImplItem::Fn(item) => match handle_fn(self_ty, item)? { - FnKind::Member(tokens) => tokens, - FnKind::Constructor(tokens) => { - constructor = tokens; - continue; - } - }, - syn::ImplItem::Verbatim(item) => handle_type_or_elem(item)?, + let bare: BareType; + let (mut def, attrs) = match child { + syn::ImplItem::Const(item) => { + (handle_const(&self_ty_expr, item)?, &item.attrs) + } + syn::ImplItem::Fn(item) => ( + match handle_fn(self_ty, item)? { + FnKind::Member(tokens) => tokens, + FnKind::Constructor(tokens) => { + constructor = tokens; + continue; + } + }, + &item.attrs, + ), + syn::ImplItem::Verbatim(item) => { + bare = syn::parse2(item.clone())?; + (handle_type_or_elem(&bare)?, &bare.attrs) + } _ => bail!(child, "unexpected item in scope"), }; + + if let Some(message) = attrs.iter().find_map(|attr| match &attr.meta { + syn::Meta::NameValue(pair) if pair.path.is_ident("deprecated") => { + Some(&pair.value) + } + _ => None, + }) { + def = quote! { #def.deprecated(#message) } + } + definitions.push(def); } @@ -61,6 +80,7 @@ pub fn scope(_: TokenStream, item: syn::Item) -> Result { #constructor } + #[allow(deprecated)] fn scope() -> #foundations::Scope { let mut scope = #foundations::Scope::deduplicating(); #(#definitions;)* @@ -78,8 +98,7 @@ fn handle_const(self_ty: &TokenStream, item: &syn::ImplItemConst) -> Result Result { - let item: BareType = syn::parse2(item.clone())?; +fn handle_type_or_elem(item: &BareType) -> Result { let ident = &item.ident; let define = if item.attrs.iter().any(|attr| attr.path().is_ident("elem")) { quote! { define_elem } diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs index c4b0e2e83..709b01553 100644 --- a/crates/typst-pdf/src/catalog.rs +++ b/crates/typst-pdf/src/catalog.rs @@ -12,7 +12,7 @@ use typst_syntax::Span; use xmp_writer::{DateTime, LangId, RenditionClass, XmpWriter}; use crate::page::PdfPageLabel; -use crate::{hash_base64, outline, TextStrExt, Timezone, WithEverything}; +use crate::{hash_base64, outline, TextStrExt, Timestamp, Timezone, WithEverything}; /// Write the document catalog. pub fn write_catalog( @@ -86,23 +86,10 @@ pub fn write_catalog( info.keywords(TextStr::trimmed(&joined)); xmp.pdf_keywords(&joined); } - - // (1) If the `document.date` is set to specific `datetime` or `none`, use it. - // (2) If the `document.date` is set to `auto` or not set, try to use the - // date from the options. - // (3) Otherwise, we don't write date metadata. - let (date, tz) = match (ctx.document.info.date, ctx.options.timestamp) { - (Smart::Custom(date), _) => (date, None), - (Smart::Auto, Some(timestamp)) => { - (Some(timestamp.datetime), Some(timestamp.timezone)) - } - _ => (None, None), - }; - if let Some(date) = date { - if let Some(pdf_date) = pdf_date(date, tz) { - info.creation_date(pdf_date); - info.modified_date(pdf_date); - } + let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); + if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { + info.creation_date(pdf_date); + info.modified_date(pdf_date); } info.finish(); @@ -154,7 +141,7 @@ pub fn write_catalog( } // Assert dominance. - if ctx.options.standards.pdfa { + if let Some((part, conformance)) = ctx.options.standards.pdfa_part { let mut extension_schemas = xmp.extension_schemas(); extension_schemas .xmp_media_management() @@ -162,8 +149,8 @@ pub fn write_catalog( .describe_instance_id(); extension_schemas.pdf().properties().describe_all(); extension_schemas.finish(); - xmp.pdfa_part(2); - xmp.pdfa_conformance("B"); + xmp.pdfa_part(part); + xmp.pdfa_conformance(conformance); } let xmp_buf = xmp.finish(None); @@ -182,13 +169,35 @@ pub fn write_catalog( catalog.viewer_preferences().direction(dir); catalog.metadata(meta_ref); - // Write the named destination tree if there are any entries. - if !ctx.references.named_destinations.dests.is_empty() { + let has_dests = !ctx.references.named_destinations.dests.is_empty(); + let has_embeddings = !ctx.references.embedded_files.is_empty(); + + // Write the `/Names` dictionary. + if has_dests || has_embeddings { + // Write the named destination tree if there are any entries. let mut name_dict = catalog.names(); - let mut dests_name_tree = name_dict.destinations(); - let mut names = dests_name_tree.names(); - for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { - names.insert(Str(name.resolve().as_bytes()), dest_ref); + if has_dests { + let mut dests_name_tree = name_dict.destinations(); + let mut names = dests_name_tree.names(); + for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { + names.insert(Str(name.resolve().as_bytes()), dest_ref); + } + } + + if has_embeddings { + let mut embedded_files = name_dict.embedded_files(); + let mut names = embedded_files.names(); + for (name, file_ref) in &ctx.references.embedded_files { + names.insert(Str(name.as_bytes()), *file_ref); + } + } + } + + if has_embeddings && ctx.options.standards.pdfa { + // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. + let mut associated_files = catalog.insert(Name(b"AF")).array().typed(); + for (_, file_ref) in ctx.references.embedded_files { + associated_files.item(file_ref).finish(); } } @@ -289,8 +298,27 @@ pub(crate) fn write_page_labels( result } +/// Resolve the document date. +/// +/// (1) If the `document.date` is set to specific `datetime` or `none`, use it. +/// (2) If the `document.date` is set to `auto` or not set, try to use the +/// date from the options. +/// (3) Otherwise, we don't write date metadata. +pub fn document_date( + document_date: Smart>, + timestamp: Option, +) -> (Option, Option) { + match (document_date, timestamp) { + (Smart::Custom(date), _) => (date, None), + (Smart::Auto, Some(timestamp)) => { + (Some(timestamp.datetime), Some(timestamp.timezone)) + } + _ => (None, None), + } +} + /// Converts a datetime to a pdf-writer date. -fn pdf_date(datetime: Datetime, tz: Option) -> Option { +pub fn pdf_date(datetime: Datetime, tz: Option) -> Option { let year = datetime.year().filter(|&y| y >= 0)? as u16; let mut pdf_date = pdf_writer::Date::new(year); diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs new file mode 100644 index 000000000..597638f4b --- /dev/null +++ b/crates/typst-pdf/src/embed.rs @@ -0,0 +1,122 @@ +use std::collections::BTreeMap; + +use ecow::EcoString; +use pdf_writer::types::AssociationKind; +use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::foundations::{NativeElement, Packed, StyleChain}; +use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; + +use crate::catalog::{document_date, pdf_date}; +use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs}; + +/// Query for all [`EmbedElem`] and write them and their file specifications. +/// +/// This returns a map of embedding names and references so that we can later +/// add them to the catalog's `/Names` dictionary. +pub fn write_embedded_files( + ctx: &WithGlobalRefs, +) -> SourceResult<(PdfChunk, BTreeMap)> { + let mut chunk = PdfChunk::new(); + let mut embedded_files = BTreeMap::default(); + + let elements = ctx.document.introspector.query(&EmbedElem::elem().select()); + for elem in &elements { + if !ctx.options.standards.embedded_files { + // PDF/A-2 requires embedded files to be PDF/A-1 or PDF/A-2, + // which we don't currently check. + bail!( + elem.span(), + "file embeddings are not currently supported for PDF/A-2"; + hint: "PDF/A-3 supports arbitrary embedded files" + ); + } + + let embed = elem.to_packed::().unwrap(); + if embed.path.derived.len() > Str::PDFA_LIMIT { + bail!(embed.span(), "embedded file path is too long"); + } + + let id = embed_file(ctx, &mut chunk, embed)?; + if embedded_files.insert(embed.path.derived.clone(), id).is_some() { + bail!( + elem.span(), + "duplicate embedded file for path `{}`", embed.path.derived; + hint: "embedded file paths must be unique", + ); + } + } + + Ok((chunk, embedded_files)) +} + +/// Write the embedded file stream and its file specification. +fn embed_file( + ctx: &WithGlobalRefs, + chunk: &mut PdfChunk, + embed: &Packed, +) -> SourceResult { + let embedded_file_stream_ref = chunk.alloc.bump(); + let file_spec_dict_ref = chunk.alloc.bump(); + + let data = embed.data.as_slice(); + let compressed = deflate(data); + + let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); + embedded_file.filter(Filter::FlateDecode); + + if let Some(mime_type) = embed.mime_type(StyleChain::default()) { + if mime_type.len() > Name::PDFA_LIMIT { + bail!(embed.span(), "embedded file MIME type is too long"); + } + embedded_file.subtype(Name(mime_type.as_bytes())); + } else if ctx.options.standards.pdfa { + bail!(embed.span(), "embedded files must have a MIME type in PDF/A-3"); + } + + let mut params = embedded_file.params(); + params.size(data.len() as i32); + + let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); + if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { + params.modification_date(pdf_date); + } else if ctx.options.standards.pdfa { + bail!( + embed.span(), + "the document must have a date when embedding files in PDF/A-3"; + hint: "`set document(date: none)` must not be used in this case" + ); + } + + params.finish(); + embedded_file.finish(); + + let mut file_spec = chunk.file_spec(file_spec_dict_ref); + file_spec.path(Str(embed.path.derived.as_bytes())); + file_spec.unic_file(TextStr(&embed.path.derived)); + file_spec + .insert(Name(b"EF")) + .dict() + .pair(Name(b"F"), embedded_file_stream_ref) + .pair(Name(b"UF"), embedded_file_stream_ref); + + if ctx.options.standards.pdfa { + // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. + file_spec.association_kind(match embed.relationship(StyleChain::default()) { + Some(EmbeddedFileRelationship::Source) => AssociationKind::Source, + Some(EmbeddedFileRelationship::Data) => AssociationKind::Data, + Some(EmbeddedFileRelationship::Alternative) => AssociationKind::Alternative, + Some(EmbeddedFileRelationship::Supplement) => AssociationKind::Supplement, + None => AssociationKind::Unspecified, + }); + } + + if let Some(description) = embed.description(StyleChain::default()) { + if description.len() > Str::PDFA_LIMIT { + bail!(embed.span(), "embedded file description is too long"); + } + file_spec.description(TextStr(description)); + } + + Ok(file_spec_dict_ref) +} diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index bff7bfefa..fa326e3e0 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -5,8 +5,10 @@ use ecow::eco_format; use image::{DynamicImage, GenericImageView, Rgba}; use pdf_writer::{Chunk, Filter, Finish, Ref}; use typst_library::diag::{At, SourceResult, StrResult}; +use typst_library::foundations::Smart; use typst_library::visualize::{ - ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage, + ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, + RasterImage, SvgImage, }; use typst_utils::Deferred; @@ -32,11 +34,13 @@ pub fn write_images( EncodedImage::Raster { data, filter, - has_color, + color_space, + bits_per_component, width, height, - icc, + compressed_icc, alpha, + interpolate, } => { let image_ref = chunk.alloc(); out.insert(image.clone(), image_ref); @@ -45,23 +49,18 @@ pub fn write_images( image.filter(*filter); image.width(*width as i32); image.height(*height as i32); - image.bits_per_component(8); + image.bits_per_component(i32::from(*bits_per_component)); + image.interpolate(*interpolate); let mut icc_ref = None; let space = image.color_space(); - if icc.is_some() { + if compressed_icc.is_some() { let id = chunk.alloc.bump(); space.icc_based(id); icc_ref = Some(id); - } else if *has_color { - color::write( - ColorSpace::Srgb, - space, - &context.globals.color_functions, - ); } else { color::write( - ColorSpace::D65Gray, + *color_space, space, &context.globals.color_functions, ); @@ -79,20 +78,27 @@ pub fn write_images( mask.width(*width as i32); mask.height(*height as i32); mask.color_space().device_gray(); - mask.bits_per_component(8); + mask.bits_per_component(i32::from(*bits_per_component)); + mask.interpolate(*interpolate); } else { image.finish(); } - if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) { - let mut stream = chunk.icc_profile(icc_ref, icc); + if let (Some(compressed_icc), Some(icc_ref)) = + (compressed_icc, icc_ref) + { + let mut stream = chunk.icc_profile(icc_ref, compressed_icc); stream.filter(Filter::FlateDecode); - if *has_color { - stream.n(3); - stream.alternate().srgb(); - } else { - stream.n(1); - stream.alternate().d65_gray(); + match color_space { + ColorSpace::Srgb => { + stream.n(3); + stream.alternate().srgb(); + } + ColorSpace::D65Gray => { + stream.n(1); + stream.alternate().d65_gray(); + } + _ => unimplemented!(), } } } @@ -122,35 +128,17 @@ pub fn deferred_image( ) -> (Deferred>, Option) { let color_space = match image.kind() { ImageKind::Raster(raster) if raster.icc().is_none() => { - if raster.dynamic().color().channel_count() > 2 { - Some(ColorSpace::Srgb) - } else { - Some(ColorSpace::D65Gray) - } + Some(to_color_space(raster.dynamic().color())) } _ => None, }; + // PDF/A does not appear to allow interpolation. + // See https://github.com/typst/typst/issues/2942. + let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); + let deferred = Deferred::new(move || match image.kind() { - ImageKind::Raster(raster) => { - let raster = raster.clone(); - let (width, height) = (raster.width(), raster.height()); - let (data, filter, has_color) = encode_raster_image(&raster); - let icc = raster.icc().map(deflate); - - let alpha = - raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster)); - - Ok(EncodedImage::Raster { - data, - filter, - has_color, - width, - height, - icc, - alpha, - }) - } + ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)), ImageKind::Svg(svg) => { let (chunk, id) = encode_svg(svg, pdfa) .map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?; @@ -161,42 +149,51 @@ pub fn deferred_image( (deferred, color_space) } -/// Encode an image with a suitable filter and return the data, filter and -/// whether the image has color. -/// -/// Skips the alpha channel as that's encoded separately. +/// Encode an image with a suitable filter. #[typst_macros::time(name = "encode raster image")] -fn encode_raster_image(image: &RasterImage) -> (Vec, Filter, bool) { +fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage { let dynamic = image.dynamic(); - let channel_count = dynamic.color().channel_count(); - let has_color = channel_count > 2; + let color_space = to_color_space(dynamic.color()); - if image.format() == RasterFormat::Jpg { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner(), Filter::DctDecode, has_color) - } else { - // TODO: Encode flate streams with PNG-predictor? - let data = match (dynamic, channel_count) { - (DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()), - (DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()), - // Grayscale image - (_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()), - // Anything else - _ => deflate(dynamic.to_rgb8().as_raw()), + let (filter, data, bits_per_component) = + if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (Filter::DctDecode, data.into_inner(), 8) + } else { + // TODO: Encode flate streams with PNG-predictor? + let (data, bits_per_component) = match (dynamic, color_space) { + // RGB image. + (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8), + // Grayscale image + (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8), + (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8), + // Anything else + _ => (deflate(dynamic.to_rgb8().as_raw()), 8), + }; + (Filter::FlateDecode, data, bits_per_component) }; - (data, Filter::FlateDecode, has_color) + + let compressed_icc = image.icc().map(|data| deflate(data)); + let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic)); + + EncodedImage::Raster { + data, + filter, + color_space, + bits_per_component, + width: image.width(), + height: image.height(), + compressed_icc, + alpha, + interpolate, } } /// Encode an image's alpha channel if present. #[typst_macros::time(name = "encode alpha")] -fn encode_alpha(raster: &RasterImage) -> (Vec, Filter) { - let pixels: Vec<_> = raster - .dynamic() - .pixels() - .map(|(_, _, Rgba([_, _, _, a]))| a) - .collect(); +fn encode_alpha(image: &DynamicImage) -> (Vec, Filter) { + let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); (deflate(&pixels), Filter::FlateDecode) } @@ -208,11 +205,7 @@ fn encode_svg( ) -> Result<(Chunk, Ref), svg2pdf::ConversionError> { svg2pdf::to_chunk( svg.tree(), - svg2pdf::ConversionOptions { - pdfa, - embed_text: !svg.flatten_text(), - ..Default::default() - }, + svg2pdf::ConversionOptions { pdfa, ..Default::default() }, ) } @@ -224,19 +217,33 @@ pub enum EncodedImage { data: Vec, /// The filter to use for the image. filter: Filter, - /// Whether the image has color. - has_color: bool, + /// Which color space this image is encoded in. + color_space: ColorSpace, + /// How many bits of each color component are stored. + bits_per_component: u8, /// The image's width. width: u32, /// The image's height. height: u32, - /// The image's ICC profile, pre-deflated, if any. - icc: Option>, + /// The image's ICC profile, deflated, if any. + compressed_icc: Option>, /// The alpha channel of the image, pre-deflated, if any. alpha: Option<(Vec, Filter)>, + /// Whether image interpolation should be enabled. + interpolate: bool, }, /// A vector graphic. /// /// The chunk is the SVG converted to PDF objects. Svg(Chunk, Ref), } + +/// Matches an [`image::ColorType`] to [`ColorSpace`]. +fn to_color_space(color: image::ColorType) -> ColorSpace { + use image::ColorType::*; + match color { + L8 | La8 | L16 | La16 => ColorSpace::D65Gray, + Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, + _ => unimplemented!(), + } +} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index f45c62bb5..88e62389c 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -4,6 +4,7 @@ mod catalog; mod color; mod color_font; mod content; +mod embed; mod extg; mod font; mod gradient; @@ -14,12 +15,13 @@ mod page; mod resources; mod tiling; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::ops::{Deref, DerefMut}; use base64::Engine; +use ecow::EcoString; use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; @@ -33,6 +35,7 @@ use typst_utils::Deferred; use crate::catalog::write_catalog; use crate::color::{alloc_color_functions_refs, ColorFunctionRefs}; use crate::color_font::{write_color_fonts, ColorFontSlice}; +use crate::embed::write_embedded_files; use crate::extg::{write_graphic_states, ExtGState}; use crate::font::write_fonts; use crate::gradient::{write_gradients, PdfGradient}; @@ -67,6 +70,7 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult, } impl PdfStandards { /// Validates a list of PDF standards for compatibility and returns their /// encapsulated representation. pub fn new(list: &[PdfStandard]) -> StrResult { - Ok(Self { pdfa: list.contains(&PdfStandard::A_2b) }) + let a2b = list.contains(&PdfStandard::A_2b); + let a3b = list.contains(&PdfStandard::A_3b); + + if a2b && a3b { + bail!("PDF cannot conform to A-2B and A-3B at the same time") + } + + let pdfa = a2b || a3b; + Ok(Self { + pdfa, + embedded_files: !a2b, + pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")), + }) } } @@ -166,10 +188,9 @@ impl Debug for PdfStandards { } } -#[allow(clippy::derivable_impls)] impl Default for PdfStandards { fn default() -> Self { - Self { pdfa: false } + Self { pdfa: false, embedded_files: true, pdfa_part: None } } } @@ -186,6 +207,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[serde(rename = "a-2b")] A_2b, + /// PDF/A-3b. + #[serde(rename = "a-3b")] + A_3b, } /// A struct to build a PDF following a fixed succession of phases. @@ -316,6 +340,8 @@ struct References { tilings: HashMap, /// The IDs of written external graphics states. ext_gs: HashMap, + /// The names and references for embedded files. + embedded_files: BTreeMap, } /// At this point, the references have been assigned to all resources. The page @@ -481,6 +507,14 @@ impl Renumber for HashMap { } } +impl Renumber for BTreeMap { + fn renumber(&mut self, offset: i32) { + for v in self.values_mut() { + v.renumber(offset); + } + } +} + impl Renumber for Option { fn renumber(&mut self, offset: i32) { if let Some(r) = self { diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index b9e71319f..eff1182c1 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -70,7 +70,7 @@ pub(crate) fn write_outline( // (not exceeding whichever is the most restrictive depth limit // of those two). while children.last().is_some_and(|last| { - last_skipped_level.map_or(true, |l| last.level < l) + last_skipped_level.is_none_or(|l| last.level < l) && last.level < leaf.level }) { children = &mut children.last_mut().unwrap().children; @@ -83,7 +83,7 @@ pub(crate) fn write_outline( // needed, following the usual rules listed above. last_skipped_level = None; children.push(leaf); - } else if last_skipped_level.map_or(true, |l| leaf.level < l) { + } else if last_skipped_level.is_none_or(|l| leaf.level < l) { // Only the topmost / lowest-level skipped heading matters when you // have consecutive skipped headings (since none of them are being // added to the bookmark tree), hence the condition above. @@ -184,8 +184,7 @@ fn write_outline_item( outline.count(-(node.children.len() as i32)); } - let body = node.element.body(); - outline.title(TextStr::trimmed(body.plain_text().trim())); + outline.title(TextStr::trimmed(node.element.body.plain_text().trim())); let loc = node.element.location().unwrap(); let pos = ctx.document.introspector.position(loc); diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 6ab6d81c5..50685a962 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -15,7 +15,7 @@ use typst_library::diag::{bail, At, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{ Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, - SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles, + SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, Synthesize, Transformation, }; use typst_library::html::{tag, HtmlElem}; @@ -28,7 +28,7 @@ use typst_library::model::{ CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike, ParElem, ParbreakElem, TermsElem, }; -use typst_library::routines::{Arenas, Pair, RealizationKind}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_syntax::Span; use typst_utils::{SliceExt, SmallBitSet}; @@ -48,17 +48,18 @@ pub fn realize<'a>( locator, arenas, rules: match kind { - RealizationKind::LayoutDocument(_) | RealizationKind::LayoutFragment => { - LAYOUT_RULES - } + RealizationKind::LayoutDocument(_) => LAYOUT_RULES, + RealizationKind::LayoutFragment(_) => LAYOUT_RULES, + RealizationKind::LayoutPar => LAYOUT_PAR_RULES, RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES, - RealizationKind::HtmlFragment => HTML_FRAGMENT_RULES, + RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES, RealizationKind::Math => MATH_RULES, }, sink: vec![], groupings: ArrayVec::new(), outside: matches!(kind, RealizationKind::LayoutDocument(_)), may_attach: false, + saw_parbreak: false, kind, }; @@ -98,6 +99,8 @@ struct State<'a, 'x, 'y, 'z> { outside: bool, /// Whether now following attach spacing can survive. may_attach: bool, + /// Whether we visited any paragraph breaks. + saw_parbreak: bool, } /// Defines a rule for how certain elements shall be grouped during realization. @@ -125,6 +128,10 @@ struct GroupingRule { struct Grouping<'a> { /// The position in `s.sink` where the group starts. start: usize, + /// Only applies to `PAR` grouping: Whether this paragraph group is + /// interrupted, but not yet finished because it may be ignored due to being + /// fully inline. + interrupted: bool, /// The rule used for this grouping. rule: &'a GroupingRule, } @@ -241,7 +248,7 @@ fn visit<'a>( return Ok(()); } - // Recurse into sequences. Styled elements and sequences can currently also + // Recurse into sequences. Styled elements and sequences can currently also // have labels, so this needs to happen before they are handled. if let Some(sequence) = content.to_packed::() { for elem in &sequence.children { @@ -295,7 +302,14 @@ fn visit_math_rules<'a>( // In normal realization, we apply regex show rules to consecutive // textual elements via `TEXTUAL` grouping. However, in math, this is // not desirable, so we just do it on a per-element basis. - if let Some(elem) = content.to_packed::() { + if let Some(elem) = content.to_packed::() { + if let Some(m) = + find_regex_match_in_str(elem.text.encode_utf8(&mut [0; 4]), styles) + { + visit_regex_match(s, &[(content, styles)], m)?; + return Ok(true); + } + } else if let Some(elem) = content.to_packed::() { if let Some(m) = find_regex_match_in_str(&elem.text, styles) { visit_regex_match(s, &[(content, styles)], m)?; return Ok(true); @@ -308,6 +322,14 @@ fn visit_math_rules<'a>( visit(s, s.store(eq), styles)?; return Ok(true); } + + // Symbols in non-math content transparently convert to `TextElem` so we + // don't have to handle them in non-math layout. + if let Some(elem) = content.to_packed::() { + let text = TextElem::packed(elem.text).spanned(elem.span()); + visit(s, s.store(text), styles)?; + return Ok(true); + } } Ok(false) @@ -560,19 +582,21 @@ fn visit_styled<'a>( for style in local.iter() { let Some(elem) = style.element() else { continue }; if elem == DocumentElem::elem() { - match &mut s.kind { - RealizationKind::LayoutDocument(info) - | RealizationKind::HtmlDocument(info) => info.populate(&local), - _ => bail!( + if let Some(info) = s.kind.as_document_mut() { + info.populate(&local) + } else { + bail!( style.span(), "document set rules are not allowed inside of containers" - ), + ); } } else if elem == PageElem::elem() { - let RealizationKind::LayoutDocument(_) = s.kind else { - let span = style.span(); - bail!(span, "page configuration is not allowed inside of containers"); - }; + if !matches!(s.kind, RealizationKind::LayoutDocument(_)) { + bail!( + style.span(), + "page configuration is not allowed inside of containers" + ); + } // When there are page styles, we "break free" from our show rule cage. pagebreak = true; @@ -635,7 +659,9 @@ fn visit_grouping_rules<'a>( } // If the element can be added to the active grouping, do it. - if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) { + if !active.interrupted + && ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content)) + { s.sink.push((content, styles)); return Ok(true); } @@ -646,7 +672,7 @@ fn visit_grouping_rules<'a>( // Start a new grouping. if let Some(rule) = matching { let start = s.sink.len(); - s.groupings.push(Grouping { start, rule }); + s.groupings.push(Grouping { start, rule, interrupted: false }); s.sink.push((content, styles)); return Ok(true); } @@ -661,22 +687,24 @@ fn visit_filter_rules<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult { - if content.is::() - && !matches!(s.kind, RealizationKind::Math | RealizationKind::HtmlFragment) - { - // Outside of maths, spaces that were not collected by the paragraph - // grouper don't interest us. + if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) { + return Ok(false); + } + + if content.is::() { + // Outside of maths and paragraph realization, spaces that were not + // collected by the paragraph grouper don't interest us. return Ok(true); } else if content.is::() { // Paragraph breaks are only a boundary for paragraph grouping, we don't // need to store them. s.may_attach = false; + s.saw_parbreak = true; return Ok(true); } else if !s.may_attach && content.to_packed::().is_some_and(|elem| elem.attach(styles)) { - // Delete attach spacing collapses if not immediately following a - // paragraph. + // Attach spacing collapses if not immediately following a paragraph. return Ok(true); } @@ -688,10 +716,21 @@ fn visit_filter_rules<'a>( /// Finishes all grouping. fn finish(s: &mut State) -> SourceResult<()> { - finish_grouping_while(s, |s| !s.groupings.is_empty())?; + finish_grouping_while(s, |s| { + // If this is a fragment realization and all we've got is inline + // content, don't turn it into a paragraph. + if is_fully_inline(s) { + *s.kind.as_fragment_mut().unwrap() = FragmentKind::Inline; + s.groupings.pop(); + collapse_spaces(&mut s.sink, 0); + false + } else { + !s.groupings.is_empty() + } + })?; - // In math, spaces are top-level. - if let RealizationKind::Math = s.kind { + // In paragraph and math realization, spaces are top-level. + if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) { collapse_spaces(&mut s.sink, 0); } @@ -707,6 +746,12 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } finish_grouping_while(s, |s| { s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem)) + && if is_fully_inline(s) { + s.groupings[0].interrupted = true; + false + } else { + true + } })?; last = Some(elem); } @@ -714,9 +759,9 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } /// Finishes groupings while `f` returns `true`. -fn finish_grouping_while(s: &mut State, f: F) -> SourceResult<()> +fn finish_grouping_while(s: &mut State, mut f: F) -> SourceResult<()> where - F: Fn(&State) -> bool, + F: FnMut(&mut State) -> bool, { // Finishing of a group may result in new content and new grouping. This // can, in theory, go on for a bit. To prevent it from becoming an infinite @@ -735,7 +780,7 @@ where /// Finishes the currently innermost grouping. fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> { // The grouping we are interrupting. - let Grouping { start, rule } = s.groupings.pop().unwrap(); + let Grouping { start, rule, .. } = s.groupings.pop().unwrap(); // Trim trailing non-trigger elements. let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind)); @@ -779,14 +824,18 @@ const MAX_GROUP_NESTING: usize = 3; /// Grouping rules used in layout realization. static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; +/// Grouping rules used in paragraph layout realization. +static LAYOUT_PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; + /// Grouping rules used in HTML root realization. static HTML_DOCUMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; /// Grouping rules used in HTML fragment realization. -static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; +static HTML_FRAGMENT_RULES: &[&GroupingRule] = + &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; -/// Grouping rules used in math realizatio. +/// Grouping rules used in math realization. static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; /// Groups adjacent textual elements for text show rule application. @@ -795,6 +844,9 @@ static TEXTUAL: GroupingRule = GroupingRule { tags: true, trigger: |content, _| { let elem = content.elem(); + // Note that `SymbolElem` converts into `TextElem` before textual show + // rules run, and we apply textual rules to elements manually during + // math realization, so we don't check for it here. elem == TextElem::elem() || elem == LinebreakElem::elem() || elem == SmartQuoteElem::elem() @@ -818,12 +870,10 @@ static PAR: GroupingRule = GroupingRule { || elem == SmartQuoteElem::elem() || elem == InlineElem::elem() || elem == BoxElem::elem() - || (matches!( - kind, - RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment - ) && content - .to_packed::() - .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) + || (kind.is_html() + && content + .to_packed::() + .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) }, inner: |content| content.elem() == SpaceElem::elem(), interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(), @@ -896,17 +946,31 @@ fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> { // transparently become part of it. // 2. There is no group at all. In this case, we create one. if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) { - s.groupings.push(Grouping { start, rule: &PAR }); + s.groupings.push(Grouping { start, rule: &PAR, interrupted: false }); } Ok(()) } /// Whether there is an active grouping, but it is not a `PAR` grouping. -fn in_non_par_grouping(s: &State) -> bool { - s.groupings - .last() - .is_some_and(|grouping| !std::ptr::eq(grouping.rule, &PAR)) +fn in_non_par_grouping(s: &mut State) -> bool { + s.groupings.last().is_some_and(|grouping| { + !std::ptr::eq(grouping.rule, &PAR) || grouping.interrupted + }) +} + +/// Whether there is exactly one active grouping, it is a `PAR` grouping, and it +/// spans the whole sink (with the exception of leading tags). +fn is_fully_inline(s: &State) -> bool { + s.kind.is_fragment() + && !s.saw_parbreak + && match s.groupings.as_slice() { + [grouping] => { + std::ptr::eq(grouping.rule, &PAR) + && s.sink[..grouping.start].iter().all(|(c, _)| c.is::()) + } + _ => false, + } } /// Builds the `ParElem` from inline-level elements. @@ -918,11 +982,11 @@ fn finish_par(mut grouped: Grouped) -> SourceResult<()> { // Collect the children. let elems = grouped.get(); let span = select_span(elems); - let (children, trunk) = StyleVec::create(elems); + let (body, trunk) = repack(elems); // Create and visit the paragraph. let s = grouped.end(); - let elem = ParElem::new(children).pack().spanned(span); + let elem = ParElem::new(body).pack().spanned(span); visit(s, s.store(elem), trunk) } @@ -1118,7 +1182,16 @@ fn visit_regex_match<'a>( m: RegexMatch<'a>, ) -> SourceResult<()> { let match_range = m.offset..m.offset + m.text.len(); - let piece = TextElem::packed(m.text); + + // Replace with the correct intuitive element kind: if matching against a + // lone symbol, return a `SymbolElem`, otherwise return a newly composed + // `TextElem`. We should only match against a `SymbolElem` during math + // realization (`RealizationKind::Math`). + let piece = match elems { + &[(lone, _)] if lone.is::() => lone.clone(), + _ => TextElem::packed(m.text), + }; + let context = Context::new(None, Some(m.styles)); let output = m.recipe.apply(s.engine, context.track(), piece)?; @@ -1141,10 +1214,16 @@ fn visit_regex_match<'a>( continue; } - // At this point, we can have a `TextElem`, `SpaceElem`, + // At this point, we can have a `TextElem`, `SymbolElem`, `SpaceElem`, // `LinebreakElem`, or `SmartQuoteElem`. We now determine the range of // the element. - let len = content.to_packed::().map_or(1, |elem| elem.text.len()); + let len = if let Some(elem) = content.to_packed::() { + elem.text.len() + } else if let Some(elem) = content.to_packed::() { + elem.text.len_utf8() + } else { + 1 // The rest are Ascii, so just one byte. + }; let elem_range = cursor..cursor + len; // If the element starts before the start of match, visit it fully or @@ -1244,3 +1323,26 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) { fn select_span(children: &[Pair]) -> Span { Span::find(children.iter().map(|(c, _)| c.span())) } + +/// Turn realized content with styles back into owned content and a trunk style +/// chain. +fn repack<'a>(buf: &[Pair<'a>]) -> (Content, StyleChain<'a>) { + let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); + let depth = trunk.links().count(); + + let mut seq = Vec::with_capacity(buf.len()); + + for (chain, group) in buf.group_by_key(|&(_, s)| s) { + let iter = group.iter().map(|&(c, _)| c.clone()); + let suffix = chain.suffix(depth); + if suffix.is_empty() { + seq.extend(iter); + } else if let &[(element, _)] = group { + seq.push(element.clone().styled_with_map(suffix)); + } else { + seq.push(Content::sequence(iter).styled_with_map(suffix)); + } + } + + (Content::sequence(seq), trunk) +} diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 27b039113..7425bdd2f 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; use tiny_skia as sk; +use typst_library::foundations::Smart; use typst_library::layout::Size; -use typst_library::visualize::{Image, ImageKind}; +use typst_library::visualize::{Image, ImageKind, ImageScaling}; use crate::{AbsExt, State}; @@ -34,7 +35,7 @@ pub fn render_image( let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32; let h = ((w as f32) / aspect).ceil() as u32; - let pixmap = scaled_texture(image, w, h)?; + let pixmap = build_texture(image, w, h)?; let paint_scale_x = view_width / pixmap.width() as f32; let paint_scale_y = view_height / pixmap.height() as f32; @@ -57,29 +58,42 @@ pub fn render_image( /// Prepare a texture for an image at a scaled size. #[comemo::memoize] -fn scaled_texture(image: &Image, w: u32, h: u32) -> Option> { - let mut pixmap = sk::Pixmap::new(w, h)?; +fn build_texture(image: &Image, w: u32, h: u32) -> Option> { + let mut texture = sk::Pixmap::new(w, h)?; match image.kind() { ImageKind::Raster(raster) => { - let downscale = w < raster.width(); - let filter = - if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; - let buf = raster.dynamic().resize(w, h, filter); - for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { + let w = texture.width(); + let h = texture.height(); + + let buf; + let dynamic = raster.dynamic(); + let resized = if (w, h) == (dynamic.width(), dynamic.height()) { + // Small optimization to not allocate in case image is not resized. + dynamic + } else { + let upscale = w > dynamic.width(); + let filter = match image.scaling() { + Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest, + _ if upscale => FilterType::CatmullRom, + _ => FilterType::Lanczos3, // downscale + }; + buf = dynamic.resize_exact(w, h, filter); + &buf + }; + + for ((_, _, src), dest) in resized.pixels().zip(texture.pixels_mut()) { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } } - // Safety: We do not keep any references to tree nodes beyond the scope - // of `with`. ImageKind::Svg(svg) => { let tree = svg.tree(); let ts = tiny_skia::Transform::from_scale( w as f32 / tree.size().width(), h as f32 / tree.size().height(), ); - resvg::render(tree, ts, &mut pixmap.as_mut()) + resvg::render(tree, ts, &mut texture.as_mut()); } } - Some(Arc::new(pixmap)) + Some(Arc::new(texture)) } diff --git a/crates/typst-svg/Cargo.toml b/crates/typst-svg/Cargo.toml index 41d355659..5416621e5 100644 --- a/crates/typst-svg/Cargo.toml +++ b/crates/typst-svg/Cargo.toml @@ -21,6 +21,7 @@ base64 = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } flate2 = { workspace = true } +image = { workspace = true } ttf-parser = { workspace = true } xmlparser = { workspace = true } xmlwriter = { workspace = true } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index ede4e76e3..d74432026 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,7 +1,11 @@ use base64::Engine; use ecow::{eco_format, EcoString}; +use image::{codecs::png::PngEncoder, ImageEncoder}; +use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; -use typst_library::visualize::{Image, ImageFormat, RasterFormat, VectorFormat}; +use typst_library::visualize::{ + ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, +}; use crate::SVGRenderer; @@ -14,6 +18,17 @@ impl SVGRenderer { self.xml.write_attribute("width", &size.x.to_pt()); self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("preserveAspectRatio", "none"); + match image.scaling() { + Smart::Auto => {} + Smart::Custom(ImageScaling::Smooth) => { + // This is still experimental and not implemented in all major browsers. + // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility + self.xml.write_attribute("style", "image-rendering: smooth") + } + Smart::Custom(ImageScaling::Pixelated) => { + self.xml.write_attribute("style", "image-rendering: pixelated") + } + } self.xml.end_element(); } } @@ -22,19 +37,32 @@ impl SVGRenderer { /// `data:image/{format};base64,`. #[comemo::memoize] pub fn convert_image_to_base64_url(image: &Image) -> EcoString { - let format = match image.format() { - ImageFormat::Raster(f) => match f { - RasterFormat::Png => "png", - RasterFormat::Jpg => "jpeg", - RasterFormat::Gif => "gif", - }, - ImageFormat::Vector(f) => match f { - VectorFormat::Svg => "svg+xml", + let mut buf; + let (format, data): (&str, &[u8]) = match image.kind() { + ImageKind::Raster(raster) => match raster.format() { + RasterFormat::Exchange(format) => ( + match format { + ExchangeFormat::Png => "png", + ExchangeFormat::Jpg => "jpeg", + ExchangeFormat::Gif => "gif", + }, + raster.data(), + ), + RasterFormat::Pixel(_) => ("png", { + buf = vec![]; + let mut encoder = PngEncoder::new(&mut buf); + if let Some(icc_profile) = raster.icc() { + encoder.set_icc_profile(icc_profile.to_vec()).ok(); + } + raster.dynamic().write_with_encoder(encoder).unwrap(); + buf.as_slice() + }), }, + ImageKind::Svg(svg) => ("svg+xml", svg.data()), }; let mut url = eco_format!("data:image/{format};base64,"); - let data = base64::engine::general_purpose::STANDARD.encode(image.data()); + let data = base64::engine::general_purpose::STANDARD.encode(data); url.push_str(&data); url } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 80de32089..e6620a59e 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -3,9 +3,12 @@ use std::io::Read; use base64::Engine; use ecow::EcoString; use ttf_parser::GlyphId; +use typst_library::foundations::Bytes; use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; use typst_library::text::{Font, TextItem}; -use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; +use typst_library::visualize::{ + ExchangeFormat, FillRule, Image, Paint, RasterImage, RelativeTo, +}; use typst_utils::hash128; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -243,7 +246,9 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64 if raster.format != ttf_parser::RasterImageFormat::PNG { return None; } - let image = Image::new(raster.data.into(), RasterFormat::Png.into(), None).ok()?; + let image = Image::plain( + RasterImage::plain(Bytes::new(raster.data.to_vec()), ExchangeFormat::Png).ok()?, + ); Some((image, raster.x as f64, raster.y as f64)) } diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 19e123727..f79e65982 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -1,14 +1,92 @@ -//! A typed layer over the untyped syntax tree. -//! -//! The AST is rooted in the [`Markup`] node. +/*! +# Abstract Syntax Tree Interface + +Typst's Abstract Syntax Tree (AST) is a lazy, typed view over the untyped +Concrete Syntax Tree (CST) and is rooted in the [`Markup`] node. + +## The AST is a View + +Most AST nodes are wrapper structs around [`SyntaxNode`] pointers. This summary +will use a running example of the [`Raw`] node type, which is declared (after +macro expansion) as: `struct Raw<'a>(&'a SyntaxNode);`. + +[`SyntaxNode`]s are generated by the parser and constitute the Concrete Syntax +Tree (CST). The CST is _concrete_ because it has the property that an in-order +tree traversal will recreate the text of the source file exactly. + +[`SyntaxNode`]s in the CST contain their [`SyntaxKind`], but don't themselves +provide access to the semantic meaning of their contents. That semantic meaning +is available through the Abstract Syntax Tree by iterating over CST nodes and +inspecting their contents. The format is prepared ahead-of-time by the parser so +that this module can unpack the abstract meaning from the CST's structure. + +Raw nodes are parsed by recognizing paired backtick delimiters, which you will +find as CST nodes with the [`RawDelim`] kind. However, the AST doesn't include +these delimiters because it _abstracts_ over the backticks. Instead, the parent +raw node will only use its child [`RawDelim`] CST nodes to determine whether the +element is a block or inline. + +## The AST is Typed + +AST nodes all implement the [`AstNode`] trait, but nodes can also implement +their own unique methods. These unique methods are the "real" interface of the +AST, and provide access to the abstract, semantic, representation of each kind +of node. For example, the [`Raw`] node provides 3 methods that specify its +abstract representation: [`Raw::lines()`] returns the raw text as an iterator of +lines, [`Raw::lang()`] provides the optionally present [`RawLang`] language tag, +and [`Raw::block()`] gives a bool for whether the raw element is a block or +inline. + +This semantic information is unavailable in the CST. Only by converting a CST +node to an AST struct will Rust let you call a method of that struct. This is a +safe interface because the only way to create an AST node outside this file is +to call [`AstNode::from_untyped`]. The `node!` macro implements `from_untyped` +by checking the node's kind before constructing it, returning `Some()` only if +the kind matches. So we know that it will have the expected children underneath, +otherwise the parser wouldn't have produced this node. + +## The AST is rooted in the [`Markup`] node + +The AST is rooted in the [`Markup`] node, which provides only one method: +[`Markup::exprs`]. This returns an iterator of the main [`Expr`] enum. [`Expr`] +is important because it contains the majority of expressions that Typst will +evaluate. Not just markup, but also math and code expressions. Not all +expression types are available from the parser at every step, but this does +decrease the amount of wrapper enums needed in the AST (and this file is long +enough already). + +Expressions also branch off into the remaining tree. You can view enums in this +file as edges on a graph: areas where the tree has paths from one type to +another (accessed through methods), then structs are the nodes of the graph, +providing methods that return enums, etc. etc. + +## The AST is Lazy + +Being lazy means that the untyped CST nodes are converted to typed AST nodes +only as the tree is traversed. If we parse a file and a raw block is contained +in a branch of an if-statement that we don't take, then we won't pay the cost of +creating an iterator over the lines or checking whether it was a block or +inline (although it will still be parsed into nodes). + +This is also a factor of the current "tree-interpreter" evaluation model. A +bytecode interpreter might instead eagerly convert the AST into bytecode, but it +would still traverse using this lazy interface. While the tree-interpreter +evaluation is straightforward and easy to add new features onto, it has to +re-traverse the AST every time a function is evaluated. A bytecode interpreter +using the lazy interface would only need to traverse each node once, improving +throughput at the cost of initial latency and development flexibility. +*/ use std::num::NonZeroUsize; use std::ops::Deref; +use std::path::Path; +use std::str::FromStr; use ecow::EcoString; use unscanny::Scanner; -use crate::{is_newline, Span, SyntaxKind, SyntaxNode}; +use crate::package::PackageSpec; +use crate::{is_ident, is_newline, Span, SyntaxKind, SyntaxNode}; /// A typed AST node. pub trait AstNode<'a>: Sized { @@ -24,8 +102,55 @@ pub trait AstNode<'a>: Sized { } } +// A generic interface for converting untyped nodes into typed AST nodes. +impl SyntaxNode { + /// Whether the node can be cast to the given AST node. + pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool { + self.cast::().is_some() + } + + /// Try to convert the node to a typed AST node. + pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option { + T::from_untyped(self) + } + + /// Find the first child that can cast to the AST type `T`. + fn try_cast_first<'a, T: AstNode<'a>>(&'a self) -> Option { + self.children().find_map(Self::cast) + } + + /// Find the last child that can cast to the AST type `T`. + fn try_cast_last<'a, T: AstNode<'a>>(&'a self) -> Option { + self.children().rev().find_map(Self::cast) + } + + /// Get the first child of AST type `T` or a placeholder if none. + fn cast_first<'a, T: AstNode<'a> + Default>(&'a self) -> T { + self.try_cast_first().unwrap_or_default() + } + + /// Get the last child of AST type `T` or a placeholder if none. + fn cast_last<'a, T: AstNode<'a> + Default>(&'a self) -> T { + self.try_cast_last().unwrap_or_default() + } +} + +/// Implements [`AstNode`] for a struct whose name matches a [`SyntaxKind`] +/// variant. +/// +/// The struct becomes a wrapper around a [`SyntaxNode`] pointer, and the +/// implementation of [`AstNode::from_untyped`] checks that the pointer's kind +/// matches when converting, returning `Some` or `None` respectively. +/// +/// The generated struct is the basis for typed accessor methods for properties +/// of this AST node. For example, the [`Raw`] struct has methods for accessing +/// its content by lines, its optional language tag, and whether the raw element +/// is inline or a block. These methods are accessible only _after_ a +/// `SyntaxNode` is coerced to the `Raw` struct type (via `from_untyped`), +/// guaranteeing their implementations will work with the expected structure. macro_rules! node { - ($(#[$attr:meta])* $name:ident) => { + ($(#[$attr:meta])* struct $name:ident) => { + // Create the struct as a wrapper around a `SyntaxNode` reference. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[repr(transparent)] $(#[$attr])* @@ -60,7 +185,7 @@ macro_rules! node { node! { /// The syntactical root capable of representing a full parsed document. - Markup + struct Markup } impl<'a> Markup<'a> { @@ -114,15 +239,17 @@ pub enum Expr<'a> { /// A section heading: `= Introduction`. Heading(Heading<'a>), /// An item in a bullet list: `- ...`. - List(ListItem<'a>), + ListItem(ListItem<'a>), /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - Enum(EnumItem<'a>), + EnumItem(EnumItem<'a>), /// An item in a term list: `/ Term: Details`. - Term(TermItem<'a>), + TermItem(TermItem<'a>), /// A mathematical equation: `$x$`, `$ x^2 $`. Equation(Equation<'a>), /// The contents of a mathematical equation: `x^2 + 1`. Math(Math<'a>), + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. + MathText(MathText<'a>), /// An identifier in math: `pi`. MathIdent(MathIdent<'a>), /// A shorthand for a unicode codepoint in math: `a <= b`. @@ -156,9 +283,9 @@ pub enum Expr<'a> { /// A quoted string: `"..."`. Str(Str<'a>), /// A code block: `{ let x = 1; x + 2 }`. - Code(CodeBlock<'a>), + CodeBlock(CodeBlock<'a>), /// A content block: `[*Hi* there!]`. - Content(ContentBlock<'a>), + ContentBlock(ContentBlock<'a>), /// A grouped expression: `(1 + 2)`. Parenthesized(Parenthesized<'a>), /// An array: `(1, "hi", 12cm)`. @@ -176,37 +303,37 @@ pub enum Expr<'a> { /// A closure: `(x, y) => z`. Closure(Closure<'a>), /// A let binding: `let x = 1`. - Let(LetBinding<'a>), + LetBinding(LetBinding<'a>), /// A destructuring assignment: `(x, y) = (1, 2)`. - DestructAssign(DestructAssignment<'a>), + DestructAssignment(DestructAssignment<'a>), /// A set rule: `set text(...)`. - Set(SetRule<'a>), + SetRule(SetRule<'a>), /// A show rule: `show heading: it => emph(it.body)`. - Show(ShowRule<'a>), + ShowRule(ShowRule<'a>), /// A contextual expression: `context text.lang`. Contextual(Contextual<'a>), /// An if-else conditional: `if x { y } else { z }`. Conditional(Conditional<'a>), /// A while loop: `while x { y }`. - While(WhileLoop<'a>), + WhileLoop(WhileLoop<'a>), /// A for loop: `for x in y { z }`. - For(ForLoop<'a>), + ForLoop(ForLoop<'a>), /// A module import: `import "utils.typ": a, b, c`. - Import(ModuleImport<'a>), + ModuleImport(ModuleImport<'a>), /// A module include: `include "chapter1.typ"`. - Include(ModuleInclude<'a>), + ModuleInclude(ModuleInclude<'a>), /// A break from a loop: `break`. - Break(LoopBreak<'a>), + LoopBreak(LoopBreak<'a>), /// A continue in a loop: `continue`. - Continue(LoopContinue<'a>), + LoopContinue(LoopContinue<'a>), /// A return from a function: `return`, `return x + 1`. - Return(FuncReturn<'a>), + FuncReturn(FuncReturn<'a>), } impl<'a> Expr<'a> { fn cast_with_space(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Space => node.cast().map(Self::Space), + SyntaxKind::Space => Some(Self::Space(Space(node))), _ => Self::from_untyped(node), } } @@ -215,63 +342,69 @@ impl<'a> Expr<'a> { impl<'a> AstNode<'a> for Expr<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Linebreak => node.cast().map(Self::Linebreak), - SyntaxKind::Parbreak => node.cast().map(Self::Parbreak), - SyntaxKind::Text => node.cast().map(Self::Text), - SyntaxKind::Escape => node.cast().map(Self::Escape), - SyntaxKind::Shorthand => node.cast().map(Self::Shorthand), - SyntaxKind::SmartQuote => node.cast().map(Self::SmartQuote), - SyntaxKind::Strong => node.cast().map(Self::Strong), - SyntaxKind::Emph => node.cast().map(Self::Emph), - SyntaxKind::Raw => node.cast().map(Self::Raw), - SyntaxKind::Link => node.cast().map(Self::Link), - SyntaxKind::Label => node.cast().map(Self::Label), - SyntaxKind::Ref => node.cast().map(Self::Ref), - SyntaxKind::Heading => node.cast().map(Self::Heading), - SyntaxKind::ListItem => node.cast().map(Self::List), - SyntaxKind::EnumItem => node.cast().map(Self::Enum), - SyntaxKind::TermItem => node.cast().map(Self::Term), - SyntaxKind::Equation => node.cast().map(Self::Equation), - SyntaxKind::Math => node.cast().map(Self::Math), - SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), - SyntaxKind::MathShorthand => node.cast().map(Self::MathShorthand), - SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), - SyntaxKind::MathDelimited => node.cast().map(Self::MathDelimited), - SyntaxKind::MathAttach => node.cast().map(Self::MathAttach), - SyntaxKind::MathPrimes => node.cast().map(Self::MathPrimes), - SyntaxKind::MathFrac => node.cast().map(Self::MathFrac), - SyntaxKind::MathRoot => node.cast().map(Self::MathRoot), - SyntaxKind::Ident => node.cast().map(Self::Ident), - SyntaxKind::None => node.cast().map(Self::None), - SyntaxKind::Auto => node.cast().map(Self::Auto), - SyntaxKind::Bool => node.cast().map(Self::Bool), - SyntaxKind::Int => node.cast().map(Self::Int), - SyntaxKind::Float => node.cast().map(Self::Float), - SyntaxKind::Numeric => node.cast().map(Self::Numeric), - SyntaxKind::Str => node.cast().map(Self::Str), - SyntaxKind::CodeBlock => node.cast().map(Self::Code), - SyntaxKind::ContentBlock => node.cast().map(Self::Content), - SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), - SyntaxKind::Array => node.cast().map(Self::Array), - SyntaxKind::Dict => node.cast().map(Self::Dict), - SyntaxKind::Unary => node.cast().map(Self::Unary), - SyntaxKind::Binary => node.cast().map(Self::Binary), - SyntaxKind::FieldAccess => node.cast().map(Self::FieldAccess), - SyntaxKind::FuncCall => node.cast().map(Self::FuncCall), - SyntaxKind::Closure => node.cast().map(Self::Closure), - SyntaxKind::LetBinding => node.cast().map(Self::Let), - SyntaxKind::DestructAssignment => node.cast().map(Self::DestructAssign), - SyntaxKind::SetRule => node.cast().map(Self::Set), - SyntaxKind::ShowRule => node.cast().map(Self::Show), - SyntaxKind::Contextual => node.cast().map(Self::Contextual), - SyntaxKind::Conditional => node.cast().map(Self::Conditional), - SyntaxKind::WhileLoop => node.cast().map(Self::While), - SyntaxKind::ForLoop => node.cast().map(Self::For), - SyntaxKind::ModuleImport => node.cast().map(Self::Import), - SyntaxKind::ModuleInclude => node.cast().map(Self::Include), - SyntaxKind::LoopBreak => node.cast().map(Self::Break), - SyntaxKind::LoopContinue => node.cast().map(Self::Continue), - SyntaxKind::FuncReturn => node.cast().map(Self::Return), + SyntaxKind::Space => Option::None, // Skipped unless using `cast_with_space`. + SyntaxKind::Linebreak => Some(Self::Linebreak(Linebreak(node))), + SyntaxKind::Parbreak => Some(Self::Parbreak(Parbreak(node))), + SyntaxKind::Text => Some(Self::Text(Text(node))), + SyntaxKind::Escape => Some(Self::Escape(Escape(node))), + SyntaxKind::Shorthand => Some(Self::Shorthand(Shorthand(node))), + SyntaxKind::SmartQuote => Some(Self::SmartQuote(SmartQuote(node))), + SyntaxKind::Strong => Some(Self::Strong(Strong(node))), + SyntaxKind::Emph => Some(Self::Emph(Emph(node))), + SyntaxKind::Raw => Some(Self::Raw(Raw(node))), + SyntaxKind::Link => Some(Self::Link(Link(node))), + SyntaxKind::Label => Some(Self::Label(Label(node))), + SyntaxKind::Ref => Some(Self::Ref(Ref(node))), + SyntaxKind::Heading => Some(Self::Heading(Heading(node))), + SyntaxKind::ListItem => Some(Self::ListItem(ListItem(node))), + SyntaxKind::EnumItem => Some(Self::EnumItem(EnumItem(node))), + SyntaxKind::TermItem => Some(Self::TermItem(TermItem(node))), + SyntaxKind::Equation => Some(Self::Equation(Equation(node))), + SyntaxKind::Math => Some(Self::Math(Math(node))), + SyntaxKind::MathText => Some(Self::MathText(MathText(node))), + SyntaxKind::MathIdent => Some(Self::MathIdent(MathIdent(node))), + SyntaxKind::MathShorthand => Some(Self::MathShorthand(MathShorthand(node))), + SyntaxKind::MathAlignPoint => { + Some(Self::MathAlignPoint(MathAlignPoint(node))) + } + SyntaxKind::MathDelimited => Some(Self::MathDelimited(MathDelimited(node))), + SyntaxKind::MathAttach => Some(Self::MathAttach(MathAttach(node))), + SyntaxKind::MathPrimes => Some(Self::MathPrimes(MathPrimes(node))), + SyntaxKind::MathFrac => Some(Self::MathFrac(MathFrac(node))), + SyntaxKind::MathRoot => Some(Self::MathRoot(MathRoot(node))), + SyntaxKind::Ident => Some(Self::Ident(Ident(node))), + SyntaxKind::None => Some(Self::None(None(node))), + SyntaxKind::Auto => Some(Self::Auto(Auto(node))), + SyntaxKind::Bool => Some(Self::Bool(Bool(node))), + SyntaxKind::Int => Some(Self::Int(Int(node))), + SyntaxKind::Float => Some(Self::Float(Float(node))), + SyntaxKind::Numeric => Some(Self::Numeric(Numeric(node))), + SyntaxKind::Str => Some(Self::Str(Str(node))), + SyntaxKind::CodeBlock => Some(Self::CodeBlock(CodeBlock(node))), + SyntaxKind::ContentBlock => Some(Self::ContentBlock(ContentBlock(node))), + SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))), + SyntaxKind::Array => Some(Self::Array(Array(node))), + SyntaxKind::Dict => Some(Self::Dict(Dict(node))), + SyntaxKind::Unary => Some(Self::Unary(Unary(node))), + SyntaxKind::Binary => Some(Self::Binary(Binary(node))), + SyntaxKind::FieldAccess => Some(Self::FieldAccess(FieldAccess(node))), + SyntaxKind::FuncCall => Some(Self::FuncCall(FuncCall(node))), + SyntaxKind::Closure => Some(Self::Closure(Closure(node))), + SyntaxKind::LetBinding => Some(Self::LetBinding(LetBinding(node))), + SyntaxKind::DestructAssignment => { + Some(Self::DestructAssignment(DestructAssignment(node))) + } + SyntaxKind::SetRule => Some(Self::SetRule(SetRule(node))), + SyntaxKind::ShowRule => Some(Self::ShowRule(ShowRule(node))), + SyntaxKind::Contextual => Some(Self::Contextual(Contextual(node))), + SyntaxKind::Conditional => Some(Self::Conditional(Conditional(node))), + SyntaxKind::WhileLoop => Some(Self::WhileLoop(WhileLoop(node))), + SyntaxKind::ForLoop => Some(Self::ForLoop(ForLoop(node))), + SyntaxKind::ModuleImport => Some(Self::ModuleImport(ModuleImport(node))), + SyntaxKind::ModuleInclude => Some(Self::ModuleInclude(ModuleInclude(node))), + SyntaxKind::LoopBreak => Some(Self::LoopBreak(LoopBreak(node))), + SyntaxKind::LoopContinue => Some(Self::LoopContinue(LoopContinue(node))), + SyntaxKind::FuncReturn => Some(Self::FuncReturn(FuncReturn(node))), _ => Option::None, } } @@ -292,11 +425,12 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Label(v) => v.to_untyped(), Self::Ref(v) => v.to_untyped(), Self::Heading(v) => v.to_untyped(), - Self::List(v) => v.to_untyped(), - Self::Enum(v) => v.to_untyped(), - Self::Term(v) => v.to_untyped(), + Self::ListItem(v) => v.to_untyped(), + Self::EnumItem(v) => v.to_untyped(), + Self::TermItem(v) => v.to_untyped(), Self::Equation(v) => v.to_untyped(), Self::Math(v) => v.to_untyped(), + Self::MathText(v) => v.to_untyped(), Self::MathIdent(v) => v.to_untyped(), Self::MathShorthand(v) => v.to_untyped(), Self::MathAlignPoint(v) => v.to_untyped(), @@ -313,8 +447,8 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Float(v) => v.to_untyped(), Self::Numeric(v) => v.to_untyped(), Self::Str(v) => v.to_untyped(), - Self::Code(v) => v.to_untyped(), - Self::Content(v) => v.to_untyped(), + Self::CodeBlock(v) => v.to_untyped(), + Self::ContentBlock(v) => v.to_untyped(), Self::Array(v) => v.to_untyped(), Self::Dict(v) => v.to_untyped(), Self::Parenthesized(v) => v.to_untyped(), @@ -323,19 +457,19 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::FieldAccess(v) => v.to_untyped(), Self::FuncCall(v) => v.to_untyped(), Self::Closure(v) => v.to_untyped(), - Self::Let(v) => v.to_untyped(), - Self::DestructAssign(v) => v.to_untyped(), - Self::Set(v) => v.to_untyped(), - Self::Show(v) => v.to_untyped(), + Self::LetBinding(v) => v.to_untyped(), + Self::DestructAssignment(v) => v.to_untyped(), + Self::SetRule(v) => v.to_untyped(), + Self::ShowRule(v) => v.to_untyped(), Self::Contextual(v) => v.to_untyped(), Self::Conditional(v) => v.to_untyped(), - Self::While(v) => v.to_untyped(), - Self::For(v) => v.to_untyped(), - Self::Import(v) => v.to_untyped(), - Self::Include(v) => v.to_untyped(), - Self::Break(v) => v.to_untyped(), - Self::Continue(v) => v.to_untyped(), - Self::Return(v) => v.to_untyped(), + Self::WhileLoop(v) => v.to_untyped(), + Self::ForLoop(v) => v.to_untyped(), + Self::ModuleImport(v) => v.to_untyped(), + Self::ModuleInclude(v) => v.to_untyped(), + Self::LoopBreak(v) => v.to_untyped(), + Self::LoopContinue(v) => v.to_untyped(), + Self::FuncReturn(v) => v.to_untyped(), } } } @@ -353,25 +487,25 @@ impl Expr<'_> { | Self::Float(_) | Self::Numeric(_) | Self::Str(_) - | Self::Code(_) - | Self::Content(_) + | Self::CodeBlock(_) + | Self::ContentBlock(_) | Self::Array(_) | Self::Dict(_) | Self::Parenthesized(_) | Self::FieldAccess(_) | Self::FuncCall(_) - | Self::Let(_) - | Self::Set(_) - | Self::Show(_) + | Self::LetBinding(_) + | Self::SetRule(_) + | Self::ShowRule(_) | Self::Contextual(_) | Self::Conditional(_) - | Self::While(_) - | Self::For(_) - | Self::Import(_) - | Self::Include(_) - | Self::Break(_) - | Self::Continue(_) - | Self::Return(_) + | Self::WhileLoop(_) + | Self::ForLoop(_) + | Self::ModuleImport(_) + | Self::ModuleInclude(_) + | Self::LoopBreak(_) + | Self::LoopContinue(_) + | Self::FuncReturn(_) ) } @@ -398,7 +532,7 @@ impl Default for Expr<'_> { node! { /// Plain text without markup. - Text + struct Text } impl<'a> Text<'a> { @@ -411,22 +545,22 @@ impl<'a> Text<'a> { node! { /// Whitespace in markup or math. Has at most one newline in markup, as more /// indicate a paragraph break. - Space + struct Space } node! { /// A forced line break: `\`. - Linebreak + struct Linebreak } node! { /// A paragraph break, indicated by one or multiple blank lines. - Parbreak + struct Parbreak } node! { /// An escape sequence: `\#`, `\u{1F5FA}`. - Escape + struct Escape } impl Escape<'_> { @@ -449,7 +583,7 @@ impl Escape<'_> { node! { /// A shorthand for a unicode codepoint. For example, `~` for a non-breaking /// space or `-?` for a soft hyphen. - Shorthand + struct Shorthand } impl Shorthand<'_> { @@ -475,7 +609,7 @@ impl Shorthand<'_> { node! { /// A smart quote: `'` or `"`. - SmartQuote + struct SmartQuote } impl SmartQuote<'_> { @@ -487,31 +621,31 @@ impl SmartQuote<'_> { node! { /// Strong content: `*Strong*`. - Strong + struct Strong } impl<'a> Strong<'a> { /// The contents of the strong node. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// Emphasized content: `_Emphasized_`. - Emph + struct Emph } impl<'a> Emph<'a> { /// The contents of the emphasis node. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// Raw text with optional syntax highlighting: `` `...` ``. - Raw + struct Raw } impl<'a> Raw<'a> { @@ -523,18 +657,18 @@ impl<'a> Raw<'a> { /// An optional identifier specifying the language to syntax-highlight in. pub fn lang(self) -> Option> { // Only blocky literals are supposed to contain a language. - let delim: RawDelim = self.0.cast_first_match()?; + let delim: RawDelim = self.0.try_cast_first()?; if delim.0.len() < 3 { return Option::None; } - self.0.cast_first_match() + self.0.try_cast_first() } /// Whether the raw text should be displayed in a separate block. pub fn block(self) -> bool { self.0 - .cast_first_match() + .try_cast_first() .is_some_and(|delim: RawDelim| delim.0.len() >= 3) && self.0.children().any(|e| { e.kind() == SyntaxKind::RawTrimmed && e.text().chars().any(is_newline) @@ -544,7 +678,7 @@ impl<'a> Raw<'a> { node! { /// A language tag at the start of raw element: ``typ ``. - RawLang + struct RawLang } impl<'a> RawLang<'a> { @@ -556,12 +690,12 @@ impl<'a> RawLang<'a> { node! { /// A raw delimiter in single or 3+ backticks: `` ` ``. - RawDelim + struct RawDelim } node! { /// A hyperlink: `https://typst.org`. - Link + struct Link } impl<'a> Link<'a> { @@ -573,7 +707,7 @@ impl<'a> Link<'a> { node! { /// A label: ``. - Label + struct Label } impl<'a> Label<'a> { @@ -585,7 +719,7 @@ impl<'a> Label<'a> { node! { /// A reference: `@target`, `@target[..]`. - Ref + struct Ref } impl<'a> Ref<'a> { @@ -600,19 +734,19 @@ impl<'a> Ref<'a> { /// Get the supplement. pub fn supplement(self) -> Option> { - self.0.cast_last_match() + self.0.try_cast_last() } } node! { /// A section heading: `= Introduction`. - Heading + struct Heading } impl<'a> Heading<'a> { /// The contents of the heading. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The section depth (number of equals signs). @@ -627,19 +761,19 @@ impl<'a> Heading<'a> { node! { /// An item in a bullet list: `- ...`. - ListItem + struct ListItem } impl<'a> ListItem<'a> { /// The contents of the list item. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An item in an enumeration (numbered list): `+ ...` or `1. ...`. - EnumItem + struct EnumItem } impl<'a> EnumItem<'a> { @@ -653,36 +787,36 @@ impl<'a> EnumItem<'a> { /// The contents of the list item. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An item in a term list: `/ Term: Details`. - TermItem + struct TermItem } impl<'a> TermItem<'a> { /// The term described by the item. pub fn term(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The description of the term. pub fn description(self) -> Markup<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A mathematical equation: `$x$`, `$ x^2 $`. - Equation + struct Equation } impl<'a> Equation<'a> { /// The contained math. pub fn body(self) -> Math<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// Whether the equation should be displayed as a separate block. @@ -696,7 +830,7 @@ impl<'a> Equation<'a> { node! { /// The contents of a mathematical equation: `x^2 + 1`. - Math + struct Math } impl<'a> Math<'a> { @@ -706,9 +840,37 @@ impl<'a> Math<'a> { } } +node! { + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. + struct MathText +} + +/// The underlying text kind. +pub enum MathTextKind<'a> { + Character(char), + Number(&'a EcoString), +} + +impl<'a> MathText<'a> { + /// Return the underlying text. + pub fn get(self) -> MathTextKind<'a> { + let text = self.0.text(); + let mut chars = text.chars(); + let c = chars.next().unwrap(); + if c.is_numeric() { + // Numbers are potentially grouped as multiple characters. This is + // done in `Lexer::math_text()`. + MathTextKind::Number(text) + } else { + assert!(chars.next().is_none()); + MathTextKind::Character(c) + } + } +} + node! { /// An identifier in math: `pi`. - MathIdent + struct MathIdent } impl<'a> MathIdent<'a> { @@ -735,7 +897,7 @@ impl Deref for MathIdent<'_> { node! { /// A shorthand for a unicode codepoint in math: `a <= b`. - MathShorthand + struct MathShorthand } impl MathShorthand<'_> { @@ -793,40 +955,40 @@ impl MathShorthand<'_> { node! { /// An alignment point in math: `&`. - MathAlignPoint + struct MathAlignPoint } node! { /// Matched delimiters in math: `[x + y]`. - MathDelimited + struct MathDelimited } impl<'a> MathDelimited<'a> { /// The opening delimiter. pub fn open(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The contents, including the delimiters. pub fn body(self) -> Math<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The closing delimiter. pub fn close(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A base with optional attachments in math: `a_1^2`. - MathAttach + struct MathAttach } impl<'a> MathAttach<'a> { /// The base, to which things are attached. pub fn base(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The bottom attachment. @@ -857,7 +1019,7 @@ impl<'a> MathAttach<'a> { node! { /// Grouped primes in math: `a'''`. - MathPrimes + struct MathPrimes } impl MathPrimes<'_> { @@ -872,24 +1034,24 @@ impl MathPrimes<'_> { node! { /// A fraction in math: `x/2` - MathFrac + struct MathFrac } impl<'a> MathFrac<'a> { /// The numerator. pub fn num(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The denominator. pub fn denom(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A root in math: `√x`, `∛x` or `∜x`. - MathRoot + struct MathRoot } impl<'a> MathRoot<'a> { @@ -905,13 +1067,13 @@ impl<'a> MathRoot<'a> { /// The radicand. pub fn radicand(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An identifier: `it`. - Ident + struct Ident } impl<'a> Ident<'a> { @@ -938,17 +1100,17 @@ impl Deref for Ident<'_> { node! { /// The `none` literal. - None + struct None } node! { /// The `auto` literal. - Auto + struct Auto } node! { /// A boolean: `true`, `false`. - Bool + struct Bool } impl Bool<'_> { @@ -960,7 +1122,7 @@ impl Bool<'_> { node! { /// An integer: `120`. - Int + struct Int } impl Int<'_> { @@ -982,7 +1144,7 @@ impl Int<'_> { node! { /// A floating-point number: `1.2`, `10e-4`. - Float + struct Float } impl Float<'_> { @@ -994,7 +1156,7 @@ impl Float<'_> { node! { /// A numeric value with a unit: `12pt`, `3cm`, `2em`, `90deg`, `50%`. - Numeric + struct Numeric } impl Numeric<'_> { @@ -1051,7 +1213,7 @@ pub enum Unit { node! { /// A quoted string: `"..."`. - Str + struct Str } impl Str<'_> { @@ -1101,19 +1263,19 @@ impl Str<'_> { node! { /// A code block: `{ let x = 1; x + 2 }`. - CodeBlock + struct CodeBlock } impl<'a> CodeBlock<'a> { /// The contained code. pub fn body(self) -> Code<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// The body of a code block. - Code + struct Code } impl<'a> Code<'a> { @@ -1125,19 +1287,19 @@ impl<'a> Code<'a> { node! { /// A content block: `[*Hi* there!]`. - ContentBlock + struct ContentBlock } impl<'a> ContentBlock<'a> { /// The contained markup. pub fn body(self) -> Markup<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// A grouped expression: `(1 + 2)`. - Parenthesized + struct Parenthesized } impl<'a> Parenthesized<'a> { @@ -1145,20 +1307,20 @@ impl<'a> Parenthesized<'a> { /// /// Should only be accessed if this is contained in an `Expr`. pub fn expr(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The wrapped pattern. /// /// Should only be accessed if this is contained in a `Pattern`. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An array: `(1, "hi", 12cm)`. - Array + struct Array } impl<'a> Array<'a> { @@ -1180,7 +1342,7 @@ pub enum ArrayItem<'a> { impl<'a> AstNode<'a> for ArrayItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1195,7 +1357,7 @@ impl<'a> AstNode<'a> for ArrayItem<'a> { node! { /// A dictionary: `(thickness: 3pt, dash: "solid")`. - Dict + struct Dict } impl<'a> Dict<'a> { @@ -1219,9 +1381,9 @@ pub enum DictItem<'a> { impl<'a> AstNode<'a> for DictItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Keyed => node.cast().map(Self::Keyed), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Keyed => Some(Self::Keyed(Keyed(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => Option::None, } } @@ -1237,13 +1399,13 @@ impl<'a> AstNode<'a> for DictItem<'a> { node! { /// A named pair: `thickness: 3pt`. - Named + struct Named } impl<'a> Named<'a> { /// The name: `thickness`. pub fn name(self) -> Ident<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the pair: `3pt`. @@ -1251,7 +1413,7 @@ impl<'a> Named<'a> { /// This should only be accessed if this `Named` is contained in a /// `DictItem`, `Arg`, or `Param`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } /// The right-hand side of the pair as a pattern. @@ -1259,19 +1421,19 @@ impl<'a> Named<'a> { /// This should only be accessed if this `Named` is contained in a /// `Destructuring`. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A keyed pair: `"spacy key": true`. - Keyed + struct Keyed } impl<'a> Keyed<'a> { /// The key: `"spacy key"`. pub fn key(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the pair: `true`. @@ -1279,13 +1441,13 @@ impl<'a> Keyed<'a> { /// This should only be accessed if this `Keyed` is contained in a /// `DictItem`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A spread: `..x` or `..x.at(0)`. - Spread + struct Spread } impl<'a> Spread<'a> { @@ -1294,7 +1456,7 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in an /// `ArrayItem`, `DictItem`, or `Arg`. pub fn expr(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The sink identifier, if present. @@ -1302,7 +1464,7 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in a /// `Param` or binding `DestructuringItem`. pub fn sink_ident(self) -> Option> { - self.0.cast_first_match() + self.0.try_cast_first() } /// The sink expressions, if present. @@ -1310,13 +1472,13 @@ impl<'a> Spread<'a> { /// This should only be accessed if this `Spread` is contained in a /// `DestructuringItem`. pub fn sink_expr(self) -> Option> { - self.0.cast_first_match() + self.0.try_cast_first() } } node! { /// A unary operation: `-x`. - Unary + struct Unary } impl<'a> Unary<'a> { @@ -1330,7 +1492,7 @@ impl<'a> Unary<'a> { /// The expression to operate on: `x`. pub fn expr(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } @@ -1376,7 +1538,7 @@ impl UnOp { node! { /// A binary operation: `a + b`. - Binary + struct Binary } impl<'a> Binary<'a> { @@ -1398,12 +1560,12 @@ impl<'a> Binary<'a> { /// The left-hand side of the operation: `a`. pub fn lhs(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The right-hand side of the operation: `b`. pub fn rhs(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } @@ -1563,41 +1725,41 @@ pub enum Assoc { node! { /// A field access: `properties.age`. - FieldAccess + struct FieldAccess } impl<'a> FieldAccess<'a> { /// The expression to access the field on. pub fn target(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The name of the field. pub fn field(self) -> Ident<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// An invocation of a function or method: `f(x, y)`. - FuncCall + struct FuncCall } impl<'a> FuncCall<'a> { /// The function to call. pub fn callee(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The arguments to the function. pub fn args(self) -> Args<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A function call's argument list: `(12pt, y)`. - Args + struct Args } impl<'a> Args<'a> { @@ -1631,8 +1793,8 @@ pub enum Arg<'a> { impl<'a> AstNode<'a> for Arg<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1648,7 +1810,7 @@ impl<'a> AstNode<'a> for Arg<'a> { node! { /// A closure: `(x, y) => z`. - Closure + struct Closure } impl<'a> Closure<'a> { @@ -1661,18 +1823,18 @@ impl<'a> Closure<'a> { /// The parameter bindings. pub fn params(self) -> Params<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The body of the closure. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A closure's parameters: `(x, y)`. - Params + struct Params } impl<'a> Params<'a> { @@ -1696,8 +1858,8 @@ pub enum Param<'a> { impl<'a> AstNode<'a> for Param<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pos), } } @@ -1727,9 +1889,9 @@ pub enum Pattern<'a> { impl<'a> AstNode<'a> for Pattern<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Underscore => node.cast().map(Self::Placeholder), - SyntaxKind::Parenthesized => node.cast().map(Self::Parenthesized), - SyntaxKind::Destructuring => node.cast().map(Self::Destructuring), + SyntaxKind::Underscore => Some(Self::Placeholder(Underscore(node))), + SyntaxKind::Parenthesized => Some(Self::Parenthesized(Parenthesized(node))), + SyntaxKind::Destructuring => Some(Self::Destructuring(Destructuring(node))), _ => node.cast().map(Self::Normal), } } @@ -1764,12 +1926,12 @@ impl Default for Pattern<'_> { node! { /// An underscore: `_` - Underscore + struct Underscore } node! { /// A destructuring pattern: `x` or `(x, _, ..y)`. - Destructuring + struct Destructuring } impl<'a> Destructuring<'a> { @@ -1806,8 +1968,8 @@ pub enum DestructuringItem<'a> { impl<'a> AstNode<'a> for DestructuringItem<'a> { fn from_untyped(node: &'a SyntaxNode) -> Option { match node.kind() { - SyntaxKind::Named => node.cast().map(Self::Named), - SyntaxKind::Spread => node.cast().map(Self::Spread), + SyntaxKind::Named => Some(Self::Named(Named(node))), + SyntaxKind::Spread => Some(Self::Spread(Spread(node))), _ => node.cast().map(Self::Pattern), } } @@ -1823,7 +1985,7 @@ impl<'a> AstNode<'a> for DestructuringItem<'a> { node! { /// A let binding: `let x = 1`. - LetBinding + struct LetBinding } /// The kind of a let binding, either a normal one or a closure. @@ -1848,11 +2010,11 @@ impl<'a> LetBindingKind<'a> { impl<'a> LetBinding<'a> { /// The kind of the let binding. pub fn kind(self) -> LetBindingKind<'a> { - match self.0.cast_first_match::() { - Some(Pattern::Normal(Expr::Closure(closure))) => { + match self.0.cast_first() { + Pattern::Normal(Expr::Closure(closure)) => { LetBindingKind::Closure(closure.name().unwrap_or_default()) } - pattern => LetBindingKind::Normal(pattern.unwrap_or_default()), + pattern => LetBindingKind::Normal(pattern), } } @@ -1862,43 +2024,43 @@ impl<'a> LetBinding<'a> { LetBindingKind::Normal(Pattern::Normal(_) | Pattern::Parenthesized(_)) => { self.0.children().filter_map(SyntaxNode::cast).nth(1) } - LetBindingKind::Normal(_) => self.0.cast_first_match(), - LetBindingKind::Closure(_) => self.0.cast_first_match(), + LetBindingKind::Normal(_) => self.0.try_cast_first(), + LetBindingKind::Closure(_) => self.0.try_cast_first(), } } } node! { /// An assignment expression `(x, y) = (1, 2)`. - DestructAssignment + struct DestructAssignment } impl<'a> DestructAssignment<'a> { /// The pattern of the assignment. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match::().unwrap_or_default() + self.0.cast_first() } /// The expression that is assigned. pub fn value(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A set rule: `set text(...)`. - SetRule + struct SetRule } impl<'a> SetRule<'a> { /// The function to set style properties for. pub fn target(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The style properties to set. pub fn args(self) -> Args<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } /// A condition under which the set rule applies. @@ -1912,7 +2074,7 @@ impl<'a> SetRule<'a> { node! { /// A show rule: `show heading: it => emph(it.body)`. - ShowRule + struct ShowRule } impl<'a> ShowRule<'a> { @@ -1927,31 +2089,31 @@ impl<'a> ShowRule<'a> { /// The transformation recipe. pub fn transform(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A contextual expression: `context text.lang`. - Contextual + struct Contextual } impl<'a> Contextual<'a> { /// The expression which depends on the context. pub fn body(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } } node! { /// An if-else conditional: `if x { y } else { z }`. - Conditional + struct Conditional } impl<'a> Conditional<'a> { /// The condition which selects the body to evaluate. pub fn condition(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to evaluate if the condition is true. @@ -1971,30 +2133,30 @@ impl<'a> Conditional<'a> { node! { /// A while loop: `while x { y }`. - WhileLoop + struct WhileLoop } impl<'a> WhileLoop<'a> { /// The condition which selects whether to evaluate the body. pub fn condition(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to evaluate while the condition is true. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A for loop: `for x in y { z }`. - ForLoop + struct ForLoop } impl<'a> ForLoop<'a> { /// The pattern to assign to. pub fn pattern(self) -> Pattern<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The expression to iterate over. @@ -2008,19 +2170,19 @@ impl<'a> ForLoop<'a> { /// The expression to evaluate for each iteration. pub fn body(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A module import: `import "utils.typ": a, b, c`. - ModuleImport + struct ModuleImport } impl<'a> ModuleImport<'a> { /// The module or path from which the items should be imported. pub fn source(self) -> Expr<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The items to be imported. @@ -2032,6 +2194,41 @@ impl<'a> ModuleImport<'a> { }) } + /// The name that will be bound for a bare import. This name must be + /// statically known. It can come from: + /// - an identifier + /// - a field access + /// - a string that is a valid file path where the file stem is a valid + /// identifier + /// - a string that is a valid package spec + pub fn bare_name(self) -> Result { + match self.source() { + Expr::Ident(ident) => Ok(ident.get().clone()), + Expr::FieldAccess(access) => Ok(access.field().get().clone()), + Expr::Str(string) => { + let string = string.get(); + let name = if string.starts_with('@') { + PackageSpec::from_str(&string) + .map_err(|_| BareImportError::PackageInvalid)? + .name + } else { + Path::new(string.as_str()) + .file_stem() + .and_then(|path| path.to_str()) + .ok_or(BareImportError::PathInvalid)? + .into() + }; + + if !is_ident(&name) { + return Err(BareImportError::PathInvalid); + } + + Ok(name) + } + _ => Err(BareImportError::Dynamic), + } + } + /// The name this module was assigned to, if it was renamed with `as` /// (`renamed` in `import "..." as renamed`). pub fn new_name(self) -> Option> { @@ -2042,6 +2239,18 @@ impl<'a> ModuleImport<'a> { } } +/// Reasons why a bare name cannot be determined for an import source. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BareImportError { + /// There is no statically resolvable binding name. + Dynamic, + /// The import source is not a valid path or the path stem not a valid + /// identifier. + PathInvalid, + /// The import source is not a valid package spec. + PackageInvalid, +} + /// The items that ought to be imported from a file. #[derive(Debug, Copy, Clone, Hash)] pub enum Imports<'a> { @@ -2053,7 +2262,7 @@ pub enum Imports<'a> { node! { /// Items to import from a module: `a, b, c`. - ImportItems + struct ImportItems } impl<'a> ImportItems<'a> { @@ -2069,7 +2278,7 @@ impl<'a> ImportItems<'a> { node! { /// A path to a submodule's imported name: `a.b.c`. - ImportItemPath + struct ImportItemPath } impl<'a> ImportItemPath<'a> { @@ -2080,7 +2289,7 @@ impl<'a> ImportItemPath<'a> { /// The name of the imported item. This is the last segment in the path. pub fn name(self) -> Ident<'a> { - self.iter().last().unwrap_or_default() + self.0.cast_last() } } @@ -2125,13 +2334,13 @@ impl<'a> ImportItem<'a> { node! { /// A renamed import item: `a as d` - RenamedImportItem + struct RenamedImportItem } impl<'a> RenamedImportItem<'a> { /// The path to the imported item. pub fn path(self) -> ImportItemPath<'a> { - self.0.cast_first_match().unwrap_or_default() + self.0.cast_first() } /// The original name of the imported item (`a` in `a as d` or `c.b.a as d`). @@ -2141,45 +2350,41 @@ impl<'a> RenamedImportItem<'a> { /// The new name of the imported item (`d` in `a as d`). pub fn new_name(self) -> Ident<'a> { - self.0 - .children() - .filter_map(SyntaxNode::cast) - .last() - .unwrap_or_default() + self.0.cast_last() } } node! { /// A module include: `include "chapter1.typ"`. - ModuleInclude + struct ModuleInclude } impl<'a> ModuleInclude<'a> { /// The module or path from which the content should be included. pub fn source(self) -> Expr<'a> { - self.0.cast_last_match().unwrap_or_default() + self.0.cast_last() } } node! { /// A break from a loop: `break`. - LoopBreak + struct LoopBreak } node! { /// A continue in a loop: `continue`. - LoopContinue + struct LoopContinue } node! { /// A return from a function: `return`, `return x + 1`. - FuncReturn + struct FuncReturn } impl<'a> FuncReturn<'a> { /// The expression to return. pub fn body(self) -> Option> { - self.0.cast_last_match() + self.0.try_cast_last() } } diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs index de8ed65c9..cd815694d 100644 --- a/crates/typst-syntax/src/highlight.rs +++ b/crates/typst-syntax/src/highlight.rs @@ -171,6 +171,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Equation => None, SyntaxKind::Math => None, + SyntaxKind::MathText => None, SyntaxKind::MathIdent => highlight_ident(node), SyntaxKind::MathShorthand => Some(Tag::Escape), SyntaxKind::MathAlignPoint => Some(Tag::MathOperator), @@ -287,6 +288,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Destructuring => None, SyntaxKind::DestructAssignment => None, + SyntaxKind::Shebang => Some(Tag::Comment), SyntaxKind::LineComment => Some(Tag::Comment), SyntaxKind::BlockComment => Some(Tag::Comment), SyntaxKind::Error => Some(Tag::Error), diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs index 0a7c160b4..c24b47fe7 100644 --- a/crates/typst-syntax/src/kind.rs +++ b/crates/typst-syntax/src/kind.rs @@ -9,6 +9,8 @@ pub enum SyntaxKind { /// An invalid sequence of characters. Error, + /// A shebang: `#! ...` + Shebang, /// A line comment: `// ...`. LineComment, /// A block comment: `/* ... */`. @@ -73,6 +75,8 @@ pub enum SyntaxKind { /// The contents of a mathematical equation: `x^2 + 1`. Math, + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `|`, `[`. + MathText, /// An identifier in math: `pi`. MathIdent, /// A shorthand for a unicode codepoint in math: `a <= b`. @@ -357,7 +361,11 @@ impl SyntaxKind { pub fn is_trivia(self) -> bool { matches!( self, - Self::LineComment | Self::BlockComment | Self::Space | Self::Parbreak + Self::Shebang + | Self::LineComment + | Self::BlockComment + | Self::Space + | Self::Parbreak ) } @@ -371,6 +379,7 @@ impl SyntaxKind { match self { Self::End => "end of tokens", Self::Error => "syntax error", + Self::Shebang => "shebang", Self::LineComment => "line comment", Self::BlockComment => "block comment", Self::Markup => "markup", @@ -401,6 +410,7 @@ impl SyntaxKind { Self::TermMarker => "term marker", Self::Equation => "equation", Self::Math => "math", + Self::MathText => "math text", Self::MathIdent => "math identifier", Self::MathShorthand => "math shorthand", Self::MathAlignPoint => "math alignment point", diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index b0cb5c464..b8f2bf25f 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -103,6 +103,7 @@ impl Lexer<'_> { self.newline = false; let kind = match self.s.eat() { Some(c) if is_space(c, self.mode) => self.whitespace(start, c), + Some('#') if start == 0 && self.s.eat_if('!') => self.shebang(), Some('/') if self.s.eat_if('/') => self.line_comment(), Some('/') if self.s.eat_if('*') => self.block_comment(), Some('*') if self.s.eat_if('/') => { @@ -151,6 +152,11 @@ impl Lexer<'_> { } } + fn shebang(&mut self) -> SyntaxKind { + self.s.eat_until(is_newline); + SyntaxKind::Shebang + } + fn line_comment(&mut self) -> SyntaxKind { self.s.eat_until(is_newline); SyntaxKind::LineComment @@ -616,6 +622,11 @@ impl Lexer<'_> { '~' if self.s.eat_if('>') => SyntaxKind::MathShorthand, '*' | '-' | '~' => SyntaxKind::MathShorthand, + '.' => SyntaxKind::Dot, + ',' => SyntaxKind::Comma, + ';' => SyntaxKind::Semicolon, + ')' => SyntaxKind::RightParen, + '#' => SyntaxKind::Hash, '_' => SyntaxKind::Underscore, '$' => SyntaxKind::Dollar, @@ -674,6 +685,7 @@ impl Lexer<'_> { if s.eat_if('.') && !s.eat_while(char::is_numeric).is_empty() { self.s = s; } + SyntaxKind::MathText } else { let len = self .s @@ -682,8 +694,53 @@ impl Lexer<'_> { .next() .map_or(0, str::len); self.s.jump(start + len); + if len > c.len_utf8() { + // Grapheme clusters are treated as normal text and stay grouped + // This may need to change in the future. + SyntaxKind::Text + } else { + SyntaxKind::MathText + } } - SyntaxKind::Text + } + + /// Handle named arguments in math function call. + pub fn maybe_math_named_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if(is_id_start) { + self.s.eat_while(is_id_continue); + // Check that a colon directly follows the identifier, and not the + // `:=` or `::=` math shorthands. + if self.s.at(':') && !self.s.at(":=") && !self.s.at("::=") { + // Check that the identifier is not just `_`. + let node = if self.s.from(start) != "_" { + SyntaxNode::leaf(SyntaxKind::Ident, self.s.from(start)) + } else { + let msg = SyntaxError::new("expected identifier, found underscore"); + SyntaxNode::error(msg, self.s.from(start)) + }; + return Some(node); + } + } + self.s.jump(cursor); + None + } + + /// Handle spread arguments in math function call. + pub fn maybe_math_spread_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if("..") { + // Check that neither a space nor a dot follows the spread syntax. + // A dot would clash with the `...` math shorthand. + if !self.space_or_end() && !self.s.at('.') { + let node = SyntaxNode::leaf(SyntaxKind::Dots, self.s.from(start)); + return Some(node); + } + } + self.s.jump(cursor); + None } } diff --git a/crates/typst-syntax/src/node.rs b/crates/typst-syntax/src/node.rs index b7e1809d7..948657ca4 100644 --- a/crates/typst-syntax/src/node.rs +++ b/crates/typst-syntax/src/node.rs @@ -5,7 +5,6 @@ use std::sync::Arc; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; -use crate::ast::AstNode; use crate::{FileId, Span, SyntaxKind}; /// A node in the untyped syntax tree. @@ -119,26 +118,6 @@ impl SyntaxNode { } } - /// Whether the node can be cast to the given AST node. - pub fn is<'a, T: AstNode<'a>>(&'a self) -> bool { - self.cast::().is_some() - } - - /// Try to convert the node to a typed AST node. - pub fn cast<'a, T: AstNode<'a>>(&'a self) -> Option { - T::from_untyped(self) - } - - /// Cast the first child that can cast to the AST type `T`. - pub fn cast_first_match<'a, T: AstNode<'a>>(&'a self) -> Option { - self.children().find_map(Self::cast) - } - - /// Cast the last child that can cast to the AST type `T`. - pub fn cast_last_match<'a, T: AstNode<'a>>(&'a self) -> Option { - self.children().rev().find_map(Self::cast) - } - /// Whether the node or its children contain an error. pub fn erroneous(&self) -> bool { match &self.0 { @@ -753,7 +732,7 @@ impl<'a> LinkedNode<'a> { // sibling's span number is larger than the target span's number. if children .peek() - .map_or(true, |next| next.span().number() > span.number()) + .is_none_or(|next| next.span().number() > span.number()) { if let Some(found) = child.find(span) { return Some(found); diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs index 387057f37..aa537863d 100644 --- a/crates/typst-syntax/src/package.rs +++ b/crates/typst-syntax/src/package.rs @@ -327,8 +327,8 @@ impl PackageVersion { /// missing in the bound are ignored. pub fn matches_eq(&self, bound: &VersionBound) -> bool { self.major == bound.major - && bound.minor.map_or(true, |minor| self.minor == minor) - && bound.patch.map_or(true, |patch| self.patch == patch) + && bound.minor.is_none_or(|minor| self.minor == minor) + && bound.patch.is_none_or(|patch| self.patch == patch) } /// Performs a `>` match with the given version bound. The match only diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 6c1778c4a..c5d13c8b3 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -3,6 +3,7 @@ use std::mem; use std::ops::{Index, IndexMut, Range}; use ecow::{eco_format, EcoString}; +use typst_utils::default_math_class; use unicode_math_class::MathClass; use crate::set::{syntax_set, SyntaxSet}; @@ -93,6 +94,8 @@ fn markup_expr(p: &mut Parser, at_start: bool, nesting: &mut usize) { p.hint("try using a backslash escape: \\]"); } + SyntaxKind::Shebang => p.eat(), + SyntaxKind::Text | SyntaxKind::Linebreak | SyntaxKind::Escape @@ -160,7 +163,7 @@ fn list_item(p: &mut Parser) { p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| { let m = p.marker(); p.assert(SyntaxKind::ListMarker); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::ListItem); }); } @@ -170,7 +173,7 @@ fn enum_item(p: &mut Parser) { p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| { let m = p.marker(); p.assert(SyntaxKind::EnumMarker); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::EnumItem); }); } @@ -184,7 +187,7 @@ fn term_item(p: &mut Parser) { markup(p, false, false, syntax_set!(Colon, RightBracket, End)); }); p.expect(SyntaxKind::Colon); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::TermItem); }); } @@ -217,16 +220,20 @@ fn math(p: &mut Parser, stop_set: SyntaxSet) { p.wrap(m, SyntaxKind::Math); } -/// Parses a sequence of math expressions. -fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) { +/// Parses a sequence of math expressions. Returns the number of expressions +/// parsed. +fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) -> usize { debug_assert!(stop_set.contains(SyntaxKind::End)); + let mut count = 0; while !p.at_set(stop_set) { if p.at_set(set::MATH_EXPR) { math_expr(p); + count += 1; } else { p.unexpected(); } } + count } /// Parses a single math expression: This includes math elements like @@ -246,7 +253,9 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { continuable = true; p.eat(); // Parse a function call for an identifier or field access. - if min_prec < 3 && p.directly_at(SyntaxKind::Text) && p.current_text() == "(" + if min_prec < 3 + && p.directly_at(SyntaxKind::MathText) + && p.current_text() == "(" { math_args(p); p.wrap(m, SyntaxKind::FuncCall); @@ -254,11 +263,19 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } } - SyntaxKind::Text | SyntaxKind::MathShorthand => { - continuable = matches!( - math_class(p.current_text()), - None | Some(MathClass::Alphabetic) - ); + SyntaxKind::Dot + | SyntaxKind::Comma + | SyntaxKind::Semicolon + | SyntaxKind::RightParen => { + p.convert_and_eat(SyntaxKind::MathText); + } + + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { + continuable = !p.at(SyntaxKind::MathShorthand) + && matches!( + math_class(p.current_text()), + None | Some(MathClass::Alphabetic) + ); if !maybe_delimited(p) { p.eat(); } @@ -303,7 +320,7 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { let mut primed = false; while !p.end() && !p.at(stop) { - if p.directly_at(SyntaxKind::Text) && p.current_text() == "!" { + if p.directly_at(SyntaxKind::MathText) && p.current_text() == "!" { p.eat(); p.wrap(m, SyntaxKind::Math); continue; @@ -398,7 +415,13 @@ fn math_delimited(p: &mut Parser) { while !p.at_set(syntax_set!(Dollar, End)) { if math_class(p.current_text()) == Some(MathClass::Closing) { p.wrap(m2, SyntaxKind::Math); - p.eat(); + // We could be at the shorthand `|]`, which shouldn't be converted + // to a `Text` kind. + if p.at(SyntaxKind::RightParen) { + p.convert_and_eat(SyntaxKind::MathText); + } else { + p.eat(); + } p.wrap(m, SyntaxKind::MathDelimited); return; } @@ -425,10 +448,10 @@ fn math_unparen(p: &mut Parser, m: Marker) { if first.text() == "(" && last.text() == ")" { first.convert_to_kind(SyntaxKind::LeftParen); last.convert_to_kind(SyntaxKind::RightParen); + // Only convert if we did have regular parens. + node.convert_to_kind(SyntaxKind::Math); } } - - node.convert_to_kind(SyntaxKind::Math); } /// The unicode math class of a string. Only returns `Some` if `text` has @@ -447,7 +470,7 @@ fn math_class(text: &str) -> Option { chars .next() .filter(|_| chars.next().is_none()) - .and_then(unicode_math_class::class) + .and_then(default_math_class) } /// Parse an argument list in math: `(a, b; c, d; size: #50%)`. @@ -455,94 +478,90 @@ fn math_args(p: &mut Parser) { let m = p.marker(); p.convert_and_eat(SyntaxKind::LeftParen); - let mut namable = true; - let mut named = None; + let mut positional = true; let mut has_arrays = false; - let mut array = p.marker(); - let mut arg = p.marker(); - // The number of math expressions per argument. - let mut count = 0; - while !p.at_set(syntax_set!(Dollar, End)) { - if namable - && (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text)) - && p.text[p.current_end()..].starts_with(':') - { - p.convert_and_eat(SyntaxKind::Ident); - p.convert_and_eat(SyntaxKind::Colon); - named = Some(arg); - arg = p.marker(); - array = p.marker(); - } + let mut maybe_array_start = p.marker(); + let mut seen = HashSet::new(); + while !p.at_set(syntax_set!(End, Dollar, RightParen)) { + positional = math_arg(p, &mut seen); - match p.current_text() { - ")" => break, - ";" => { - maybe_wrap_in_math(p, arg, count, named); - p.wrap(array, SyntaxKind::Array); - p.convert_and_eat(SyntaxKind::Semicolon); - array = p.marker(); - arg = p.marker(); - count = 0; - namable = true; - named = None; - has_arrays = true; - continue; - } - "," => { - maybe_wrap_in_math(p, arg, count, named); - p.convert_and_eat(SyntaxKind::Comma); - arg = p.marker(); - count = 0; - namable = true; - if named.is_some() { - array = p.marker(); - named = None; + match p.current() { + SyntaxKind::Comma => { + p.eat(); + if !positional { + maybe_array_start = p.marker(); } - continue; } - _ => {} - } + SyntaxKind::Semicolon => { + if !positional { + maybe_array_start = p.marker(); + } - if p.at_set(set::MATH_EXPR) { - math_expr(p); - count += 1; - } else { - p.unexpected(); - } - - namable = false; - } - - if arg != p.marker() { - maybe_wrap_in_math(p, arg, count, named); - if named.is_some() { - array = p.marker(); + // Parses an array: `a, b, c;`. + // The semicolon merges preceding arguments separated by commas + // into an array argument. + p.wrap(maybe_array_start, SyntaxKind::Array); + p.eat(); + maybe_array_start = p.marker(); + has_arrays = true; + } + SyntaxKind::End | SyntaxKind::Dollar | SyntaxKind::RightParen => {} + _ => p.expected("comma or semicolon"), } } - if has_arrays && array != p.marker() { - p.wrap(array, SyntaxKind::Array); - } - - if p.at(SyntaxKind::Text) && p.current_text() == ")" { - p.convert_and_eat(SyntaxKind::RightParen); - } else { - p.expected("closing paren"); - p.balanced = false; + // Check if we need to wrap the preceding arguments in an array. + if maybe_array_start != p.marker() && has_arrays && positional { + p.wrap(maybe_array_start, SyntaxKind::Array); } + p.expect_closing_delimiter(m, SyntaxKind::RightParen); p.wrap(m, SyntaxKind::Args); } -/// Wrap math function arguments to join adjacent math content or create an -/// empty 'Math' node for when we have 0 args. +/// Parses a single argument in a math argument list. /// -/// We don't wrap when `count == 1`, since wrapping would change the type of the -/// expression from potentially non-content to content. Ex: `$ func(#12pt) $` -/// would change the type from size to content if wrapped. -fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: Option) { +/// Returns whether the parsed argument was positional or not. +fn math_arg<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) -> bool { + let m = p.marker(); + let start = p.current_start(); + + if p.at(SyntaxKind::Dot) { + // Parses a spread argument: `..args`. + if let Some(spread) = p.lexer.maybe_math_spread_arg(start) { + p.token.node = spread; + p.eat(); + math_expr(p); + p.wrap(m, SyntaxKind::Spread); + return true; + } + } + + let mut positional = true; + if p.at_set(syntax_set!(MathText, MathIdent, Underscore)) { + // Parses a named argument: `thickness: #12pt`. + if let Some(named) = p.lexer.maybe_math_named_arg(start) { + p.token.node = named; + let text = p.current_text(); + p.eat(); + p.convert_and_eat(SyntaxKind::Colon); + if !seen.insert(text) { + p[m].convert_to_error(eco_format!("duplicate argument: {text}")); + } + positional = false; + } + } + + // Parses a normal positional argument. + let arg = p.marker(); + let count = math_exprs(p, syntax_set!(End, Dollar, Comma, Semicolon, RightParen)); if count == 0 { + // Named argument requires a value. + if !positional { + p.expected("expression"); + } + // Flush trivia so that the new empty Math node will be wrapped _inside_ // any `SyntaxKind::Array` elements created in `math_args`. // (And if we don't follow by wrapping in an array, it has no effect.) @@ -553,13 +572,19 @@ fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: Option true, }, AtNewline::StopParBreak => parbreak, - AtNewline::RequireColumn(min_col) => match column { - Some(column) => column <= min_col, - None => false, // Don't stop if we had no column. - }, + AtNewline::RequireColumn(min_col) => { + // Don't stop if this newline doesn't start a column (this may + // be checked on the boundary of lexer modes, since we only + // report a column in Markup). + column.is_some_and(|column| column <= min_col) + } } } } @@ -1684,10 +1711,13 @@ impl<'s> Parser<'s> { self.token.newline.is_some() } - /// The number of characters until the most recent newline from the current - /// token, or 0 if it did not follow a newline. + /// The number of characters until the most recent newline from the start of + /// the current token. Uses a cached value from the newline mode if present. fn current_column(&self) -> usize { - self.token.newline.and_then(|newline| newline.column).unwrap_or(0) + self.token + .newline + .and_then(|newline| newline.column) + .unwrap_or_else(|| self.lexer.column(self.token.start)) } /// The current token's text. diff --git a/crates/typst-syntax/src/set.rs b/crates/typst-syntax/src/set.rs index 014aaf2f7..a7b9a594a 100644 --- a/crates/typst-syntax/src/set.rs +++ b/crates/typst-syntax/src/set.rs @@ -59,7 +59,12 @@ pub const MATH_EXPR: SyntaxSet = syntax_set!( Hash, MathIdent, FieldAccess, + Dot, + Comma, + Semicolon, + RightParen, Text, + MathText, MathShorthand, Linebreak, MathAlignPoint, diff --git a/crates/typst-timing/Cargo.toml b/crates/typst-timing/Cargo.toml index 2d42269fc..dbc2813c7 100644 --- a/crates/typst-timing/Cargo.toml +++ b/crates/typst-timing/Cargo.toml @@ -17,5 +17,11 @@ parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { workspace = true, features = ["Window", "WorkerGlobalScope", "Performance"], optional = true } + +[features] +wasm = ["dep:web-sys"] + [lints] workspace = true diff --git a/crates/typst-timing/src/lib.rs b/crates/typst-timing/src/lib.rs index b4653170b..6da2cdf02 100644 --- a/crates/typst-timing/src/lib.rs +++ b/crates/typst-timing/src/lib.rs @@ -1,149 +1,13 @@ //! Performance timing for Typst. -#![cfg_attr(target_arch = "wasm32", allow(dead_code, unused_variables))] - -use std::hash::Hash; use std::io::Write; use std::num::NonZeroU64; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Relaxed; -use std::thread::ThreadId; -use std::time::{Duration, SystemTime}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use parking_lot::Mutex; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -/// Whether the timer is enabled. Defaults to `false`. -static ENABLED: AtomicBool = AtomicBool::new(false); - -/// The global event recorder. -static RECORDER: Mutex = Mutex::new(Recorder::new()); - -/// The recorder of events. -struct Recorder { - /// The events that have been recorded. - events: Vec, - /// The discriminator of the next event. - discriminator: u64, -} - -impl Recorder { - /// Create a new recorder. - const fn new() -> Self { - Self { events: Vec::new(), discriminator: 0 } - } -} - -/// An event that has been recorded. -#[derive(Clone, Copy, Eq, PartialEq, Hash)] -struct Event { - /// Whether this is a start or end event. - kind: EventKind, - /// The start time of this event. - timestamp: SystemTime, - /// The discriminator of this event. - id: u64, - /// The name of this event. - name: &'static str, - /// The raw value of the span of code that this event was recorded in. - span: Option, - /// The thread ID of this event. - thread_id: ThreadId, -} - -/// Whether an event marks the start or end of a scope. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -enum EventKind { - Start, - End, -} - -/// Enable the timer. -#[inline] -pub fn enable() { - // We only need atomicity and no synchronization of other - // operations, so `Relaxed` is fine. - ENABLED.store(true, Relaxed); -} - -/// Whether the timer is enabled. -#[inline] -pub fn is_enabled() -> bool { - ENABLED.load(Relaxed) -} - -/// Clears the recorded events. -#[inline] -pub fn clear() { - RECORDER.lock().events.clear(); -} - -/// A scope that records an event when it is dropped. -pub struct TimingScope { - name: &'static str, - span: Option, - id: u64, - thread_id: ThreadId, -} - -impl TimingScope { - /// Create a new scope if timing is enabled. - #[inline] - pub fn new(name: &'static str) -> Option { - Self::with_span(name, None) - } - - /// Create a new scope with a span if timing is enabled. - /// - /// The span is a raw number because `typst-timing` can't depend on - /// `typst-syntax` (or else `typst-syntax` couldn't depend on - /// `typst-timing`). - #[inline] - pub fn with_span(name: &'static str, span: Option) -> Option { - #[cfg(not(target_arch = "wasm32"))] - if is_enabled() { - return Some(Self::new_impl(name, span)); - } - None - } - - /// Create a new scope without checking if timing is enabled. - fn new_impl(name: &'static str, span: Option) -> Self { - let timestamp = SystemTime::now(); - let thread_id = std::thread::current().id(); - - let mut recorder = RECORDER.lock(); - let id = recorder.discriminator; - recorder.discriminator += 1; - recorder.events.push(Event { - kind: EventKind::Start, - timestamp, - id, - name, - span, - thread_id, - }); - - Self { name, span, id, thread_id } - } -} - -impl Drop for TimingScope { - fn drop(&mut self) { - let event = Event { - kind: EventKind::End, - timestamp: SystemTime::now(), - id: self.id, - name: self.name, - span: self.span, - thread_id: self.thread_id, - }; - - RECORDER.lock().events.push(event); - } -} - /// Creates a timing scope around an expression. /// /// The output of the expression is returned. @@ -179,6 +43,46 @@ macro_rules! timed { }}; } +thread_local! { + /// Data that is initialized once per thread. + static THREAD_DATA: ThreadData = ThreadData { + id: { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + static COUNTER: AtomicU64 = AtomicU64::new(1); + COUNTER.fetch_add(1, Ordering::Relaxed) + }, + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer::new(), + }; +} + +/// Whether the timer is enabled. Defaults to `false`. +static ENABLED: AtomicBool = AtomicBool::new(false); + +/// The list of collected events. +static EVENTS: Mutex> = Mutex::new(Vec::new()); + +/// Enable the timer. +#[inline] +pub fn enable() { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + ENABLED.store(true, Ordering::Relaxed); +} + +/// Whether the timer is enabled. +#[inline] +pub fn is_enabled() -> bool { + ENABLED.load(Ordering::Relaxed) +} + +/// Clears the recorded events. +#[inline] +pub fn clear() { + EVENTS.lock().clear(); +} + /// Export data as JSON for Chrome's tracing tool. /// /// The `source` function is called for each span to get the source code @@ -205,19 +109,15 @@ pub fn export_json( line: u32, } - let recorder = RECORDER.lock(); - let run_start = recorder - .events - .first() - .map(|event| event.timestamp) - .unwrap_or_else(SystemTime::now); + let lock = EVENTS.lock(); + let events = lock.as_slice(); let mut serializer = serde_json::Serializer::new(writer); let mut seq = serializer - .serialize_seq(Some(recorder.events.len())) + .serialize_seq(Some(events.len())) .map_err(|e| format!("failed to serialize events: {e}"))?; - for event in recorder.events.iter() { + for event in events.iter() { seq.serialize_element(&Entry { name: event.name, cat: "typst", @@ -225,17 +125,9 @@ pub fn export_json( EventKind::Start => "B", EventKind::End => "E", }, - ts: event - .timestamp - .duration_since(run_start) - .unwrap_or(Duration::ZERO) - .as_nanos() as f64 - / 1_000.0, + ts: event.timestamp.micros_since(events[0].timestamp), pid: 1, - tid: unsafe { - // Safety: `thread_id` is a `ThreadId` which is a `u64`. - std::mem::transmute_copy(&event.thread_id) - }, + tid: event.thread_id, args: event.span.map(&mut source).map(|(file, line)| Args { file, line }), }) .map_err(|e| format!("failed to serialize event: {e}"))?; @@ -245,3 +137,173 @@ pub fn export_json( Ok(()) } + +/// A scope that records an event when it is dropped. +pub struct TimingScope { + name: &'static str, + span: Option, + thread_id: u64, +} + +impl TimingScope { + /// Create a new scope if timing is enabled. + #[inline] + pub fn new(name: &'static str) -> Option { + Self::with_span(name, None) + } + + /// Create a new scope with a span if timing is enabled. + /// + /// The span is a raw number because `typst-timing` can't depend on + /// `typst-syntax` (or else `typst-syntax` couldn't depend on + /// `typst-timing`). + #[inline] + pub fn with_span(name: &'static str, span: Option) -> Option { + if is_enabled() { + return Some(Self::new_impl(name, span)); + } + None + } + + /// Create a new scope without checking if timing is enabled. + fn new_impl(name: &'static str, span: Option) -> Self { + let (thread_id, timestamp) = + THREAD_DATA.with(|data| (data.id, Timestamp::now_with(data))); + EVENTS.lock().push(Event { + kind: EventKind::Start, + timestamp, + name, + span, + thread_id, + }); + Self { name, span, thread_id } + } +} + +impl Drop for TimingScope { + fn drop(&mut self) { + let timestamp = Timestamp::now(); + EVENTS.lock().push(Event { + kind: EventKind::End, + timestamp, + name: self.name, + span: self.span, + thread_id: self.thread_id, + }); + } +} + +/// An event that has been recorded. +struct Event { + /// Whether this is a start or end event. + kind: EventKind, + /// The time at which this event occurred. + timestamp: Timestamp, + /// The name of this event. + name: &'static str, + /// The raw value of the span of code that this event was recorded in. + span: Option, + /// The thread ID of this event. + thread_id: u64, +} + +/// Whether an event marks the start or end of a scope. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum EventKind { + Start, + End, +} + +/// A cross-platform way to get the current time. +#[derive(Copy, Clone)] +struct Timestamp { + #[cfg(not(target_arch = "wasm32"))] + inner: std::time::SystemTime, + #[cfg(target_arch = "wasm32")] + inner: f64, +} + +impl Timestamp { + fn now() -> Self { + #[cfg(target_arch = "wasm32")] + return THREAD_DATA.with(Self::now_with); + + #[cfg(not(target_arch = "wasm32"))] + Self { inner: std::time::SystemTime::now() } + } + + #[allow(unused_variables)] + fn now_with(data: &ThreadData) -> Self { + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + return Self { inner: data.timer.now() }; + + #[cfg(all(target_arch = "wasm32", not(feature = "wasm")))] + return Self { inner: 0.0 }; + + #[cfg(not(target_arch = "wasm32"))] + Self::now() + } + + fn micros_since(self, start: Self) -> f64 { + #[cfg(target_arch = "wasm32")] + return (self.inner - start.inner) * 1000.0; + + #[cfg(not(target_arch = "wasm32"))] + (self + .inner + .duration_since(start.inner) + .unwrap_or(std::time::Duration::ZERO) + .as_nanos() as f64 + / 1_000.0) + } +} + +/// Per-thread data. +struct ThreadData { + /// The thread's ID. + /// + /// In contrast to `std::thread::current().id()`, this is wasm-compatible + /// and also a bit cheaper to access because the std version does a bit more + /// stuff (including cloning an `Arc`). + id: u64, + /// A way to get the time from WebAssembly. + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer, +} + +/// A way to get the time from WebAssembly. +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +struct WasmTimer { + /// The cached JS performance handle for the thread. + perf: web_sys::Performance, + /// The cached JS time origin. + time_origin: f64, +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +impl WasmTimer { + fn new() -> Self { + // Retrieve `performance` from global object, either the window or + // globalThis. + let perf = web_sys::window() + .and_then(|window| window.performance()) + .or_else(|| { + use web_sys::wasm_bindgen::JsCast; + web_sys::js_sys::global() + .dyn_into::() + .ok() + .and_then(|scope| scope.performance()) + }) + .expect("failed to get JS performance handle"); + + // Every thread gets its own time origin. To make the results consistent + // across threads, we need to add this to each `now()` call. + let time_origin = perf.time_origin(); + + Self { perf, time_origin } + } + + fn now(&self) -> f64 { + self.time_origin + self.perf.now() + } +} diff --git a/crates/typst-utils/Cargo.toml b/crates/typst-utils/Cargo.toml index 5f828cff9..360e07d89 100644 --- a/crates/typst-utils/Cargo.toml +++ b/crates/typst-utils/Cargo.toml @@ -18,6 +18,7 @@ portable-atomic = { workspace = true } rayon = { workspace = true } siphasher = { workspace = true } thin-vec = { workspace = true } +unicode-math-class = { workspace = true } [lints] workspace = true diff --git a/crates/typst-utils/src/hash.rs b/crates/typst-utils/src/hash.rs index 3dbadbe20..9687da20b 100644 --- a/crates/typst-utils/src/hash.rs +++ b/crates/typst-utils/src/hash.rs @@ -162,3 +162,74 @@ impl Debug for LazyHash { self.value.fmt(f) } } + +/// A wrapper type with a manually computed hash. +/// +/// This can be used to turn an unhashable type into a hashable one where the +/// hash is provided manually. Typically, the hash is derived from the data +/// which was used to construct to the unhashable type. +/// +/// For instance, you could hash the bytes that were parsed into an unhashable +/// data structure. +/// +/// # Equality +/// Because Typst uses high-quality 128 bit hashes in all places, the risk of a +/// hash collision is reduced to an absolute minimum. Therefore, this type +/// additionally provides `PartialEq` and `Eq` implementations that compare by +/// hash instead of by value. For this to be correct, your hash implementation +/// **must feed all information relevant to the `PartialEq` impl to the +/// hasher.** +#[derive(Clone)] +pub struct ManuallyHash { + /// A manually computed hash. + hash: u128, + /// The underlying value. + value: T, +} + +impl ManuallyHash { + /// Wraps an item with a pre-computed hash. + /// + /// The hash should be computed with `typst_utils::hash128`. + #[inline] + pub fn new(value: T, hash: u128) -> Self { + Self { hash, value } + } + + /// Returns the wrapped value. + #[inline] + pub fn into_inner(self) -> T { + self.value + } +} + +impl Hash for ManuallyHash { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u128(self.hash); + } +} + +impl Eq for ManuallyHash {} + +impl PartialEq for ManuallyHash { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Deref for ManuallyHash { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Debug for ManuallyHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 61703250a..b346a8096 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -15,7 +15,7 @@ mod scalar; pub use self::bitset::{BitSet, SmallBitSet}; pub use self::deferred::Deferred; pub use self::duration::format_duration; -pub use self::hash::LazyHash; +pub use self::hash::{LazyHash, ManuallyHash}; pub use self::pico::{PicoStr, ResolvedPicoStr}; pub use self::round::{round_int_with_precision, round_with_precision}; pub use self::scalar::Scalar; @@ -31,6 +31,7 @@ use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher13}; +use unicode_math_class::MathClass; /// Turn a closure into a struct implementing [`Debug`]. pub fn debug(f: F) -> impl Debug @@ -128,6 +129,20 @@ pub trait SliceExt { where F: FnMut(&T) -> K, K: PartialEq; + + /// Computes two indices which split a slice into three parts. + /// + /// - A prefix which matches `f` + /// - An inner portion + /// - A suffix which matches `f` and does not overlap with the prefix + /// + /// If all elements match `f`, the prefix becomes `self` and the suffix + /// will be empty. + /// + /// Returns the indices at which the inner portion and the suffix start. + fn split_prefix_suffix(&self, f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool; } impl SliceExt for [T] { @@ -157,6 +172,19 @@ impl SliceExt for [T] { fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> { GroupByKey { slice: self, f } } + + fn split_prefix_suffix(&self, mut f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool, + { + let start = self.iter().position(|v| !f(v)).unwrap_or(self.len()); + let end = self + .iter() + .skip(start) + .rposition(|v| !f(v)) + .map_or(start, |i| start + i + 1); + (start, end) + } } /// This struct is created by [`SliceExt::group_by_key`]. @@ -276,6 +304,15 @@ pub trait Get { fn set(&mut self, index: Index, component: Self::Component) { *self.get_mut(index) = component; } + + /// Builder-style method for setting a component. + fn with(mut self, index: Index, component: Self::Component) -> Self + where + Self: Sized, + { + self.set(index, component); + self + } } /// A numeric type. @@ -301,3 +338,43 @@ pub trait Numeric: /// Whether `self` consists only of finite parts. fn is_finite(self) -> bool; } + +/// Returns the default math class of a character in Typst, if it has one. +/// +/// This is determined by the Unicode math class, with some manual overrides. +pub fn default_math_class(c: char) -> Option { + match c { + // Better spacing. + // https://github.com/typst/typst/commit/2e039cb052fcb768027053cbf02ce396f6d7a6be + ':' => Some(MathClass::Relation), + + // Better spacing when used alongside + PLUS SIGN. + // https://github.com/typst/typst/pull/1726 + '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), + + // Better spacing. + // https://github.com/typst/typst/pull/1855 + '.' | '/' => Some(MathClass::Normal), + + // ⊥ UP TACK should not be a relation, contrary to ⟂ PERPENDICULAR. + // https://github.com/typst/typst/pull/5714 + '\u{22A5}' => Some(MathClass::Normal), + + // Used as a binary connector in linear logic, where it is referred to + // as "par". + // https://github.com/typst/typst/issues/5764 + '⅋' => Some(MathClass::Binary), + + // Those overrides should become the default in the next revision of + // MathClass.txt. + // https://github.com/typst/typst/issues/5764#issuecomment-2632435247 + '⎰' | '⟅' => Some(MathClass::Opening), + '⎱' | '⟆' => Some(MathClass::Closing), + + // Both ∨ and ⟑ are classified as Binary. + // https://github.com/typst/typst/issues/5764 + '⟇' => Some(MathClass::Binary), + + c => unicode_math_class::class(c), + } +} diff --git a/crates/typst-utils/src/scalar.rs b/crates/typst-utils/src/scalar.rs index 4036c2310..6d84fbfdf 100644 --- a/crates/typst-utils/src/scalar.rs +++ b/crates/typst-utils/src/scalar.rs @@ -28,7 +28,7 @@ impl Scalar { /// /// If the value is NaN, then it is set to `0.0` in the result. pub const fn new(x: f64) -> Self { - Self(if is_nan(x) { 0.0 } else { x }) + Self(if x.is_nan() { 0.0 } else { x }) } /// Gets the value of this [`Scalar`]. @@ -37,17 +37,6 @@ impl Scalar { } } -// We have to detect NaNs this way since `f64::is_nan` isn’t const -// on stable yet: -// ([tracking issue](https://github.com/rust-lang/rust/issues/57241)) -#[allow(clippy::unusual_byte_groupings)] -const fn is_nan(x: f64) -> bool { - // Safety: all bit patterns are valid for u64, and f64 has no padding bits. - // We cannot use `f64::to_bits` because it is not const. - let x_bits = unsafe { std::mem::transmute::(x) }; - (x_bits << 1 >> (64 - 12 + 1)) == 0b0_111_1111_1111 && (x_bits << 12) != 0 -} - impl Numeric for Scalar { fn zero() -> Self { Self(0.0) diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 7d02aa426..580ba9e80 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -333,8 +333,6 @@ pub static ROUTINES: Routines = Routines { realize: typst_realize::realize, layout_fragment: typst_layout::layout_fragment, layout_frame: typst_layout::layout_frame, - layout_inline: typst_layout::layout_inline, - layout_box: typst_layout::layout_box, layout_list: typst_layout::layout_list, layout_enum: typst_layout::layout_enum, layout_grid: typst_layout::layout_grid, diff --git a/docs/Cargo.toml b/docs/Cargo.toml index 41a5645e8..acc551754 100644 --- a/docs/Cargo.toml +++ b/docs/Cargo.toml @@ -17,6 +17,8 @@ cli = ["clap", "typst-render", "serde_json"] [dependencies] typst = { workspace = true } +typst-render = { workspace = true, optional = true } +typst-utils = { workspace = true } typst-assets = { workspace = true, features = ["fonts"] } typst-dev-assets = { workspace = true } clap = { workspace = true, optional = true } @@ -28,7 +30,7 @@ serde_json = { workspace = true, optional = true } serde_yaml = { workspace = true } syntect = { workspace = true, features = ["html"] } typed-arena = { workspace = true } -typst-render = { workspace = true, optional = true } +unicode-math-class = { workspace = true } unscanny = { workspace = true } yaml-front-matter = { workspace = true } diff --git a/docs/changelog/0.13.0.md b/docs/changelog/0.13.0.md new file mode 100644 index 000000000..6c2fe4275 --- /dev/null +++ b/docs/changelog/0.13.0.md @@ -0,0 +1,344 @@ +--- +title: 0.13.0 +description: Changes in Typst 0.13.0 +--- + +# Version 0.13.0 (February 19, 2025) + +## Highlights +- There is now a distinction between [proper paragraphs]($par) and just + inline-level content. This is important for future work on accessibility and + means that [first line indent]($par.first-line-indent) can now be enabled for + all paragraphs instead of just consecutive ones. +- The [`outline`] has a better out-of-the-box look and is more customizable +- The new [`curve`] function (that supersedes the `path` function) provides a + simpler and more flexible interface for creating Bézier curves +- The `image` function now supports raw [pixel raster formats]($image.format) + for generating images from within Typst +- Functions that accept [file paths]($syntax/#paths) now also accept raw + [bytes], for full flexibility +- WebAssembly [plugins]($plugin) are more flexible and automatically run + multi-threaded +- Fixed a long-standing bug where single-letter strings in math (`[$"a"$]`) + would be displayed in italics +- You can now specify which charset should be [covered]($text.font) by which + font family +- The [`pdf.embed`] function lets you embed arbitrary files in the exported + PDF +- HTML export is currently under active development. The feature is still _very_ + incomplete, but already available for experimentation behind a feature flag. + +## Model +- There is now a distinction between [proper paragraphs]($par) and just + inline-level content **(Breaking change)** + - All text at the root of a document is wrapped in paragraphs. Meanwhile, text + in a container (like a block) is only wrapped in a paragraph if the + container holds any block-level content. If all of the content is + inline-level, no paragraph is created. + - In the laid-out document, it's not immediately visible whether text became + part of a paragraph. However, it is still important for accessibility, HTML + export, and for properties like `first-line-indent`. + - Show rules on `par` now only affect proper paragraphs + - The `first-line-indent` and `hanging-indent` properties also only affect + proper paragraphs + - Creating a `{par[..]}` with body content that is not fully inline-level will + result in a warning + - The default show rules of various built-in elements like lists, quotes, etc. + were adjusted to ensure they produce/don't produce paragraphs as appropriate + - Removed support for booleans and content in [`outline.indent`] +- The [`outline`] function was fully reworked to improve its out-of-the-box + behavior **(Breaking change)** + - [Outline entries]($outline.entry) are now [blocks]($block) and are thus + affected by block spacing + - The `{auto}` indentation mode now aligns numberings and titles outline-wide + for a grid-like look + - Automatic indentation now also indents entries without a numbering + - Titles wrapping over multiple lines now have hanging indent + - The page number won't appear alone on its own line anymore + - The link now spans the full entry instead of just the title and page number + - The default spacing between outline leader dots was increased + - The [`fill`]($outline.entry.fill) parameter was moved from `outline` to + `outline.entry` and can thus be configured through show-set rules + - Removed `body` and `page` fields from outline entry + - Added `indented`, `prefix`, `inner`, `body`, and `page` methods on outline + entries to simplify writing of show rules +- Added configuration to [`par.first-line-indent`] for indenting all paragraphs + instead of just consecutive ones +- Added [`form`]($ref.form) parameter to `ref` function. Setting the form to + `{"page"}` will produce a page reference instead of a textual one. +- Added [`document.description`] field, which results in corresponding PDF and + HTML metadata +- Added [`enum.reversed`] parameter +- Added support for Greek [numbering] +- When the [`link`] function wraps around a container like a [block], it will + now generate only one link for the whole block instead of individual links for + all the visible leaf elements. This significantly reduces PDF file sizes when + combining `link` and [`repeat`]. +- The [`link`] function will now only strip one prefix (like `mailto:` or + `tel:`) instead of multiple +- The link function now suppresses hyphenation via a built-in show-set rule + rather than through its default show rule +- Displaying the page counter without a specified numbering will now take the + page numbering into account + +## Visualization +- Added new [`curve`] function that supersedes the [`path`] function and + provides a simpler and more flexible interface. The `path` function is now + deprecated. +- The `image` function now supports raw [pixel raster formats]($image.format). + This can be used to generate images from within Typst without the need for + encoding in an image exchange format. +- Added [`image.scaling`] parameter for configuring how an image is scaled by + PNG export and PDF viewers (smooth or pixelated) +- Added [`image.icc`] parameter for providing or overriding the ICC profile of + an image +- Renamed `pattern` to [`tiling`]. The name `pattern` remains as a deprecated + alias. +- Added [`gradient.center`], [`gradient.radius`], [`gradient.focal-center`], and + [`gradient.focal-radius`] methods +- Fixed interaction of clipping and outset on [`box`] and [`block`] +- Fixed panic with [`path`] of infinite length +- Fixed non-solid (e.g. tiling) text fills in clipped blocks +- Fixed a crash for images with a DPI value of zero +- Fixed floating-point error in [`gradient.repeat`] +- Auto-detection of image formats from a raw buffer now has support for SVGs + +## Scripting +- Functions that accept [file paths]($syntax/#paths) now also accept raw + [bytes] + - [`image`], [`cbor`], [`csv`], [`json`], [`toml`], [`xml`], and [`yaml`] now + support a path string or bytes and their `.decode` variants are deprecated + - [`plugin`], [`bibliography`], [`bibliography.style`], [`cite.style`], + [`raw.theme`], and [`raw.syntaxes`] now accept bytes in addition to path + strings. These did not have `.decode` variants, so this adds new + flexibility. + - The `path` argument/field of [`image`] and [`bibliography`] was renamed to + `source` and `sources`, respectively **(Minor breaking change)** +- Improved WebAssembly [plugins]($plugin) + - The `plugin` type is replaced by a [`plugin` function]($plugin) that returns + a [module] containing normal Typst functions. This module can be used with + import syntax. **(Breaking change)** + - Plugins now automatically run in multiple threads without any changes by + plugin authors + - A new [`plugin.transition`] API is introduced which allows plugins to run + impure initialization in a way that doesn't break Typst's purity guarantees +- The variable name bound by a bare import (no renaming, no import list) is now + determined statically and dynamic imports without `{as}` renaming (e.g. + `{import "ot" + "her.typ"}`) are a hard error **(Breaking change)** +- Values of the [`arguments`] type can now be added with `+` and + [joined]($scripting/#blocks) in curly-braced code blocks +- Functions in an element function's scope can now be called with method syntax, + bringing elements and types closer (in anticipation of a future full + unification of the two). Currently, this is only useful for [`outline.entry`] + as no other element function defines methods. +- Added [`calc.norm`] function +- Added support for 32-bit floats in [`float.from-bytes`] and [`float.to-bytes`] +- The [`decimal`] constructor now also accepts decimal values +- Improved `repr` of [symbols]($symbol), [arguments], and [types]($type) +- Duplicate [symbol] variants and modifiers are now a hard error + **(Breaking change)** + +## Math +- Fixed a bug where single letter strings in math (`[$"a"$]`) would be displayed + in italics +- Math function calls can now have hyphenated named arguments and support + [argument spreading]($arguments/#spreading) +- Better looking accents thanks to support for the `flac` (Flattened Accent + Forms) and `dtls` (Dotless Forms) OpenType features +- Added `lcm` [text operator]($math.op) +- The [`bold`]($math.bold) function now works with ϝ and Ϝ +- The [`italic`]($math.italic) function now works with ħ +- Fixed a bug where the extent of a math equation was wrongly affected by + internal metadata +- Fixed interaction of [`lr`]($math.lr) and [context] expressions +- Fixed weak spacing being unconditionally ignored in [`lr`]($math.lr) +- Fixed sub/superscripts sometimes being in the wrong position with + [`lr`]($math.lr) +- Fixed multi-line annotations (e.g. overbrace) changing the math baseline +- Fixed merging of attachments when the base is a nested equation +- Fixed resolving of contextual (em-based) text sizes within math +- Fixed spacing around up tacks (⊥) + +## Bibliography +- Prose and author-only citations now use editor names if the author names are + unavailable +- Some non-standard but widely used BibLaTeX `editortype`s like `producer`, + `writer`, `scriptwriter`, and `none` (defined by widespread style + `biblatex-chicago` to mean performers within `music` and `video` entries) are + now recognized +- CSL styles can now render affixes around the bibliography +- For BibTeX entries with `eprinttype = {pubmed}`, the PubMed ID will now be + correctly processed +- Whitespace handling for strings delimiting initialized names has been improved +- Uppercase spelling after apostrophes used as quotation marks is now possible +- Fixed bugs around the handling of CSL delimiting characters +- Fixed a problem with parsing multibyte characters in page ranges that could + prevent Hayagriva from parsing some BibTeX page ranges +- Updated CSL APA style +- Updated CSL locales for Finnish, Swiss German, Austrian German, German, and + Arabic + +## Text +- Added support for specifying which charset should be [covered]($text.font) by + which font family +- Added [`all`]($smallcaps.all) parameter to `smallcaps` function that also + enables small capitals on uppercase letters +- Added basic i18n for Basque and Bulgarian +- [Justification]($par.justify) does not affect [raw] blocks anymore +- [CJK-Latin-spacing]($text.cjk-latin-spacing) does not affect [raw] text + anymore +- Fixed wrong language codes being used for Greek and Ukrainian +- Fixed default quotes for Croatian and Bulgarian +- Fixed crash in RTL text handling +- Added support for [`raw`] syntax highlighting for a few new languages: CFML, + NSIS, and WGSL +- New font metadata exception for New Computer Modern Sans Math +- Updated bundled New Computer Modern fonts to version 7.0.1 + +## Layout +- Fixed various bugs with footnotes + - Fixed footnotes getting lost when multiple footnotes were nested within + another footnote + - Fixed endless loops with empty and overlarge footnotes + - Fixed crash with overlarge footnotes within a floating placement +- Fixed sizing of quadratic shapes ([`square`] and [`circle`]) +- Fixed [`block.sticky`] not working properly at the top of a container +- Fixed crash due to consecutive weak spacing +- Fixed crash when a [block] or text have negative sizes +- Fixed unnecessary hyphenations occurring in rare scenarios due to a bad + interaction between padding and paragraph optimization +- Fixed lone [citations]($cite) in [`align`] not becoming their own paragraph + +## Syntax +- Top-level closing square brackets that do not have a matching opening square + bracket are now a hard error **(Minor breaking change)** +- Adding a space between the identifier and the parentheses in a set rule is not + allowed anymore **(Minor breaking change)** +- Numbers with a unit cannot have a base prefix anymore, e.g. `0b100000pt` is + not allowed anymore. Previously, it was syntactically allowed but always + resolved to a value of zero. **(Minor breaking change)** +- Using `is` as an identifier will now warn as it might become a keyword in the + future +- Fixed minor whitespace handling bugs + - in math mode argument lists + - at the end of headings + - between a term list's term and description +- Fixed parsing of empty single line raw blocks with 3+ backticks and a language + tag +- Fixed minor bug with parentheses parsing in math +- Markup that can only appear at the start of the line (headings, lists) can now + also appear at the start of a list item +- A shebang `#!` at the very start of a file is now ignored + +## PDF export +- Added [`pdf.embed`] function for embedding arbitrary files in the exported PDF +- Added support for PDF/A-3b export +- The PDF timestamp will now contain the timezone by default + +## HTML export +**Note:** HTML export is currently under active development. The feature is +still _very_ incomplete, but already available for experimentation behind a +feature flag. + +- Added HTML output support for some (but not all) of the built-in elements +- Added [`html.elem`] function for outputting an arbitrary HTML element +- Added [`html.frame`] function for integrating content that requires layout + into HTML (by embedding an SVG) +- Added [`target`] function which returns either `{"paged"}` or `{"html"}` + depending on the export target + +## Tooling and Diagnostics +- Autocompletion improvements + - Added autocompletion for file paths + - Smarter autocompletion of variables: Completing `{rect(fill: |)}` will now + only show variables which contain a valid fill (either directly or nested, + e.g. a dictionary containing a valid fill) + - Different functions will now autocomplete with different brackets (round vs + square) depending on which kind is more useful + - Positional parameters which are already provided aren't autocompleted again + anymore + - Fixed variable autocompletion not considering parameters + - Added autocompletion snippets for common figure usages + - Fixed autocompletion after half-completed import item + - Fixed autocompletion for `cite` function +- Added warning when an unconditional return in a code block discards joined + content +- Fixed error message when accessing non-existent label +- Fixed handling of nested imports in IDE functionality + +## Command Line Interface +- Added `--features` argument and `TYPST_FEATURES` environment variable for + opting into experimental features. The only feature so far is `html`. +- Added a live reloading HTTP server to `typst watch` when targeting HTML +- Fixed self-update not being aware about certain target architectures +- Fixed crash when piping `typst fonts` output to another command +- Fixed handling of relative paths in `--make-deps` output +- Fixed handling of multipage SVG and PNG export in `--make-deps` output +- Colons in filenames are now correctly escaped in `--make-deps` output + +## Symbols +- New + - `inter`, `inter.and`, `inter.big`, `inter.dot`, `inter.double`, `inter.sq`, + `inter.sq.big`, `inter.sq.double`, `integral.inter` + - `asymp`, `asymp.not` + - `mapsto`, `mapsto.long` + - `divides.not.rev`, `divides.struck` + - `interleave`, `interleave.big`, `interleave.struck` + - `eq.triple.not`, `eq.dots`, `eq.dots.down`, `eq.dots.up` + - `smt`, `smt.eq`, `lat`, `lat.eq` + - `colon.tri`, `colon.tri.op` + - `dagger.triple`, `dagger.l`, `dagger.r`, `dagger.inv` + - `hourglass.stroked`, `hourglass.filled` + - `die.six`, `die.five`, `die.four`, `die.three`, `die.two`, `die.one` + - `errorbar.square.stroked`, `errorbar.square.filled`, + `errorbar.diamond.stroked`, `errorbar.diamond.filled`, + `errorbar.circle.stroked`, `errorbar.circle.filled` + - `numero` + - `Omega.inv` +- Renamed + - `ohm.inv` to `Omega.inv` +- Changed codepoint + - `angle.l.double` from `《` to `⟪` + - `angle.r.double` from `》` to `⟫` + - `angstrom` from U+212B (`Å`) to U+00C5 (`Å`) +- Deprecated + - `sect` and all its variants in favor of `inter` + - `integral.sect` in favor of `integral.inter` +- Removed + - `degree.c` in favor of `°C` (`[$upright(°C)$]` or `[$upright(degree C)$]` in math) + - `degree.f` in favor of `°F` (`[$upright(°F)$]` or `[$upright(degree F)$]` in math) + - `kelvin` in favor of just K (`[$upright(K)$]` in math) + +## Deprecations +- The [`path`] function in favor of the [`curve`] function +- The name `pattern` for tiling patterns in favor of the new name [`tiling`] +- [`image.decode`], [`cbor.decode`], [`csv.decode`], [`json.decode`], + [`toml.decode`], [`xml.decode`], [`yaml.decode`] in favor of the top-level + functions directly accepting both paths and bytes +- The `sect` and its variants in favor of `inter`, and `integral.sect` in favor + of `integral.inter` +- The compatibility behavior of type/str comparisons (e.g. `{int == "integer"}`) + which was temporarily introduced in Typst 0.8 now emits warnings. It will be + removed in Typst 0.14. + +## Removals +- Removed `style` function and `styles` argument of [`measure`], use a [context] + expression instead **(Breaking change)** +- Removed `state.display` function, use [`state.get`] instead + **(Breaking change)** +- Removed `location` argument of [`state.at`], [`counter.at`], and [`query`] + **(Breaking change)** +- Removed compatibility behavior where [`counter.display`] worked without + [context] **(Breaking change)** +- Removed compatibility behavior of [`locate`] **(Breaking change)** + +## Development +- The `typst::compile` function is now generic and can return either a + `PagedDocument` or an `HtmlDocument` +- `typst-timing` now supports WebAssembly targets via `web-sys` when the `wasm` + feature is enabled +- Increased minimum supported Rust version to 1.80 +- Fixed linux/arm64 Docker image + +## Contributors + diff --git a/docs/changelog/welcome.md b/docs/changelog/welcome.md index 12b6b896b..8fb85f870 100644 --- a/docs/changelog/welcome.md +++ b/docs/changelog/welcome.md @@ -10,6 +10,7 @@ forward. This section documents all changes to Typst since its initial public release. ## Versions +- [Typst 0.13.0]($changelog/0.13.0) - [Typst 0.12.0]($changelog/0.12.0) - [Typst 0.11.1]($changelog/0.11.1) - [Typst 0.11.0]($changelog/0.11.0) diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md index 743afa5a6..5137ae1a9 100644 --- a/docs/guides/guide-for-latex-users.md +++ b/docs/guides/guide-for-latex-users.md @@ -657,7 +657,8 @@ applicable, contains possible workarounds. - **Well-established plotting ecosystem.** LaTeX users often create elaborate charts along with their documents in PGF/TikZ. The Typst ecosystem does not yet offer the same breadth of available options, but the ecosystem around the - [`cetz`](https://github.com/cetz-package/cetz) package is catching up quickly. + [`cetz` package](https://typst.app/universe/package/cetz) is catching up + quickly. - **Change page margins without a pagebreak.** In LaTeX, margins can always be adjusted, even without a pagebreak. To change margins in Typst, you use the @@ -670,4 +671,6 @@ applicable, contains possible workarounds. format, but you can easily convert both into SVG files with [online tools](https://cloudconvert.com/pdf-to-svg) or [Inkscape](https://inkscape.org/). The web app will automatically convert PDF - files to SVG files upon uploading them. + files to SVG files upon uploading them. You can also use the + community-provided [`muchpdf` package](https://typst.app/universe/package/muchpdf) + to embed PDFs. It internally converts PDFs to SVGs on-the-fly. diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md index c93a778e2..36ed0fa23 100644 --- a/docs/guides/page-setup.md +++ b/docs/guides/page-setup.md @@ -56,7 +56,7 @@ requirements with examples. Typst's default page size is A4 paper. Depending on your region and your use case, you will want to change this. You can do this by using the [`{page}`]($page) set rule and passing it a string argument to use a common page -size. Options include the complete ISO 216 series (e.g. `"iso-a4"`, `"iso-c2"`), +size. Options include the complete ISO 216 series (e.g. `"a4"` and `"iso-c2"`), customary US formats like `"us-legal"` or `"us-letter"`, and more. Check out the reference for the [page's paper argument]($page.paper) to learn about all available options. diff --git a/docs/guides/tables.md b/docs/guides/tables.md index 5c9cf11da..5b7efdc48 100644 --- a/docs/guides/tables.md +++ b/docs/guides/tables.md @@ -886,8 +886,8 @@ everything else by providing an array in the `align` argument: stroke: none, table.header[Day][Location][Hotel or Apartment][Activities], - [1], [Paris, France], [Hotel de L'Europe], [Arrival, Evening River Cruise], - [2], [Paris, France], [Hotel de L'Europe], [Louvre Museum, Eiffel Tower], + [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise], + [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower], [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting], [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum], [5], [Zermatt, Switzerland], [Alpine Lodge], [Visit Matterhorn, Skiing], @@ -911,8 +911,8 @@ bottom-aligned. Let's use a function instead to do so: stroke: none, table.header[Day][Location][Hotel or Apartment][Activities], - [1], [Paris, France], [Hotel de L'Europe], [Arrival, Evening River Cruise], - [2], [Paris, France], [Hotel de L'Europe], [Louvre Museum, Eiffel Tower], + [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise], + [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower], <<< // ... remaining days omitted >>> [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting], >>> [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum], diff --git a/docs/reference/export/html.md b/docs/reference/export/html.md new file mode 100644 index 000000000..330c2e136 --- /dev/null +++ b/docs/reference/export/html.md @@ -0,0 +1,61 @@ +

+ +Typst's HTML export is currently under active development. The feature is still +very incomplete and only available for experimentation behind a feature flag. Do +not use this feature for production use cases. In the CLI, you can experiment +with HTML export by passing `--features html` or setting the `TYPST_FEATURES` +environment variables to `html`. In the web app, HTML export is not available at +this time. Visit the [tracking issue](https://github.com/typst/typst/issues/5512) +to follow progress on HTML export and learn more about planned features. +
+ +HTML files describe a document structurally. The aim of Typst's HTML export is +to capture the structure of an input document and produce semantically rich HTML +that retains this structure. The resulting HTML should be accessible, +human-readable, and editable by hand and downstream tools. + +PDF, PNG, and SVG export, in contrast, all produce _visual_ representations of a +fully-laid out document. This divergence in the formats' intents means that +Typst cannot simply produce perfect HTML for your existing Typst documents. It +cannot always know what the best semantic HTML representation of your content +is. + +Instead, it gives _you_ full control: You can check the current export format +through the [`target`] function and when it is set to HTML, generate [raw HTML +elements]($html.elem). The primary intended use of these elements is in +templates and show rules. This way, the document's contents can be fully +agnostic to the export target and content can be shared between PDF and HTML +export. + +Currently, Typst will always output a single HTML file. Support for outputting +directories with multiple HTML documents and assets, as well as support for +outputting fragments that can be integrated into other HTML documents is +planned. + +Typst currently does not output CSS style sheets, instead focussing on emitting +semantic markup. You can of course write your own CSS styles and still benefit +from sharing your _content_ between PDF and HTML. For the future, we plan to +give you the option of automatically emitting CSS, taking more of your existing +set rules into account. + +# Exporting as HTML +## Command Line +Pass `--format html` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.html`. Note that you must also pass `--features html` +or set `TYPST_FEATURES=html` to enable this experimental export target. + +When using `typst watch`, Typst will spin up a live-reloading HTTP server. You +can configure it as follows: + +- Pass `--port` to change the port. (Defaults to the first free port in the + range 3000-3005.) +- Pass `--no-reload` to disable injection of a live reload script. (The HTML + that is written to disk isn't affected either way.) +- Pass `--no-serve` to disable the server altogether. + +## Web App +Not currently available. + +# HTML-specific functionality +Typst exposes HTML-specific functionality in the global `html` module. See below +for the definitions it contains. diff --git a/docs/reference/export/pdf.md b/docs/reference/export/pdf.md new file mode 100644 index 000000000..b220ae946 --- /dev/null +++ b/docs/reference/export/pdf.md @@ -0,0 +1,71 @@ +PDF files focus on accurately describing documents visually, but also have +facilities for annotating their structure. This hybrid approach makes +them a good fit for document exchange: They render exactly the same on every +device, but also support extraction of a document's content and structure (at +least to an extent). Unlike PNG files, PDFs are not bound to a specific +resolution. Hence, you can view them at any size without incurring a loss of +quality. + +# PDF standards +The International Standards Organization (ISO) has published the base PDF +standard and various standards that extend it to make PDFs more suitable for +specific use-cases. By default, Typst exports PDF 1.7 files. Adobe Acrobat 8 and +later as well as all other commonly used PDF viewers are compatible with this +PDF version. + +## PDF/A +Typst optionally supports emitting PDF/A-conformant files. PDF/A files are +geared towards maximum compatibility with current and future PDF tooling. They +do not rely on difficult-to-implement or proprietary features and contain +exhaustive metadata. This makes them suitable for long-term archival. + +The PDF/A Standard has multiple versions (_parts_ in ISO terminology) and most +parts have multiple profiles that indicate the file's conformance level. +Currently, Typst supports these PDF/A output profiles: + +- PDF/A-2b: The basic conformance level of ISO 19005-2. This version of PDF/A is + based on PDF 1.7 and results in self-contained, archivable PDF files. + +- PDF/A-3b: The basic conformance level of ISO 19005-3. This version of PDF/A is + based on PDF 1.7 and results in archivable PDF files that can contain + arbitrary other related files as [attachments]($pdf.embed). The only + difference between it and PDF/A-2b is the capability to embed + non-PDF/A-conformant files within. + +When choosing between exporting PDF/A and regular PDF, keep in mind that PDF/A +files contain additional metadata, and that some readers will prevent the user +from modifying a PDF/A file. Some features of Typst may be disabled depending on +the PDF standard you choose. + +# Exporting as PDF +## Command Line +PDF is Typst's default export format. Running the `compile` or `watch` +subcommand without specifying a format will create a PDF. When exporting to PDF, +you have the following configuration options: + +- Which PDF standards Typst should enforce conformance with by specifying + `--pdf-standard` followed by one or multiple comma-separated standards. Valid + standards are `1.7`, `a-2b`, and `a-3b`. By default, Typst outputs + PDF-1.7-compliant files. + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click the quick download button at the top right to export a PDF with default +settings. For further configuration, click "File" > "Export as" > "PDF" or click +the downwards-facing arrow next to the quick download button and select "Export +as PDF". When exporting to PDF, you have the following configuration options: + +- Which PDF standards Typst should enforce conformance with. By default, Typst + outputs PDF-1.7-compliant files. Valid additional standards are `A-2b` and + `A-3b`. + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. + +# PDF-specific functionality +Typst exposes PDF-specific functionality in the global `pdf` module. See below +for the definitions it contains. diff --git a/docs/reference/export/png.md b/docs/reference/export/png.md new file mode 100644 index 000000000..0e817e0f1 --- /dev/null +++ b/docs/reference/export/png.md @@ -0,0 +1,61 @@ +Instead of creating a PDF, Typst can also directly render pages to PNG raster +graphics. PNGs are losslessly compressed images that can contain one page at a +time. When exporting a multi-page document, Typst will emit multiple PNGs. PNGs +are a good choice when you want to use Typst's output in an image editing +software or when you can use none of Typst's other export formats. + +In contrast to Typst's other export formats, PNGs are bound to a specific +resolution. When exporting to PNG, you can configure the resolution as pixels +per inch (PPI). If the medium you view the PNG on has a finer resolution than +the PNG you exported, you will notice a loss of quality. Typst calculates the +resolution of your PNGs based on each page's physical dimensions and the PPI. If +you need guidance for choosing a PPI value, consider the following: + +- A value of 300 or 600 is typical for desktop printing. +- Professional prints of detailed graphics can go up to 1200 PPI. +- If your document is only viewed at a distance, e.g. a poster, you may choose a + smaller value than 300. +- If your document is viewed on screens, a typical PPI value for a smartphone is + 400-500. + +Because PNGs only contain a pixel raster, the text within cannot be extracted +automatically (without OCR), for example by copy/paste or a screen reader. If +you need the text to be accessible, export a PDF or HTML file instead. + +PNGs can have transparent backgrounds. By default, Typst will output a PNG with +an opaque white background. You can make the background transparent using +`[#set page(fill: none)]`. Learn more on the +[`page` function's reference page]($page.fill). + +# Exporting as PNG +## Command Line +Pass `--format png` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.png`. + +If your document has more than one page, Typst will create multiple image files. +The output file name must then be a template string containing at least one of +- `[{p}]`, which will be replaced by the page number +- `[{0p}]`, which will be replaced by the zero-padded page number (so that all + numbers have the same length) +- `[{t}]`, which will be replaced by the total number of pages + +When exporting to PNG, you have the following configuration options: + +- Which resolution to render at by specifying `--ppi` followed by a number of + pixels per inch. The default is `144`. + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click "File" > "Export as" > "PNG" or click the downwards-facing arrow next to +the quick download button and select "Export as PNG". When exporting to PNG, you +have the following configuration options: + +- The resolution at which the pages should be rendered, as a number of pixels + per inch. The default is `144`. + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. diff --git a/docs/reference/export/svg.md b/docs/reference/export/svg.md new file mode 100644 index 000000000..630ab8452 --- /dev/null +++ b/docs/reference/export/svg.md @@ -0,0 +1,48 @@ +Instead of creating a PDF, Typst can also directly render pages to scalable +vector graphics (SVGs), which are the preferred format for embedding vector +graphics in web pages. Like PDF files, SVGs display your document exactly how +you have laid it out in Typst. Likewise, they share the benefit of not being +bound to a specific resolution. Hence, you can print or view SVG files on any +device without incurring a loss of quality. (Note that font printing quality may +be better with a PDF.) In contrast to a PDF, an SVG cannot contain multiple +pages. When exporting a multi-page document, Typst will emit multiple SVGs. + +SVGs can represent text in two ways: By embedding the text itself and rendering +it with the fonts available on the viewer's computer or by embedding the shapes +of each glyph in the font used to create the document. To ensure that the SVG +file looks the same across all devices it is viewed on, Typst chooses the latter +method. This means that the text in the SVG cannot be extracted automatically, +for example by copy/paste or a screen reader. If you need the text to be +accessible, export a PDF or HTML file instead. + +SVGs can have transparent backgrounds. By default, Typst will output an SVG with +an opaque white background. You can make the background transparent using +`[#set page(fill: none)]`. Learn more on the +[`page` function's reference page]($page.fill). + +# Exporting as SVG +## Command Line +Pass `--format svg` to the `compile` or `watch` subcommand or provide an output +file name that ends with `.svg`. + +If your document has more than one page, Typst will create multiple image files. +The output file name must then be a template string containing at least one of +- `[{p}]`, which will be replaced by the page number +- `[{0p}]`, which will be replaced by the zero-padded page number (so that all + numbers have the same length) +- `[{t}]`, which will be replaced by the total number of pages + +When exporting to SVG, you have the following configuration options: + +- Which pages to export by specifying `--pages` followed by a comma-separated + list of numbers or dash-separated number ranges. Ranges can be half-open. + Example: `2,3,7-9,11-`. + +## Web App +Click "File" > "Export as" > "SVG" or click the downwards-facing arrow next to +the quick download button and select "Export as SVG". When exporting to SVG, you +have the following configuration options: + +- Which pages to export. Valid options are "All pages", "Current page", and + "Custom ranges". Custom ranges are a comma-separated list of numbers or + dash-separated number ranges. Ranges can be half-open. Example: `2,3,7-9,11-`. diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 961d675dc..8fea3a1f2 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -170,8 +170,8 @@ category: symbols path: ["emoji"] details: | - Named emoji. + Named emojis. For example, `#emoji.face` produces the 😀 emoji. If you frequently use certain emojis, you can also import them from the `emoji` module (`[#import - emoji: face]`) to use them without the `#emoji.` prefix. + emoji: face]`) to use them without the `emoji.` prefix. diff --git a/docs/reference/context.md b/docs/reference/language/context.md similarity index 100% rename from docs/reference/context.md rename to docs/reference/language/context.md diff --git a/docs/reference/scripting.md b/docs/reference/language/scripting.md similarity index 97% rename from docs/reference/scripting.md rename to docs/reference/language/scripting.md index 89508eee0..5e0f1555e 100644 --- a/docs/reference/scripting.md +++ b/docs/reference/language/scripting.md @@ -120,7 +120,7 @@ You can use the underscore to discard elements in a destructuring pattern: The y coordinate is #y. ``` -Destructuring also work in argument lists of functions ... +Destructuring also works in argument lists of functions ... ```example #let left = (2, 4, 5) @@ -145,7 +145,7 @@ swap variables among other things. ## Conditionals With a conditional, you can display or compute different things depending on whether some condition is fulfilled. Typst supports `{if}`, `{else if}` and -`{else}` expression. When the condition evaluates to `{true}`, the conditional +`{else}` expressions. When the condition evaluates to `{true}`, the conditional yields the value resulting from the if's body, otherwise yields the value resulting from the else's body. @@ -269,7 +269,7 @@ the following two equivalent ways: The structure of a method call is `{value.method(..args)}` and its equivalent full function call is `{type(value).method(value, ..args)}`. The documentation -of each type lists it's scoped functions. You cannot currently define your own +of each type lists its scoped functions. You cannot currently define your own methods. ```example @@ -363,7 +363,7 @@ and can be achieved using functions from the | `{not in}` | Check if not in collection | Binary | 4 | | `{not}` | Logical "not" | Unary | 3 | | `{and}` | Short-circuiting logical "and" | Binary | 3 | -| `{or}` | Short-circuiting logical "or | Binary | 2 | +| `{or}` | Short-circuiting logical "or" | Binary | 2 | | `{=}` | Assignment | Binary | 1 | | `{+=}` | Add-Assignment | Binary | 1 | | `{-=}` | Subtraction-Assignment | Binary | 1 | diff --git a/docs/reference/styling.md b/docs/reference/language/styling.md similarity index 100% rename from docs/reference/styling.md rename to docs/reference/language/styling.md diff --git a/docs/reference/syntax.md b/docs/reference/language/syntax.md similarity index 100% rename from docs/reference/syntax.md rename to docs/reference/language/syntax.md diff --git a/docs/reference/library/data-loading.md b/docs/reference/library/data-loading.md new file mode 100644 index 000000000..659a8cccc --- /dev/null +++ b/docs/reference/library/data-loading.md @@ -0,0 +1,4 @@ +Data loading from external files. + +These functions help you with loading and embedding data, for example from the +results of an experiment. diff --git a/docs/reference/library/foundations.md b/docs/reference/library/foundations.md new file mode 100644 index 000000000..738c3789d --- /dev/null +++ b/docs/reference/library/foundations.md @@ -0,0 +1,4 @@ +Foundational types and functions. + +Here, you'll find documentation for basic data types like [integers]($int) and +[strings]($str) as well as details about core computational functions. diff --git a/docs/reference/library/introspection.md b/docs/reference/library/introspection.md new file mode 100644 index 000000000..f48a9937c --- /dev/null +++ b/docs/reference/library/introspection.md @@ -0,0 +1,10 @@ +Interactions between document parts. + +This category is home to Typst's introspection capabilities: With the `counter` +function, you can access and manipulate page, section, figure, and equation +counters or create custom ones. Meanwhile, the `query` function lets you search +for elements in the document to construct things like a list of figures or +headers which show the current chapter title. + +Most of the functions are _contextual._ It is recommended to read the chapter on +[context] before continuing here. diff --git a/docs/reference/library/layout.md b/docs/reference/library/layout.md new file mode 100644 index 000000000..450058d4c --- /dev/null +++ b/docs/reference/library/layout.md @@ -0,0 +1,3 @@ +Arranging elements on the page in different ways. + +By combining layout functions, you can create complex and automatic layouts. diff --git a/docs/reference/library/math.md b/docs/reference/library/math.md new file mode 100644 index 000000000..61f2bb58f --- /dev/null +++ b/docs/reference/library/math.md @@ -0,0 +1,101 @@ +Typst has special [syntax]($syntax/#math) and library functions to typeset +mathematical formulas. Math formulas can be displayed inline with text or as +separate blocks. They will be typeset into their own block if they start and end +with at least one space (e.g. `[$ x^2 $]`). + +# Variables +In math, single letters are always displayed as is. Multiple letters, however, +are interpreted as variables and functions. To display multiple letters +verbatim, you can place them into quotes and to access single letter variables, +you can use the [hash syntax]($scripting/#expressions). + +```example +$ A = pi r^2 $ +$ "area" = pi dot "radius"^2 $ +$ cal(A) := + { x in RR | x "is natural" } $ +#let x = 5 +$ #x < 17 $ +``` + +# Symbols +Math mode makes a wide selection of [symbols]($category/symbols/sym) like `pi`, +`dot`, or `RR` available. Many mathematical symbols are available in different +variants. You can select between different variants by applying +[modifiers]($symbol) to the symbol. Typst further recognizes a number of +shorthand sequences like `=>` that approximate a symbol. When such a shorthand +exists, the symbol's documentation lists it. + +```example +$ x < y => x gt.eq.not y $ +``` + +# Line Breaks +Formulas can also contain line breaks. Each line can contain one or multiple +_alignment points_ (`&`) which are then aligned. + +```example +$ sum_(k=0)^n k + &= 1 + ... + n \ + &= (n(n+1)) / 2 $ +``` + +# Function calls +Math mode supports special function calls without the hash prefix. In these +"math calls", the argument list works a little differently than in code: + +- Within them, Typst is still in "math mode". Thus, you can write math directly + into them, but need to use hash syntax to pass code expressions (except for + strings, which are available in the math syntax). +- They support positional and named arguments, as well as argument spreading. +- They don't support trailing content blocks. +- They provide additional syntax for 2-dimensional argument lists. The semicolon + (`;`) merges preceding arguments separated by commas into an array argument. + +```example +$ frac(a^2, 2) $ +$ vec(1, 2, delim: "[") $ +$ mat(1, 2; 3, 4) $ +$ mat(..#range(1, 5).chunks(2)) $ +$ lim_x = + op("lim", limits: #true)_x $ +``` + +To write a verbatim comma or semicolon in a math call, escape it with a +backslash. The colon on the other hand is only recognized in a special way if +directly preceded by an identifier, so to display it verbatim in those cases, +you can just insert a space before it. + +Functions calls preceded by a hash are normal code function calls and not +affected by these rules. + +# Alignment +When equations include multiple _alignment points_ (`&`), this creates blocks of +alternatingly right- and left-aligned columns. In the example below, the +expression `(3x + y) / 7` is right-aligned and `= 9` is left-aligned. The word +"given" is also left-aligned because `&&` creates two alignment points in a row, +alternating the alignment twice. `& &` and `&&` behave exactly the same way. +Meanwhile, "multiply by 7" is right-aligned because just one `&` precedes it. +Each alignment point simply alternates between right-aligned/left-aligned. + +```example +$ (3x + y) / 7 &= 9 && "given" \ + 3x + y &= 63 & "multiply by 7" \ + 3x &= 63 - y && "subtract y" \ + x &= 21 - y/3 & "divide by 3" $ +``` + +# Math fonts +You can set the math font by with a [show-set rule]($styling/#show-rules) as +demonstrated below. Note that only special OpenType math fonts are suitable for +typesetting maths. + +```example +#show math.equation: set text(font: "Fira Math") +$ sum_(i in NN) 1 + i $ +``` + +# Math module +All math functions are part of the `math` [module]($scripting/#modules), which +is available by default in equations. Outside of equations, they can be accessed +with the `math.` prefix. diff --git a/docs/reference/library/model.md b/docs/reference/library/model.md new file mode 100644 index 000000000..e433ed53b --- /dev/null +++ b/docs/reference/library/model.md @@ -0,0 +1,5 @@ +Document structuring. + +Here, you can find functions to structure your document and interact with that +structure. This includes section headings, figures, bibliography management, +cross-referencing and more. diff --git a/docs/reference/library/symbols.md b/docs/reference/library/symbols.md new file mode 100644 index 000000000..2e6f48cdb --- /dev/null +++ b/docs/reference/library/symbols.md @@ -0,0 +1,5 @@ +These two modules give names to symbols and emoji to make them easy to insert +with a normal keyboard. Alternatively, you can also always directly enter +Unicode symbols into your text and formulas. In addition to the symbols listed +below, math mode defines `dif` and `Dif`. These are not normal symbol values +because they also affect spacing and font style. diff --git a/docs/reference/library/text.md b/docs/reference/library/text.md new file mode 100644 index 000000000..239c0b265 --- /dev/null +++ b/docs/reference/library/text.md @@ -0,0 +1,3 @@ +Text styling. + +The [text function]($text) is of particular interest. diff --git a/docs/reference/library/visualize.md b/docs/reference/library/visualize.md new file mode 100644 index 000000000..9259401f8 --- /dev/null +++ b/docs/reference/library/visualize.md @@ -0,0 +1,5 @@ +Drawing and data visualization. + +If you want to create more advanced drawings or plots, also have a look at the +[CetZ](https://github.com/johannes-wolf/cetz) package as well as more +specialized [packages]($universe) for your use case. diff --git a/docs/reference/packages.md b/docs/reference/packages.md deleted file mode 100644 index bfd1ef580..000000000 --- a/docs/reference/packages.md +++ /dev/null @@ -1,6 +0,0 @@ -Typst [packages]($scripting/#packages) encapsulate reusable building blocks -and make them reusable across projects. Below is a list of Typst packages -created by the community. Due to the early and experimental nature of Typst's -package management, they all live in a `preview` namespace. Click on a package's -name to view its documentation and use the copy button on the right to get a -full import statement for it. diff --git a/docs/src/html.rs b/docs/src/html.rs index a1206032d..9077d5c47 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -301,7 +301,10 @@ impl<'a> Handler<'a> { return; } - let default = self.peeked.as_ref().map(|text| text.to_kebab_case()); + let body = self.peeked.as_ref(); + let default = body.map(|text| text.to_kebab_case()); + let has_id = id_slot.is_some(); + let id: &'a str = match (&id_slot, default) { (Some(id), default) => { if Some(*id) == default.as_deref() { @@ -316,10 +319,10 @@ impl<'a> Handler<'a> { *id_slot = (!id.is_empty()).then_some(id); // Special case for things like "v0.3.0". - let name = if id.starts_with('v') && id.contains('.') { - id.into() - } else { - id.to_title_case().into() + let name = match &body { + _ if id.starts_with('v') && id.contains('.') => id.into(), + Some(body) if !has_id => body.as_ref().into(), + _ => id.to_title_case().into(), }; let mut children = &mut self.outline; @@ -486,7 +489,7 @@ impl World for DocWorld { fn file(&self, id: FileId) -> FileResult { assert!(id.package().is_none()); - Ok(Bytes::from_static( + Ok(Bytes::new( typst_dev_assets::get_by_name( &id.vpath().as_rootless_path().to_string_lossy(), ) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 5ca3724ab..e9771738d 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -12,24 +12,20 @@ pub use self::model::*; use std::collections::HashSet; use ecow::{eco_format, EcoString}; +use heck::ToTitleCase; use serde::Deserialize; use serde_yaml as yaml; use std::sync::LazyLock; use typst::diag::{bail, StrResult}; use typst::foundations::{ - AutoValue, Bytes, CastInfo, Category, Func, Module, NoneValue, ParamInfo, Repr, - Scope, Smart, Type, Value, FOUNDATIONS, + AutoValue, Binding, Bytes, CastInfo, Func, Module, NoneValue, ParamInfo, Repr, Scope, + Smart, Type, Value, }; -use typst::introspection::INTROSPECTION; -use typst::layout::{Abs, Margin, PageElem, PagedDocument, LAYOUT}; -use typst::loading::DATA_LOADING; -use typst::math::MATH; -use typst::model::MODEL; -use typst::symbols::SYMBOLS; -use typst::text::{Font, FontBook, TEXT}; +use typst::layout::{Abs, Margin, PageElem, PagedDocument}; +use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::visualize::VISUALIZE; -use typst::Library; +use typst::{Category, Feature, Library, LibraryBuilder}; +use unicode_math_class::MathClass; macro_rules! load { ($path:literal) => { @@ -46,8 +42,8 @@ static GROUPS: LazyLock> = LazyLock::new(|| { .module() .scope() .iter() - .filter(|(_, v, _)| matches!(v, Value::Func(_))) - .map(|(k, _, _)| k.clone()) + .filter(|(_, b)| matches!(b.read(), Value::Func(_))) + .map(|(k, _)| k.clone()) .collect(); } } @@ -55,13 +51,16 @@ static GROUPS: LazyLock> = LazyLock::new(|| { }); static LIBRARY: LazyLock> = LazyLock::new(|| { - let mut lib = Library::default(); + let mut lib = LibraryBuilder::default() + .with_features([Feature::Html].into_iter().collect()) + .build(); let scope = lib.global.scope_mut(); // Add those types, so that they show up in the docs. - scope.category(FOUNDATIONS); + scope.start_category(Category::Foundations); scope.define_type::(); scope.define_type::(); + scope.reset_category(); // Adjust the default look. lib.styles @@ -77,7 +76,7 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { static FONTS: LazyLock<(LazyHash, Vec)> = LazyLock::new(|| { 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(); let book = FontBook::from_fonts(&fonts); (LazyHash::new(book), fonts) @@ -150,19 +149,24 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { let mut page = md_page(resolver, resolver.base(), load!("reference/welcome.md")); let base = format!("{}reference/", resolver.base()); page.children = vec![ - md_page(resolver, &base, load!("reference/syntax.md")).with_part("Language"), - md_page(resolver, &base, load!("reference/styling.md")), - md_page(resolver, &base, load!("reference/scripting.md")), - md_page(resolver, &base, load!("reference/context.md")), - category_page(resolver, FOUNDATIONS).with_part("Library"), - category_page(resolver, MODEL), - category_page(resolver, TEXT), - category_page(resolver, MATH), - category_page(resolver, SYMBOLS), - category_page(resolver, LAYOUT), - category_page(resolver, VISUALIZE), - category_page(resolver, INTROSPECTION), - category_page(resolver, DATA_LOADING), + md_page(resolver, &base, load!("reference/language/syntax.md")) + .with_part("Language"), + md_page(resolver, &base, load!("reference/language/styling.md")), + md_page(resolver, &base, load!("reference/language/scripting.md")), + md_page(resolver, &base, load!("reference/language/context.md")), + category_page(resolver, Category::Foundations).with_part("Library"), + category_page(resolver, Category::Model), + category_page(resolver, Category::Text), + category_page(resolver, Category::Math), + category_page(resolver, Category::Symbols), + category_page(resolver, Category::Layout), + category_page(resolver, Category::Visualize), + category_page(resolver, Category::Introspection), + category_page(resolver, Category::DataLoading), + category_page(resolver, Category::Pdf).with_part("Export"), + category_page(resolver, Category::Html), + category_page(resolver, Category::Png), + category_page(resolver, Category::Svg), ]; page } @@ -184,6 +188,7 @@ fn changelog_pages(resolver: &dyn Resolver) -> PageModel { let mut page = md_page(resolver, resolver.base(), load!("changelog/welcome.md")); let base = format!("{}changelog/", resolver.base()); page.children = vec![ + md_page(resolver, &base, load!("changelog/0.13.0.md")), md_page(resolver, &base, load!("changelog/0.12.0.md")), md_page(resolver, &base, load!("changelog/0.11.1.md")), md_page(resolver, &base, load!("changelog/0.11.0.md")), @@ -212,14 +217,16 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { let mut markup = vec![]; let mut math = vec![]; - let (module, path): (&Module, &[&str]) = if category == MATH { - (&LIBRARY.math, &["math"]) - } else { - (&LIBRARY.global, &[]) + let docs = category_docs(category); + let (module, path): (&Module, &[&str]) = match category { + Category::Math => (&LIBRARY.math, &["math"]), + Category::Pdf => (get_module(&LIBRARY.global, "pdf").unwrap(), &["pdf"]), + Category::Html => (get_module(&LIBRARY.global, "html").unwrap(), &["html"]), + _ => (&LIBRARY.global, &[]), }; // Add groups. - for group in GROUPS.iter().filter(|g| g.category == category.name()).cloned() { + for group in GROUPS.iter().filter(|g| g.category == category).cloned() { if matches!(group.name.as_str(), "sym" | "emoji") { let subpage = symbols_page(resolver, &route, &group); let BodyModel::Symbols(model) = &subpage.body else { continue }; @@ -236,7 +243,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { items.push(CategoryItem { name: group.name.clone(), route: subpage.route.clone(), - oneliner: oneliner(category.docs()).into(), + oneliner: oneliner(docs).into(), code: true, }); children.push(subpage); @@ -249,15 +256,15 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } // Add symbol pages. These are ordered manually. - if category == SYMBOLS { + if category == Category::Symbols { shorthands = Some(ShorthandsModel { markup, math }); } let mut skip = HashSet::new(); - if category == MATH { + if category == Category::Math { skip = GROUPS .iter() - .filter(|g| g.category == category.name()) + .filter(|g| g.category == category) .flat_map(|g| &g.filter) .map(|s| s.as_str()) .collect(); @@ -266,10 +273,15 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { skip.insert("text"); } + // Tiling would be duplicate otherwise. + if category == Category::Visualize { + skip.insert("pattern"); + } + // Add values and types. let scope = module.scope(); - for (name, value, _) in scope.iter() { - if scope.get_category(name) != Some(category) { + for (name, binding) in scope.iter() { + if binding.category() != Some(category) { continue; } @@ -277,11 +289,11 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { continue; } - match value { + match binding.read() { Value::Func(func) => { let name = func.name().unwrap(); - - let subpage = func_page(resolver, &route, func, path); + let subpage = + func_page(resolver, &route, func, path, binding.deprecation()); items.push(CategoryItem { name: name.into(), route: subpage.route.clone(), @@ -304,31 +316,39 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } } - if category != SYMBOLS { + if category != Category::Symbols { children.sort_by_cached_key(|child| child.title.clone()); items.sort_by_cached_key(|item| item.name.clone()); } - let name = category.title(); - let details = Html::markdown(resolver, category.docs(), Some(1)); + let title = EcoString::from(match category { + Category::Pdf | Category::Html | Category::Png | Category::Svg => { + category.name().to_uppercase() + } + _ => category.name().to_title_case(), + }); + + let details = Html::markdown(resolver, docs, Some(1)); let mut outline = vec![OutlineItem::from_name("Summary")]; outline.extend(details.outline()); - outline.push(OutlineItem::from_name("Definitions")); + if !items.is_empty() { + outline.push(OutlineItem::from_name("Definitions")); + } if shorthands.is_some() { outline.push(OutlineItem::from_name("Shorthands")); } PageModel { route, - title: name.into(), + title: title.clone(), description: eco_format!( - "Documentation for functions related to {name} in Typst." + "Documentation for functions related to {title} in Typst." ), part: None, outline, body: BodyModel::Category(CategoryModel { name: category.name(), - title: category.title(), + title, details, items, shorthands, @@ -337,14 +357,34 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { } } +/// Retrieve the docs for a category. +fn category_docs(category: Category) -> &'static str { + match category { + Category::Foundations => load!("reference/library/foundations.md"), + Category::Introspection => load!("reference/library/introspection.md"), + Category::Layout => load!("reference/library/layout.md"), + Category::DataLoading => load!("reference/library/data-loading.md"), + Category::Math => load!("reference/library/math.md"), + Category::Model => load!("reference/library/model.md"), + Category::Symbols => load!("reference/library/symbols.md"), + Category::Text => load!("reference/library/text.md"), + Category::Visualize => load!("reference/library/visualize.md"), + Category::Pdf => load!("reference/export/pdf.md"), + Category::Html => load!("reference/export/html.md"), + Category::Svg => load!("reference/export/svg.md"), + Category::Png => load!("reference/export/png.md"), + } +} + /// Create a page for a function. fn func_page( resolver: &dyn Resolver, parent: &str, func: &Func, path: &[&str], + deprecation: Option<&'static str>, ) -> PageModel { - let model = func_model(resolver, func, path, false); + let model = func_model(resolver, func, path, false, deprecation); let name = func.name().unwrap(); PageModel { route: eco_format!("{parent}{}/", urlify(name)), @@ -363,6 +403,7 @@ fn func_model( func: &Func, path: &[&str], nested: bool, + deprecation: Option<&'static str>, ) -> FuncModel { let name = func.name().unwrap(); let scope = func.scope().unwrap(); @@ -376,7 +417,11 @@ fn func_model( } let mut returns = vec![]; - casts(resolver, &mut returns, &mut vec![], func.returns().unwrap()); + let mut strings = vec![]; + casts(resolver, &mut returns, &mut strings, func.returns().unwrap()); + if !strings.is_empty() && !returns.contains(&"str") { + returns.push("str"); + } returns.sort_by_key(|ty| type_index(ty)); if returns == ["none"] { returns.clear(); @@ -394,6 +439,7 @@ fn func_model( oneliner: oneliner(details), element: func.element().is_some(), contextual: func.contextual().unwrap_or(false), + deprecation, details: Html::markdown(resolver, details, nesting), example: example.map(|md| Html::markdown(resolver, md, None)), self_, @@ -474,9 +520,9 @@ fn casts( fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec { scope .iter() - .filter_map(|(_, value, _)| { - let Value::Func(func) = value else { return None }; - Some(func_model(resolver, func, &[name], true)) + .filter_map(|(_, binding)| { + let Value::Func(func) = binding.read() else { return None }; + Some(func_model(resolver, func, &[name], true, binding.deprecation())) }) .collect() } @@ -504,8 +550,6 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec { .collect(), }); } - - outline.extend(scope_outline(&model.scope)); } else { outline.extend(model.params.iter().map(|param| OutlineItem { id: eco_format!("{id_base}-{}", urlify(param.name)), @@ -514,27 +558,30 @@ fn func_outline(model: &FuncModel, id_base: &str) -> Vec { })); } + outline.extend(scope_outline(&model.scope, id_base)); + outline } /// Produce an outline for a function scope. -fn scope_outline(scope: &[FuncModel]) -> Option { +fn scope_outline(scope: &[FuncModel], id_base: &str) -> Option { if scope.is_empty() { return None; } - Some(OutlineItem { - id: "definitions".into(), - name: "Definitions".into(), - children: scope - .iter() - .map(|func| { - let id = urlify(&eco_format!("definitions-{}", func.name)); - let children = func_outline(func, &id); - OutlineItem { id, name: func.title.into(), children } - }) - .collect(), - }) + let dash = if id_base.is_empty() { "" } else { "-" }; + let id = eco_format!("{id_base}{dash}definitions"); + + let children = scope + .iter() + .map(|func| { + let id = urlify(&eco_format!("{id}-{}", func.name)); + let children = func_outline(func, &id); + OutlineItem { id, name: func.title.into(), children } + }) + .collect(); + + Some(OutlineItem { id, name: "Definitions".into(), children }) } /// Create a page for a group of functions. @@ -552,9 +599,11 @@ fn group_page( let mut outline_items = vec![]; for name in &group.filter { - let value = group.module().scope().get(name).unwrap(); - let Ok(ref func) = value.clone().cast::() else { panic!("not a function") }; - let func = func_model(resolver, func, &path, true); + let binding = group.module().scope().get(name).unwrap(); + let Ok(ref func) = binding.read().clone().cast::() else { + panic!("not a function") + }; + let func = func_model(resolver, func, &path, true, binding.deprecation()); let id_base = urlify(&eco_format!("functions-{}", func.name)); let children = func_outline(&func, &id_base); outline_items.push(OutlineItem { @@ -621,7 +670,7 @@ fn type_model(resolver: &dyn Resolver, ty: &Type) -> TypeModel { constructor: ty .constructor() .ok() - .map(|func| func_model(resolver, &func, &[], true)), + .map(|func| func_model(resolver, &func, &[], true, None)), scope: scope_models(resolver, ty.short_name(), ty.scope()), } } @@ -639,7 +688,7 @@ fn type_outline(model: &TypeModel) -> Vec { }); } - outline.extend(scope_outline(&model.scope)); + outline.extend(scope_outline(&model.scope, "")); outline } @@ -660,8 +709,8 @@ fn symbols_page(resolver: &dyn Resolver, parent: &str, group: &GroupData) -> Pag /// Produce a symbol list's model. fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { let mut list = vec![]; - for (name, value, _) in group.module().scope().iter() { - let Value::Symbol(symbol) = value else { continue }; + for (name, binding) in group.module().scope().iter() { + let Value::Symbol(symbol) = binding.read() else { continue }; let complete = |variant: &str| { if variant.is_empty() { name.clone() @@ -675,10 +724,19 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) }; + let name = complete(variant); + let deprecation = match name.as_str() { + "integral.sect" => { + Some("`integral.sect` is deprecated, use `integral.inter` instead") + } + _ => binding.deprecation(), + }; + list.push(SymbolModel { - name: complete(variant), + name, markup_shorthand: shorthand(typst::syntax::ast::Shorthand::LIST), math_shorthand: shorthand(typst::syntax::ast::MathShorthand::LIST), + math_class: typst_utils::default_math_class(c).map(math_class_name), codepoint: c as _, accent: typst::math::Accent::combine(c).is_some(), alternates: symbol @@ -686,6 +744,7 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { .filter(|(other, _)| other != &variant) .map(|(other, _)| complete(other)) .collect(), + deprecation, }); } } @@ -701,7 +760,7 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { /// Extract a module from another module. #[track_caller] fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> { - match parent.scope().get(name) { + match parent.scope().get(name).map(Binding::read) { Some(Value::Module(module)) => Ok(module), _ => bail!("module doesn't contain module `{name}`"), } @@ -762,12 +821,32 @@ const TYPE_ORDER: &[&str] = &[ "stroke", ]; +fn math_class_name(class: MathClass) -> &'static str { + match class { + MathClass::Normal => "Normal", + MathClass::Alphabetic => "Alphabetic", + MathClass::Binary => "Binary", + MathClass::Closing => "Closing", + MathClass::Diacritic => "Diacritic", + MathClass::Fence => "Fence", + MathClass::GlyphPart => "Glyph Part", + MathClass::Large => "Large", + MathClass::Opening => "Opening", + MathClass::Punctuation => "Punctuation", + MathClass::Relation => "Relation", + MathClass::Space => "Space", + MathClass::Unary => "Unary", + MathClass::Vary => "Vary", + MathClass::Special => "Special", + } +} + /// Data about a collection of functions. #[derive(Debug, Clone, Deserialize)] struct GroupData { name: EcoString, title: EcoString, - category: EcoString, + category: Category, #[serde(default)] path: Vec, #[serde(default)] diff --git a/docs/src/link.rs b/docs/src/link.rs index 375cc8c2b..2e836b6ce 100644 --- a/docs/src/link.rs +++ b/docs/src/link.rs @@ -1,5 +1,5 @@ use typst::diag::{bail, StrResult}; -use typst::foundations::Func; +use typst::foundations::{Binding, Func}; use crate::{get_module, GROUPS, LIBRARY}; @@ -44,6 +44,8 @@ fn resolve_known(head: &str, base: &str) -> Option { "$styling" => format!("{base}reference/styling"), "$scripting" => format!("{base}reference/scripting"), "$context" => format!("{base}reference/context"), + "$html" => format!("{base}reference/html"), + "$pdf" => format!("{base}reference/pdf"), "$guides" => format!("{base}guides"), "$changelog" => format!("{base}changelog"), "$universe" => "https://typst.app/universe".into(), @@ -59,7 +61,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { while let Some(name) = parts.peek() { if category.is_none() { - category = focus.scope().get_category(name); + category = focus.scope().get(name).and_then(Binding::category); } let Ok(module) = get_module(focus, name) else { break }; focus = module; @@ -69,15 +71,18 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { let Some(category) = category else { bail!("{head} has no category") }; let name = parts.next().ok_or("link is missing first part")?; - let value = focus.field(name)?; + let value = focus.field(name, ())?; // Handle grouped functions. if let Some(group) = GROUPS.iter().find(|group| { - group.category == category.name() && group.filter.iter().any(|func| func == name) + group.category == category && group.filter.iter().any(|func| func == name) }) { let mut route = format!( "{}reference/{}/{}/#functions-{}", - base, group.category, group.name, name + base, + group.category.name(), + group.name, + name ); if let Some(param) = parts.next() { route.push('-'); @@ -88,7 +93,7 @@ fn resolve_definition(head: &str, base: &str) -> StrResult { let mut route = format!("{}reference/{}/{name}", base, category.name()); if let Some(next) = parts.next() { - if let Ok(field) = value.field(next) { + if let Ok(field) = value.field(next, ()) { route.push_str("/#definitions-"); route.push_str(next); if let Some(next) = parts.next() { diff --git a/docs/src/main.rs b/docs/src/main.rs index 064bf9a8f..d14c1347a 100644 --- a/docs/src/main.rs +++ b/docs/src/main.rs @@ -46,11 +46,11 @@ impl Resolver for CliResolver<'_> { if let Some(code) = source { let code_safe = code.as_str(); Html::new(format!( - r#"
{code_safe}
Preview
"# + r#"
{code_safe}
Preview
"# )) } else { Html::new(format!( - r#"
Preview
"# + r#"
Preview
"# )) } } diff --git a/docs/src/model.rs b/docs/src/model.rs index b222322a7..801c60c7f 100644 --- a/docs/src/model.rs +++ b/docs/src/model.rs @@ -64,7 +64,7 @@ pub enum BodyModel { #[derive(Debug, Serialize)] pub struct CategoryModel { pub name: &'static str, - pub title: &'static str, + pub title: EcoString, pub details: Html, pub items: Vec, pub shorthands: Option, @@ -89,6 +89,7 @@ pub struct FuncModel { pub oneliner: &'static str, pub element: bool, pub contextual: bool, + pub deprecation: Option<&'static str>, pub details: Html, /// This example is only for nested function models. Others can have /// their example directly in their details. @@ -163,6 +164,8 @@ pub struct SymbolModel { pub alternates: Vec, pub markup_shorthand: Option<&'static str>, pub math_shorthand: Option<&'static str>, + pub math_class: Option<&'static str>, + pub deprecation: Option<&'static str>, } /// Shorthands listed on a category page. diff --git a/flake.lock b/flake.lock index c02466422..ad47d29cd 100644 --- a/flake.lock +++ b/flake.lock @@ -112,13 +112,13 @@ "rust-manifest": { "flake": false, "locked": { - "narHash": "sha256-Yqu2/i9170R7pQhvOCR1f5SyFr7PcFbO6xcMr9KWruQ=", + "narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=", "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" }, "original": { "type": "file", - "url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" + "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml" } }, "systems": { diff --git a/flake.nix b/flake.nix index abdad27aa..6938f6e57 100644 --- a/flake.nix +++ b/flake.nix @@ -10,7 +10,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; rust-manifest = { - url = "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml"; + url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"; flake = false; }; }; diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index 37e21deb9..3dedfb737 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -19,7 +19,7 @@ struct FuzzWorld { impl FuzzWorld { fn new(text: &str) -> Self { let data = typst_assets::fonts().next().unwrap(); - let font = Font::new(Bytes::from_static(data), 0).unwrap(); + let font = Font::new(Bytes::new(data), 0).unwrap(); let book = FontBook::from_fonts([&font]); Self { library: LazyHash::new(Library::default()), diff --git a/tests/ref/baseline-box.png b/tests/ref/baseline-box.png index 2a9e51758..e07e22ea7 100644 Binary files a/tests/ref/baseline-box.png and b/tests/ref/baseline-box.png differ diff --git a/tests/ref/bibliography-before-content.png b/tests/ref/bibliography-before-content.png index ea5ece267..eb9f26d83 100644 Binary files a/tests/ref/bibliography-before-content.png and b/tests/ref/bibliography-before-content.png differ diff --git a/tests/ref/bibliography-grid-par.png b/tests/ref/bibliography-grid-par.png new file mode 100644 index 000000000..5befbcc54 Binary files /dev/null and b/tests/ref/bibliography-grid-par.png differ diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png new file mode 100644 index 000000000..02278124b Binary files /dev/null and b/tests/ref/bibliography-indent-par.png differ diff --git a/tests/ref/bibliography-math.png b/tests/ref/bibliography-math.png index 9391154db..6b60fef4a 100644 Binary files a/tests/ref/bibliography-math.png and b/tests/ref/bibliography-math.png differ diff --git a/tests/ref/box-clip-outset.png b/tests/ref/box-clip-outset.png index 21538e85f..8ea40e968 100644 Binary files a/tests/ref/box-clip-outset.png and b/tests/ref/box-clip-outset.png differ diff --git a/tests/ref/box-clip-radius-without-stroke.png b/tests/ref/box-clip-radius-without-stroke.png index 121373582..2be772cc5 100644 Binary files a/tests/ref/box-clip-radius-without-stroke.png and b/tests/ref/box-clip-radius-without-stroke.png differ diff --git a/tests/ref/box-clip-radius.png b/tests/ref/box-clip-radius.png index da20fa5bf..530d53309 100644 Binary files a/tests/ref/box-clip-radius.png and b/tests/ref/box-clip-radius.png differ diff --git a/tests/ref/cases-content-symbol.png b/tests/ref/cases-content-symbol.png new file mode 100644 index 000000000..b0b8a65e3 Binary files /dev/null and b/tests/ref/cases-content-symbol.png differ diff --git a/tests/ref/cases-content-text.png b/tests/ref/cases-content-text.png new file mode 100644 index 000000000..d97675829 Binary files /dev/null and b/tests/ref/cases-content-text.png differ diff --git a/tests/ref/cite-footnote.png b/tests/ref/cite-footnote.png index 87c1dbd96..dd2cf8bdb 100644 Binary files a/tests/ref/cite-footnote.png and b/tests/ref/cite-footnote.png differ diff --git a/tests/ref/closure-path-resolve-in-layout-phase.png b/tests/ref/closure-path-resolve-in-layout-phase.png index baaed3564..a3d699981 100644 Binary files a/tests/ref/closure-path-resolve-in-layout-phase.png and b/tests/ref/closure-path-resolve-in-layout-phase.png differ diff --git a/tests/ref/coma.png b/tests/ref/coma.png index 2c59ae870..a1d743a49 100644 Binary files a/tests/ref/coma.png and b/tests/ref/coma.png differ diff --git a/tests/ref/curve-multiple-non-closed.png b/tests/ref/curve-multiple-non-closed.png new file mode 100644 index 000000000..f4332e363 Binary files /dev/null and b/tests/ref/curve-multiple-non-closed.png differ diff --git a/tests/ref/enum-par.png b/tests/ref/enum-par.png new file mode 100644 index 000000000..ca923a526 Binary files /dev/null and b/tests/ref/enum-par.png differ diff --git a/tests/ref/figure-par.png b/tests/ref/figure-par.png new file mode 100644 index 000000000..d70bbcb12 Binary files /dev/null and b/tests/ref/figure-par.png differ diff --git a/tests/ref/footnote-in-caption.png b/tests/ref/footnote-in-caption.png index 12a5fde5e..79b2b5d0f 100644 Binary files a/tests/ref/footnote-in-caption.png and b/tests/ref/footnote-in-caption.png differ diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png index 062a6fc71..e110eac6d 100644 Binary files a/tests/ref/footnote-in-table.png and b/tests/ref/footnote-in-table.png differ diff --git a/tests/ref/gradient-math-misc.png b/tests/ref/gradient-math-misc.png index acf14c6fe..13f5c27b3 100644 Binary files a/tests/ref/gradient-math-misc.png and b/tests/ref/gradient-math-misc.png differ diff --git a/tests/ref/heading-hanging-indent-auto.png b/tests/ref/heading-hanging-indent-auto.png new file mode 100644 index 000000000..823feb145 Binary files /dev/null and b/tests/ref/heading-hanging-indent-auto.png differ diff --git a/tests/ref/heading-hanging-indent-length.png b/tests/ref/heading-hanging-indent-length.png new file mode 100644 index 000000000..e371674ed Binary files /dev/null and b/tests/ref/heading-hanging-indent-length.png differ diff --git a/tests/ref/heading-hanging-indent-zero.png b/tests/ref/heading-hanging-indent-zero.png new file mode 100644 index 000000000..659ddbefb Binary files /dev/null and b/tests/ref/heading-hanging-indent-zero.png differ diff --git a/tests/ref/heading-par.png b/tests/ref/heading-par.png new file mode 100644 index 000000000..affc0df47 Binary files /dev/null and b/tests/ref/heading-par.png differ diff --git a/tests/ref/html/basic-table.html b/tests/ref/html/basic-table.html new file mode 100644 index 000000000..189a5b314 --- /dev/null +++ b/tests/ref/html/basic-table.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Thefirstand
thesecondrow
FooBazBar
12
34
Thelastrow
+ + diff --git a/tests/ref/html/block-html.html b/tests/ref/html/block-html.html new file mode 100644 index 000000000..d1716c6d7 --- /dev/null +++ b/tests/ref/html/block-html.html @@ -0,0 +1,11 @@ + + + + + + + +

Paragraph

+
Div
+ + diff --git a/tests/ref/html/box-html.html b/tests/ref/html/box-html.html new file mode 100644 index 000000000..b2a26533b --- /dev/null +++ b/tests/ref/html/box-html.html @@ -0,0 +1,10 @@ + + + + + + + +

Text Span.

+ + diff --git a/tests/ref/html/col-gutter-table.html b/tests/ref/html/col-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/col-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
abc
def
ghi
+ + diff --git a/tests/ref/html/col-row-gutter-table.html b/tests/ref/html/col-row-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/col-row-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
abc
def
ghi
+ + diff --git a/tests/ref/html/enum-par.html b/tests/ref/html/enum-par.html new file mode 100644 index 000000000..60d4592b7 --- /dev/null +++ b/tests/ref/html/enum-par.html @@ -0,0 +1,36 @@ + + + + + + + +
+
    +
  1. Hello
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +

    The

    +
  2. +
  3. +

    World

    +
  4. +
+
+ + diff --git a/tests/ref/html/enum-start.html b/tests/ref/html/enum-start.html new file mode 100644 index 000000000..fc9b3c061 --- /dev/null +++ b/tests/ref/html/enum-start.html @@ -0,0 +1,13 @@ + + + + + + + +
    +
  1. Skipping
  2. +
  3. Ahead
  4. +
+ + diff --git a/tests/ref/html/heading-html-basic.html b/tests/ref/html/heading-html-basic.html new file mode 100644 index 000000000..54a22faf4 --- /dev/null +++ b/tests/ref/html/heading-html-basic.html @@ -0,0 +1,16 @@ + + + + + + + +

Level 1

+

Level 2

+

Level 3

+
Level 4
+
Level 5
+
Level 6
+
Level 7
+ + diff --git a/tests/ref/html/html-elem-alone-context.html b/tests/ref/html/html-elem-alone-context.html new file mode 100644 index 000000000..69e9da411 --- /dev/null +++ b/tests/ref/html/html-elem-alone-context.html @@ -0,0 +1,2 @@ + + diff --git a/tests/ref/html/html-elem-metadata.html b/tests/ref/html/html-elem-metadata.html new file mode 100644 index 000000000..c37a7d2ef --- /dev/null +++ b/tests/ref/html/html-elem-metadata.html @@ -0,0 +1,2 @@ + +Hi diff --git a/tests/ref/html/link-basic.html b/tests/ref/html/link-basic.html index 1f4e02e12..89cb54db5 100644 --- a/tests/ref/html/link-basic.html +++ b/tests/ref/html/link-basic.html @@ -5,17 +5,9 @@ -

- https://example.com/ -

-

- Some text text text -

-

- This link appears in the middle of a paragraph. -

-

- Contact hi@typst.app or call 123 for more information. -

+

https://example.com/

+

Some text text text

+

This link appears in the middle of a paragraph.

+

Contact hi@typst.app or call 123 for more information.

- \ No newline at end of file + diff --git a/tests/ref/html/list-par.html b/tests/ref/html/list-par.html new file mode 100644 index 000000000..7c747ff44 --- /dev/null +++ b/tests/ref/html/list-par.html @@ -0,0 +1,36 @@ + + + + + + + +
+
    +
  • Hello
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +

    The

    +
  • +
  • +

    World

    +
  • +
+
+ + diff --git a/tests/ref/html/par-semantic-html.html b/tests/ref/html/par-semantic-html.html new file mode 100644 index 000000000..09c7d2fd0 --- /dev/null +++ b/tests/ref/html/par-semantic-html.html @@ -0,0 +1,16 @@ + + + + + + + +

Heading is no paragraph

+

I'm a paragraph.

+
I'm not.
+
+

We are two.

+

So we are paragraphs.

+
+ + diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html new file mode 100644 index 000000000..c12d2ae2d --- /dev/null +++ b/tests/ref/html/quote-attribution-link.html @@ -0,0 +1,11 @@ + + + + + + + +
Compose papers faster
+

typst.com

+ + diff --git a/tests/ref/html/quote-nesting-html.html b/tests/ref/html/quote-nesting-html.html new file mode 100644 index 000000000..6b05a94a0 --- /dev/null +++ b/tests/ref/html/quote-nesting-html.html @@ -0,0 +1,10 @@ + + + + + + + +

When you said that “he surely meant that ‘she intended to say “I'm sorry”’”, I was quite confused.

+ + diff --git a/tests/ref/html/quote-plato.html b/tests/ref/html/quote-plato.html new file mode 100644 index 000000000..039835082 --- /dev/null +++ b/tests/ref/html/quote-plato.html @@ -0,0 +1,13 @@ + + + + + + + +
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
+

— Plato

+
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.
+

— from the Henry Cary literal translation of 1897

+ + diff --git a/tests/ref/html/row-gutter-table.html b/tests/ref/html/row-gutter-table.html new file mode 100644 index 000000000..54170f534 --- /dev/null +++ b/tests/ref/html/row-gutter-table.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +
abc
def
ghi
+ + diff --git a/tests/ref/html/terms-par.html b/tests/ref/html/terms-par.html new file mode 100644 index 000000000..78bc5df16 --- /dev/null +++ b/tests/ref/html/terms-par.html @@ -0,0 +1,42 @@ + + + + + + + +
+
+
Hello
+
A
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+

The

+
+
World
+
+

B

+
+
+
+ + diff --git a/tests/ref/image-baseline-with-box.png b/tests/ref/image-baseline-with-box.png index dc8e8bc57..ade90e2f5 100644 Binary files a/tests/ref/image-baseline-with-box.png and b/tests/ref/image-baseline-with-box.png differ diff --git a/tests/ref/image-decode-detect-format.png b/tests/ref/image-decode-detect-format.png index 6ecb7dcda..cee71bb93 100644 Binary files a/tests/ref/image-decode-detect-format.png and b/tests/ref/image-decode-detect-format.png differ diff --git a/tests/ref/image-decode-specify-format.png b/tests/ref/image-decode-specify-format.png index 6ecb7dcda..cee71bb93 100644 Binary files a/tests/ref/image-decode-specify-format.png and b/tests/ref/image-decode-specify-format.png differ diff --git a/tests/ref/image-fit.png b/tests/ref/image-fit.png index 24946fdbd..432daf741 100644 Binary files a/tests/ref/image-fit.png and b/tests/ref/image-fit.png differ diff --git a/tests/ref/image-pixmap-luma8.png b/tests/ref/image-pixmap-luma8.png new file mode 100644 index 000000000..cf5790cb7 Binary files /dev/null and b/tests/ref/image-pixmap-luma8.png differ diff --git a/tests/ref/image-pixmap-lumaa8.png b/tests/ref/image-pixmap-lumaa8.png new file mode 100644 index 000000000..b8eea6fd6 Binary files /dev/null and b/tests/ref/image-pixmap-lumaa8.png differ diff --git a/tests/ref/image-pixmap-rgb8.png b/tests/ref/image-pixmap-rgb8.png new file mode 100644 index 000000000..d905c1eee Binary files /dev/null and b/tests/ref/image-pixmap-rgb8.png differ diff --git a/tests/ref/image-pixmap-rgba8.png b/tests/ref/image-pixmap-rgba8.png new file mode 100644 index 000000000..b87a6e411 Binary files /dev/null and b/tests/ref/image-pixmap-rgba8.png differ diff --git a/tests/ref/image-scaling-methods.png b/tests/ref/image-scaling-methods.png new file mode 100644 index 000000000..9d543e114 Binary files /dev/null and b/tests/ref/image-scaling-methods.png differ diff --git a/tests/ref/image-sizing.png b/tests/ref/image-sizing.png index e1201cf22..7cb59fc76 100644 Binary files a/tests/ref/image-sizing.png and b/tests/ref/image-sizing.png differ diff --git a/tests/ref/image-svg-auto-detection.png b/tests/ref/image-svg-auto-detection.png new file mode 100644 index 000000000..0240f8f5c Binary files /dev/null and b/tests/ref/image-svg-auto-detection.png differ diff --git a/tests/ref/issue-1041-smartquotes-in-outline.png b/tests/ref/issue-1041-smartquotes-in-outline.png index 19a78ac69..00c276c11 100644 Binary files a/tests/ref/issue-1041-smartquotes-in-outline.png and b/tests/ref/issue-1041-smartquotes-in-outline.png differ diff --git a/tests/ref/issue-2048-outline-multiline.png b/tests/ref/issue-2048-outline-multiline.png new file mode 100644 index 000000000..0ecc2d80f Binary files /dev/null and b/tests/ref/issue-2048-outline-multiline.png differ diff --git a/tests/ref/issue-4361-transparency-leak.png b/tests/ref/issue-4361-transparency-leak.png index 4060d43ac..660798166 100644 Binary files a/tests/ref/issue-4361-transparency-leak.png and b/tests/ref/issue-4361-transparency-leak.png differ diff --git a/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png b/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png new file mode 100644 index 000000000..c7c359a1b Binary files /dev/null and b/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png differ diff --git a/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png b/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png deleted file mode 100644 index 94d06f1a4..000000000 Binary files a/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png and /dev/null differ diff --git a/tests/ref/issue-4859-outline-entry-show-set.png b/tests/ref/issue-4859-outline-entry-show-set.png new file mode 100644 index 000000000..33ff442d9 Binary files /dev/null and b/tests/ref/issue-4859-outline-entry-show-set.png differ diff --git a/tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png b/tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png new file mode 100644 index 000000000..acadc3be5 Binary files /dev/null and b/tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png differ diff --git a/tests/ref/issue-5176-cjk-title.png b/tests/ref/issue-5176-cjk-title.png deleted file mode 100644 index e904fbd77..000000000 Binary files a/tests/ref/issue-5176-cjk-title.png and /dev/null differ diff --git a/tests/ref/issue-5176-outline-cjk-title.png b/tests/ref/issue-5176-outline-cjk-title.png new file mode 100644 index 000000000..a206f92ca Binary files /dev/null and b/tests/ref/issue-5176-outline-cjk-title.png differ diff --git a/tests/ref/issue-5370-figure-caption-separator-outline.png b/tests/ref/issue-5370-figure-caption-separator-outline.png deleted file mode 100644 index a9b0d06e1..000000000 Binary files a/tests/ref/issue-5370-figure-caption-separator-outline.png and /dev/null differ diff --git a/tests/ref/issue-5503-enum-in-align.png b/tests/ref/issue-5503-enum-in-align.png new file mode 100644 index 000000000..4857e731b Binary files /dev/null and b/tests/ref/issue-5503-enum-in-align.png differ diff --git a/tests/ref/issue-5503-enum-interrupted-by-par-align.png b/tests/ref/issue-5503-enum-interrupted-by-par-align.png deleted file mode 100644 index 9cc942b4c..000000000 Binary files a/tests/ref/issue-5503-enum-interrupted-by-par-align.png and /dev/null differ diff --git a/tests/ref/issue-5503-list-interrupted-by-par-align.png b/tests/ref/issue-5503-list-in-align.png similarity index 100% rename from tests/ref/issue-5503-list-interrupted-by-par-align.png rename to tests/ref/issue-5503-list-in-align.png diff --git a/tests/ref/issue-5503-terms-interrupted-by-par-align.png b/tests/ref/issue-5503-terms-in-align.png similarity index 100% rename from tests/ref/issue-5503-terms-interrupted-by-par-align.png rename to tests/ref/issue-5503-terms-in-align.png diff --git a/tests/ref/issue-5719-enum-nested.png b/tests/ref/issue-5719-enum-nested.png new file mode 100644 index 000000000..767045449 Binary files /dev/null and b/tests/ref/issue-5719-enum-nested.png differ diff --git a/tests/ref/issue-5719-heading-nested.png b/tests/ref/issue-5719-heading-nested.png new file mode 100644 index 000000000..95bea42b2 Binary files /dev/null and b/tests/ref/issue-5719-heading-nested.png differ diff --git a/tests/ref/issue-5719-list-nested.png b/tests/ref/issue-5719-list-nested.png new file mode 100644 index 000000000..9c9a7cc62 Binary files /dev/null and b/tests/ref/issue-5719-list-nested.png differ diff --git a/tests/ref/issue-5719-terms-nested.png b/tests/ref/issue-5719-terms-nested.png new file mode 100644 index 000000000..8428ae4ee Binary files /dev/null and b/tests/ref/issue-5719-terms-nested.png differ diff --git a/tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png b/tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png new file mode 100644 index 000000000..962427332 Binary files /dev/null and b/tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png differ diff --git a/tests/ref/issue-5831-par-constructor-args.png b/tests/ref/issue-5831-par-constructor-args.png new file mode 100644 index 000000000..440b612ba Binary files /dev/null and b/tests/ref/issue-5831-par-constructor-args.png differ diff --git a/tests/ref/issue-622-hide-meta-outline.png b/tests/ref/issue-622-hide-meta-outline.png index 6d8702b48..d627e0881 100644 Binary files a/tests/ref/issue-622-hide-meta-outline.png and b/tests/ref/issue-622-hide-meta-outline.png differ diff --git a/tests/ref/issue-758-link-repeat.png b/tests/ref/issue-758-link-repeat.png new file mode 100644 index 000000000..aaec20d23 Binary files /dev/null and b/tests/ref/issue-758-link-repeat.png differ diff --git a/tests/ref/issue-785-cite-locate.png b/tests/ref/issue-785-cite-locate.png index 5240aa772..d387ed0d5 100644 Binary files a/tests/ref/issue-785-cite-locate.png and b/tests/ref/issue-785-cite-locate.png differ diff --git a/tests/ref/issue-math-realize-scripting.png b/tests/ref/issue-math-realize-scripting.png index ee2d4cdf7..7d721ed77 100644 Binary files a/tests/ref/issue-math-realize-scripting.png and b/tests/ref/issue-math-realize-scripting.png differ diff --git a/tests/ref/link-empty-block.png b/tests/ref/link-empty-block.png new file mode 100644 index 000000000..ae10bdcf6 Binary files /dev/null and b/tests/ref/link-empty-block.png differ diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png index 8fb7f6c66..eeeb264b9 100644 Binary files a/tests/ref/link-on-block.png and b/tests/ref/link-on-block.png differ diff --git a/tests/ref/list-par.png b/tests/ref/list-par.png new file mode 100644 index 000000000..5f0003d17 Binary files /dev/null and b/tests/ref/list-par.png differ diff --git a/tests/ref/math-accent-align.png b/tests/ref/math-accent-align.png index 84e8dc8cc..efc66ec3f 100644 Binary files a/tests/ref/math-accent-align.png and b/tests/ref/math-accent-align.png differ diff --git a/tests/ref/math-accent-bounds.png b/tests/ref/math-accent-bounds.png index 6ddc54366..4c347d432 100644 Binary files a/tests/ref/math-accent-bounds.png and b/tests/ref/math-accent-bounds.png differ diff --git a/tests/ref/math-accent-dotless.png b/tests/ref/math-accent-dotless.png index 81eb4fa2b..389ceb634 100644 Binary files a/tests/ref/math-accent-dotless.png and b/tests/ref/math-accent-dotless.png differ diff --git a/tests/ref/math-accent-func.png b/tests/ref/math-accent-func.png index 00821f70d..fb3a8c6f1 100644 Binary files a/tests/ref/math-accent-func.png and b/tests/ref/math-accent-func.png differ diff --git a/tests/ref/math-accent-sym-call.png b/tests/ref/math-accent-sym-call.png index 0837a86c9..609197f3c 100644 Binary files a/tests/ref/math-accent-sym-call.png and b/tests/ref/math-accent-sym-call.png differ diff --git a/tests/ref/math-call-named-args.png b/tests/ref/math-call-named-args.png new file mode 100644 index 000000000..bbe293e9f Binary files /dev/null and b/tests/ref/math-call-named-args.png differ diff --git a/tests/ref/math-call-spread-shorthand-clash.png b/tests/ref/math-call-spread-shorthand-clash.png new file mode 100644 index 000000000..4129ef5d2 Binary files /dev/null and b/tests/ref/math-call-spread-shorthand-clash.png differ diff --git a/tests/ref/math-equation-auto-wrapping.png b/tests/ref/math-equation-auto-wrapping.png index 9c600172e..2476d668c 100644 Binary files a/tests/ref/math-equation-auto-wrapping.png and b/tests/ref/math-equation-auto-wrapping.png differ diff --git a/tests/ref/math-lr-unparen.png b/tests/ref/math-lr-unparen.png new file mode 100644 index 000000000..d418b14ea Binary files /dev/null and b/tests/ref/math-lr-unparen.png differ diff --git a/tests/ref/math-mat-align-explicit-alternating.png b/tests/ref/math-mat-align-explicit-alternating.png index 37e8dc06a..1ebcc7b68 100644 Binary files a/tests/ref/math-mat-align-explicit-alternating.png and b/tests/ref/math-mat-align-explicit-alternating.png differ diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png index 09ce93982..cb9819248 100644 Binary files a/tests/ref/math-mat-align-explicit-left.png and b/tests/ref/math-mat-align-explicit-left.png differ diff --git a/tests/ref/math-mat-align-explicit-right.png b/tests/ref/math-mat-align-explicit-right.png index 3592c0cf5..b537e6571 100644 Binary files a/tests/ref/math-mat-align-explicit-right.png and b/tests/ref/math-mat-align-explicit-right.png differ diff --git a/tests/ref/math-mat-align-implicit.png b/tests/ref/math-mat-align-implicit.png index 0c508efc4..b184d9140 100644 Binary files a/tests/ref/math-mat-align-implicit.png and b/tests/ref/math-mat-align-implicit.png differ diff --git a/tests/ref/math-mat-gaps.png b/tests/ref/math-mat-gaps.png index 5c954766c..405358776 100644 Binary files a/tests/ref/math-mat-gaps.png and b/tests/ref/math-mat-gaps.png differ diff --git a/tests/ref/math-mat-spread-1d.png b/tests/ref/math-mat-spread-1d.png new file mode 100644 index 000000000..0ac4e620b Binary files /dev/null and b/tests/ref/math-mat-spread-1d.png differ diff --git a/tests/ref/math-mat-spread-2d.png b/tests/ref/math-mat-spread-2d.png new file mode 100644 index 000000000..85cb4a6ad Binary files /dev/null and b/tests/ref/math-mat-spread-2d.png differ diff --git a/tests/ref/math-mat-spread.png b/tests/ref/math-mat-spread.png new file mode 100644 index 000000000..dc8b2bf7e Binary files /dev/null and b/tests/ref/math-mat-spread.png differ diff --git a/tests/ref/math-par.png b/tests/ref/math-par.png new file mode 100644 index 000000000..30d64794c Binary files /dev/null and b/tests/ref/math-par.png differ diff --git a/tests/ref/math-shorthands-noncontinuable.png b/tests/ref/math-shorthands-noncontinuable.png new file mode 100644 index 000000000..2f1ad1d14 Binary files /dev/null and b/tests/ref/math-shorthands-noncontinuable.png differ diff --git a/tests/ref/math-spacing-decorated.png b/tests/ref/math-spacing-decorated.png index b8846ff05..2f3c704e5 100644 Binary files a/tests/ref/math-spacing-decorated.png and b/tests/ref/math-spacing-decorated.png differ diff --git a/tests/ref/math-vec-align-explicit-alternating.png b/tests/ref/math-vec-align-explicit-alternating.png index 37e8dc06a..1ebcc7b68 100644 Binary files a/tests/ref/math-vec-align-explicit-alternating.png and b/tests/ref/math-vec-align-explicit-alternating.png differ diff --git a/tests/ref/outline-bookmark.png b/tests/ref/outline-bookmark.png index 66e5329d8..83c74444a 100644 Binary files a/tests/ref/outline-bookmark.png and b/tests/ref/outline-bookmark.png differ diff --git a/tests/ref/outline-entry-complex.png b/tests/ref/outline-entry-complex.png index d0491179b..d2ad49e79 100644 Binary files a/tests/ref/outline-entry-complex.png and b/tests/ref/outline-entry-complex.png differ diff --git a/tests/ref/outline-entry-inner.png b/tests/ref/outline-entry-inner.png new file mode 100644 index 000000000..5376c9961 Binary files /dev/null and b/tests/ref/outline-entry-inner.png differ diff --git a/tests/ref/outline-entry.png b/tests/ref/outline-entry.png index a46e483ce..acaa87d41 100644 Binary files a/tests/ref/outline-entry.png and b/tests/ref/outline-entry.png differ diff --git a/tests/ref/outline-first-line-indent.png b/tests/ref/outline-first-line-indent.png index e40b44094..e3341295c 100644 Binary files a/tests/ref/outline-first-line-indent.png and b/tests/ref/outline-first-line-indent.png differ diff --git a/tests/ref/outline-heading-start-of-page.png b/tests/ref/outline-heading-start-of-page.png new file mode 100644 index 000000000..e6dbbb5f1 Binary files /dev/null and b/tests/ref/outline-heading-start-of-page.png differ diff --git a/tests/ref/outline-indent-auto-mixed-prefix-short.png b/tests/ref/outline-indent-auto-mixed-prefix-short.png new file mode 100644 index 000000000..4b8c71079 Binary files /dev/null and b/tests/ref/outline-indent-auto-mixed-prefix-short.png differ diff --git a/tests/ref/outline-indent-auto-mixed-prefix.png b/tests/ref/outline-indent-auto-mixed-prefix.png new file mode 100644 index 000000000..097e0bf88 Binary files /dev/null and b/tests/ref/outline-indent-auto-mixed-prefix.png differ diff --git a/tests/ref/outline-indent-auto-no-prefix.png b/tests/ref/outline-indent-auto-no-prefix.png new file mode 100644 index 000000000..e746b35b6 Binary files /dev/null and b/tests/ref/outline-indent-auto-no-prefix.png differ diff --git a/tests/ref/outline-indent-auto.png b/tests/ref/outline-indent-auto.png new file mode 100644 index 000000000..53517abd8 Binary files /dev/null and b/tests/ref/outline-indent-auto.png differ diff --git a/tests/ref/outline-indent-fixed.png b/tests/ref/outline-indent-fixed.png new file mode 100644 index 000000000..16df5d88c Binary files /dev/null and b/tests/ref/outline-indent-fixed.png differ diff --git a/tests/ref/outline-indent-func.png b/tests/ref/outline-indent-func.png new file mode 100644 index 000000000..b9a4948c3 Binary files /dev/null and b/tests/ref/outline-indent-func.png differ diff --git a/tests/ref/outline-indent-no-numbering.png b/tests/ref/outline-indent-no-numbering.png deleted file mode 100644 index 7c3a0ec0b..000000000 Binary files a/tests/ref/outline-indent-no-numbering.png and /dev/null differ diff --git a/tests/ref/outline-indent-numbering.png b/tests/ref/outline-indent-numbering.png deleted file mode 100644 index e3195f766..000000000 Binary files a/tests/ref/outline-indent-numbering.png and /dev/null differ diff --git a/tests/ref/outline-indent-zero.png b/tests/ref/outline-indent-zero.png new file mode 100644 index 000000000..e85cba484 Binary files /dev/null and b/tests/ref/outline-indent-zero.png differ diff --git a/tests/ref/outline-par.png b/tests/ref/outline-par.png new file mode 100644 index 000000000..04c63f62c Binary files /dev/null and b/tests/ref/outline-par.png differ diff --git a/tests/ref/outline-spacing.png b/tests/ref/outline-spacing.png new file mode 100644 index 000000000..897a5f746 Binary files /dev/null and b/tests/ref/outline-spacing.png differ diff --git a/tests/ref/outline-styled-text.png b/tests/ref/outline-styled-text.png index 89f48070a..e4520d122 100644 Binary files a/tests/ref/outline-styled-text.png and b/tests/ref/outline-styled-text.png differ diff --git a/tests/ref/outline.png b/tests/ref/outline.png deleted file mode 100644 index aeb4ef32e..000000000 Binary files a/tests/ref/outline.png and /dev/null differ diff --git a/tests/ref/pad-followed-by-content.png b/tests/ref/pad-followed-by-content.png index 90b48232a..534a97870 100644 Binary files a/tests/ref/pad-followed-by-content.png and b/tests/ref/pad-followed-by-content.png differ diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png new file mode 100644 index 000000000..27ca0cf6b Binary files /dev/null and b/tests/ref/par-contains-block.png differ diff --git a/tests/ref/par-contains-parbreak.png b/tests/ref/par-contains-parbreak.png new file mode 100644 index 000000000..27ca0cf6b Binary files /dev/null and b/tests/ref/par-contains-parbreak.png differ diff --git a/tests/ref/par-explicit-trim-space.png b/tests/ref/par-explicit-trim-space.png new file mode 100644 index 000000000..ff542274a Binary files /dev/null and b/tests/ref/par-explicit-trim-space.png differ diff --git a/tests/ref/par-first-line-indent-all-enum.png b/tests/ref/par-first-line-indent-all-enum.png new file mode 100644 index 000000000..38cdea792 Binary files /dev/null and b/tests/ref/par-first-line-indent-all-enum.png differ diff --git a/tests/ref/par-first-line-indent-all-list.png b/tests/ref/par-first-line-indent-all-list.png new file mode 100644 index 000000000..cf731e79f Binary files /dev/null and b/tests/ref/par-first-line-indent-all-list.png differ diff --git a/tests/ref/par-first-line-indent-all-terms.png b/tests/ref/par-first-line-indent-all-terms.png new file mode 100644 index 000000000..4d5c8a69c Binary files /dev/null and b/tests/ref/par-first-line-indent-all-terms.png differ diff --git a/tests/ref/par-first-line-indent-all.png b/tests/ref/par-first-line-indent-all.png new file mode 100644 index 000000000..f283d1a46 Binary files /dev/null and b/tests/ref/par-first-line-indent-all.png differ diff --git a/tests/ref/par-hanging-indent-semantic.png b/tests/ref/par-hanging-indent-semantic.png new file mode 100644 index 000000000..e05795c7f Binary files /dev/null and b/tests/ref/par-hanging-indent-semantic.png differ diff --git a/tests/ref/par-semantic-align.png b/tests/ref/par-semantic-align.png new file mode 100644 index 000000000..eda496411 Binary files /dev/null and b/tests/ref/par-semantic-align.png differ diff --git a/tests/ref/par-semantic-tag.png b/tests/ref/par-semantic-tag.png new file mode 100644 index 000000000..2b26e5aee Binary files /dev/null and b/tests/ref/par-semantic-tag.png differ diff --git a/tests/ref/par-semantic.png b/tests/ref/par-semantic.png new file mode 100644 index 000000000..c2eca74d9 Binary files /dev/null and b/tests/ref/par-semantic.png differ diff --git a/tests/ref/par-show-children.png b/tests/ref/par-show-children.png new file mode 100644 index 000000000..bc81c5bdc Binary files /dev/null and b/tests/ref/par-show-children.png differ diff --git a/tests/ref/par-show-styles.png b/tests/ref/par-show-styles.png new file mode 100644 index 000000000..13a8065ad Binary files /dev/null and b/tests/ref/par-show-styles.png differ diff --git a/tests/ref/query-running-header.png b/tests/ref/query-running-header.png index 395bc2ae8..b19eec22b 100644 Binary files a/tests/ref/query-running-header.png and b/tests/ref/query-running-header.png differ diff --git a/tests/ref/quote-par.png b/tests/ref/quote-par.png new file mode 100644 index 000000000..89d15ddf9 Binary files /dev/null and b/tests/ref/quote-par.png differ diff --git a/tests/ref/smallcaps-all.png b/tests/ref/smallcaps-all.png new file mode 100644 index 000000000..f3be53f82 Binary files /dev/null and b/tests/ref/smallcaps-all.png differ diff --git a/tests/ref/symbol-sect-deprecated.png b/tests/ref/symbol-sect-deprecated.png new file mode 100644 index 000000000..da647d5f7 Binary files /dev/null and b/tests/ref/symbol-sect-deprecated.png differ diff --git a/tests/ref/table-cell-par.png b/tests/ref/table-cell-par.png new file mode 100644 index 000000000..24a2a55f2 Binary files /dev/null and b/tests/ref/table-cell-par.png differ diff --git a/tests/ref/terms-par.png b/tests/ref/terms-par.png new file mode 100644 index 000000000..910b5e5da Binary files /dev/null and b/tests/ref/terms-par.png differ diff --git a/tests/src/collect.rs b/tests/src/collect.rs index c6deba77b..33f4f7366 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -149,7 +149,7 @@ impl Collector { for entry in walkdir::WalkDir::new(crate::SUITE_PATH).sort_by_file_name() { let entry = entry.unwrap(); let path = entry.path(); - if !path.extension().is_some_and(|ext| ext == "typ") { + if path.extension().is_none_or(|ext| ext != "typ") { continue; } @@ -168,7 +168,7 @@ impl Collector { for entry in walkdir::WalkDir::new(crate::REF_PATH).sort_by_file_name() { let entry = entry.unwrap(); let path = entry.path(); - if !path.extension().is_some_and(|ext| ext == "png") { + if path.extension().is_none_or(|ext| ext != "png") { continue; } diff --git a/tests/src/run.rs b/tests/src/run.rs index f9a3c0434..4d08362cf 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -161,7 +161,7 @@ impl<'a> Runner<'a> { // Compare against reference output if available. // Test that is ok doesn't need to be updated. - if ref_data.as_ref().map_or(false, |r| D::matches(&live, r)) { + if ref_data.as_ref().is_ok_and(|r| D::matches(&live, r)) { return; } diff --git a/tests/src/world.rs b/tests/src/world.rs index a08f1efa8..5c2678328 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -98,7 +98,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 { @@ -140,8 +140,8 @@ impl FileSlot { self.file .get_or_init(|| { read(&system_path(self.id)?).map(|cow| match cow { - Cow::Owned(buf) => buf.into(), - Cow::Borrowed(buf) => Bytes::from_static(buf), + Cow::Owned(buf) => Bytes::new(buf), + Cow::Borrowed(buf) => Bytes::new(buf), }) }) .clone() diff --git a/tests/suite/scripting/arguments.typ b/tests/suite/foundations/arguments.typ similarity index 60% rename from tests/suite/scripting/arguments.typ rename to tests/suite/foundations/arguments.typ index e82f49624..1439b6be5 100644 --- a/tests/suite/scripting/arguments.typ +++ b/tests/suite/foundations/arguments.typ @@ -16,3 +16,12 @@ #let args = arguments(0, 1, a: 2, 3) // Error: 2-14 arguments do not contain key "b" and no default value was specified #args.at("b") + +--- arguments-plus-sum-join --- +#let lhs = arguments(0, "1", key: "value", 3) +#let rhs = arguments(other-key: 4, key: "other value", 3) +#let result = arguments(0, "1", 3, other-key: 4, key: "other value", 3) +#test(lhs + rhs, result) +#test({lhs; rhs}, result) +#test((lhs, rhs).sum(), result) +#test((lhs, rhs).join(), result) diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ index 31ef1c54c..c3c119e33 100644 --- a/tests/suite/foundations/content.typ +++ b/tests/suite/foundations/content.typ @@ -50,12 +50,14 @@ `raw` --- content-fields-complex --- -// Integrated test for content fields. +// Integrated test for content fields. The idea is to parse a normal looking +// equation and symbolically evaluate it with the given variable values. + #let compute(equation, ..vars) = { let vars = vars.named() let f(elem) = { let func = elem.func() - if func == text { + if elem.has("text") { let text = elem.text if regex("^\d+$") in text { int(text) @@ -74,7 +76,7 @@ elem .children .filter(v => v != [ ]) - .split[+] + .split($+$.body) .map(xs => xs.fold(1, (prod, v) => prod * f(v))) .fold(0, (sum, v) => sum + v) } @@ -83,13 +85,15 @@ [With ] vars .pairs() - .map(p => $#p.first() = #p.last()$) + .map(((name, value)) => $#symbol(name) = value$) .join(", ", last: " and ") [ we have:] $ equation = result $ } #compute($x y + y^2$, x: 2, y: 3) +// This should generate the same output as: +// With $x = 2$ and $y = 3$ we have: $ x y + y^2 = 15 $ --- content-label-has-method --- // Test whether the label is accessible through the `has` method. diff --git a/tests/suite/foundations/plugin.typ b/tests/suite/foundations/plugin.typ index 0842980ec..9feacc030 100644 --- a/tests/suite/foundations/plugin.typ +++ b/tests/suite/foundations/plugin.typ @@ -9,6 +9,37 @@ bytes("value3-value1-value2"), ) +--- plugin-func --- +#let p = plugin("/assets/plugins/hello.wasm") +#test(type(p.hello), function) +#test(("a", "b").map(bytes).map(p.double_it), ("a.a", "b.b").map(bytes)) + +--- plugin-import --- +#import plugin("/assets/plugins/hello.wasm"): hello, double_it + +#test(hello(), bytes("Hello from wasm!!!")) +#test(double_it(bytes("hey!")), bytes("hey!.hey!")) + +--- plugin-transition --- +#let empty = plugin("/assets/plugins/hello-mut.wasm") +#test(str(empty.get()), "[]") + +#let hello = plugin.transition(empty.add, bytes("hello")) +#test(str(empty.get()), "[]") +#test(str(hello.get()), "[hello]") + +#let world = plugin.transition(empty.add, bytes("world")) +#let hello_you = plugin.transition(hello.add, bytes("you")) + +#test(str(empty.get()), "[]") +#test(str(hello.get()), "[hello]") +#test(str(world.get()), "[world]") +#test(str(hello_you.get()), "[hello, you]") + +#let hello2 = plugin.transition(empty.add, bytes("hello")) +#test(hello == world, false) +#test(hello == hello2, true) + --- plugin-wrong-number-of-arguments --- #let p = plugin("/assets/plugins/hello.wasm") diff --git a/tests/suite/foundations/repr.typ b/tests/suite/foundations/repr.typ index 36823e98a..2f2c055ad 100644 --- a/tests/suite/foundations/repr.typ +++ b/tests/suite/foundations/repr.typ @@ -37,8 +37,8 @@ #t(() => none, `(..) => ..`) // Types. -#t(int, `integer`) -#t(type("hi"), `string`) +#t(int, `int`) +#t(type("hi"), `str`) #t(type((a: 1)), `dictionary`) // Constants. diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ index 56756416d..66fb912c0 100644 --- a/tests/suite/foundations/str.typ +++ b/tests/suite/foundations/str.typ @@ -86,6 +86,13 @@ // Error: 2-28 0x110000 is not a valid codepoint #str.from-unicode(0x110000) // 0x10ffff is the highest valid code point +--- str-normalize --- +// Test the `normalize` method. +#test("e\u{0301}".normalize(form: "nfc"), "é") +#test("é".normalize(form: "nfd"), "e\u{0301}") +#test("ſ\u{0301}".normalize(form: "nfkc"), "ś") +#test("ſ\u{0301}".normalize(form: "nfkd"), "s\u{0301}") + --- string-len --- // Test the `len` method. #test("Hello World!".len(), 12) diff --git a/tests/suite/html/elem.typ b/tests/suite/html/elem.typ new file mode 100644 index 000000000..b416fdf94 --- /dev/null +++ b/tests/suite/html/elem.typ @@ -0,0 +1,15 @@ +--- html-elem-alone-context html --- +#context html.elem("html") + +--- html-elem-not-alone html --- +// Error: 2-19 `` element must be the only element in the document +#html.elem("html") +Text + +--- html-elem-metadata html --- +#html.elem("html", context { + let val = query().first().value + test(val, "Hi") + val +}) +#metadata("Hi") diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ index bb53a0411..f15ddfe4a 100644 --- a/tests/suite/layout/container.typ +++ b/tests/suite/layout/container.typ @@ -264,6 +264,13 @@ First! image("/assets/images/rhino.png", width: 30pt) ) +--- box-html html --- +Text #box[Span]. + +--- block-html html --- +Paragraph +#block[Div] + --- container-layoutable-child --- // Test box/block sizing with directly layoutable child. // diff --git a/tests/suite/layout/dir.typ b/tests/suite/layout/dir.typ index 139a2285d..e6db54da5 100644 --- a/tests/suite/layout/dir.typ +++ b/tests/suite/layout/dir.typ @@ -1,10 +1,35 @@ +--- dir-from --- +#test(direction.from(left), ltr) +#test(direction.from(right), rtl) +#test(direction.from(top), ttb) +#test(direction.from(bottom), btt) + +--- dir-from-invalid --- +// Error: 17-23 cannot convert this alignment to a side +#direction.from(center) + +--- dir-to --- +#test(direction.to(left), rtl) +#test(direction.to(right), ltr) +#test(direction.to(top), btt) +#test(direction.to(bottom), ttb) + +-- dir-to-invalid --- +// Error: 15-21 cannot convert this alignment to a side +#direction.to(center) + --- dir-axis --- -// Test direction methods. #test(ltr.axis(), "horizontal") #test(rtl.axis(), "horizontal") #test(ttb.axis(), "vertical") #test(btt.axis(), "vertical") +--- dir-sign --- +#test(ltr.sign(), 1) +#test(rtl.sign(), -1) +#test(ttb.sign(), 1) +#test(btt.sign(), -1) + --- dir-start --- #test(ltr.start(), left) #test(rtl.start(), right) diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ new file mode 100644 index 000000000..10345cb06 --- /dev/null +++ b/tests/suite/layout/grid/html.typ @@ -0,0 +1,59 @@ +--- basic-table html --- +#table( + columns: 3, + rows: 3, + + table.header( + [The], + [first], + [and], + [the], + [second], + [row], + table.hline(stroke: red) + ), + + table.cell(x: 1, rowspan: 2)[Baz], + [Foo], + [Bar], + + [1], + // Baz spans into the next cell + [2], + + table.cell(colspan: 2)[3], + [4], + + table.footer( + [The], + [last], + [row], + ), +) + +--- col-gutter-table html --- +#table( + columns: 3, + column-gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) + +--- row-gutter-table html --- +#table( + columns: 3, + row-gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) + +--- col-row-gutter-table html --- +#table( + columns: 3, + gutter: 3pt, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) diff --git a/tests/suite/layout/table.typ b/tests/suite/layout/table.typ index f59d8b424..5c2b07492 100644 --- a/tests/suite/layout/table.typ +++ b/tests/suite/layout/table.typ @@ -310,6 +310,17 @@ ) } +--- table-cell-par --- +// Ensure that table cells aren't considered paragraphs by default. +#show par: highlight + +#table( + columns: 3, + [A], + block[B], + par[C], +) + --- grid-cell-in-table --- // Error: 8-19 cannot use `grid.cell` as a table cell // Hint: 8-19 use `table.cell` instead diff --git a/tests/suite/loading/cbor.typ b/tests/suite/loading/cbor.typ new file mode 100644 index 000000000..4b50bb9c3 --- /dev/null +++ b/tests/suite/loading/cbor.typ @@ -0,0 +1,3 @@ +--- cbor-decode-deprecated --- +// Warning: 15-21 `cbor.decode` is deprecated, directly pass bytes to `cbor` instead +#let _ = cbor.decode diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ index 415488fcc..6f57ec458 100644 --- a/tests/suite/loading/csv.typ +++ b/tests/suite/loading/csv.typ @@ -25,3 +25,11 @@ // Test error numbering with dictionary rows. // Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3) #csv("/assets/data/bad.csv", row-type: dictionary) + +--- csv-invalid-delimiter --- +// Error: 41-51 delimiter must be an ASCII character +#csv("/assets/data/zoo.csv", delimiter: "\u{2008}") + +--- csv-decode-deprecated --- +// Warning: 14-20 `csv.decode` is deprecated, directly pass bytes to `csv` instead +#let _ = csv.decode diff --git a/tests/suite/loading/json.typ b/tests/suite/loading/json.typ index 3ebeaf2f7..c8df1ff6e 100644 --- a/tests/suite/loading/json.typ +++ b/tests/suite/loading/json.typ @@ -9,6 +9,10 @@ // Error: 7-30 failed to parse JSON (expected value at line 3 column 14) #json("/assets/data/bad.json") +--- json-decode-deprecated --- +// Warning: 15-21 `json.decode` is deprecated, directly pass bytes to `json` instead +#let _ = json.decode + --- issue-3363-json-large-number --- // Big numbers (larger than what i64 can store) should just lose some precision // but not overflow diff --git a/tests/suite/loading/toml.typ b/tests/suite/loading/toml.typ index 855ca995d..a4318a015 100644 --- a/tests/suite/loading/toml.typ +++ b/tests/suite/loading/toml.typ @@ -39,3 +39,7 @@ --- toml-invalid --- // Error: 7-30 failed to parse TOML (expected `.`, `=` at line 1 column 16) #toml("/assets/data/bad.toml") + +--- toml-decode-deprecated --- +// Warning: 15-21 `toml.decode` is deprecated, directly pass bytes to `toml` instead +#let _ = toml.decode diff --git a/tests/suite/loading/xml.typ b/tests/suite/loading/xml.typ index 41cd20e74..933f3c480 100644 --- a/tests/suite/loading/xml.typ +++ b/tests/suite/loading/xml.typ @@ -26,3 +26,7 @@ --- xml-invalid --- // Error: 6-28 failed to parse XML (found closing tag 'data' instead of 'hello' in line 3) #xml("/assets/data/bad.xml") + +--- xml-decode-deprecated --- +// Warning: 14-20 `xml.decode` is deprecated, directly pass bytes to `xml` instead +#let _ = xml.decode diff --git a/tests/suite/loading/yaml.typ b/tests/suite/loading/yaml.typ index bbfea41cb..a8089052c 100644 --- a/tests/suite/loading/yaml.typ +++ b/tests/suite/loading/yaml.typ @@ -15,3 +15,7 @@ --- yaml-invalid --- // Error: 7-30 failed to parse YAML (did not find expected ',' or ']' at line 2 column 1, while parsing a flow sequence at line 1 column 18) #yaml("/assets/data/bad.yaml") + +--- yaml-decode-deprecated --- +// Warning: 15-21 `yaml.decode` is deprecated, directly pass bytes to `yaml` instead +#let _ = yaml.decode diff --git a/tests/suite/math/alignment.typ b/tests/suite/math/alignment.typ index 63033ef5c..941c20556 100644 --- a/tests/suite/math/alignment.typ +++ b/tests/suite/math/alignment.typ @@ -4,10 +4,10 @@ // Test alignment step functions. #set page(width: 225pt) $ -"a" &= c \ -&= c + 1 & "By definition" \ -&= d + 100 + 1000 \ -&= x && "Even longer" \ +a &= c \ + &= c + 1 & "By definition" \ + &= d + 100 + 1000 \ + &= x && "Even longer" \ $ --- math-align-post-fix --- diff --git a/tests/suite/math/call.typ b/tests/suite/math/call.typ index 2477d9b6d..5caacfac6 100644 --- a/tests/suite/math/call.typ +++ b/tests/suite/math/call.typ @@ -8,14 +8,120 @@ $ pi(a,) $ $ pi(a,b) $ $ pi(a,b,) $ +--- math-call-unclosed-func --- +#let func(x) = x +// Error: 6-7 unclosed delimiter +$func(a$ + +--- math-call-unclosed-non-func --- +// Error: 5-6 unclosed delimiter +$sin(x$ + +--- math-call-named-args --- +#let func1(my: none) = my +#let func2(_my: none) = _my +#let func3(my-body: none) = my-body +#let func4(_my-body: none) = _my-body +#let func5(m: none) = m +$ func1(my: a) $ +$ func2(_my: a) $ +$ func3(my-body: a) $ +$ func4(_my-body: a) $ +$ func5(m: a) $ +$ func5(m: sigma : f) $ +$ func5(m: sigma:pi) $ + +--- math-call-named-args-no-expr --- +#let func(m: none) = m +// Error: 10 expected expression +$ func(m: ) $ + +--- math-call-named-args-duplicate --- +#let func(my: none) = my +// Error: 15-17 duplicate argument: my +$ func(my: a, my: b) $ + +--- math-call-named-args-shorthand-clash-1 --- +#let func(m: none) = m +// Error: 18-21 unexpected argument +$func(m: =) func(m:=)$ + +--- math-call-named-args-shorthand-clash-2 --- +#let func(m: none) = m +// Error: 41-45 unexpected argument +$func(m::) func(m: :=) func(m:: =) func(m::=)$ + +--- math-call-named-single-underscore --- +#let func(x) = x +// Error: 8-9 expected identifier, found underscore +$ func(_: a) $ + +--- math-call-named-single-char-error --- +#let func(m: none) = m +// Error: 8-13 unexpected argument +$ func(m : a) $ + +--- math-call-named-args-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(_a: a)$, "arguments(_a: [a])") +#check($args(_a-b: a)$, "arguments(_a-b: [a])") +#check($args(a-b: a)$, "arguments(a-b: [a])") +#check($args(a-b-c: a)$, "arguments(a-b-c: [a])") +#check($args(a--c: a)$, "arguments(a--c: [a])") +#check($args(a: a-b)$, "arguments(a: sequence([a], [−], [b]))") +#check($args(a-b: a-b)$, "arguments(a-b: sequence([a], [−], [b]))") +#check($args(a-b)$, "arguments(sequence([a], [−], [b]))") + +--- math-call-spread-content-error --- +#let args(..body) = body +// Error: 7-16 cannot spread content +$args(..(a + b))$ + +--- math-call-spread-multiple-exprs --- +#let args(..body) = body +// Error: 10 expected comma or semicolon +$args(..a + b)$ + +--- math-call-spread-unexpected-dots --- +#let args(..body) = body +// Error: 8-10 unexpected dots +$args(#..range(1, 5).chunks(2))$ + +--- math-call-spread-shorthand-clash --- +#let func(body) = body +$func(...)$ + +--- math-call-spread-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(..#range(0, 4).chunks(2))$, "arguments((0, 1), (2, 3))") +#check($#args(range(1, 5).chunks(2))$, "arguments(((1, 2), (3, 4)))") +#check($#args(..range(1, 5).chunks(2))$, "arguments((1, 2), (3, 4))") +#check($args(#(..range(2, 6).chunks(2)))$, "arguments(((2, 3), (4, 5)))") +#let nums = range(0, 4).chunks(2) +#check($args(..nums)$, "arguments((0, 1), (2, 3))") +#check($args(..nums;)$, "arguments(((0, 1), (2, 3)))") +#check($args(..nums, ..nums)$, "arguments((0, 1), (2, 3), (0, 1), (2, 3))") +#check($args(..nums, 4, 5)$, "arguments((0, 1), (2, 3), [4], [5])") +#check($args(..nums, ..#range(4, 6))$, "arguments((0, 1), (2, 3), 4, 5)") +#check($args(..nums, #range(4, 6))$, "arguments((0, 1), (2, 3), (4, 5))") +#check($args(..nums, 1, 2; 3, 4)$, "arguments(((0, 1), (2, 3), [1], [2]), ([3], [4]))") +#check($args(1, 2; ..nums)$, "arguments(([1], [2]), ((0, 1), (2, 3)))") +#check($args(1, 2; 3, 4)$, "arguments(([1], [2]), ([3], [4]))") +#check($args(1, 2; 3, 4; ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4]), (5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7);)$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7),)$, "arguments(([1], [2]), ([3], [4], 5, 6))") + --- math-call-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(a)$, "([a])") -#check($args(a,)$, "([a])") -#check($args(a,b)$, "([a], [b])") -#check($args(a,b,)$, "([a], [b])") -#check($args(,a,b,,,)$, "([], [a], [b], [], [])") +#check($args(a)$, "arguments([a])") +#check($args(a,)$, "arguments([a])") +#check($args(a,b)$, "arguments([a], [b])") +#check($args(a,b,)$, "arguments([a], [b])") +#check($args(,a,b,,,)$, "arguments([], [a], [b], [], [])") --- math-call-2d-non-func --- // Error: 6-7 expected content, found array @@ -31,21 +137,49 @@ $ mat(#"code"; "wins") $ --- math-call-2d-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(a;b)$, "(([a],), ([b],))") -#check($args(a,b;c)$, "(([a], [b]), ([c],))") -#check($args(a,b;c,d;e,f)$, "(([a], [b]), ([c], [d]), ([e], [f]))") +#check($args(a;b)$, "arguments(([a],), ([b],))") +#check($args(a,b;c)$, "arguments(([a], [b]), ([c],))") +#check($args(a,b;c,d;e,f)$, "arguments(([a], [b]), ([c], [d]), ([e], [f]))") + +--- math-call-2d-named-repr --- +#let args(..body) = (body.pos(), body.named()) +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a: b)$, "((), (a: [b]))") +#check($args(1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (:))") +#check($args(a: b, 1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, a: b, 2; 3, 4)$, "(([1], ([2],), ([3], [4])), (a: [b]))") +#check($args(1, 2, a: b; 3, 4)$, "(([1], [2], (), ([3], [4])), (a: [b]))") +#check($args(1, 2; a: b, 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, 2; 3, a: b, 4)$, "((([1], [2]), [3], ([4],)), (a: [b]))") +#check($args(1, 2; 3, 4, a: b)$, "((([1], [2]), [3], [4]), (a: [b]))") +#check($args(a: b, 1, 2, 3, c: d)$, "(([1], [2], [3]), (a: [b], c: [d]))") +#check($args(1, 2, 3; a: b)$, "((([1], [2], [3]),), (a: [b]))") +#check($args(a-b: a,, e:f;; d)$, "(([], (), ([],), ([d],)), (a-b: [a], e: [f]))") +#check($args(a: b, ..#range(0, 4))$, "((0, 1, 2, 3), (a: [b]))") + +--- math-call-2d-escape-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a\;b)$, "arguments(sequence([a], [;], [b]))") +#check($args(a\,b;c)$, "arguments((sequence([a], [,], [b]),), ([c],))") +#check($args(b\;c\,d;e)$, "arguments((sequence([b], [;], [c], [,], [d]),), ([e],))") +#check($args(a\: b)$, "arguments(sequence([a], [:], [ ], [b]))") +#check($args(a : b)$, "arguments(sequence([a], [ ], [:], [ ], [b]))") +#check($args(\..a)$, "arguments(sequence([.], [.], [a]))") +#check($args(.. a)$, "arguments(sequence([.], [.], [ ], [a]))") +#check($args(a..b)$, "arguments(sequence([a], [.], [.], [b]))") --- math-call-2d-repr-structure --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args( a; b; )$, "(([a],), ([b],))") -#check($args(a; ; c)$, "(([a],), ([],), ([c],))") -#check($args(a b,/**/; b)$, "((sequence([a], [ ], [b]), []), ([b],))") -#check($args(a/**/b, ; b)$, "((sequence([a], [b]), []), ([b],))") -#check($args( ;/**/a/**/b/**/; )$, "(([],), (sequence([a], [b]),))") -#check($args( ; , ; )$, "(([],), ([], []))") +#check($args( a; b; )$, "arguments(([a],), ([b],))") +#check($args(a; ; c)$, "arguments(([a],), ([],), ([c],))") +#check($args(a b,/**/; b)$, "arguments((sequence([a], [ ], [b]), []), ([b],))") +#check($args(a/**/b, ; b)$, "arguments((sequence([a], [b]), []), ([b],))") +#check($args( ;/**/a/**/b/**/; )$, "arguments(([],), (sequence([a], [b]),))") +#check($args( ; , ; )$, "arguments(([],), ([], []))") #check($args(/**/; // funky whitespace/trivia - , /**/ ;/**/)$, "(([],), ([], []))") + , /**/ ;/**/)$, "arguments(([],), ([], []))") --- math-call-empty-args-non-func --- // Trailing commas and empty args introduce blank content in math @@ -56,9 +190,9 @@ $ sin( ,/**/x/**/, , /**/y, ,/**/, ) $ --- math-call-empty-args-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(,x,,y,,)$, "([], [x], [], [y], [])") +#check($args(,x,,y,,)$, "arguments([], [x], [], [y], [])") // with whitespace/trivia: -#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "([], [x], [], [y], [], [])") +#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "arguments([], [x], [], [y], [], [])") --- math-call-value-non-func --- $ sin(1) $ diff --git a/tests/suite/math/class.typ b/tests/suite/math/class.typ index d25071dbd..9f014810c 100644 --- a/tests/suite/math/class.typ +++ b/tests/suite/math/class.typ @@ -45,3 +45,8 @@ $class("large", ->)_a$ $limits(class("normal", ->))_a$ $ scripts(class("relation", x))_a $ + +--- issue-4985-up-tack-is-normal-perp-is-relation --- +$ top = 1 \ + bot = 2 \ + a perp b $ diff --git a/tests/suite/math/delimited.typ b/tests/suite/math/delimited.typ index 226740501..794ffd8aa 100644 --- a/tests/suite/math/delimited.typ +++ b/tests/suite/math/delimited.typ @@ -41,8 +41,8 @@ $floor(x/2), ceil(x/2), abs(x), norm(x)$ --- math-lr-color --- // Test colored delimiters $ lr( - text("(", fill: #green) a/b - text(")", fill: #blue) + text(\(, fill: #green) a/b + text(\), fill: #blue) ) $ --- math-lr-mid --- @@ -125,3 +125,11 @@ $ lr(size: #3em, |)_a^b lr(size: #3em, zws|)_a^b --- issue-4188-lr-corner-brackets --- // Test positioning of U+231C to U+231F $⌜a⌟⌞b⌝$ = $⌜$$a$$⌟$$⌞$$b$$⌝$ + +--- math-lr-unparen --- +// Test that unparen with brackets stays as an LrElem. +#let item = $limits(sum)_i$ +$ + 1 / ([item]) quad + 1 / [item] +$ diff --git a/tests/suite/math/mat.typ b/tests/suite/math/mat.typ index 391ff1677..b7d6a6871 100644 --- a/tests/suite/math/mat.typ +++ b/tests/suite/math/mat.typ @@ -54,6 +54,30 @@ $ a + mat(delim: #none, 1, 2; 3, 4) + b $ $ mat(1, 2; 3, 4; delim: "[") $, ) +--- math-mat-spread --- +// Test argument spreading in matrix. +$ mat(..#range(1, 5).chunks(2)) + mat(#(..range(2).map(_ => range(2)))) $ + +#let nums = ((1,) * 5).intersperse(0).chunks(3) +$ mat(..nums, delim: "[") $ + +--- math-mat-spread-1d --- +$ mat(..#range(1, 5) ; 1, ..#range(2, 5)) + mat(..#range(1, 3), ..#range(3, 5) ; ..#range(1, 4), 4) $ + +--- math-mat-spread-2d --- +#let nums = range(0, 2).map(i => (i, i+1)) +$ mat(..nums, delim: "|",) + mat(..nums; delim: "|",) $ +$ mat(..nums) mat(..nums;) \ + mat(..nums;,) mat(..nums,) $ + +--- math-mat-spread-expected-array-error --- +#let nums = range(0, 2).map(i => (i, i+1)) +// Error: 15-16 expected array, found content +$ mat(..nums, 0, 1) $ + --- math-mat-gap --- #set math.mat(gap: 1em) $ mat(1, 2; 3, 4) $ @@ -61,6 +85,8 @@ $ mat(1, 2; 3, 4) $ --- math-mat-gaps --- #set math.mat(row-gap: 1em, column-gap: 2em) $ mat(1, 2; 3, 4) $ +$ mat(column-gap: #1em, 1, 2; 3, 4) + mat(row-gap: #2em, 1, 2; 3, 4) $ --- math-mat-augment --- // Test matrix line drawing (augmentation). diff --git a/tests/suite/math/stretch.typ b/tests/suite/math/stretch.typ index 1377f4d21..d145f72a1 100644 --- a/tests/suite/math/stretch.typ +++ b/tests/suite/math/stretch.typ @@ -63,8 +63,8 @@ $ ext(bar.v) quad ext(bar.v.double) quad // Test stretch when base is given with shorthand. $stretch(||, size: #2em)$ $stretch(\(, size: #2em)$ -$stretch("⟧", size: #2em)$ -$stretch("|", size: #2em)$ +$stretch(⟧, size: #2em)$ +$stretch(|, size: #2em)$ $stretch(->, size: #2em)$ $stretch(↣, size: #2em)$ @@ -87,7 +87,7 @@ $ body^"text" $ #{ let body = $stretch(=)$ for i in range(24) { - body = $body$ + body = $body$ } $body^"long text"$ } diff --git a/tests/suite/math/symbols.typ b/tests/suite/math/symbols.typ index 65a483162..6dd9db622 100644 --- a/tests/suite/math/symbols.typ +++ b/tests/suite/math/symbols.typ @@ -2,7 +2,7 @@ --- math-symbol-basic --- #let sym = symbol("s", ("basic", "s")) -#test($sym.basic$, $#"s"$) +#test($sym.basic$, $s$) --- math-symbol-underscore --- #let sym = symbol("s", ("test_underscore", "s")) @@ -16,7 +16,7 @@ $sym.test-dash$ --- math-symbol-double --- #let sym = symbol("s", ("test.basic", "s")) -#test($sym.test.basic$, $#"s"$) +#test($sym.test.basic$, $s$) --- math-symbol-double-underscore --- #let sym = symbol("s", ("one.test_underscore", "s")) diff --git a/tests/suite/math/syntax.typ b/tests/suite/math/syntax.typ index cd1124c37..7091d908c 100644 --- a/tests/suite/math/syntax.typ +++ b/tests/suite/math/syntax.typ @@ -13,6 +13,11 @@ $ underline(f' : NN -> RR) \ 1 - 0 thick &..., ) $ +--- math-shorthands-noncontinuable --- +// Test that shorthands are not continuable. +$ x >=(y) / z \ + x >= (y) / z $ + --- math-common-symbols --- // Test common symbols. $ dot \ dots \ ast \ tilde \ star $ diff --git a/tests/suite/math/text.typ b/tests/suite/math/text.typ index 760910f4d..8c7611114 100644 --- a/tests/suite/math/text.typ +++ b/tests/suite/math/text.typ @@ -43,3 +43,8 @@ $sum_(k in NN)^prime 1/k^2$ // Test script-script in a fraction. $ 1/(x^A) $ #[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$] + +--- math-par --- +// Ensure that math does not produce paragraphs. +#show par: highlight +$ a + "bc" + #[c] + #box[d] + #block[e] $ diff --git a/tests/suite/model/bibliography.typ b/tests/suite/model/bibliography.typ index 20eb8acd9..6de44e240 100644 --- a/tests/suite/model/bibliography.typ +++ b/tests/suite/model/bibliography.typ @@ -53,6 +53,24 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read @Zee04 #bibliography("/assets/bib/works_too.bib", style: "mla") +--- bibliography-grid-par --- +// Ensure that a grid-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib") + +--- bibliography-indent-par --- +// Ensure that an indent-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib", style: "mla") + --- issue-4618-bibliography-set-heading-level --- // Test that the bibliography block's heading is set to 2 by the show rule, // and therefore should be rendered like a level-2 heading. Notably, this diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index 258c6f6bc..7176b04e2 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -101,6 +101,13 @@ a + 0. [Red], [Green], [Blue], [Red], ) +--- enum-start html --- +#enum( + start: 3, + [Skipping], + [Ahead], +) + --- enum-numbering-closure-nested --- // Test numbering with closure and nested lists. #set enum(numbering: n => super[#n]) @@ -176,19 +183,51 @@ a + 0. #set enum(number-align: horizon) #set enum(number-align: bottom) +--- enum-par render html --- +// Check whether the contents of enum items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + + Hello + + World +] + +#block[ + + Hello // Paragraphs + + From + + World // No paragraph because it's a tight enum +] + +#block[ + + Hello // Paragraphs + + From + + The + + + World // Paragraph because it's a wide enum +] + --- issue-2530-enum-item-panic --- // Enum item (pre-emptive) #enum.item(none)[Hello] #enum.item(17)[Hello] ---- issue-5503-enum-interrupted-by-par-align --- -// `align` is block-level and should interrupt an enum -// but not a `par` +--- issue-5503-enum-in-align --- +// `align` is block-level and should interrupt an enum. + a + b -#par(leading: 5em)[+ par] +#align(right)[+ c] + d -#par[+ par] -+ f -#align(right)[+ align] -+ h + +--- issue-5719-enum-nested --- +// Enums can be immediately nested. +1. A +2. 1. B + 2. C ++ + D + + E ++ = F + G diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index 0e5db4d07..37fb4ecda 100644 --- a/tests/suite/model/figure.typ +++ b/tests/suite/model/figure.typ @@ -180,6 +180,17 @@ We can clearly see that @fig-cylinder and caption: [Underlined], ) +--- figure-par --- +// Ensure that a figure body is considered a paragraph. +#show par: highlight + +#figure[Text] + +#figure( + [Text], + caption: [A caption] +) + --- figure-and-caption-show --- // Test creating custom figure and custom caption @@ -289,9 +300,3 @@ HI#footnote.entry(clearance: 2.5em)[There] ) #c - ---- issue-5370-figure-caption-separator-outline --- -// Test that language-dependant caption separator is respected in outline. -#outline(title: none, target: figure) -#set text(lang: "ru") -#figure(rect(), caption: [Rectangle]) diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ index d182724c8..4e04e5c56 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -97,6 +97,18 @@ comment spans lines = Fake level 2 == Fake level 3 +--- heading-hanging-indent-auto --- +#set heading(numbering: "1.1.a.") += State of the Art + +--- heading-hanging-indent-zero --- +#set heading(numbering: "1.1.a.", hanging-indent: 0pt) += State of the Art + +--- heading-hanging-indent-length --- +#set heading(numbering: "1.1.a.", hanging-indent: 2em) += State of the Art In Multi-Line + --- heading-offset-and-level --- // Passing level directly still overrides all other set values #set heading(numbering: "1.1", offset: 1) @@ -115,3 +127,33 @@ Not in heading // Error: 1:19-1:25 cannot reference heading without numbering // Hint: 1:19-1:25 you can enable heading numbering with `#set heading(numbering: "1.")` Cannot be used as @intro + +--- heading-par --- +// Ensure that heading text isn't considered a paragraph. +#show par: highlight += Heading + +--- heading-html-basic html --- +// level 1 => h2 +// ... +// level 5 => h6 +// level 6 => div with role=heading and aria-level=7 +// ... + += Level 1 +== Level 2 +=== Level 3 +==== Level 4 +===== Level 5 +// Warning: 1-15 heading of level 6 was transformed to
, which is not supported by all assistive technology +// Hint: 1-15 HTML only supports

to

, not +// Hint: 1-15 you may want to restructure your document so that it doesn't contain deep headings +====== Level 6 +// Warning: 1-16 heading of level 7 was transformed to
, which is not supported by all assistive technology +// Hint: 1-16 HTML only supports

to

, not +// Hint: 1-16 you may want to restructure your document so that it doesn't contain deep headings +======= Level 7 + +--- issue-5719-heading-nested --- +// Headings may not be nested like this. += = A diff --git a/tests/suite/model/link.typ b/tests/suite/model/link.typ index 7cced8560..bd6c8a307 100644 --- a/tests/suite/model/link.typ +++ b/tests/suite/model/link.typ @@ -75,3 +75,14 @@ Text Text // Error: 2-20 label `` occurs multiple times in the document #link()[Nope.] + +--- link-empty-block --- +#link("", block(height: 10pt, width: 100%)) + +--- issue-758-link-repeat --- +#let url = "https://typst.org/" +#let body = [Hello #box(width: 1fr, repeat[.])] + +Inline: #link(url, body) + +#link(url, block(inset: 4pt, [Block: ] + body)) diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 138abf70e..9bed930bb 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -77,6 +77,49 @@ _Shopping list_ #test(indented, manual) +--- list-indent-bracket-nesting --- +// Test list indent nesting behavior when directly at a starting bracket. + +#let indented = { + [- indented + - less + ] + [- indented + - same + - then less + - then same + ] + [- indented + - more + - then same + - then less + ] +} + +#let item = list.item +#let manual = { + { + item[indented]; [ ] + item[less]; [ ] + } + { + item[indented]; [ ] + item[same]; [ ] + item[then less #{ + item[then same] + }]; [ ] + } + { + item[indented #{ + item[more] + }]; [ ] + item[then same]; [ ] + item[then less]; [ ] + } +} + +#test(indented, manual) + --- list-tabs --- // This works because tabs are used consistently. - A with 1 tab @@ -195,6 +238,33 @@ World #text(red)[- World] #text(green)[- What up?] +--- list-par render html --- +// Check whether the contents of list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +#block[ + // No paragraphs. + - Hello + - World +] + +#block[ + - Hello // Paragraphs + + From + - World // No paragraph because it's a tight list. +] + +#block[ + - Hello // Paragraphs either way + + From + + The + + - World // Paragraph because it's a wide list. +] + --- issue-2530-list-item-panic --- // List item (pre-emptive) #list.item[Hello] @@ -219,17 +289,18 @@ World part($ x $ + parbreak() + parbreak() + list[A]) } ---- issue-5503-list-interrupted-by-par-align --- -// `align` is block-level and should interrupt a list -// but not a `par` +--- issue-5503-list-in-align --- +// `align` is block-level and should interrupt a list. #show list: [List] - a - b -#par(leading: 5em)[- c] -- d -- e -#par[- f] -- g -- h #align(right)[- i] - j + +--- issue-5719-list-nested --- +// Lists can be immediately nested. +- A +- - B + - C +- = D + E diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index a8426d6c6..49fd7d7cb 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -1,10 +1,195 @@ ---- outline --- -#set page(height: 200pt, margin: (bottom: 20pt), numbering: "1") +--- outline-spacing --- +#set heading(numbering: "1.a.") +#set outline.entry(fill: none) +#show outline.entry.where(level: 1): set block(above: 1.2em) + +#outline() + +#show heading: none += A +== B +== C += D +== E + +--- outline-indent-auto --- +#set heading(numbering: "I.i.") +#set page(width: 150pt) +#show heading: none + +#context test(outline.indent, auto) +#outline() + += A +== B +== C +== D +=== Title that breaks across lines += E +== F +=== Aligned + +--- outline-indent-auto-mixed-prefix --- +#show heading: none +#show outline.entry.where(level: 1): strong + +#outline() + +#set heading(numbering: "I.i.") += A +== B +=== Title that breaks += C +== D += E +#[ + #set heading(numbering: none) + = F + == Numberless title that breaks + === G +] += H + +--- outline-indent-auto-mixed-prefix-short --- +#show heading: none + +#outline() + +#set heading(numbering: "I.i.") += A +#set heading(numbering: none) += B + +--- outline-indent-auto-no-prefix --- +#show heading: none + +#outline() + += A +== B +=== Title that breaks across lines += C +== D +=== E + +--- outline-indent-zero --- +#set heading(numbering: "1.a.") +#show heading: none + +#outline(indent: 0pt) + += A +== B +=== C +==== Title that breaks across lines +#set heading(numbering: none) +== E += F + +--- outline-indent-fixed --- +#set heading(numbering: "1.a.") +#show heading: none + +#outline(indent: 1em) + += A +== B +=== C +==== Title that breaks +#set heading(numbering: none) +== E += F + +--- outline-indent-func --- +#set heading(numbering: "1.a.") +#show heading: none + +#outline(indent: n => (0pt, 1em, 2.5em, 3em).at(n)) + += A +== B +=== C +==== Title breaks +#set heading(numbering: none) +== E += F + +--- outline-indent-bad-type --- +// Error: 2-35 expected relative length, found dictionary +#outline(indent: n => (a: "dict")) + += Heading + +--- outline-entry --- +#set page(width: 150pt) +#set heading(numbering: "1.") + +#show outline.entry.where(level: 1): set block(above: 12pt) +#show outline.entry.where(level: 1): strong + +#outline(indent: auto) + +#show heading: none += Introduction += Background +== History +== State of the Art += Analysis +== Setup + +--- outline-entry-complex --- +#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt)) +#set heading(numbering: "1.") + +#set outline.entry(fill: repeat[--]) +#show outline.entry.where(level: 1): it => link( + it.element.location(), + it.indented(it.prefix(), { + emph(it.body()) + [ ] + text(luma(100), box(width: 1fr, repeat[--·--])) + [ ] + it.page() + }) +) + +#counter(page).update(3) +#outline() + +#show heading: none + += Top heading +== Not top heading +=== Lower heading +=== Lower too +== Also not top + +#pagebreak() +#set page(numbering: "1") + += Another top heading +== Middle heading +=== Lower heading + +--- outline-entry-inner --- +#set heading(numbering: "1.") +#show outline.entry: it => block(it.inner()) +#show heading: none + +#set outline.entry(fill: repeat[ -- ]) +#outline() + += A += B + +--- outline-heading-start-of-page --- +#set page(width: 140pt, height: 200pt, margin: (bottom: 20pt), numbering: "1") #set heading(numbering: "(1/a)") #show heading.where(level: 1): set text(12pt) #show heading.where(level: 2): set text(10pt) -#outline(fill: none) +#set outline.entry(fill: none) +#outline() = A = B @@ -23,66 +208,28 @@ A == F ==== G +--- outline-bookmark --- +// Ensure that `bookmarked` option doesn't affect the outline +#set heading(numbering: "(I)", bookmarked: false) +#set outline.entry(fill: none) +#show heading: none +#outline() + += A + --- outline-styled-text --- #outline(title: none) = #text(blue)[He]llo ---- outline-bookmark --- -#outline(title: none, fill: none) - -// Ensure 'bookmarked' option doesn't affect the outline -#set heading(numbering: "(I)", bookmarked: false) -= A - ---- outline-indent-numbering --- -// With heading numbering -#set page(width: 200pt) -#set heading(numbering: "1.a.") -#show heading: none -#set outline(fill: none) - -#context test(outline.indent, none) -#outline(indent: none) -#outline(indent: auto) -#outline(indent: 2em) -#outline(indent: n => ([-], [], [==], [====]).at(n)) - -= A -== B -== C -=== D -==== E - ---- outline-indent-no-numbering --- -// Without heading numbering -#set page(width: 200pt) -#show heading: none -#set outline(fill: none) - -#outline(indent: none) -#outline(indent: auto) -#outline(indent: n => 2em * n) - -= About -== History - ---- outline-indent-bad-type --- -// Error: 2-35 expected relative length or content, found dictionary -#outline(indent: n => (a: "dict")) - -= Heading - --- outline-first-line-indent --- #set par(first-line-indent: 1.5em) #set heading(numbering: "1.1.a.") -#show outline.entry.where(level: 1): it => { - v(0.5em, weak: true) - strong(it) -} +#show outline.entry.where(level: 1): strong #outline() +#show heading: none = Introduction = Background == History @@ -90,85 +237,63 @@ A = Analysis == Setup ---- outline-entry --- -#set page(width: 150pt) -#set heading(numbering: "1.") - -#show outline.entry.where( - level: 1 -): it => { - v(12pt, weak: true) - strong(it) -} - -#outline(indent: auto) -#v(1.2em, weak: true) - -#set text(8pt) -#show heading: set block(spacing: 0.65em) - -= Introduction -= Background -== History -== State of the Art -= Analysis -== Setup - ---- outline-entry-complex --- -#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt)) -#set heading(numbering: "1.") -#show outline.entry.where(level: 1): it => [ - #let loc = it.element.location() - #let num = numbering(loc.page-numbering(), ..counter(page).at(loc)) - #emph(link(loc, it.body)) - #text(luma(100), box(width: 1fr, repeat[#it.fill.body;·])) - #link(loc, num) -] - -#counter(page).update(3) -#outline(indent: auto, fill: repeat[--]) -#v(1.2em, weak: true) - -#set text(8pt) -#show heading: set block(spacing: 0.65em) - -= Top heading -== Not top heading -=== Lower heading -=== Lower too -== Also not top - -#pagebreak() -#set page(numbering: "1") - -= Another top heading -== Middle heading -=== Lower heading - --- outline-bad-element --- // Error: 2-27 cannot outline metadata #outline(target: metadata) #metadata("hello") +--- outline-par --- +// Ensure that an outline does not produce paragraphs. +#show par: highlight + +#outline() + += A += B += C + +--- issue-2048-outline-multiline --- +// Without the word joiner between the dots and the page number, +// the page number would be alone in its line. +#set page(width: 125pt) +#set heading(numbering: "1.a.") +#show heading: none + +#outline() + += A +== This just fits here + --- issue-2530-outline-entry-panic-text --- // Outline entry (pre-emptive) -// Error: 2-48 cannot outline text -#outline.entry(1, [Hello], [World!], none, [1]) +// Error: 2-27 cannot outline text +#outline.entry(1, [Hello]) --- issue-2530-outline-entry-panic-heading --- // Outline entry (pre-emptive, improved error) -// Error: 2-55 heading must have a location -// Hint: 2-55 try using a query or a show rule to customize the outline.entry instead -#outline.entry(1, heading[Hello], [World!], none, [1]) +// Error: 2-34 heading must have a location +// Hint: 2-34 try using a show rule to customize the outline.entry instead +#outline.entry(1, heading[Hello]) ---- issue-4476-rtl-title-ending-in-ltr-text --- +--- issue-4476-outline-rtl-title-ending-in-ltr-text --- #set text(lang: "he") #outline() +#show heading: none = הוקוס Pocus = זוהי כותרת שתורגמה על ידי מחשב ---- issue-5176-cjk-title --- +--- issue-4859-outline-entry-show-set --- +#set heading(numbering: "1.a.") +#show outline.entry.where(level: 1): set outline.entry(fill: none) +#show heading: none + +#outline() + += A +== B + +--- issue-5176-outline-cjk-title --- #set text(font: "Noto Serif CJK SC") #show heading: none diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index 0c2b5cb54..d2c3416e0 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -19,6 +19,105 @@ heaven Would through the airy region stream so bright That birds would sing and think it were not night. See, how she leans her cheek upon her hand! O, that I were a glove upon that hand, That I might touch that cheek! +--- par-semantic --- +#show par: highlight + +I'm a paragraph. + +#align(center, table( + columns: 3, + + // No paragraphs. + [A], + block[B], + block[C *D*], + + // Paragraphs. + par[E], + [ + + F + ], + [ + G + + ], + + // Paragraphs. + parbreak() + [H], + [I] + parbreak(), + parbreak() + [J] + parbreak(), + + // Paragraphs. + [K #v(10pt)], + [#v(10pt) L], + [#place[] M], + + // Paragraphs. + [ + N + + O + ], + [#par[P]#par[Q]], + // No paragraphs. + [#block[R]#block[S]], +)) + +--- par-semantic-html html --- += Heading is no paragraph + +I'm a paragraph. + +#html.elem("div")[I'm not.] + +#html.elem("div")[ + We are two. + + So we are paragraphs. +] + +--- par-semantic-tag --- +#show par: highlight +#block[ + #metadata(none) + A + #metadata(none) +] + +#block(width: 100%, metadata(none) + align(center)[A]) +#block(width: 100%, align(center)[A] + metadata(none)) + +--- par-semantic-align --- +#show par: highlight +#show bibliography: none +#set block(width: 100%, stroke: 1pt, inset: 5pt) + +#bibliography("/assets/bib/works.bib") + +#block[ + #set align(right) + Hello +] + +#block[ + #set align(right) + Hello + @netwok +] + +#block[ + Hello + #align(right)[World] + You +] + +#block[ + Hello + #align(right)[@netwok] + You +] + --- par-leading-and-spacing --- // Test changing leading and spacing. #set par(spacing: 1em, leading: 2pt) @@ -57,6 +156,57 @@ starts a paragraph, also with indent. ثم يصبح النص رطبًا وقابل للطرق ويبدو المستند رائعًا. +--- par-first-line-indent-all --- +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) +#set block(spacing: 1.2em) +#show heading: set text(size: 10pt) + += Heading +All paragraphs are indented. + +Even the first. + +--- par-first-line-indent-all-list --- +#show list.where(tight: false): set list(spacing: 1.2em) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + +- A #parbreak() B #line(length: 100%) C + +- D + +--- par-first-line-indent-all-enum --- +#show enum.where(tight: false): set enum(spacing: 1.2em) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + ++ A #parbreak() B #line(length: 100%) C + ++ D + +--- par-first-line-indent-all-terms --- +#show terms.where(tight: false): set terms(spacing: 1.2em) +#set terms(hanging-indent: 10pt) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + +/ Term A: B \ C #parbreak() D #line(length: 100%) E + +/ Term F: G + --- par-spacing-and-first-line-indent --- // This is madness. #set par(first-line-indent: 12pt) @@ -69,6 +219,12 @@ Why would anybody ever ... #set par(hanging-indent: 15pt, justify: true) #lorem(10) +--- par-hanging-indent-semantic --- +#set par(hanging-indent: 15pt) += I am not affected + +I am affected by hanging indent. + --- par-hanging-indent-manual-linebreak --- #set par(hanging-indent: 1em) Welcome \ here. Does this work well? @@ -83,6 +239,22 @@ Welcome \ here. Does this work well? // Ensure that trailing whitespace layouts as intended. #box(fill: aqua, " ") +--- par-contains-parbreak --- +#par[ + Hello + // Warning: 4-14 parbreak may not occur inside of a paragraph and was ignored + #parbreak() + World +] + +--- par-contains-block --- +#par[ + Hello + // Warning: 4-11 block may not occur inside of a paragraph and was ignored + #block[] + World +] + --- par-empty-metadata --- // Check that metadata still works in a zero length paragraph. #block(height: 0pt)[#""#metadata(false)] @@ -94,6 +266,49 @@ Welcome \ here. Does this work well? #set text(hyphenate: false) Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. +--- par-show-children --- +// Variant 1: Prevent recursion by checking the children. +#let p = counter("p") +#let step = p.step() +#let nr = context p.display() +#show par: it => { + if it.body.at("children", default: ()).at(0, default: none) == step { + return it + } + par(step + [§#nr ] + it.body) +} + += A + +B + +C #parbreak() D + +#block[E] + +#block[F #parbreak() G] + +--- par-show-styles --- +// Variant 2: Prevent recursion by observing a style. +#let revoke = metadata("revoke") +#show par: it => { + if bibliography.title == revoke { return it } + set bibliography(title: revoke) + let p = counter("p") + par[#p.step()§#context p.display() #it.body] +} + += A + +B + +C + +--- par-explicit-trim-space --- +A + +#par[ B ] + --- issue-4278-par-trim-before-equation --- #set par(justify: true) #lorem(6) aa $a = c + b$ @@ -107,6 +322,20 @@ Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. #context test(query().len(), 1) +--- issue-5831-par-constructor-args --- +// Make sure that all arguments are also respected in the constructor. +A +#par( + leading: 2pt, + spacing: 20pt, + justify: true, + linebreaks: "simple", + first-line-indent: (amount: 1em, all: true), + hanging-indent: 5pt, +)[ + The par function has a constructor and justification. +] + --- show-par-set-block-hint --- // Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore // Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore diff --git a/tests/suite/model/quote.typ b/tests/suite/model/quote.typ index 2c93f92cd..51c4bba59 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -84,3 +84,37 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. // With custom quotes. #set smartquote(quotes: (single: ("<", ">"), double: ("(", ")"))) #quote[A #quote[nested] quote] + +--- quote-plato html --- +#set quote(block: true) + +#quote(attribution: [Plato])[ + ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι + ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. +] +#quote(attribution: [from the Henry Cary literal translation of 1897])[ + ... I seem, then, in just this little thing to be wiser than this man at + any rate, that what I do not know I do not think I know either. +] + +--- quote-nesting-html html --- +When you said that #quote[he surely meant that #quote[she intended to say #quote[I'm sorry]]], I was quite confused. + +--- quote-attribution-link html --- +#quote( + block: true, + attribution: link("https://typst.app/home")[typst.com] +)[ + Compose papers faster +] + +--- quote-par --- +// Ensure that an inline quote is part of a paragraph, but a block quote +// does not result in paragraphs. +#show par: highlight + +An inline #quote[quote.] + +#quote(block: true, attribution: [The Test Author])[ + A block-level quote. +] diff --git a/tests/suite/model/terms.typ b/tests/suite/model/terms.typ index 61fe20b0d..103a8033e 100644 --- a/tests/suite/model/terms.typ +++ b/tests/suite/model/terms.typ @@ -59,6 +59,34 @@ Not in list // Error: 8 expected colon / Hello +--- terms-par render html --- +// Check whether the contents of term list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + / Hello: A + / World: B +] + +#block[ + / Hello: A // Paragraphs + + From + / World: B // No paragraphs because it's a tight term list. +] + +#block[ + / Hello: A // Paragraphs + + From + + The + + / World: B // Paragraph because it's a wide term list. +] + + --- issue-1050-terms-indent --- #set page(width: 110pt) #set par(first-line-indent: 0.5cm) @@ -76,17 +104,15 @@ Not in list // Term item (pre-emptive) #terms.item[Hello][World!] ---- issue-5503-terms-interrupted-by-par-align --- -// `align` is block-level and should interrupt a `terms` -// but not a `par` +--- issue-5503-terms-in-align --- +// `align` is block-level and should interrupt a `terms`. #show terms: [Terms] / a: a -/ b: b -#par(leading: 5em)[/ c: c] -/ d: d -/ e: e -#par[/ f: f] -/ g: g -/ h: h #align(right)[/ i: i] / j: j + +--- issue-5719-terms-nested --- +// Term lists can be immediately nested. +/ Term A: 1 +/ Term B: / Term C: 2 + / Term D: 3 diff --git a/tests/suite/pdf/embed.typ b/tests/suite/pdf/embed.typ new file mode 100644 index 000000000..83f006d63 --- /dev/null +++ b/tests/suite/pdf/embed.typ @@ -0,0 +1,30 @@ +// Test file embeddings. The tests here so far are unsatisfactory because we +// have no PDF testing infrastructure. That should be improved in the future. + +--- pdf-embed --- +#pdf.embed("/assets/text/hello.txt") +#pdf.embed( + "/assets/data/details.toml", + relationship: "supplement", + mime-type: "application/toml", + description: "Information about a secret project", +) + +--- pdf-embed-bytes --- +#pdf.embed("hello.txt", read("/assets/text/hello.txt", encoding: none)) +#pdf.embed( + "a_file_name.txt", + read("/assets/text/hello.txt", encoding: none), + relationship: "supplement", + mime-type: "text/plain", + description: "A description", +) + +--- pdf-embed-invalid-relationship --- +#pdf.embed( + "/assets/text/hello.txt", + // Error: 17-23 expected "source", "data", "alternative", "supplement", or none + relationship: "test", + mime-type: "text/plain", + description: "A test file", +) diff --git a/tests/suite/scripting/call.typ b/tests/suite/scripting/call.typ index 5a5fb326d..af5f5eaab 100644 --- a/tests/suite/scripting/call.typ +++ b/tests/suite/scripting/call.typ @@ -141,7 +141,7 @@ #{ let save(..args) = { test(type(args), arguments) - test(repr(args), "(three: true, 1, 2)") + test(repr(args), "arguments(three: true, 1, 2)") } save(1, 2, three: true) @@ -159,7 +159,7 @@ #{ let more = (c: 3, d: 4) let tostr(..args) = repr(args) - test(tostr(a: 1, ..more, b: 2), "(a: 1, c: 3, d: 4, b: 2)") + test(tostr(a: 1, ..more, b: 2), "arguments(a: 1, c: 3, d: 4, b: 2)") } --- call-args-spread-none --- diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ index 95214db76..49b66ee56 100644 --- a/tests/suite/scripting/import.typ +++ b/tests/suite/scripting/import.typ @@ -145,6 +145,34 @@ #test(module.item(1, 2), 3) #test(module.push(2), 3) +--- import-from-file-bare-invalid --- +// Error: 9-33 module name would not be a valid identifier +// Hint: 9-33 you can rename the import with `as` +#import "modules/with space.typ" + +--- import-from-file-bare-dynamic --- +// Error: 9-26 dynamic import requires an explicit name +// Hint: 9-26 you can name the import with `as` +#import "mod" + "ule.typ" + +--- import-from-var-bare --- +#let p = "module.typ" +// Error: 9-10 dynamic import requires an explicit name +// Hint: 9-10 you can name the import with `as` +#import p +#test(p.b, 1) + +--- import-from-dict-field-bare --- +#let d = (p: "module.typ") +// Error: 9-12 dynamic import requires an explicit name +// Hint: 9-12 you can name the import with `as` +#import d.p +#test(p.b, 1) + +--- import-from-file-renamed-dynamic --- +#import "mod" + "ule.typ" as mod +#test(mod.b, 1) + --- import-from-file-renamed --- // A renamed module import without items. #import "module.typ" as other @@ -160,6 +188,10 @@ #test(item(1, 2), 3) #test(newname.item(1, 2), 3) +--- import-from-function-scope-bare --- +// Warning: 9-13 this import has no effect +#import enum + --- import-from-function-scope-renamed --- // Renamed module import with function scopes. #import enum as othernum @@ -171,6 +203,23 @@ #import asrt: ne as asne #asne(1, 2) +--- import-from-module-bare --- +#import "modules/chap1.typ" as mymod +// Warning: 9-14 this import has no effect +#import mymod +// The name `chap1` is not bound. +// Error: 2-7 unknown variable: chap1 +#chap1 + +--- import-module-nested --- +#import std.calc: pi +#test(pi, calc.pi) + +--- import-module-nested-bare --- +#import "module.typ" +#import module.chap2 +#test(chap2.name, "Peter") + --- import-module-item-name-mutating --- // Edge case for module access that isn't fixed. #import "module.typ" @@ -206,6 +255,10 @@ // Warning: 17-21 unnecessary import rename to same name #import enum as enum +--- import-rename-necessary --- +#import "module.typ" as module: a +#test(module.a, a) + --- import-rename-unnecessary-mixed --- // Warning: 17-21 unnecessary import rename to same name #import enum as enum: item @@ -215,8 +268,8 @@ #import enum as enum: item as item --- import-item-rename-unnecessary-but-ok --- -// No warning on a case that isn't obviously pathological -#import "module.typ" as module +#import "modul" + "e.typ" as module +#test(module.b, 1) --- import-from-closure-invalid --- // Can't import from closures. @@ -359,6 +412,15 @@ This is never reached. #import "@test/adder:0.1.0" #test(adder.add(2, 8), 10) +--- import-from-package-dynamic --- +// Error: 9-33 dynamic import requires an explicit name +// Hint: 9-33 you can name the import with `as` +#import "@test/" + "adder:0.1.0" + +--- import-from-package-renamed-dynamic --- +#import "@test/" + "adder:0.1.0" as adder +#test(adder.add(2, 8), 10) + --- import-from-package-items --- // Test import with items. #import "@test/adder:0.1.0": add diff --git a/tests/suite/scripting/methods.typ b/tests/suite/scripting/methods.typ index 5deea2cfa..566e9d9a5 100644 --- a/tests/suite/scripting/methods.typ +++ b/tests/suite/scripting/methods.typ @@ -31,7 +31,7 @@ #numbers.fun() --- method-unknown-but-field-exists --- -// Error: 2:4-2:10 type content has no method `stroke` +// Error: 2:4-2:10 element line has no method `stroke` // Hint: 2:4-2:10 did you mean to access the field `stroke`? #let l = line(stroke: red) #l.stroke() diff --git a/tests/suite/scripting/modules/with space.typ b/tests/suite/scripting/modules/with space.typ new file mode 100644 index 000000000..9138f3c3f --- /dev/null +++ b/tests/suite/scripting/modules/with space.typ @@ -0,0 +1 @@ +// SKIP diff --git a/tests/suite/scripting/params.typ b/tests/suite/scripting/params.typ index 688124f20..0f14fc3ee 100644 --- a/tests/suite/scripting/params.typ +++ b/tests/suite/scripting/params.typ @@ -29,17 +29,17 @@ // Spread at beginning. #{ let f(..a, b) = (a, b) - test(repr(f(1)), "((), 1)") - test(repr(f(1, 2, 3)), "((1, 2), 3)") - test(repr(f(1, 2, 3, 4, 5)), "((1, 2, 3, 4), 5)") + test(repr(f(1)), "(arguments(), 1)") + test(repr(f(1, 2, 3)), "(arguments(1, 2), 3)") + test(repr(f(1, 2, 3, 4, 5)), "(arguments(1, 2, 3, 4), 5)") } --- params-sink-in-middle --- // Spread in the middle. #{ let f(a, ..b, c) = (a, b, c) - test(repr(f(1, 2)), "(1, (), 2)") - test(repr(f(1, 2, 3, 4, 5)), "(1, (2, 3, 4), 5)") + test(repr(f(1, 2)), "(1, arguments(), 2)") + test(repr(f(1, 2, 3, 4, 5)), "(1, arguments(2, 3, 4), 5)") } --- params-sink-unnamed-empty --- diff --git a/tests/suite/scripting/recursion.typ b/tests/suite/scripting/recursion.typ index 6be96c1ec..e92b67fb7 100644 --- a/tests/suite/scripting/recursion.typ +++ b/tests/suite/scripting/recursion.typ @@ -44,18 +44,21 @@ --- recursion-via-include-in-layout --- // Test cyclic imports during layout. // Error: 2-38 maximum show rule depth exceeded -// Hint: 2-38 check whether the show rule matches its own output +// Hint: 2-38 maybe a show rule matches its own output +// Hint: 2-38 maybe there are too deeply nested elements #layout(_ => include "recursion.typ") --- recursion-show-math --- // Test recursive show rules. // Error: 22-25 maximum show rule depth exceeded -// Hint: 22-25 check whether the show rule matches its own output +// Hint: 22-25 maybe a show rule matches its own output +// Hint: 22-25 maybe there are too deeply nested elements #show math.equation: $x$ $ x $ --- recursion-show-math-realize --- // Error: 22-33 maximum show rule depth exceeded -// Hint: 22-33 check whether the show rule matches its own output +// Hint: 22-33 maybe a show rule matches its own output +// Hint: 22-33 maybe there are too deeply nested elements #show heading: it => heading[it] $ #heading[hi] $ diff --git a/tests/suite/symbols/symbol.typ b/tests/suite/symbols/symbol.typ index 4c64700ac..6d2513c1f 100644 --- a/tests/suite/symbols/symbol.typ +++ b/tests/suite/symbols/symbol.typ @@ -147,3 +147,7 @@ repr(envelope.fly), `symbol("🖅")`.text, ) + +--- symbol-sect-deprecated --- +// Warning: 5-9 `sect` is deprecated, use `inter` instead +$ A sect B = A inter B $ diff --git a/tests/suite/syntax/shebang.typ b/tests/suite/syntax/shebang.typ new file mode 100644 index 000000000..c2eb2e43c --- /dev/null +++ b/tests/suite/syntax/shebang.typ @@ -0,0 +1,7 @@ +// Test shebang support. + +--- shebang --- +#!typst compile + +// Error: 2-3 the character `!` is not valid in code +#!not-a-shebang diff --git a/tests/suite/text/case.typ b/tests/suite/text/case.typ index 2bf68bc33..964ff28b6 100644 --- a/tests/suite/text/case.typ +++ b/tests/suite/text/case.typ @@ -6,6 +6,14 @@ #test(upper(memes), "ARE MEMES GREAT?") #test(upper("Ελλάδα"), "ΕΛΛΆΔΑ") +--- cases-content-text --- +// Check that cases are applied to text nested in content +#lower(box("HI!")) + +--- cases-content-symbol --- +// Check that cases are applied to symbols nested in content +#lower($H I !$.body) + --- upper-bad-type --- // Error: 8-9 expected string or content, found integer #upper(1) diff --git a/tests/suite/text/font.typ b/tests/suite/text/font.typ index 9e5c0150d..60a1cd94d 100644 --- a/tests/suite/text/font.typ +++ b/tests/suite/text/font.typ @@ -77,11 +77,6 @@ I #let var = text(font: ("list-of", "nonexistent-fonts"))[don't] #var ---- text-font-linux-libertine --- -// Warning: 17-34 Typst's default font has changed from Linux Libertine to its successor Libertinus Serif -// Hint: 17-34 please set the font to `"Libertinus Serif"` instead -#set text(font: "Linux Libertine") - --- issue-5499-text-fill-in-clip-block --- #let t = tiling( diff --git a/tests/suite/text/raw.typ b/tests/suite/text/raw.typ index 1ba216302..a7f58a8d0 100644 --- a/tests/suite/text/raw.typ +++ b/tests/suite/text/raw.typ @@ -676,6 +676,17 @@ a b c -------------------- `code` ``` +--- issue-5760-disable-cjk-latin-spacing-in-raw --- + +```typ +#let hi = "你好world" +``` + +#show raw: set text(cjk-latin-spacing: auto) +```typ +#let hi = "你好world" +``` + --- raw-theme-set-to-auto --- ```typ #let hi = "Hello World" diff --git a/tests/suite/text/smallcaps.typ b/tests/suite/text/smallcaps.typ index 6f36a028b..981e41c4c 100644 --- a/tests/suite/text/smallcaps.typ +++ b/tests/suite/text/smallcaps.typ @@ -10,3 +10,7 @@ #show smallcaps: set text(fill: red) #smallcaps[Smallcaps] + +--- smallcaps-all --- +#smallcaps(all: false)[Test 012] \ +#smallcaps(all: true)[Test 012] diff --git a/tests/suite/visualize/curve.typ b/tests/suite/visualize/curve.typ index f98f634a7..14a1c0cc8 100644 --- a/tests/suite/visualize/curve.typ +++ b/tests/suite/visualize/curve.typ @@ -38,6 +38,16 @@ curve.close(mode: "smooth"), ) +--- curve-multiple-non-closed --- +#curve( + stroke: 2pt, + curve.line((20pt, 0pt)), + curve.move((0pt, 10pt)), + curve.line((20pt, 10pt)), + curve.move((0pt, 20pt)), + curve.line((20pt, 20pt)), +) + --- curve-line --- #curve( fill: purple, diff --git a/tests/suite/visualize/gradient.typ b/tests/suite/visualize/gradient.typ index b221f411d..811b8b605 100644 --- a/tests/suite/visualize/gradient.typ +++ b/tests/suite/visualize/gradient.typ @@ -658,3 +658,11 @@ $ A = mat( height: 10pt, fill: gradient.linear(violet, blue, space: cmyk) ) + +--- issue-5819-gradient-repeat --- +// Ensure the gradient constructor generates monotonic stops which can be fed +// back into the gradient constructor itself. +#let my-gradient = gradient.linear(red, blue).repeat(5) +#let _ = gradient.linear(..my-gradient.stops()) +#let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true) +#let _ = gradient.linear(..my-gradient2.stops()) diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 846650c68..7ce0c8c0a 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -65,6 +65,93 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B caption: [Bilingual text] ) +--- image-svg-auto-detection --- +#image(bytes( + ``` + + + + + + ```.text +)) + +--- image-pixmap-rgb8 --- +#image( + bytes(( + 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80, + )), + format: ( + encoding: "rgb8", + width: 3, + height: 3, + ), + width: 1cm, +) + +--- image-pixmap-rgba8 --- +#image( + bytes(( + 0xFF, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, + 0xFF, 0x00, 0x00, 0x80, 0x00, 0xFF, 0x00, 0x80, 0x00, 0x00, 0xFF, 0x80, + 0xFF, 0x00, 0x00, 0x10, 0x00, 0xFF, 0x00, 0x10, 0x00, 0x00, 0xFF, 0x10, + )), + format: ( + encoding: "rgba8", + width: 3, + height: 3, + ), + width: 1cm, +) + +--- image-pixmap-luma8 --- +#image( + bytes(range(16).map(x => x * 16)), + format: ( + encoding: "luma8", + width: 4, + height: 4, + ), + width: 1cm, +) + +--- image-pixmap-lumaa8 --- +#image( + bytes(range(16).map(x => (0x80, x * 16)).flatten()), + format: ( + encoding: "lumaa8", + width: 4, + height: 4, + ), + width: 1cm, +) + +--- image-scaling-methods --- +#let img(scaling) = image( + bytes(( + 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80, + )), + format: ( + encoding: "rgb8", + width: 3, + height: 3, + ), + width: 1cm, + scaling: scaling, +) + +#stack( + dir: ltr, + spacing: 4pt, + img(auto), + img("smooth"), + img("pixelated"), +) + --- image-natural-dpi-sizing --- // Test that images aren't upscaled. // Image is just 48x80 at 220dpi. It should not be scaled to fit the page @@ -76,8 +163,8 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B #image("path/does/not/exist") --- image-bad-format --- -// Error: 2-22 unknown image format -#image("./image.typ") +// Error: 2-37 unknown image format +#image("/assets/plugins/hello.wasm") --- image-bad-svg --- // Error: 2-33 failed to parse SVG (found closing tag 'g' instead of 'style' in line 4) @@ -85,24 +172,81 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B --- image-decode-svg --- // Test parsing from svg data +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") --- image-decode-bad-svg --- // Error: 2-168 failed to parse SVG (missing root node) +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(``.text, format: "svg") --- image-decode-detect-format --- // Test format auto detect +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), width: 80%) --- image-decode-specify-format --- // Test format manual +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "jpg", width: 80%) --- image-decode-specify-wrong-format --- // Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.) +// Warning: 8-14 `image.decode` is deprecated, directly pass bytes to `image` instead #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%) +--- image-pixmap-empty --- +// Error: 1:2-8:2 zero-sized images are not allowed +#image( + bytes(()), + format: ( + encoding: "rgb8", + width: 0, + height: 0, + ), +) + +--- image-pixmap-invalid-size --- +// Error: 1:2-8:2 pixel dimensions and pixel data do not match +#image( + bytes((0x00, 0x00, 0x00)), + format: ( + encoding: "rgb8", + width: 16, + height: 16, + ), +) + +--- image-pixmap-unknown-attribute --- +#image( + bytes((0x00, 0x00, 0x00)), + // Error: 1:11-6:4 unexpected key "stowaway", valid keys are "encoding", "width", and "height" + format: ( + encoding: "rgb8", + width: 1, + height: 1, + stowaway: "I do work here, promise", + ), +) + +--- image-pixmap-but-png-format --- +#image( + bytes((0x00, 0x00, 0x00)), + // Error: 1:11-5:4 expected "rgb8", "rgba8", "luma8", or "lumaa8" + format: ( + encoding: "png", + width: 1, + height: 1, + ), +) + +--- image-png-but-pixmap-format --- +#image( + read("/assets/images/tiger.jpg", encoding: none), + // Error: 11-18 expected "png", "jpg", "gif", dictionary, "svg", or auto + format: "rgba8", +) + --- issue-870-image-rotation --- // Ensure that EXIF rotation is applied. // https://github.com/image-rs/image/issues/1045 diff --git a/tests/suite/visualize/path.typ b/tests/suite/visualize/path.typ index 55c0f5340..e44b2270e 100644 --- a/tests/suite/visualize/path.typ +++ b/tests/suite/visualize/path.typ @@ -6,6 +6,7 @@ columns: (1fr, 1fr), rows: (1fr, 1fr, 1fr), align: center + horizon, + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, closed: true, @@ -14,6 +15,7 @@ ((0%, 50%), (4%, 4%)), ((50%, 0%), (4%, 4%)), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: purple, stroke: 1pt, @@ -22,6 +24,7 @@ (0pt, 30pt), (30pt, 0pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: blue, stroke: 1pt, @@ -30,6 +33,7 @@ ((30%, 60%), (-20%, 0%), (0%, 0%)), ((50%, 30%), (60%, -30%), (60%, 0%)), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( stroke: 5pt, closed: true, @@ -37,6 +41,7 @@ (30pt, 30pt), (15pt, 0pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, fill-rule: "non-zero", @@ -47,6 +52,7 @@ (0pt, 20pt), (40pt, 50pt), ), + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( fill: red, fill-rule: "even-odd", @@ -61,18 +67,22 @@ --- path-bad-vertex --- // Error: 7-9 path vertex must have 1, 2, or 3 points +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(()) --- path-bad-point-count --- // Error: 7-47 path vertex must have 1, 2, or 3 points +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(((0%, 0%), (0%, 0%), (0%, 0%), (0%, 0%))) --- path-bad-point-array --- // Error: 7-31 point array must contain exactly two entries +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path(((0%, 0%), (0%, 0%, 0%))) --- path-infinite-length --- // Error: 2-42 cannot create path with infinite length +// Warning: 2-6 the `path` function is deprecated, use `curve` instead #path((0pt, 0pt), (float.inf * 1pt, 0pt)) --- issue-path-in-sized-container --- @@ -82,6 +92,7 @@ fill: aqua, width: 20pt, height: 15pt, + // Warning: 3-7 the `path` function is deprecated, use `curve` instead path( (0pt, 0pt), (10pt, 10pt), diff --git a/tests/suite/visualize/tiling.typ b/tests/suite/visualize/tiling.typ index 5e61aa43a..904133411 100644 --- a/tests/suite/visualize/tiling.typ +++ b/tests/suite/visualize/tiling.typ @@ -159,5 +159,7 @@ --- tiling-pattern-compatibility --- #set page(width: auto, height: auto, margin: 0pt) + +// Warning: 10-17 the name `pattern` is deprecated, use `tiling` instead #let t = pattern(size: (10pt, 10pt), line(stroke: 4pt, start: (0%, 0%), end: (100%, 100%))) #rect(width: 50pt, height: 50pt, fill: t) diff --git a/tools/test-helper/package.json b/tools/test-helper/package.json index d34213fb0..08a60fa31 100644 --- a/tools/test-helper/package.json +++ b/tools/test-helper/package.json @@ -1,104 +1,107 @@ { - "name": "typst-test-helper", - "publisher": "typst", - "displayName": "Typst Test Helper", - "description": "Helps to run, compare and update Typst tests.", - "version": "0.0.1", - "categories": [ - "Other" - ], - "activationEvents": [ - "workspaceContains:tests/suite/playground.typ" - ], - "main": "./dist/extension.js", - "contributes": { - "commands": [ - { - "command": "typst-test-helper.refreshFromPreview", - "title": "Refresh preview", - "category": "Typst Test Helper", - "icon": "$(refresh)" - }, - { - "command": "typst-test-helper.runFromPreview", - "title": "Run test", - "category": "Typst Test Helper", - "icon": "$(debug-start)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.saveFromPreview", - "title": "Run and save reference output", - "category": "Typst Test Helper", - "icon": "$(save)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.copyImageFilePathFromPreviewContext", - "title": "Copy image file path", - "category": "Typst Test Helper" - }, - { - "command": "typst-test-helper.increaseResolution", - "title": "Render at higher resolution", - "category": "Typst Test Helper", - "icon": "$(zoom-in)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.decreaseResolution", - "title": "Render at lower resolution", - "category": "Typst Test Helper", - "icon": "$(zoom-out)", - "enablement": "typst-test-helper.runButtonEnabled" - } - ], - "menus": { - "editor/title": [ - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.refreshFromPreview", - "group": "navigation@1" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.runFromPreview", - "group": "navigation@2" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.saveFromPreview", - "group": "navigation@3" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.increaseResolution", - "group": "navigation@4" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.decreaseResolution", - "group": "navigation@4" - } - ], - "webview/context": [ - { - "command": "typst-test-helper.copyImageFilePathFromPreviewContext", - "when": "webviewId == typst-test-helper.preview && (webviewSection == png || webviewSection == ref)" - } - ] - } - }, - "scripts": { - "build": "tsc -p ./", - "watch": "tsc -watch -p ./" - }, - "devDependencies": { - "@types/node": "18.x", - "@types/vscode": "^1.88.0", - "typescript": "^5.3.3" - }, - "engines": { - "vscode": "^1.88.0" - } -} + "name": "typst-test-helper", + "publisher": "typst", + "displayName": "Typst Test Helper", + "description": "Helps to run, compare and update Typst tests.", + "version": "0.0.1", + "categories": [ + "Other" + ], + "activationEvents": [ + "workspaceContains:tests/suite/playground.typ" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "typst-test-helper.refreshFromPreview", + "title": "Refresh preview", + "category": "Typst Test Helper", + "icon": "$(refresh)" + }, + { + "command": "typst-test-helper.runFromPreview", + "title": "Run test", + "category": "Typst Test Helper", + "icon": "$(debug-start)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.saveFromPreview", + "title": "Run and save reference output", + "category": "Typst Test Helper", + "icon": "$(save)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.copyImageFilePathFromPreviewContext", + "title": "Copy image file path", + "category": "Typst Test Helper" + }, + { + "command": "typst-test-helper.increaseResolution", + "title": "Render at higher resolution", + "category": "Typst Test Helper", + "icon": "$(zoom-in)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.decreaseResolution", + "title": "Render at lower resolution", + "category": "Typst Test Helper", + "icon": "$(zoom-out)", + "enablement": "typst-test-helper.runButtonEnabled" + } + ], + "menus": { + "editor/title": [ + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.refreshFromPreview", + "group": "navigation@1" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.runFromPreview", + "group": "navigation@2" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.saveFromPreview", + "group": "navigation@3" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.increaseResolution", + "group": "navigation@4" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.decreaseResolution", + "group": "navigation@4" + } + ], + "webview/context": [ + { + "command": "typst-test-helper.copyImageFilePathFromPreviewContext", + "when": "webviewId == typst-test-helper.preview && (webviewSection == png || webviewSection == ref)" + } + ] + } + }, + "scripts": { + "build": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "18.x", + "@types/vscode": "^1.88.0", + "typescript": "^5.3.3" + }, + "engines": { + "vscode": "^1.88.0" + }, + "__metadata": { + "size": 35098973 + } +} \ No newline at end of file diff --git a/tools/test-helper/src/extension.ts b/tools/test-helper/src/extension.ts index 2e2b7d218..b98b4bad4 100644 --- a/tools/test-helper/src/extension.ts +++ b/tools/test-helper/src/extension.ts @@ -474,7 +474,7 @@ function getWebviewContent( data-vscode-context='{"webviewSection":"png"}' src="${webViewSrcs.png}" alt="Placeholder" - /> + >
@@ -484,7 +484,7 @@ function getWebviewContent( data-vscode-context='{"webviewSection":"ref"}' src="${webViewSrcs.ref}" alt="Placeholder" - /> + >
${stdoutHtml}