diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7c6214ff..3dca52497 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.74.0 + - uses: dtolnay/rust-toolchain@1.77.0 - uses: Swatinem/rust-cache@v2 - run: cargo check --workspace @@ -64,7 +64,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2023-09-13 + toolchain: nightly-2024-06-01 - uses: Swatinem/rust-cache@v2 - run: cargo install --locked cargo-fuzz@0.12.0 - run: cd tests/fuzz && cargo fuzz build --dev diff --git a/Cargo.lock b/Cargo.lock index 3e99ea814..a92c0d228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,10 +9,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] -name = "aho-corasick" -version = "1.1.2" +name = "ahash" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -34,47 +46,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -94,6 +107,9 @@ name = "arbitrary" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arrayref" @@ -109,9 +125,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "az" @@ -121,15 +137,9 @@ checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" [[package]] name = "base64" -version = "0.21.7" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "biblatex" @@ -176,9 +186,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -197,15 +207,21 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.14.3" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -215,12 +231,13 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -249,14 +266,14 @@ checksum = "7588475145507237ded760e52bf2f1085495245502033756d28ea72ade0e498b" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -292,15 +309,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d259fe9fd78ffa05a119581d20fddb50bfba428311057b12741ffb9015123d0b" dependencies = [ - "quick-xml", + "quick-xml 0.31.0", "serde", ] [[package]] name = "clap" -version = "4.5.2" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" dependencies = [ "clap_builder", "clap_derive", @@ -308,9 +325,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" dependencies = [ "anstream", "anstyle", @@ -320,18 +337,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.1" +version = "4.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" +checksum = "1d598e88f6874d4b888ed40c71efbcbf4076f1dfbae128a08a8c9e45f710605d" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck", "proc-macro2", @@ -341,15 +358,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "clap_mangen" -version = "0.2.20" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1dd95b5ebb5c1c54581dd6346f3ed6a79a3eef95dd372fc2ac13d535535300e" +checksum = "f50dde5bc0c853d6248de457e5eb6e5a674a54b93810a34ded88d882ca1fe2de" dependencies = [ "clap", "roff", @@ -379,9 +396,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "comemo" @@ -433,18 +450,18 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] @@ -470,9 +487,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -516,6 +533,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dirs" version = "5.0.1" @@ -539,9 +567,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", @@ -550,24 +578,24 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "ecow" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba31a30727c42ff5e60468d695c7f21e43a6db2808b7195adcab908fbd9f794" +checksum = "54bfbb1708988623190a6c4dbedaeaf0f53c20c6395abd6a01feb327b3146f4b" dependencies = [ "serde", ] [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "embedded-io" @@ -613,9 +641,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -648,9 +676,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" @@ -669,15 +697,15 @@ checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "windows-sys 0.52.0", ] [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -774,9 +802,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -795,9 +823,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -811,9 +839,12 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] [[package]] name = "hayagriva" @@ -824,11 +855,11 @@ dependencies = [ "biblatex", "ciborium", "citationberg", - "indexmap 2.2.5", + "indexmap 2.2.6", "numerals", "paste", "serde", - "serde_yaml 0.9.32", + "serde_yaml 0.9.34+deprecated", "thiserror", "unic-langid", "unicode-segmentation", @@ -838,9 +869,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hypher" @@ -873,9 +904,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "137d96353afc8544d437e8a99eceb10ab291352699573b0de5b08bda38c78c60" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", "serde", @@ -886,9 +917,9 @@ dependencies = [ [[package]] name = "icu_locid" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0aa2536adc14c07e2a521e95512b75ed8ef832f0fdf9299d4a0a45d2be2a9d" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", @@ -899,9 +930,9 @@ dependencies = [ [[package]] name = "icu_locid_transform" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c17d8f6524fdca4471101dd71f0a132eb6382b5d6d7f2970441cb25f6f435a" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" dependencies = [ "displaydoc", "icu_locid", @@ -913,15 +944,15 @@ dependencies = [ [[package]] name = "icu_locid_transform_data" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545c6c3e8bf9580e2dafee8de6f9ec14826aaf359787789c7724f1f85f47d3dc" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" [[package]] name = "icu_properties" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976e296217453af983efa25f287a4c1da04b9a63bf1ed63719455068e4453eb5" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", @@ -935,15 +966,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6a86c0e384532b06b6c104814f9c1b13bcd5b64409001c0d05713a1f3529d99" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" [[package]] name = "icu_provider" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba58e782287eb6950247abbf11719f83f5d4e4a5c1f2cd490d30a334bc47c2f4" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", "icu_locid", @@ -960,9 +991,9 @@ dependencies = [ [[package]] name = "icu_provider_adapters" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a229f978260da7c3aabb68cb7dc7316589936680570fe55e50fdd3f97711a4dd" +checksum = "d6324dfd08348a8e0374a447ebd334044d766b1839bb8d5ccf2482a99a77c0bc" dependencies = [ "icu_locid", "icu_locid_transform", @@ -973,9 +1004,9 @@ dependencies = [ [[package]] name = "icu_provider_blob" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a7202cddda672db167c6352719959e9b01cb1ca576d32fa79103f61b5a73601" +checksum = "c24b98d1365f55d78186c205817631a4acf08d7a45bdf5dc9dcf9c5d54dccf51" dependencies = [ "icu_provider", "postcard", @@ -987,9 +1018,9 @@ dependencies = [ [[package]] name = "icu_provider_macros" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2abdd3a62551e8337af119c5899e600ca0c88ec8f23a46c60ba216c803dcf1a" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", @@ -998,9 +1029,9 @@ dependencies = [ [[package]] name = "icu_segmenter" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2dc1e8f4ba33a6a4956770ac5c08570f255d6605519fb3a859a0c0a270a2f8f" +checksum = "a717725612346ffc2d7b42c94b820db6908048f39434504cb130e8b46256b0de" dependencies = [ "core_maths", "displaydoc", @@ -1015,9 +1046,9 @@ dependencies = [ [[package]] name = "icu_segmenter_data" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3673d6698dcffce08cfe8fc5da3c11c3f2c663d5d6137fd58ab2cbf44235ab46" +checksum = "f739ee737260d955e330bc83fdeaaf1631f7fb7ed218761d3c04bb13bb7d79df" [[package]] name = "idna" @@ -1035,21 +1066,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" -[[package]] -name = "image" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "gif", - "jpeg-decoder", - "num-traits", - "png", -] - [[package]] name = "image" version = "0.25.1" @@ -1084,12 +1100,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "rayon", "serde", ] @@ -1122,9 +1138,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -1149,16 +1165,22 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.10" +name = "is_terminal_polyfill" +version = "1.70.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -1217,12 +1239,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.155" @@ -1231,18 +1247,18 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libdeflate-sys" -version = "1.19.3" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9caa76c8cc6ee8c4efcf8f4514a812ebcad3aa7d3b548efe4d26da1203f177" +checksum = "669ea17f9257bcb48c09c7ee4bef3957777504acffac557263e20c11001977bc" dependencies = [ "cc", ] [[package]] name = "libdeflater" -version = "1.19.3" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "265a985bd31e5f22e2b2ac107cbed44c6ccf40ae236e46963cd00dd213e4bd03" +checksum = "8dfd6424f7010ee0a3416f1d796d0450e3ad3ac237a237644f728277c4ded016" dependencies = [ "libdeflate-sys", ] @@ -1266,22 +1282,12 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "libc", - "redox_syscall", -] - -[[package]] -name = "line-wrap" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", ] [[package]] @@ -1292,15 +1298,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lipsum" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5e9ef2d2ad6fe67a59ace27c203c8d3a71d195532ee82e3bbe0d5f9a9ca541" +checksum = "636860251af8963cc40f6b4baadee105f02e21b28131d76eba8e40ce84ab8064" dependencies = [ "rand", "rand_chacha", @@ -1308,28 +1314,34 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d642685b028806386b2b6e75685faadd3eb65a85fff7df711ce18446a422da" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" dependencies = [ "serde", ] [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] -name = "log" -version = "0.4.21" +name = "lockfree-object-pool" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lzma-sys" @@ -1344,9 +1356,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" @@ -1359,9 +1371,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", "simd-adler32", @@ -1379,6 +1391,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multi-stash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" + [[package]] name = "mutate_once" version = "0.1.1" @@ -1387,11 +1405,10 @@ checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -1409,7 +1426,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "crossbeam-channel", "filetime", "fsevent-sys", @@ -1424,11 +1441,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -1439,6 +1455,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1450,9 +1477,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] @@ -1471,9 +1498,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "open" -version = "5.1.1" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b3fbb0d52bf0cbb5225ba3d2c303aa136031d43abff98284332a9981ecddec" +checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37" dependencies = [ "is-wsl", "libc", @@ -1486,7 +1513,7 @@ version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "cfg-if", "foreign-types", "libc", @@ -1514,18 +1541,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.3+3.2.1" +version = "300.3.1+3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -1542,14 +1569,16 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "oxipng" -version = "9.0.0" +version = "9.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e5c341ef78a228e47a551bfd15ff885d8c501af49f953358763a538c01f14d" +checksum = "3f398c53eb34e0cf71d9e0bc676cfa7c611e3844dd14ab05e92fb7b423c98ecf" dependencies = [ "bitvec", + "clap", + "clap_mangen", "crossbeam-channel", "filetime", - "indexmap 2.2.5", + "indexmap 2.2.6", "libdeflater", "log", "rayon", @@ -1561,9 +1590,9 @@ dependencies = [ [[package]] name = "palette" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfc23a4b76642983d57e4ad00bb4504eb30a8ce3c70f4aee1f725610e36d97a" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" dependencies = [ "approx", "fast-srgb8", @@ -1573,10 +1602,11 @@ dependencies = [ [[package]] name = "palette_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8890702dbec0bad9116041ae586f84805b13eecd1d8b1df27c29998a9969d6d" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" dependencies = [ + "by_address", "proc-macro2", "quote", "syn", @@ -1584,9 +1614,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1594,22 +1624,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pathdiff" @@ -1623,7 +1653,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af6a7882fda7808481d43c51cadfc3ec934c6af72612a1fe6985ce329a2f0469" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "itoa", "memchr", "ryu", @@ -1700,14 +1730,13 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plist" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ - "base64 0.21.7", - "indexmap 2.2.5", - "line-wrap", - "quick-xml", + "base64", + "indexmap 2.2.6", + "quick-xml 0.32.0", "serde", "time", ] @@ -1756,9 +1785,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1778,7 +1807,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "getopts", "memchr", "unicase", @@ -1801,10 +1830,19 @@ dependencies = [ ] [[package]] -name = "quote" -version = "1.0.35" +name = "quick-xml" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1842,9 +1880,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1870,10 +1908,19 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.4.4" +name = "redox_syscall" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -1882,9 +1929,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -1894,9 +1941,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -1905,9 +1952,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "resvg" @@ -1927,9 +1974,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.37" +version = "0.8.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +checksum = "1aee83dc281d5a3200d37b299acd13b81066ea126a7f16f0eae70fc9aed241d9" dependencies = [ "bytemuck", ] @@ -1969,11 +2016,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -1982,17 +2029,17 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rustybuzz" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7730060ad401b0d1807c904ea56735288af101430aa0d2ab8358b789f5f37002" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "bytemuck", "smallvec", "ttf-parser", @@ -2004,15 +2051,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" - -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2040,11 +2081,11 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -2053,9 +2094,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2074,24 +2115,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", @@ -2100,9 +2141,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -2111,9 +2152,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -2132,11 +2173,11 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.32" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd075d994154d4a774f95b51fb96bdc2832b0ea48425c92546073816cda1f2f" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -2187,9 +2228,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "spin" @@ -2226,25 +2267,36 @@ dependencies = [ ] [[package]] -name = "strsim" -version = "0.11.0" +name = "string-interner" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "1c6a0d765f5807e98a091107bae0a56ea3799f66a5de47b2c84c94a39c09974e" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.1" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", @@ -2264,7 +2316,7 @@ version = "0.11.0" source = "git+https://github.com/typst/svg2pdf?rev=39f8ad3#39f8ad3b35e14cfcabf3d5d916899f7ac78790f7" dependencies = [ "fontdb", - "image 0.25.1", + "image", "log", "miniz_oxide", "once_cell", @@ -2290,9 +2342,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "2f0209b68b3613b093e0ec905354eccaedcfe83b8cb37cbdeae64026c3064c16" dependencies = [ "proc-macro2", "quote", @@ -2340,9 +2392,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -2356,7 +2408,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.1", + "fastrand 2.1.0", "rustix", "windows-sys 0.52.0", ] @@ -2378,18 +2430,18 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -2455,9 +2507,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "serde", @@ -2466,9 +2518,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -2481,9 +2533,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.8.10" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", @@ -2493,20 +2545,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.6" +version = "0.22.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", @@ -2521,9 +2573,9 @@ checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "two-face" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37bed2135b2459c7eefba72c906d374697eb15949c205f2f124e3636a46b5eeb" +checksum = "0ccd4843ea031c609fe9c16cae00e9657bad8a9f735a3cc2e420955d802b4268" dependencies = [ "once_cell", "serde", @@ -2541,7 +2593,7 @@ name = "typst" version = "0.11.0" dependencies = [ "az", - "bitflags 2.4.2", + "bitflags 2.6.0", "chinese-number", "ciborium", "comemo", @@ -2557,8 +2609,8 @@ dependencies = [ "icu_provider_blob", "icu_segmenter", "if_chain", - "image 0.24.9", - "indexmap 2.2.5", + "image", + "indexmap 2.2.6", "kamadak-exif", "kurbo", "lipsum", @@ -2575,7 +2627,7 @@ dependencies = [ "rustybuzz", "serde", "serde_json", - "serde_yaml 0.9.32", + "serde_yaml 0.9.34+deprecated", "siphasher 1.0.1", "smallvec", "stacker", @@ -2604,8 +2656,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13f85360328da54847dd7fefaf272dfa5b6d1fdeb53f32938924c39bf5b2c6c" +source = "git+https://github.com/typst/typst-assets?rev=4ee794c#4ee794cf8fb98eb67194e757c9820ab8562d853b" [[package]] name = "typst-cli" @@ -2636,7 +2687,7 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_yaml 0.9.32", + "serde_yaml 0.9.34+deprecated", "shell-escape", "tar", "tempfile", @@ -2656,7 +2707,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.11.0" -source = "git+https://github.com/typst/typst-dev-assets?rev=48a924d9de82b631bc775124a69384c8d860db04#48a924d9de82b631bc775124a69384c8d860db04" +source = "git+https://github.com/typst/typst-dev-assets?rev=48a924d#48a924d9de82b631bc775124a69384c8d860db04" [[package]] name = "typst-docs" @@ -2670,7 +2721,7 @@ dependencies = [ "pulldown-cmark", "serde", "serde_json", - "serde_yaml 0.9.32", + "serde_yaml 0.9.34+deprecated", "syntect", "typed-arena", "typst", @@ -2723,12 +2774,12 @@ dependencies = [ name = "typst-pdf" version = "0.11.0" dependencies = [ - "base64 0.22.0", + "base64", "bytemuck", "comemo", "ecow", - "image 0.24.9", - "indexmap 2.2.5", + "image", + "indexmap 2.2.6", "miniz_oxide", "once_cell", "pdf-writer", @@ -2750,7 +2801,7 @@ version = "0.11.0" dependencies = [ "bytemuck", "comemo", - "image 0.24.9", + "image", "pixglyph", "resvg", "roxmltree 0.20.0", @@ -2766,7 +2817,7 @@ dependencies = [ name = "typst-svg" version = "0.11.0" dependencies = [ - "base64 0.22.0", + "base64", "comemo", "ecow", "flate2", @@ -2785,6 +2836,7 @@ dependencies = [ "ecow", "once_cell", "serde", + "toml", "typst-utils", "unicode-ident", "unicode-math-class", @@ -2840,18 +2892,18 @@ dependencies = [ [[package]] name = "unic-langid" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238722e6d794ed130f91f4ea33e01fcff4f188d92337a21297892521c72df516" +checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" dependencies = [ "unic-langid-impl", ] [[package]] name = "unic-langid-impl" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd55a2063fdea4ef1f8633243a7b0524cbeef1905ae04c31a1c9b9775c55bc6" +checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" dependencies = [ "serde", "tinystr", @@ -2931,15 +2983,15 @@ checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unsafe-libyaml" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "unscanny" @@ -2949,11 +3001,11 @@ checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" [[package]] name = "ureq" -version = "2.9.6" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ - "base64 0.21.7", + "base64", "flate2", "log", "native-tls", @@ -2965,9 +3017,9 @@ dependencies = [ [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2981,7 +3033,7 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" dependencies = [ - "base64 0.22.0", + "base64", "data-url", "flate2", "fontdb", @@ -3010,9 +3062,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" @@ -3098,28 +3150,37 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasmi" -version = "0.31.2" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8281d1d660cdf54c76a3efa9ddd0c270cada1383a995db3ccb43d166456c7" +checksum = "dae81666d9dc76cb125fe16fa3e2f0fe08d15cd7b2ad9a5013cadf31826edee5" dependencies = [ + "arrayvec", + "multi-stash", + "num-derive", + "num-traits", "smallvec", "spin", - "wasmi_arena", + "wasmi_collections", "wasmi_core", "wasmparser-nostd", ] [[package]] -name = "wasmi_arena" -version = "0.4.1" +name = "wasmi_collections" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "104a7f73be44570cac297b3035d76b169d6599637631cf37a1703326a0727073" +checksum = "b422568fe52d13bc889033799e548c43032ee71836c6e9bb7261c1fc039327b9" +dependencies = [ + "ahash", + "hashbrown 0.14.5", + "string-interner", +] [[package]] name = "wasmi_core" -version = "0.13.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf1a7db34bff95b85c261002720c00c3a6168256dcb93041d3fa2054d19856a" +checksum = "5fa2efecce566705221bc05f518b124ceb7d94b78f4e9035cb0572d9f369982e" dependencies = [ "downcast-rs", "libm", @@ -3129,9 +3190,9 @@ dependencies = [ [[package]] name = "wasmparser-nostd" -version = "0.100.1" +version = "0.100.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157cab83003221bfd385833ab587a039f5d6fa7304854042ba358a3b09e0724" +checksum = "d5a015fe95f3504a94bb1462c717aae75253e39b9dd6c3fb1062c934535c64aa" dependencies = [ "indexmap-nostd", ] @@ -3160,11 +3221,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3179,7 +3240,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -3197,7 +3258,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -3217,17 +3278,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "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", ] [[package]] @@ -3238,9 +3300,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -3250,9 +3312,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -3262,9 +3324,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -3274,9 +3342,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -3286,9 +3354,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -3298,9 +3366,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -3310,24 +3378,24 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] [[package]] name = "writeable" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad7bb64b8ef9c0aa27b6da38b452b0ee9fd82beaf276a87dd796fb55cbae14e" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wyz" @@ -3397,9 +3465,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e71b2e4f287f467794c671e2b8f8a5f3716b3c829079a1c44740148eff07e4" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" dependencies = [ "serde", "stable_deref_trait", @@ -3409,9 +3477,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6936f0cce458098a201c245a11bef556c6a0181129c7034d10d76d1ec3a2b8" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", @@ -3420,19 +3488,39 @@ dependencies = [ ] [[package]] -name = "zerofrom" -version = "0.1.3" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655b0814c5c0b19ade497851070c640773304939a6c0fd5f5fb43da0696d05b7" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6a647510471d372f2e6c2e6b7219e44d8c574d24fdc11c610a61455782f18c3" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", @@ -3442,9 +3530,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0594125a0574fb93059c92c588ab209cc036a23d1baeb3410fa9181bea551a0" +checksum = "fb594dd55d87335c5f60177cee24f19457a5ec10a065e0a3014722ad252d0a1f" dependencies = [ "displaydoc", "litemap", @@ -3454,9 +3542,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.10.1" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff4439ae91fb5c72b8abc12f3f2dbf51bd27e6eadb9f8a5bc8898dddb0e27ea" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "serde", "yoke", @@ -3466,9 +3554,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4e5997cbf58990550ef1f0e5124a05e47e1ebd33a84af25739be6031a62c20" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", @@ -3477,26 +3565,33 @@ dependencies = [ [[package]] name = "zip" -version = "0.6.6" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" dependencies = [ - "byteorder", + "arbitrary", "crc32fast", "crossbeam-utils", + "displaydoc", "flate2", + "indexmap 2.2.6", + "memchr", + "thiserror", + "zopfli", ] [[package]] name = "zopfli" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1f48f3508a3a3f2faee01629564400bc12260f6214a056d06a3aaaa6ef0736" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" dependencies = [ + "bumpalo", "crc32fast", + "lockfree-object-pool", "log", + "once_cell", "simd-adler32", - "typed-arena", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ee50b6667..e26f058ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] version = "0.11.0" -rust-version = "1.74" # also change in ci.yml +rust-version = "1.77" # also change in ci.yml authors = ["The Typst Project Developers"] edition = "2021" homepage = "https://typst.app" @@ -26,8 +26,8 @@ typst-svg = { path = "crates/typst-svg", version = "0.11.0" } typst-syntax = { path = "crates/typst-syntax", version = "0.11.0" } typst-timing = { path = "crates/typst-timing", version = "0.11.0" } typst-utils = { path = "crates/typst-utils", version = "0.11.0" } -typst-assets = "0.11.0" -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "48a924d9de82b631bc775124a69384c8d860db04" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "4ee794c" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "48a924d" } az = "1.2" base64 = "0.22" bitflags = { version = "2", features = ["serde"] } @@ -49,7 +49,7 @@ flate2 = "1" fontdb = { version = "0.18", default-features = false } fs_extra = "1.3" hayagriva = "0.5.3" -heck = "0.4" +heck = "0.5" hypher = "0.1.4" icu_properties = { version = "1.4", features = ["serde"] } icu_provider = { version = "1.4", features = ["sync"] } @@ -57,7 +57,7 @@ icu_provider_adapters = "1.4" icu_provider_blob = "1.4" icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" -image = { version = "0.24", default-features = false, features = ["png", "jpeg", "gif"] } +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.5" kurbo = "0.11" @@ -109,7 +109,7 @@ time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] } tiny-skia = "0.11" toml = { version = "0.8", default-features = false, features = ["parse", "display"] } ttf-parser = "0.21.0" -two-face = { version = "0.3.0", default-features = false, features = ["syntect-fancy"] } +two-face = { version = "0.4.0", default-features = false, features = ["syntect-fancy"] } typed-arena = "2" unicode-bidi = "0.3.13" unicode-ident = "1.0" @@ -121,13 +121,13 @@ unscanny = "0.1" ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } usvg = { version = "0.42", default-features = false, features = ["text"] } walkdir = "2" -wasmi = "0.31.0" +wasmi = "0.34.0" xmlparser = "0.13.5" xmlwriter = "0.1.0" xmp-writer = "0.2" -xz2 = "0.1" +xz2 = { version = "0.1", features = ["static"] } yaml-front-matter = "0.1" -zip = { version = "0.6", default-features = false, features = ["deflate"] } +zip = { version = "2", default-features = false, features = ["deflate"] } [profile.dev.package."*"] opt-level = 2 diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 9f90f430d..7ada123cb 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -85,3 +85,13 @@ vendor-openssl = ["openssl/vendored"] [lints] workspace = true + +# The following metadata is used by `cargo-binstall`, and should be synchronized +# with `.github/workflows/release.yml`. +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/typst-{ target }{ archive-suffix }" +bin-dir = "typst-{ target }/typst{ binary-ext }" +pkg-fmt = "txz" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-fmt = "zip" diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 9648d8efa..09189de81 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use std::str::FromStr; use chrono::{DateTime, Utc}; -use clap::builder::ValueParser; +use clap::builder::{TypedValueParser, ValueParser}; use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum}; use semver::Version; @@ -77,7 +77,7 @@ pub struct CompileCommand { /// must be present if the source document renders to multiple pages. Use `{p}` for page /// numbers, `{0p}` for zero padded page numbers and `{t}` for page count. For example, /// `page-{0p}-of-{t}.png` creates `page-01-of-10.png`, `page-02-of-10.png` and so on. - #[clap(required_if_eq("input", "-"), value_parser = ValueParser::new(output_value_parser))] + #[clap(required_if_eq("input", "-"), value_parser = make_output_value_parser())] pub output: Option, /// Which pages to export. When unspecified, all document pages are exported. @@ -100,9 +100,11 @@ pub struct CompileCommand { #[arg(long = "format", short = 'f')] pub format: Option, - /// Opens the output file using the default viewer after compilation. - /// Ignored if output is stdout - #[arg(long = "open")] + /// Opens the output file with the default viewer or a specific program after + /// compilation + /// + /// Ignored if output is stdout. + #[arg(long = "open", value_name = "VIEWER")] pub open: Option>, /// The PPI (pixels per inch) to use for PNG export @@ -175,7 +177,7 @@ pub enum SerializationFormat { #[derive(Debug, Clone, Args)] pub struct SharedArgs { /// Path to input Typst file. Use `-` to read input from stdin - #[clap(value_parser = input_value_parser)] + #[clap(value_parser = make_input_value_parser())] pub input: Input, /// Configures the project root (for absolute paths) @@ -277,26 +279,30 @@ impl Display for Output { } /// The clap value parser used by `SharedArgs.input` -fn input_value_parser(value: &str) -> Result { - if value.is_empty() { - Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) - } else if value == "-" { - Ok(Input::Stdin) - } else { - Ok(Input::Path(value.into())) - } +fn make_input_value_parser() -> impl TypedValueParser { + clap::builder::OsStringValueParser::new().try_map(|value| { + if value.is_empty() { + Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) + } else if value == "-" { + Ok(Input::Stdin) + } else { + Ok(Input::Path(value.into())) + } + }) } /// The clap value parser used by `CompileCommand.output` -fn output_value_parser(value: &str) -> Result { - // Empty value also handled by clap for `Option` - if value.is_empty() { - Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) - } else if value == "-" { - Ok(Output::Stdout) - } else { - Ok(Output::Path(value.into())) - } +fn make_output_value_parser() -> impl TypedValueParser { + clap::builder::OsStringValueParser::new().try_map(|value| { + // Empty value also handled by clap for `Option` + if value.is_empty() { + Err(clap::Error::new(clap::error::ErrorKind::InvalidValue)) + } else if value == "-" { + Ok(Output::Stdout) + } else { + Ok(Output::Path(value.into())) + } + }) } /// Parses key/value pairs split by the first equal sign. diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index ae712a85e..d15264255 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -5,16 +5,16 @@ use std::path::{Path, PathBuf}; use chrono::{Datelike, Timelike}; use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::term; -use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use ecow::{eco_format, EcoString}; use parking_lot::RwLock; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use typst::diag::{bail, FileError, Severity, SourceDiagnostic, StrResult, Warned}; +use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned}; use typst::foundations::{Datetime, Smart}; use typst::layout::{Frame, PageRanges}; use typst::model::Document; use typst::syntax::{FileId, Source, Span}; use typst::visualize::Color; -use typst::{World, WorldExt}; +use typst::WorldExt; use crate::args::{ CompileCommand, DiagnosticFormat, Input, Output, OutputFormat, PageRangeArgument, @@ -96,21 +96,6 @@ pub fn compile_once( Status::Compiling.print(command).unwrap(); } - if let Err(errors) = world - .source(world.main()) - .map_err(|err| hint_invalid_main_file(err, &command.common.input)) - { - set_failed(); - if watching { - Status::Error.print(command).unwrap(); - } - - print_diagnostics(world, &errors, &[], command.common.diagnostic_format) - .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; - - return Ok(()); - } - let Warned { output, warnings } = typst::compile(world); match output { @@ -472,60 +457,30 @@ fn write_make_deps(world: &mut SystemWorld, command: &CompileCommand) -> StrResu /// Opens the given file using: /// - The default file viewer if `open` is `None`. /// - The given viewer provided by `open` if it is `Some`. +/// +/// If the file could not be opened, an error is returned. fn open_file(open: Option<&str>, path: &Path) -> StrResult<()> { + // Some resource openers require the path to be canonicalized. + let path = path + .canonicalize() + .map_err(|err| eco_format!("failed to canonicalize path ({err})"))?; if let Some(app) = open { - open::with_in_background(path, app); + open::with_detached(&path, app) + .map_err(|err| eco_format!("failed to open file with {} ({})", app, err)) } else { - open::that_in_background(path); + open::that_detached(&path).map_err(|err| { + let openers = open::commands(path) + .iter() + .map(|command| command.get_program().to_string_lossy()) + .collect::>() + .join(", "); + eco_format!( + "failed to open file with any of these resource openers: {} ({})", + openers, + err, + ) + }) } - - Ok(()) -} - -/// Adds useful hints when the main source file couldn't be read -/// and returns the final diagnostic. -fn hint_invalid_main_file( - file_error: FileError, - input: &Input, -) -> EcoVec { - let is_utf8_error = matches!(file_error, FileError::InvalidUtf8); - let mut diagnostic = - SourceDiagnostic::error(Span::detached(), EcoString::from(file_error)); - - // Attempt to provide helpful hints for UTF-8 errors. - // Perhaps the user mistyped the filename. - // For example, they could have written "file.pdf" instead of - // "file.typ". - if is_utf8_error { - if let Input::Path(path) = input { - let extension = path.extension(); - if extension.is_some_and(|extension| extension == "typ") { - // No hints if the file is already a .typ file. - // The file is indeed just invalid. - return eco_vec![diagnostic]; - } - - match extension { - Some(extension) => { - diagnostic.hint(eco_format!( - "a file with the `.{}` extension is not usually a Typst file", - extension.to_string_lossy() - )); - } - - None => { - diagnostic - .hint("a file without an extension is not usually a Typst file"); - } - }; - - if path.with_extension("typ").exists() { - diagnostic.hint("check if you meant to use the `.typ` extension instead"); - } - } - } - - eco_vec![diagnostic] } /// Print diagnostic messages to the terminal. diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 8e8b305f5..5a0814a86 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -192,8 +192,8 @@ impl World for SystemWorld { &self.book } - fn main(&self) -> Source { - self.source(self.main).unwrap() + fn main(&self) -> FileId { + self.main } fn source(&self, id: FileId) -> FileResult { diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index ecb73dcec..c37795562 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -49,7 +49,6 @@ pub fn analyze_expr( pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option { // Use span in the node for resolving imports with relative paths. let source_span = source.span(); - let (source, _) = analyze_expr(world, source).into_iter().next()?; if source.scope().is_some() { return Some(source); @@ -73,6 +72,7 @@ pub fn analyze_import(world: &dyn World, source: &LinkedNode) -> Option { Scopes::new(Some(world.library())), Span::detached(), ); + typst::eval::import(&mut vm, source, source_span, true) .ok() .map(Value::Module) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index cdcac956d..f6c96d001 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -17,8 +17,10 @@ use typst::visualize::Color; use typst::World; use unscanny::Scanner; -use crate::analyze::{analyze_expr, analyze_import, analyze_labels}; -use crate::{plain_docs_sentence, summarize_font_family}; +use crate::{ + analyze_expr, analyze_import, analyze_labels, named_items, plain_docs_sentence, + summarize_font_family, +}; /// Autocomplete a cursor position in a source file. /// @@ -334,6 +336,13 @@ fn math_completions(ctx: &mut CompletionContext) { /// Complete field accesses. fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { + // Used to determine whether trivia nodes are allowed before '.'. + // During an inline expression in markup mode trivia nodes exit the inline expression. + let in_markup: bool = matches!( + ctx.leaf.parent_kind(), + None | Some(SyntaxKind::Markup) | Some(SyntaxKind::Ref) + ); + // Behind an expression plus dot: "emoji.|". if_chain! { if ctx.leaf.kind() == SyntaxKind::Dot @@ -341,6 +350,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { && ctx.leaf.text() == "."); if ctx.leaf.range().end == ctx.cursor; if let Some(prev) = ctx.leaf.prev_sibling(); + if !in_markup || prev.range().end == ctx.leaf.range().start; if prev.is::(); if prev.parent_kind() != Some(SyntaxKind::Markup) || prev.prev_sibling_kind() == Some(SyntaxKind::Hash); @@ -376,12 +386,12 @@ fn field_access_completions( value: &Value, styles: &Option, ) { - for (name, value) in value.ty().scope().iter() { + for (name, value, _) in value.ty().scope().iter() { ctx.value_completion(Some(name.clone()), value, true, None); } if let Some(scope) = value.scope() { - for (name, value) in scope.iter() { + for (name, value, _) in scope.iter() { ctx.value_completion(Some(name.clone()), value, true, None); } } @@ -547,7 +557,7 @@ fn import_item_completions<'a>( ctx.snippet_completion("*", "*", "Import everything."); } - for (name, value) in scope.iter() { + for (name, value, _) in scope.iter() { if existing.iter().all(|item| item.original_name().as_str() != name) { ctx.value_completion(Some(name.clone()), value, false, None); } @@ -1319,62 +1329,12 @@ impl<'a> CompletionContext<'a> { /// Filters the global/math scope with the given filter. fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) { let mut defined = BTreeSet::new(); - - let mut ancestor = Some(self.leaf.clone()); - while let Some(node) = &ancestor { - let mut sibling = Some(node.clone()); - while let Some(node) = &sibling { - if let Some(v) = node.cast::() { - for ident in v.kind().bindings() { - defined.insert(ident.get().clone()); - } - } - - if let Some(v) = node.cast::() { - let imports = v.imports(); - match imports { - None | Some(ast::Imports::Wildcard) => { - if let Some(value) = node - .children() - .find(|child| child.is::()) - .and_then(|source| analyze_import(self.world, &source)) - { - if imports.is_none() { - defined.extend(value.name().map(Into::into)); - } else if let Some(scope) = value.scope() { - for (name, _) in scope.iter() { - defined.insert(name.clone()); - } - } - } - } - Some(ast::Imports::Items(items)) => { - for item in items.iter() { - defined.insert(item.bound_name().get().clone()); - } - } - } - } - - sibling = node.prev_sibling(); + named_items(self.world, self.leaf.clone(), |name| { + if name.value().as_ref().map_or(true, &filter) { + defined.insert(name.name().clone()); } - - if let Some(parent) = node.parent() { - if let Some(v) = parent.cast::() { - if node.prev_sibling_kind() != Some(SyntaxKind::In) { - let pattern = v.pattern(); - for ident in pattern.bindings() { - defined.insert(ident.get().clone()); - } - } - } - - ancestor = Some(parent.clone()); - continue; - } - - break; - } + None::<()> + }); let in_math = matches!( self.leaf.parent_kind(), @@ -1385,7 +1345,7 @@ impl<'a> CompletionContext<'a> { ); let scope = if in_math { self.math } else { self.global }; - for (name, value) in scope.iter() { + for (name, value, _) in scope.iter() { if filter(value) && !defined.contains(name) { self.value_completion(Some(name.clone()), value, parens, None); } @@ -1433,6 +1393,16 @@ mod tests { test("#().", 4, &["insert", "remove", "len", "all"], &["foo"]); } + #[test] + fn test_whitespace_in_autocomplete() { + //Check that extra space before '.' is handled correctly. + test("#() .", 5, &[], &["insert", "remove", "len", "all"]); + test("#{() .}", 6, &["insert", "remove", "len", "all"], &["foo"]); + + test("#() .a", 6, &[], &["insert", "remove", "len", "all"]); + test("#{() .a}", 7, &["at", "any", "all"], &["foo"]); + } + #[test] fn test_before_window_char_boundary() { // Check that the `before_window` doesn't slice into invalid byte diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs new file mode 100644 index 000000000..4323226d3 --- /dev/null +++ b/crates/typst-ide/src/definition.rs @@ -0,0 +1,262 @@ +use ecow::EcoString; +use typst::foundations::{Label, Module, Selector, Value}; +use typst::model::Document; +use typst::syntax::ast::AstNode; +use typst::syntax::{ast, LinkedNode, Side, Source, Span, SyntaxKind}; +use typst::World; + +use crate::{analyze_import, deref_target, named_items, DerefTarget, NamedItem}; + +/// Find the definition of the item under the cursor. +/// +/// Passing a `document` (from a previous compilation) is optional, but enhances +/// the definition search. Label definitions, for instance, are only generated +/// when the document is available. +pub fn definition( + world: &dyn World, + document: Option<&Document>, + source: &Source, + cursor: usize, + side: Side, +) -> Option { + let root = LinkedNode::new(source.root()); + let leaf = root.leaf_at(cursor, side)?; + + let mut use_site = match deref_target(leaf.clone())? { + DerefTarget::VarAccess(node) | DerefTarget::Callee(node) => node, + DerefTarget::IncludePath(path) | DerefTarget::ImportPath(path) => { + let import_item = + analyze_import(world, &path).and_then(|v| v.cast::().ok())?; + return Some(Definition::module(&import_item, path.span(), Span::detached())); + } + DerefTarget::Ref(r) => { + let label = Label::new(r.cast::()?.target()); + let sel = Selector::Label(label); + let elem = document?.introspector.query_first(&sel)?; + let span = elem.span(); + return Some(Definition { + kind: DefinitionKind::Label, + name: label.as_str().into(), + value: Some(Value::Label(label)), + span, + name_span: Span::detached(), + }); + } + DerefTarget::Label(..) | DerefTarget::Code(..) => { + return None; + } + }; + + let mut has_path = false; + while let Some(node) = use_site.cast::() { + has_path = true; + use_site = use_site.find(node.target().span())?; + } + + let name = use_site.cast::()?.get().clone(); + let src = named_items(world, use_site, |item: NamedItem| { + if *item.name() != name { + return None; + } + + match item { + NamedItem::Var(name) => { + let name_span = name.span(); + let span = find_let_binding(source, name_span); + Some(Definition::item(name.get().clone(), span, name_span, None)) + } + NamedItem::Fn(name) => { + let name_span = name.span(); + let span = find_let_binding(source, name_span); + Some( + Definition::item(name.get().clone(), span, name_span, None) + .with_kind(DefinitionKind::Function), + ) + } + NamedItem::Module(item, site) => Some(Definition::module( + item, + site.span(), + matches!(site.kind(), SyntaxKind::Ident) + .then_some(site.span()) + .unwrap_or_else(Span::detached), + )), + NamedItem::Import(name, name_span, value) => Some(Definition::item( + name.clone(), + Span::detached(), + name_span, + value.cloned(), + )), + } + }); + + let src = src.or_else(|| { + let in_math = matches!( + leaf.parent_kind(), + Some(SyntaxKind::Equation) + | Some(SyntaxKind::Math) + | Some(SyntaxKind::MathFrac) + | Some(SyntaxKind::MathAttach) + ); + + let library = world.library(); + let scope = if in_math { library.math.scope() } else { library.global.scope() }; + for (item_name, value, span) in scope.iter() { + if *item_name == name { + return Some(Definition::item( + name, + span, + Span::detached(), + Some(value.clone()), + )); + } + } + + None + })?; + + (!has_path).then_some(src) +} + +/// A definition of some item. +#[derive(Debug, Clone)] +pub struct Definition { + /// The name of the definition. + pub name: EcoString, + /// The kind of the definition. + pub kind: DefinitionKind, + /// An instance of the definition, if available. + pub value: Option, + /// The source span of the entire definition. May be detached if unknown. + pub span: Span, + /// The span of the definition's name. May be detached if unknown. + pub name_span: Span, +} + +impl Definition { + fn item(name: EcoString, span: Span, name_span: Span, value: Option) -> Self { + Self { + name, + kind: match value { + Some(Value::Func(_)) => DefinitionKind::Function, + _ => DefinitionKind::Variable, + }, + value, + span, + name_span, + } + } + + fn module(module: &Module, span: Span, name_span: Span) -> Self { + Definition { + name: module.name().clone(), + kind: DefinitionKind::Module, + value: Some(Value::Module(module.clone())), + span, + name_span, + } + } + + fn with_kind(self, kind: DefinitionKind) -> Self { + Self { kind, ..self } + } +} + +/// A kind of item that is definition. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum DefinitionKind { + /// ```plain + /// let foo; + /// ^^^^^^^^ span + /// ^^^ name_span + /// ``` + Variable, + /// ```plain + /// let foo(it) = it; + /// ^^^^^^^^^^^^^^^^^ span + /// ^^^ name_span + /// ``` + Function, + /// Case 1 + /// ```plain + /// import "foo.typ": * + /// ^^^^^^^^^ span + /// name_span is detached + /// ``` + /// + /// Case 2 + /// ```plain + /// import "foo.typ" as bar: * + /// span ^^^ + /// name_span ^^^ + /// ``` + Module, + /// ```plain + /// + /// ^^^^^ span + /// name_span is detached + /// ``` + Label, +} + +fn find_let_binding(source: &Source, name_span: Span) -> Span { + let node = LinkedNode::new(source.root()); + std::iter::successors(node.find(name_span).as_ref(), |n| n.parent()) + .find(|n| matches!(n.kind(), SyntaxKind::LetBinding)) + .map(|s| s.span()) + .unwrap_or_else(Span::detached) +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use typst::foundations::{IntoValue, Label, NativeElement, Value}; + use typst::syntax::Side; + use typst::WorldExt; + + use super::{definition, DefinitionKind as Kind}; + use crate::tests::TestWorld; + + #[track_caller] + fn test( + text: &str, + cursor: usize, + name: &str, + kind: Kind, + value: Option, + range: Option>, + ) where + T: IntoValue, + { + let world = TestWorld::new(text); + let doc = typst::compile(&world).output.ok(); + let actual = definition(&world, doc.as_ref(), &world.main, cursor, Side::After) + .map(|d| (d.kind, d.name, world.range(d.span), d.value)); + assert_eq!( + actual, + Some((kind, name.into(), range, value.map(IntoValue::into_value))) + ); + } + + #[test] + fn test_definition() { + test("#let x; #x", 9, "x", Kind::Variable, None::, Some(1..6)); + test("#let x() = {}; #x", 16, "x", Kind::Function, None::, Some(1..13)); + test( + "#table", + 1, + "table", + Kind::Function, + Some(typst::model::TableElem::elem()), + None, + ); + test( + "#figure[] See @hi", + 21, + "hi", + Kind::Label, + Some(Label::new("hi")), + Some(1..9), + ); + } +} diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs index 1f8562fd2..403a36ba0 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -2,12 +2,16 @@ mod analyze; mod complete; +mod definition; mod jump; +mod matchers; mod tooltip; -pub use self::analyze::analyze_labels; +pub use self::analyze::{analyze_expr, analyze_import, analyze_labels}; pub use self::complete::{autocomplete, Completion, CompletionKind}; +pub use self::definition::{definition, Definition, DefinitionKind}; pub use self::jump::{jump_from_click, jump_from_cursor, Jump}; +pub use self::matchers::{deref_target, named_items, DerefTarget, NamedItem}; pub use self::tooltip::{tooltip, Tooltip}; use std::fmt::Write; @@ -135,8 +139,8 @@ mod tests { &self.base.book } - fn main(&self) -> Source { - self.main.clone() + fn main(&self) -> FileId { + self.main.id() } fn source(&self, id: FileId) -> FileResult { diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs new file mode 100644 index 000000000..1daec8193 --- /dev/null +++ b/crates/typst-ide/src/matchers.rs @@ -0,0 +1,270 @@ +use ecow::EcoString; +use typst::foundations::{Module, Value}; +use typst::syntax::ast::AstNode; +use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode}; +use typst::World; + +use crate::analyze_import; + +/// Find the named items starting from the given position. +pub fn named_items( + world: &dyn World, + position: LinkedNode, + mut recv: impl FnMut(NamedItem) -> Option, +) -> Option { + let mut ancestor = Some(position); + while let Some(node) = &ancestor { + let mut sibling = Some(node.clone()); + while let Some(node) = &sibling { + if let Some(v) = node.cast::() { + let kind = if matches!(v.kind(), ast::LetBindingKind::Closure(..)) { + NamedItem::Fn + } else { + NamedItem::Var + }; + for ident in v.kind().bindings() { + if let Some(res) = recv(kind(ident)) { + return Some(res); + } + } + } + + 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(); + + // 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); + } + } + } + + // Seeing the imported items. + match imports { + // ```plain + // import "foo"; + // ``` + None => {} + // ```plain + // 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(res) = recv(item) { + return Some(res); + } + } + } + } + // ```plain + // import "foo": 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 value = scope.and_then(|s| s.get(&original)); + if let Some(res) = + recv(NamedItem::Import(bound.get(), span, value)) + { + return Some(res); + } + } + } + } + } + + sibling = node.prev_sibling(); + } + + if let Some(parent) = node.parent() { + if let Some(v) = parent.cast::() { + if node.prev_sibling_kind() != Some(SyntaxKind::In) { + let pattern = v.pattern(); + for ident in pattern.bindings() { + if let Some(res) = recv(NamedItem::Var(ident)) { + return Some(res); + } + } + } + } + + ancestor = Some(parent.clone()); + continue; + } + + break; + } + + None +} + +/// An item that is named. +pub enum NamedItem<'a> { + /// A variable item. + Var(ast::Ident<'a>), + /// A function item. + Fn(ast::Ident<'a>), + /// A (imported) module item. + Module(&'a Module, &'a SyntaxNode), + /// An imported item. + Import(&'a EcoString, Span, Option<&'a Value>), +} + +impl<'a> NamedItem<'a> { + pub(crate) fn name(&self) -> &'a EcoString { + match self { + NamedItem::Var(ident) => ident.get(), + NamedItem::Fn(ident) => ident.get(), + NamedItem::Module(value, _) => value.name(), + NamedItem::Import(name, _, _) => name, + } + } + + pub(crate) fn value(&self) -> Option { + match self { + NamedItem::Var(..) | NamedItem::Fn(..) => None, + NamedItem::Module(value, _) => Some(Value::Module((*value).clone())), + NamedItem::Import(_, _, value) => value.cloned(), + } + } +} + +/// Categorize an expression into common classes IDE functionality can operate +/// on. +pub fn deref_target(node: LinkedNode) -> Option> { + // Move to the first ancestor that is an expression. + let mut ancestor = node; + while !ancestor.is::() { + ancestor = ancestor.parent()?.clone(); + } + + // Identify convenient expression kinds. + let expr_node = ancestor; + let expr = expr_node.cast::()?; + Some(match expr { + ast::Expr::Label(..) => DerefTarget::Label(expr_node), + ast::Expr::Ref(..) => DerefTarget::Ref(expr_node), + 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::Ident(..) | ast::Expr::MathIdent(..) | ast::Expr::FieldAccess(..) => { + DerefTarget::VarAccess(expr_node) + } + ast::Expr::Str(..) => { + let parent = expr_node.parent()?; + if parent.kind() == SyntaxKind::ModuleImport { + DerefTarget::ImportPath(expr_node) + } else if parent.kind() == SyntaxKind::ModuleInclude { + DerefTarget::IncludePath(expr_node) + } else { + DerefTarget::Code(expr_node.kind(), expr_node) + } + } + _ if expr.hash() + || matches!(expr_node.kind(), SyntaxKind::MathIdent | SyntaxKind::Error) => + { + DerefTarget::Code(expr_node.kind(), expr_node) + } + _ => return None, + }) +} + +/// Classes of expressions that can be operated on by IDE functionality. +#[derive(Debug, Clone)] +pub enum DerefTarget<'a> { + /// A label expression. + Label(LinkedNode<'a>), + /// A reference expression. + Ref(LinkedNode<'a>), + /// A variable access expression. + /// + /// It can be either an identifier or a field access. + VarAccess(LinkedNode<'a>), + /// A function call expression. + Callee(LinkedNode<'a>), + /// An import path expression. + ImportPath(LinkedNode<'a>), + /// An include path expression. + IncludePath(LinkedNode<'a>), + /// Any code expression. + Code(SyntaxKind, LinkedNode<'a>), +} + +#[cfg(test)] +mod tests { + use typst::syntax::{LinkedNode, Side}; + + use crate::{named_items, tests::TestWorld}; + + #[track_caller] + fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool { + let world = TestWorld::new(text); + + let src = world.main.clone(); + let node = LinkedNode::new(src.root()); + let leaf = node.leaf_at(cursor, Side::After).unwrap(); + + let res = named_items(&world, leaf, |s| { + if containing == s.name() { + return Some(true); + } + + None + }); + + res.unwrap_or_default() + } + + #[test] + fn test_simple_named_items() { + // Has named items + assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a")); + assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a")); + + // Doesn't have named items + assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b")); + } + + #[test] + fn test_import_named_items() { + // Cannot test much. + assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a")); + } +} diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 3bf8bb14a..02fb3ec62 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -11,14 +11,13 @@ use typst::syntax::{ast, LinkedNode, Side, Source, SyntaxKind}; use typst::utils::{round_2, Numeric}; use typst::World; -use crate::analyze::{analyze_expr, analyze_labels}; -use crate::{plain_docs_sentence, summarize_font_family}; +use crate::{analyze_expr, analyze_labels, plain_docs_sentence, summarize_font_family}; /// Describe the item under the cursor. /// /// Passing a `document` (from a previous compilation) is optional, but enhances -/// the autocompletions. Label completions, for instance, are only generated -/// when the document is available. +/// the tooltips. Label tooltips, for instance, are only generated when the +/// document is available. pub fn tooltip( world: &dyn World, document: Option<&Document>, @@ -127,7 +126,7 @@ fn closure_tooltip(leaf: &LinkedNode) -> Option { let captures = visitor.finish(); let mut names: Vec<_> = - captures.iter().map(|(name, _)| eco_format!("`{name}`")).collect(); + captures.iter().map(|(name, ..)| eco_format!("`{name}`")).collect(); if names.is_empty() { return None; } diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index 8ae2c424d..da9e4ed44 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -92,7 +92,7 @@ pub struct Builder<'a, R = ()> { state: State, /// Stack of saved graphic states. saves: Vec, - /// Wheter any stroke or fill was not totally opaque. + /// Whether any stroke or fill was not totally opaque. uses_opacities: bool, /// All clickable links that are present in this content. links: Vec<(Destination, Rect)>, @@ -129,7 +129,7 @@ struct State { /// The color space of the current fill paint. fill_space: Option>, /// The current external graphic state. - external_graphics_state: Option, + external_graphics_state: ExtGState, /// The current stroke paint. stroke: Option, /// The color space of the current stroke paint. @@ -148,7 +148,7 @@ impl State { font: None, fill: None, fill_space: None, - external_graphics_state: None, + external_graphics_state: ExtGState::default(), stroke: None, stroke_space: None, text_rendering_mode: TextRenderingMode::Fill, @@ -191,12 +191,13 @@ impl Builder<'_, ()> { } fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) { - let current_state = self.state.external_graphics_state.as_ref(); - if current_state != Some(graphics_state) { + let current_state = &self.state.external_graphics_state; + if current_state != graphics_state { let index = self.resources.ext_gs.insert(*graphics_state); let name = eco_format!("Gs{index}"); self.content.set_parameters(Name(name.as_bytes())); + self.state.external_graphics_state = *graphics_state; if graphics_state.uses_opacities() { self.uses_opacities = true; } @@ -204,29 +205,27 @@ impl Builder<'_, ()> { } fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) { - let stroke_opacity = stroke - .map(|stroke| { - let color = match &stroke.paint { - Paint::Solid(color) => *color, - Paint::Gradient(_) | Paint::Pattern(_) => return 255, - }; + let get_opacity = |paint: &Paint| { + let color = match paint { + Paint::Solid(color) => *color, + Paint::Gradient(_) | Paint::Pattern(_) => return 255, + }; - color.alpha().map_or(255, |v| (v * 255.0).round() as u8) - }) - .unwrap_or(255); - let fill_opacity = fill - .map(|paint| { - let color = match paint { - Paint::Solid(color) => *color, - Paint::Gradient(_) | Paint::Pattern(_) => return 255, - }; + color.alpha().map_or(255, |v| (v * 255.0).round() as u8) + }; - color.alpha().map_or(255, |v| (v * 255.0).round() as u8) - }) - .unwrap_or(255); + let stroke_opacity = stroke.map_or(255, |stroke| get_opacity(&stroke.paint)); + let fill_opacity = fill.map_or(255, get_opacity); self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity }); } + fn reset_opacities(&mut self) { + self.set_external_graphics_state(&ExtGState { + stroke_opacity: 255, + fill_opacity: 255, + }); + } + pub fn transform(&mut self, transform: Transform) { let Transform { sx, ky, kx, sy, tx, ty } = transform; self.state.transform = self.state.transform.pre_concat(transform); @@ -542,6 +541,8 @@ fn write_color_glyphs(ctx: &mut Builder, pos: Point, text: TextItemView) { let mut last_font = None; + ctx.reset_opacities(); + ctx.content.begin_text(); ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]); // So that the next call to ctx.set_font() will change the font to one that @@ -671,6 +672,8 @@ fn write_image(ctx: &mut Builder, x: f32, y: f32, image: &Image, size: Size) { image }); + ctx.reset_opacities(); + let name = eco_format!("Im{index}"); let w = size.x.to_f32(); let h = size.y.to_f32(); diff --git a/crates/typst-syntax/Cargo.toml b/crates/typst-syntax/Cargo.toml index 816f0d340..e9c399920 100644 --- a/crates/typst-syntax/Cargo.toml +++ b/crates/typst-syntax/Cargo.toml @@ -17,6 +17,7 @@ typst-utils = { workspace = true } ecow = { workspace = true } once_cell = { workspace = true } serde = { workspace = true } +toml = { workspace = true } unicode-ident = { workspace = true } unicode-math-class = { workspace = true } unicode-script = { workspace = true } diff --git a/crates/typst-syntax/src/file.rs b/crates/typst-syntax/src/file.rs index 356337f3f..89aaa55e1 100644 --- a/crates/typst-syntax/src/file.rs +++ b/crates/typst-syntax/src/file.rs @@ -91,6 +91,11 @@ impl FileId { Self::new(self.package().cloned(), self.vpath().join(path)) } + /// The same file location, but with a different extension. + pub fn with_extension(&self, extension: &str) -> Self { + Self::new(self.package().cloned(), self.vpath().with_extension(extension)) + } + /// Construct from a raw number. pub(crate) const fn from_raw(v: u16) -> Self { Self(v) diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index 9f08a4bca..a15f0a18a 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -650,7 +650,7 @@ impl Lexer<'_> { Some('-') if !s.at(['-', '?']) => {} Some('.') if !s.at("..") => {} Some('h') if !s.at("ttp://") && !s.at("ttps://") => {} - Some('@') if !s.at(is_id_start) => {} + Some('@') if !s.at(is_valid_in_label_literal) => {} _ => break, } diff --git a/crates/typst-syntax/src/package.rs b/crates/typst-syntax/src/package.rs index fb0031c85..c96aebe01 100644 --- a/crates/typst-syntax/src/package.rs +++ b/crates/typst-syntax/src/package.rs @@ -1,37 +1,101 @@ //! Package manifest parsing. +use std::collections::BTreeMap; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; use ecow::{eco_format, EcoString}; +use serde::de::IgnoredAny; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use unscanny::Scanner; use crate::is_ident; +/// A type alias for a map of key-value pairs used to collect unknown fields +/// where values are completely discarded. +pub type UnknownFields = BTreeMap; + /// A parsed package manifest. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +/// +/// The `unknown_fields` contains fields which were found but not expected. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PackageManifest { /// Details about the package itself. pub package: PackageInfo, /// Details about the template, if the package is one. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "Option::is_none")] pub template: Option, + /// The tools section for third-party configuration. + #[serde(default)] + pub tool: ToolInfo, + /// All parsed but unknown fields, this can be used for validation. + #[serde(flatten, skip_serializing)] + pub unknown_fields: UnknownFields, +} + +/// The `[tool]` key in the manifest. This field can be used to retrieve +/// 3rd-party tool configuration. +/// +// # Examples +/// ``` +/// # use serde::{Deserialize, Serialize}; +/// # use ecow::EcoString; +/// # use typst_syntax::package::PackageManifest; +/// #[derive(Debug, PartialEq, Serialize, Deserialize)] +/// struct MyTool { +/// key: EcoString, +/// } +/// +/// let mut manifest: PackageManifest = toml::from_str(r#" +/// [package] +/// name = "package" +/// version = "0.1.0" +/// entrypoint = "src/lib.typ" +/// +/// [tool.my-tool] +/// key = "value" +/// "#)?; +/// +/// let my_tool = manifest +/// .tool +/// .sections +/// .remove("my-tool") +/// .ok_or("tool.my-tool section missing")?; +/// let my_tool = MyTool::deserialize(my_tool)?; +/// +/// assert_eq!(my_tool, MyTool { key: "value".into() }); +/// # Ok::<_, Box>(()) +/// ``` +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct ToolInfo { + /// Any fields parsed in the tool section. + #[serde(flatten)] + pub sections: BTreeMap, } /// The `[template]` key in the manifest. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +/// +/// The `unknown_fields` contains fields which were found but not expected. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TemplateInfo { - /// The path of the starting point within the package. + /// The directory within the package that contains the files that should be + /// copied into the user's new project directory. pub path: EcoString, - /// The path of the entrypoint relative to the starting point's `path`. + /// A path relative to the template's path that points to the file serving + /// as the compilation target. pub entrypoint: EcoString, + /// A path relative to the package's root that points to a PNG or lossless + /// WebP thumbnail for the template. + pub thumbnail: EcoString, + /// All parsed but unknown fields, this can be used for validation. + #[serde(flatten, skip_serializing)] + pub unknown_fields: UnknownFields, } /// The `[package]` key in the manifest. /// -/// More fields are specified, but they are not relevant to the compiler. -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +/// The `unknown_fields` contains fields which were found but not expected. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PackageInfo { /// The name of the package within its namespace. pub name: EcoString, @@ -39,8 +103,42 @@ pub struct PackageInfo { pub version: PackageVersion, /// The path of the entrypoint into the package. pub entrypoint: EcoString, + /// A list of the package's authors. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub authors: Vec, + /// The package's license. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub license: Option, + /// A short description of the package. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub description: Option, + /// A link to the package's web presence. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub homepage: Option, + /// A link to the repository where this package is developed. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub repository: Option, + /// An array of search keywords for the package. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub keywords: Vec, + /// An array with up to three of the predefined categories to help users + /// discover the package. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub categories: Vec, + /// An array of disciplines defining the target audience for which the + /// package is useful. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub disciplines: Vec, /// The minimum required compiler version for the package. + #[serde(default, skip_serializing_if = "Option::is_none")] pub compiler: Option, + /// An array of globs specifying files that should not be part of the + /// published bundle. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub exclude: Vec, + /// All parsed but unknown fields, this can be used for validation. + #[serde(flatten, skip_serializing)] + pub unknown_fields: UnknownFields, } impl PackageManifest { @@ -423,4 +521,97 @@ mod tests { assert!(!v1_1_1.matches_lt(&VersionBound::from_str("1.1").unwrap())); assert!(v1_1_1.matches_lt(&VersionBound::from_str("1.2").unwrap())); } + + #[test] + fn minimal_manifest() { + assert_eq!( + toml::from_str::( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + "# + ), + Ok(PackageManifest { + package: PackageInfo { + name: "package".into(), + version: PackageVersion { major: 0, minor: 1, patch: 0 }, + entrypoint: "src/lib.typ".into(), + authors: vec![], + license: None, + description: None, + homepage: None, + repository: None, + keywords: vec![], + categories: vec![], + disciplines: vec![], + compiler: None, + exclude: vec![], + unknown_fields: BTreeMap::new(), + }, + template: None, + tool: ToolInfo { sections: BTreeMap::new() }, + unknown_fields: BTreeMap::new(), + }) + ); + } + + #[test] + fn tool_section() { + // NOTE: tool section must be table of tables, but we can't easily + // compare the error structurally + assert!(toml::from_str::( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + + [tool] + not-table = "str" + "# + ) + .is_err()); + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct MyTool { + key: EcoString, + } + + let mut manifest: PackageManifest = toml::from_str( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + + [tool.my-tool] + key = "value" + "#, + ) + .unwrap(); + + let my_tool = manifest.tool.sections.remove("my-tool").unwrap(); + let my_tool = MyTool::deserialize(my_tool).unwrap(); + + assert_eq!(my_tool, MyTool { key: "value".into() }); + } + + #[test] + fn unknown_keys() { + let manifest: PackageManifest = toml::from_str( + r#" + [package] + name = "package" + version = "0.1.0" + entrypoint = "src/lib.typ" + + [unknown] + "#, + ) + .unwrap(); + + assert!(manifest.unknown_fields.contains_key("unknown")); + } } diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index e8681c46a..30e08de7a 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -395,11 +395,6 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { continue; } - // Separate primes and superscripts to different attachments. - if primed && p.current() == SyntaxKind::Hat { - p.wrap(m, SyntaxKind::MathAttach); - } - let Some((kind, stop, assoc, mut prec)) = math_op(p.current()) else { // No attachments, so we need to wrap primes as attachment. if primed { @@ -431,7 +426,7 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { math_expr_prec(p, prec, stop); math_unparen(p, m2); - if p.eat_if(SyntaxKind::Underscore) || (!primed && p.eat_if(SyntaxKind::Hat)) { + if p.eat_if(SyntaxKind::Underscore) || p.eat_if(SyntaxKind::Hat) { let m3 = p.marker(); math_expr_prec(p, prec, SyntaxKind::End); math_unparen(p, m3); diff --git a/crates/typst-syntax/src/path.rs b/crates/typst-syntax/src/path.rs index b561128c1..6c625642a 100644 --- a/crates/typst-syntax/src/path.rs +++ b/crates/typst-syntax/src/path.rs @@ -85,6 +85,11 @@ impl VirtualPath { Self::new(path) } } + + /// The same path, but with a different extension. + pub fn with_extension(&self, extension: &str) -> Self { + Self(self.0.with_extension(extension)) + } } impl Debug for VirtualPath { diff --git a/crates/typst-syntax/src/span.rs b/crates/typst-syntax/src/span.rs index 8138a3166..4d5dd9c37 100644 --- a/crates/typst-syntax/src/span.rs +++ b/crates/typst-syntax/src/span.rs @@ -83,6 +83,15 @@ impl Span { self.0.get() & ((1 << Self::BITS) - 1) } + /// Return `other` if `self` is detached and `self` otherwise. + pub fn or(self, other: Self) -> Self { + if self.is_detached() { + other + } else { + self + } + } + /// Resolve a file location relative to this span's source. pub fn resolve_path(self, path: &str) -> Result { let Some(file) = self.id() else { diff --git a/crates/typst/src/eval/call.rs b/crates/typst/src/eval/call.rs index f3d3b313d..5ce7ebe47 100644 --- a/crates/typst/src/eval/call.rs +++ b/crates/typst/src/eval/call.rs @@ -1,7 +1,10 @@ use comemo::{Tracked, TrackedMut}; -use ecow::{eco_format, EcoVec}; +use ecow::{eco_format, EcoString, EcoVec}; -use crate::diag::{bail, error, At, HintedStrResult, SourceResult, Trace, Tracepoint}; +use crate::diag::{ + bail, error, At, HintedStrResult, HintedString, SourceDiagnostic, SourceResult, + Trace, Tracepoint, +}; use crate::engine::{Engine, Sink, Traced}; use crate::eval::{Access, Eval, FlowEvent, Route, Vm}; use crate::foundations::{ @@ -10,7 +13,7 @@ use crate::foundations::{ }; use crate::introspection::Introspector; use crate::math::LrElem; -use crate::syntax::ast::{self, AstNode}; +use crate::syntax::ast::{self, AstNode, Ident}; use crate::syntax::{Span, Spanned, SyntaxNode}; use crate::text::TextElem; use crate::utils::LazyHash; @@ -32,135 +35,25 @@ impl Eval for ast::FuncCall<'_> { } // Try to evaluate as a call to an associated function or field. - let (callee, mut args) = if let ast::Expr::FieldAccess(access) = callee { + let (callee, args) = if let ast::Expr::FieldAccess(access) = callee { let target = access.target(); - let target_span = target.span(); let field = access.field(); - let field_span = field.span(); - - let target = if is_mutating_method(&field) { - let mut args = args.eval(vm)?.spanned(span); - let target = target.access(vm)?; - - // Only arrays and dictionaries have mutable methods. - if matches!(target, Value::Array(_) | Value::Dict(_)) { - args.span = span; - let point = || Tracepoint::Call(Some(field.get().clone())); - return call_method_mut(target, &field, args, span).trace( - vm.world(), - point, - span, - ); - } - - target.clone() - } else { - access.target().eval(vm)? - }; - - let mut args = args.eval(vm)?.spanned(span); - - // Handle plugins. - if let Value::Plugin(plugin) = &target { - let bytes = args.all::()?; - args.finish()?; - return Ok(plugin.call(&field, bytes).at(span)?.into_value()); - } - - // Prioritize associated functions on the value's type (i.e., - // methods) over its fields. A function call on a field is only - // allowed for functions, types, modules (because they are scopes), - // and symbols (because they have modifiers). - // - // For dictionaries, it is not allowed because it would be ambiguous - // (prioritizing associated functions would make an addition of a - // new associated function a breaking change and prioritizing fields - // would break associated functions for certain dictionaries). - if let Some(callee) = target.ty().scope().get(&field) { - let this = Arg { - span: target_span, - name: None, - value: Spanned::new(target, target_span), - }; - args.span = span; - args.items.insert(0, this); - (callee.clone(), args) - } else if matches!( - target, - Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) - ) { - (target.field(&field).at(field_span)?, args) - } else { - let mut error = error!( - field_span, - "type {} has no method `{}`", - target.ty(), - field.as_str() - ); - - let mut field_hint = || { - if target.field(&field).is_ok() { - error.hint(eco_format!( - "did you mean to access the field `{}`?", - field.as_str() - )); - } - }; - - match target { - Value::Dict(ref dict) => { - if matches!(dict.get(&field), Ok(Value::Func(_))) { - error.hint(eco_format!( - "to call the function stored in the dictionary, surround \ - the field access with parentheses, e.g. `(dict.{})(..)`", - field.as_str(), - )); - } else { - field_hint(); - } - } - _ => field_hint(), - } - - bail!(error); + match eval_field_call(target, field, args, span, vm)? { + FieldCall::Normal(callee, args) => (callee, args), + FieldCall::Resolved(value) => return Ok(value), } } else { + // Function call order: we evaluate the callee before the arguments. (callee.eval(vm)?, args.eval(vm)?.spanned(span)) }; let func_result = callee.clone().cast::(); if in_math && func_result.is_err() { - // For non-functions in math, we wrap the arguments in parentheses. - let mut body = Content::empty(); - for (i, arg) in args.all::()?.into_iter().enumerate() { - if i > 0 { - body += TextElem::packed(','); - } - body += arg; - } - if trailing_comma { - body += TextElem::packed(','); - } - return Ok(Value::Content( - callee.display().spanned(callee_span) - + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) - .pack(), - )); + return wrap_args_in_math(callee, callee_span, args, trailing_comma); } let func = func_result - .map_err(|mut err| { - if let ast::Expr::Ident(ident) = self.callee() { - let ident = ident.get(); - if vm.scopes.check_std_shadowed(ident) { - err.hint(eco_format!( - "use `std.{}` to access the shadowed standard library function", - ident, - )); - } - } - err - }) + .map_err(|err| hint_if_shadowed_std(vm, &self.callee(), err)) .at(callee_span)?; let point = || Tracepoint::Call(func.name().map(Into::into)); @@ -371,6 +264,108 @@ pub(crate) fn call_closure( Ok(output) } +/// This used only as the return value of `eval_field_call`. +/// - `Normal` means that we have a function to call and the arguments to call it with. +/// - `Resolved` means that we have already resolved the call and have the value. +enum FieldCall { + Normal(Value, Args), + Resolved(Value), +} + +/// Evaluate a field call's callee and arguments. +/// +/// This follows the normal function call order: we evaluate the callee before the +/// arguments. +/// +/// Prioritize associated functions on the value's type (e.g., methods) over its fields. +/// A function call on a field is only allowed for functions, types, modules (because +/// they are scopes), and symbols (because they have modifiers or associated functions). +/// +/// For dictionaries, it is not allowed because it would be ambiguous - prioritizing +/// associated functions would make an addition of a new associated function a breaking +/// change and prioritizing fields would break associated functions for certain +/// dictionaries. +fn eval_field_call( + target_expr: ast::Expr, + field: Ident, + args: ast::Args, + span: Span, + vm: &mut Vm, +) -> SourceResult { + // Evaluate the field-call's target and overall arguments. + let (target, mut args) = if is_mutating_method(&field) { + // If `field` looks like a mutating method, we evaluate the arguments first, + // because `target_expr.access(vm)` mutably borrows the `vm`, so that we can't + // evaluate the arguments after it. + let args = args.eval(vm)?.spanned(span); + // However, this difference from the normal call order is not observable because + // expressions like `(1, arr.len(), 2, 3).push(arr.pop())` evaluate the target to + // a temporary which we disallow mutation on (returning an error). + // Theoretically this could be observed if a method matching `is_mutating_method` + // was added to some type in the future and we didn't update this function. + match target_expr.access(vm)? { + // Only arrays and dictionaries have mutable methods. + target @ (Value::Array(_) | Value::Dict(_)) => { + let value = call_method_mut(target, &field, args, span); + let point = || Tracepoint::Call(Some(field.get().clone())); + return Ok(FieldCall::Resolved(value.trace(vm.world(), point, span)?)); + } + target => (target.clone(), args), + } + } else { + let target = target_expr.eval(vm)?; + let args = args.eval(vm)?.spanned(span); + (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) { + args.insert(0, target_expr.span(), target); + Ok(FieldCall::Normal(callee.clone(), args)) + } 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())?; + Ok(FieldCall::Normal(value, args)) + } else { + // Otherwise we cannot call this field. + bail!(missing_field_call_error(target, field)) + } +} + +/// 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()); + + match target { + Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => { + error.hint(eco_format!( + "to call the function stored in the dictionary, surround \ + the field access with parentheses, e.g. `(dict.{})(..)`", + field.as_str(), + )); + } + _ if target.field(&field).is_ok() => { + error.hint(eco_format!( + "did you mean to access the field `{}`?", + field.as_str(), + )); + } + _ => {} + } + error +} + +/// Check if the expression is in a math context. fn in_math(expr: ast::Expr) -> bool { match expr { ast::Expr::MathIdent(_) => true, @@ -379,6 +374,46 @@ fn in_math(expr: ast::Expr) -> bool { } } +/// For non-functions in math, we wrap the arguments in parentheses. +fn wrap_args_in_math( + callee: Value, + callee_span: Span, + mut args: Args, + trailing_comma: bool, +) -> SourceResult { + let mut body = Content::empty(); + for (i, arg) in args.all::()?.into_iter().enumerate() { + if i > 0 { + body += TextElem::packed(','); + } + body += arg; + } + if trailing_comma { + body += TextElem::packed(','); + } + Ok(Value::Content( + callee.display().spanned(callee_span) + + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')).pack(), + )) +} + +/// Provide a hint if the callee is a shadowed standard library function. +fn hint_if_shadowed_std( + vm: &mut Vm, + callee: &ast::Expr, + mut err: HintedString, +) -> HintedString { + if let ast::Expr::Ident(ident) = callee { + let ident = ident.get(); + if vm.scopes.check_std_shadowed(ident) { + err.hint(eco_format!( + "use `std.{ident}` to access the shadowed standard library function", + )); + } + } + err +} + /// A visitor that determines which variables to capture for a closure. pub struct CapturesVisitor<'a> { external: Option<&'a Scopes<'a>>, @@ -410,9 +445,11 @@ 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, Scopes::get), + Some(ast::Expr::Ident(ident)) => { + self.capture(ident.get(), ident.span(), Scopes::get) + } Some(ast::Expr::MathIdent(ident)) => { - self.capture(&ident, Scopes::get_in_math) + self.capture(ident.get(), ident.span(), Scopes::get_in_math) } // Code and content blocks create a scope. @@ -520,13 +557,14 @@ impl<'a> CapturesVisitor<'a> { /// Bind a new internal variable. fn bind(&mut self, ident: ast::Ident) { - self.internal.top.define(ident.get().clone(), Value::None); + self.internal.top.define_ident(ident, Value::None); } /// Capture a variable if it isn't internal. fn capture( &mut self, - ident: &str, + ident: &EcoString, + span: Span, getter: impl FnOnce(&'a Scopes<'a>, &str) -> HintedStrResult<&'a Value>, ) { if self.internal.get(ident).is_err() { @@ -538,7 +576,12 @@ impl<'a> CapturesVisitor<'a> { return; }; - self.captures.define_captured(ident, value.clone(), self.capturer); + self.captures.define_captured( + ident.clone(), + value.clone(), + self.capturer, + span, + ); } } } @@ -561,7 +604,7 @@ mod tests { visitor.visit(&root); let captures = visitor.finish(); - let mut names: Vec<_> = captures.iter().map(|(k, _)| k).collect(); + let mut names: Vec<_> = captures.iter().map(|(k, ..)| k).collect(); names.sort(); assert_eq!(names, result); diff --git a/crates/typst/src/eval/import.rs b/crates/typst/src/eval/import.rs index 2375fcadb..7451e6030 100644 --- a/crates/typst/src/eval/import.rs +++ b/crates/typst/src/eval/import.rs @@ -31,7 +31,7 @@ impl Eval for ast::ModuleImport<'_> { } } - if let Some(new_name) = &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() { // Warn on `import x as x` @@ -44,7 +44,7 @@ impl Eval for ast::ModuleImport<'_> { } // Define renamed module on the scope. - vm.scopes.top.define(new_name.as_str(), source.clone()); + vm.scopes.top.define_ident(new_name, source.clone()); } let scope = source.scope().unwrap(); @@ -57,8 +57,8 @@ impl Eval for ast::ModuleImport<'_> { } } Some(ast::Imports::Wildcard) => { - for (var, value) in scope.iter() { - vm.scopes.top.define(var.clone(), value.clone()); + for (var, value, span) in scope.iter() { + vm.scopes.top.define_spanned(var.clone(), value.clone(), span); } } Some(ast::Imports::Items(items)) => { diff --git a/crates/typst/src/eval/math.rs b/crates/typst/src/eval/math.rs index 0e5e0eef0..548c935dc 100644 --- a/crates/typst/src/eval/math.rs +++ b/crates/typst/src/eval/math.rs @@ -54,7 +54,11 @@ impl Eval for ast::MathAttach<'_> { if let Some(expr) = self.top() { elem.push_t(Some(expr.eval_display(vm)?)); - } else if let Some(primes) = self.primes() { + } + + // Always attach primes in scripts style (not limits style), + // i.e. at the top-right corner. + if let Some(primes) = self.primes() { elem.push_tr(Some(primes.eval(vm)?)); } diff --git a/crates/typst/src/eval/mod.rs b/crates/typst/src/eval/mod.rs index e4221df77..dc8a18020 100644 --- a/crates/typst/src/eval/mod.rs +++ b/crates/typst/src/eval/mod.rs @@ -86,7 +86,7 @@ pub fn eval( .unwrap_or_default() .to_string_lossy(); - Ok(Module::new(name, vm.scopes.top).with_content(output)) + Ok(Module::new(name, vm.scopes.top).with_content(output).with_file_id(id)) } /// Evaluate a string as code and return the resulting value. diff --git a/crates/typst/src/eval/vm.rs b/crates/typst/src/eval/vm.rs index 27960eb6d..4d346870c 100644 --- a/crates/typst/src/eval/vm.rs +++ b/crates/typst/src/eval/vm.rs @@ -47,7 +47,7 @@ impl<'a> Vm<'a> { if self.inspected == Some(var.span()) { self.trace(value.clone()); } - self.scopes.top.define(var.get().clone(), value); + self.scopes.top.define_ident(var, value); } /// Trace a value. diff --git a/crates/typst/src/foundations/args.rs b/crates/typst/src/foundations/args.rs index c59e49855..d580be3ce 100644 --- a/crates/typst/src/foundations/args.rs +++ b/crates/typst/src/foundations/args.rs @@ -76,6 +76,18 @@ impl Args { self.items.iter().filter(|slot| slot.name.is_none()).count() } + /// Insert a positional argument at a specific index. + pub fn insert(&mut self, index: usize, span: Span, value: Value) { + self.items.insert( + index, + Arg { + span: self.span, + name: None, + value: Spanned::new(value, span), + }, + ) + } + /// Push a positional argument. pub fn push(&mut self, span: Span, value: Value) { self.items.push(Arg { diff --git a/crates/typst/src/foundations/array.rs b/crates/typst/src/foundations/array.rs index 2caefc133..bd7e7bca4 100644 --- a/crates/typst/src/foundations/array.rs +++ b/crates/typst/src/foundations/array.rs @@ -259,7 +259,7 @@ impl Array { .ok_or_else(|| out_of_bounds_no_default(index, self.len())) } - /// Extracts a subslice of the array. Fails with an error if the start or + /// Extracts a subslice of the array. Fails with an error if the start or end /// index is out of bounds. #[func] pub fn slice( diff --git a/crates/typst/src/foundations/bytes.rs b/crates/typst/src/foundations/bytes.rs index 1e7958595..5b77aa2a6 100644 --- a/crates/typst/src/foundations/bytes.rs +++ b/crates/typst/src/foundations/bytes.rs @@ -127,7 +127,7 @@ impl Bytes { .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 + /// Extracts a subslice of the bytes. Fails with an error if the start or end /// index is out of bounds. #[func] pub fn slice( diff --git a/crates/typst/src/foundations/dict.rs b/crates/typst/src/foundations/dict.rs index 991f3f7af..6e61838eb 100644 --- a/crates/typst/src/foundations/dict.rs +++ b/crates/typst/src/foundations/dict.rs @@ -261,7 +261,7 @@ 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, v, _)| (Str::from(k.clone()), v.clone())).collect()), } impl Debug for Dict { diff --git a/crates/typst/src/foundations/mod.rs b/crates/typst/src/foundations/mod.rs index eb9f6d661..b7783dda9 100644 --- a/crates/typst/src/foundations/mod.rs +++ b/crates/typst/src/foundations/mod.rs @@ -290,7 +290,7 @@ pub fn eval( let dict = scope; let mut scope = Scope::new(); for (key, value) in dict { - scope.define(key, value); + scope.define_spanned(key, value, span); } crate::eval::eval_string(engine.world, &text, span, mode, scope) } diff --git a/crates/typst/src/foundations/module.rs b/crates/typst/src/foundations/module.rs index 580d09ef1..91b508554 100644 --- a/crates/typst/src/foundations/module.rs +++ b/crates/typst/src/foundations/module.rs @@ -5,6 +5,7 @@ use ecow::{eco_format, EcoString}; use crate::diag::StrResult; use crate::foundations::{repr, ty, Content, Scope, Value}; +use crate::syntax::FileId; /// An evaluated module, either built-in or resulting from a file. /// @@ -43,6 +44,8 @@ struct Repr { scope: Scope, /// The module's layoutable contents. content: Content, + /// The id of the file which defines the module, if any. + file_id: Option, } impl Module { @@ -50,7 +53,7 @@ impl Module { pub fn new(name: impl Into, scope: Scope) -> Self { Self { name: name.into(), - inner: Arc::new(Repr { scope, content: Content::empty() }), + inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), } } @@ -72,6 +75,12 @@ impl Module { self } + /// Update the module's file id. + pub fn with_file_id(mut self, file_id: FileId) -> Self { + Arc::make_mut(&mut self.inner).file_id = Some(file_id); + self + } + /// Get the module's name. pub fn name(&self) -> &EcoString { &self.name @@ -82,6 +91,13 @@ impl Module { &self.inner.scope } + /// Access the module's file id. + /// + /// Some modules are not associated with a file, like the built-in modules. + pub fn file_id(&self) -> Option { + self.inner.file_id + } + /// Access the module's scope, mutably. pub fn scope_mut(&mut self) -> &mut Scope { &mut Arc::make_mut(&mut self.inner).scope diff --git a/crates/typst/src/foundations/plugin.rs b/crates/typst/src/foundations/plugin.rs index 80644ea53..57dea827e 100644 --- a/crates/typst/src/foundations/plugin.rs +++ b/crates/typst/src/foundations/plugin.rs @@ -234,12 +234,12 @@ impl Plugin { let ty = func.ty(store.as_context()); // Check function signature. - if ty.params().iter().any(|&v| v != wasmi::core::ValueType::I32) { + 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" ); } - if ty.results() != [wasmi::core::ValueType::I32] { + if ty.results() != [wasmi::core::ValType::I32] { bail!("plugin function `{name}` does not return exactly one 32-bit integer"); } @@ -257,14 +257,14 @@ impl Plugin { // Collect the lengths of the argument buffers. let lengths = args .iter() - .map(|a| wasmi::Value::I32(a.len() as i32)) + .map(|a| wasmi::Val::I32(a.len() as i32)) .collect::>(); // Store the input data. store.data_mut().args = args; // Call the function. - let mut code = wasmi::Value::I32(-1); + let mut code = wasmi::Val::I32(-1); func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) .map_err(|err| eco_format!("plugin panicked: {err}"))?; if let Some(MemoryError { offset, length, write }) = @@ -281,8 +281,8 @@ impl Plugin { // Parse the functions return value. match code { - wasmi::Value::I32(0) => {} - wasmi::Value::I32(1) => match std::str::from_utf8(&output) { + wasmi::Val::I32(0) => {} + wasmi::Val::I32(1) => match std::str::from_utf8(&output) { Ok(message) => bail!("plugin errored with: {message}"), Err(_) => { bail!("plugin errored, but did not return a valid error message") diff --git a/crates/typst/src/foundations/scope.rs b/crates/typst/src/foundations/scope.rs index 0313df7a9..b118540e1 100644 --- a/crates/typst/src/foundations/scope.rs +++ b/crates/typst/src/foundations/scope.rs @@ -9,6 +9,8 @@ use crate::foundations::{ Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, NativeType, Type, Value, }; +use crate::syntax::ast::{self, AstNode}; +use crate::syntax::Span; use crate::utils::Static; use crate::Library; @@ -152,6 +154,23 @@ impl Scope { /// 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)] @@ -159,8 +178,24 @@ impl Scope { panic!("duplicate definition: {name}"); } - self.map - .insert(name, Slot::new(value.into_value(), Kind::Normal, self.category)); + 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. @@ -191,19 +226,6 @@ impl Scope { self.define(module.name().clone(), module); } - /// Define a captured, immutable binding. - pub fn define_captured( - &mut self, - var: impl Into, - value: impl IntoValue, - capturer: Capturer, - ) { - self.map.insert( - var.into(), - Slot::new(value.into_value(), Kind::Captured(capturer), self.category), - ); - } - /// Try to access a variable immutably. pub fn get(&self, var: &str) -> Option<&Value> { self.map.get(var).map(Slot::read) @@ -217,14 +239,19 @@ impl Scope { .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 } /// Iterate over all definitions. - pub fn iter(&self) -> impl Iterator { - self.map.iter().map(|(k, v)| (k, v.read())) + pub fn iter(&self) -> impl Iterator { + self.map.iter().map(|(k, v)| (k, v.read(), v.span)) } } @@ -264,6 +291,8 @@ struct Slot { value: Value, /// The kind of slot, determines how the value can be accessed. kind: Kind, + /// A span associated with the stored value. + span: Span, /// The category of the slot. category: Option, } @@ -288,8 +317,8 @@ pub enum Capturer { impl Slot { /// Create a new slot. - fn new(value: Value, kind: Kind, category: Option) -> Self { - Self { value, kind, category } + fn new(value: Value, span: Span, kind: Kind, category: Option) -> Self { + Self { value, span, kind, category } } /// Read the value. diff --git a/crates/typst/src/introspection/mod.rs b/crates/typst/src/introspection/mod.rs index c9dba244a..6c982afb4 100644 --- a/crates/typst/src/introspection/mod.rs +++ b/crates/typst/src/introspection/mod.rs @@ -116,6 +116,6 @@ impl Tag { impl Debug for Tag { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Tag({:?})", self.elem) + write!(f, "Tag({:?})", self.elem.elem().name()) } } diff --git a/crates/typst/src/layout/flow.rs b/crates/typst/src/layout/flow.rs index cdf6034d5..a1f1402e4 100644 --- a/crates/typst/src/layout/flow.rs +++ b/crates/typst/src/layout/flow.rs @@ -47,51 +47,7 @@ impl Packed { styles: StyleChain, regions: Regions, ) -> SourceResult { - if !regions.size.x.is_finite() && regions.expand.x { - bail!(self.span(), "cannot expand into infinite width"); - } - if !regions.size.y.is_finite() && regions.expand.y { - bail!(self.span(), "cannot expand into infinite height"); - } - - // Check whether we have just a single multiple-layoutable element. In - // that case, we do not set `expand.y` to `false`, but rather keep it at - // its original value (since that element can take the full space). - // - // Consider the following code: `block(height: 5cm, pad(10pt, align(bottom, ..)))` - // Thanks to the code below, the expansion will be passed all the way - // through the block & pad and reach the innermost flow, so that things - // are properly bottom-aligned. - let mut alone = false; - if let [child] = self.children().elements() { - alone = child.is::(); - } - - let mut layouter = FlowLayouter::new(locator, styles, regions, alone); - for (child, styles) in self.children().chain(&styles) { - if let Some(elem) = child.to_packed::() { - layouter.layout_tag(elem); - } else if child.is::() { - layouter.flush(engine)?; - } else if let Some(elem) = child.to_packed::() { - layouter.layout_spacing(engine, elem, styles)?; - } else if let Some(elem) = child.to_packed::() { - layouter.layout_par(engine, elem, styles)?; - } else if let Some(elem) = child.to_packed::() { - layouter.layout_block(engine, elem, styles)?; - } else if let Some(placed) = child.to_packed::() { - layouter.layout_placed(engine, placed, styles)?; - } else if child.is::() { - if !layouter.regions.backlog.is_empty() || layouter.regions.last.is_some() - { - layouter.finish_region(engine, true)?; - } - } else { - bail!(child.span(), "unexpected flow child"); - } - } - - layouter.finish(engine) + FlowLayouter::new(engine, self, locator, &styles, regions).layout() } } @@ -103,13 +59,17 @@ impl Debug for FlowElem { } /// Performs flow layout. -struct FlowLayouter<'a> { +struct FlowLayouter<'a, 'e> { + /// The engine. + engine: &'a mut Engine<'e>, + /// The children that will be arranged into a flow. + flow: &'a Packed, /// Whether this is the root flow. root: bool, /// Provides unique locations to the flow's children. locator: SplitLocator<'a>, /// The shared styles. - styles: StyleChain<'a>, + styles: &'a StyleChain<'a>, /// The regions to layout children into. regions: Regions<'a>, /// Whether the flow should expand to fill the region. @@ -124,7 +84,7 @@ struct FlowLayouter<'a> { /// Spacing and layouted blocks for the current region. items: Vec, /// A queue of tags that will be attached to the next frame. - pending_tags: Vec, + pending_tags: Vec<&'a Tag>, /// A queue of floating elements. pending_floats: Vec, /// Whether we have any footnotes in the current region. @@ -157,18 +117,27 @@ enum FlowItem { align: Axes, /// Whether the frame sticks to the item after it (for orphan prevention). sticky: bool, - /// Whether the frame is movable; that is, kept together with its footnotes. + /// Whether the frame is movable; that is, kept together with its + /// footnotes. /// - /// This is true for frames created by paragraphs and [`LayoutSingle`] elements. + /// This is true for frames created by paragraphs and + /// [`BlockElem::single_layouter`] elements. movable: bool, }, /// An absolutely placed frame. Placed { + /// The layouted content. frame: Frame, + /// Where to place the content horizontally. x_align: FixedAlignment, + /// Where to place the content vertically. y_align: Smart>, + /// A translation to apply to the content. delta: Axes>, + /// Whether the content floats --- i.e. collides with in-flow content. float: bool, + /// The amount of space that needs to be kept between the placed content + /// and in-flow content. Only relevant if `float` is `true`. clearance: Abs, }, /// A footnote frame (can also be the separator). @@ -193,24 +162,41 @@ impl FlowItem { } } -impl<'a> FlowLayouter<'a> { +impl<'a, 'e> FlowLayouter<'a, 'e> { /// Create a new flow layouter. fn new( + engine: &'a mut Engine<'e>, + flow: &'a Packed, locator: Locator<'a>, - styles: StyleChain<'a>, + styles: &'a StyleChain<'a>, mut regions: Regions<'a>, - alone: bool, ) -> Self { - let expand = regions.expand; - let root = std::mem::replace(&mut regions.root, false); + // Check whether we have just a single multiple-layoutable element. In + // that case, we do not set `expand.y` to `false`, but rather keep it at + // its original value (since that element can take the full space). + // + // Consider the following code: `block(height: 5cm, pad(10pt, + // align(bottom, ..)))`. Thanks to the code below, the expansion will be + // passed all the way through the block & pad and reach the innermost + // flow, so that things are properly bottom-aligned. + let mut alone = false; + if let [child] = flow.children.elements() { + alone = child.is::(); + } // Disable vertical expansion when there are multiple or not directly // layoutable children. + let expand = regions.expand; if !alone { regions.expand.y = false; } + // The children aren't root. + let root = std::mem::replace(&mut regions.root, false); + Self { + engine, + flow, root, locator: locator.split(), styles, @@ -223,52 +209,84 @@ impl<'a> FlowLayouter<'a> { pending_floats: vec![], has_footnotes: false, footnote_config: FootnoteConfig { - separator: FootnoteEntry::separator_in(styles), - clearance: FootnoteEntry::clearance_in(styles), - gap: FootnoteEntry::gap_in(styles), + separator: FootnoteEntry::separator_in(*styles), + clearance: FootnoteEntry::clearance_in(*styles), + gap: FootnoteEntry::gap_in(*styles), }, finished: vec![], } } + /// Layout the flow. + fn layout(mut self) -> SourceResult { + for (child, styles) in self.flow.children.chain(self.styles) { + if let Some(elem) = child.to_packed::() { + self.handle_tag(elem); + } else if let Some(elem) = child.to_packed::() { + self.handle_v(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_colbreak(elem)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_par(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_block(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_place(elem, styles)?; + } else if let Some(elem) = child.to_packed::() { + self.handle_flush(elem)?; + } else { + bail!(child.span(), "unexpected flow child"); + } + } + + self.finish() + } + /// Place explicit metadata into the flow. - fn layout_tag(&mut self, elem: &Packed) { - self.pending_tags.push(elem.tag.clone()); + fn handle_tag(&mut self, elem: &'a Packed) { + self.pending_tags.push(&elem.tag); } /// Layout vertical spacing. - fn layout_spacing( - &mut self, - engine: &mut Engine, - v: &Packed, - styles: StyleChain, - ) -> SourceResult<()> { - self.layout_item( - engine, - match v.amount() { - Spacing::Rel(rel) => FlowItem::Absolute( - rel.resolve(styles).relative_to(self.initial.y), - v.weakness(styles) > 0, - ), - Spacing::Fr(fr) => FlowItem::Fractional(*fr), - }, - ) + fn handle_v(&mut self, v: &'a Packed, styles: StyleChain) -> SourceResult<()> { + self.handle_item(match v.amount { + Spacing::Rel(rel) => FlowItem::Absolute( + // Resolve the spacing relative to the current base height. + rel.resolve(styles).relative_to(self.initial.y), + v.weakness(styles) > 0, + ), + Spacing::Fr(fr) => FlowItem::Fractional(fr), + }) + } + + /// Layout a column break. + fn handle_colbreak(&mut self, _: &'a Packed) -> SourceResult<()> { + // If there is still an available region, skip to it. + // TODO: Turn this into a region abstraction. + if !self.regions.backlog.is_empty() || self.regions.last.is_some() { + self.finish_region(true)?; + } + Ok(()) } /// Layout a paragraph. - fn layout_par( + fn handle_par( &mut self, - engine: &mut Engine, - par: &Packed, + par: &'a Packed, styles: StyleChain, ) -> SourceResult<()> { + // Fetch properties. let align = AlignElem::alignment_in(styles).resolve(styles); let leading = ParElem::leading_in(styles); + + // Layout the paragraph into lines. This only depends on the base size, + // not on the Y position. let consecutive = self.last_was_par; + let locator = self.locator.next(&par.span()); let lines = par .layout( - engine, - self.locator.next(&par.span()), + self.engine, + locator, styles, consecutive, self.regions.base(), @@ -280,39 +298,26 @@ impl<'a> FlowLayouter<'a> { // previous sticky frame to the next region (if available) if let Some(first) = lines.first() { while !self.regions.size.y.fits(first.height()) && !self.regions.in_last() { - let mut sticky = self.items.len(); - for (i, item) in self.items.iter().enumerate().rev() { - match *item { - FlowItem::Absolute(_, _) => {} - FlowItem::Frame { sticky: true, .. } => sticky = i, - _ => break, - } - } - - let carry: Vec<_> = self.items.drain(sticky..).collect(); - self.finish_region(engine, false)?; - let in_last = self.regions.in_last(); - - for item in carry { - self.layout_item(engine, item)?; - } - + let in_last = self.finish_region_with_migration()?; if in_last { break; } } } + // Layout the lines. for (i, mut frame) in lines.into_iter().enumerate() { if i > 0 { - self.layout_item(engine, FlowItem::Absolute(leading, true))?; + self.handle_item(FlowItem::Absolute(leading, true))?; } self.drain_tag(&mut frame); - self.layout_item( - engine, - FlowItem::Frame { frame, align, sticky: false, movable: true }, - )?; + self.handle_item(FlowItem::Frame { + frame, + align, + sticky: false, + movable: true, + })?; } self.last_was_par = true; @@ -320,56 +325,54 @@ impl<'a> FlowLayouter<'a> { } /// Layout into multiple regions. - fn layout_block( + fn handle_block( &mut self, - engine: &mut Engine, block: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { - // Temporarily delegate rootness to the columns. + // Fetch properties. + let sticky = block.sticky(styles); + let align = AlignElem::alignment_in(styles).resolve(styles); + + // If the block is "rootable" it may host footnotes. In that case, we + // defer rootness to it temporarily. We disable our own rootness to + // prevent duplicate footnotes. let is_root = self.root; if is_root && block.rootable(styles) { self.root = false; self.regions.root = true; } + // Skip directly if region is already full. if self.regions.is_full() { - // Skip directly if region is already full. - self.finish_region(engine, false)?; + self.finish_region(false)?; } // Layout the block itself. - let sticky = block.sticky(styles); let fragment = block.layout( - engine, + self.engine, self.locator.next(&block.span()), styles, self.regions, )?; - // How to align the block. - let align = AlignElem::alignment_in(styles).resolve(styles); - let mut notes = Vec::new(); for (i, mut frame) in fragment.into_iter().enumerate() { // Find footnotes in the frame. if self.root { - find_footnotes(&mut notes, &frame); + collect_footnotes(&mut notes, &frame); } if i > 0 { - self.finish_region(engine, false)?; + self.finish_region(false)?; } self.drain_tag(&mut frame); frame.post_process(styles); - self.layout_item( - engine, - FlowItem::Frame { frame, align, sticky, movable: false }, - )?; + self.handle_item(FlowItem::Frame { frame, align, sticky, movable: false })?; } - self.try_handle_footnotes(engine, notes)?; + self.try_handle_footnotes(notes)?; self.root = is_root; self.regions.root = false; @@ -379,50 +382,56 @@ impl<'a> FlowLayouter<'a> { } /// Layout a placed element. - fn layout_placed( + fn handle_place( &mut self, - engine: &mut Engine, - placed: &Packed, + placed: &'a Packed, styles: StyleChain, ) -> SourceResult<()> { + // Fetch properties. let float = placed.float(styles); let clearance = placed.clearance(styles); let alignment = placed.alignment(styles); let delta = Axes::new(placed.dx(styles), placed.dy(styles)).resolve(styles); + let x_align = alignment.map_or(FixedAlignment::Center, |align| { align.x().unwrap_or_default().resolve(styles) }); let y_align = alignment.map(|align| align.y().map(|y| y.resolve(styles))); + let mut frame = placed .layout( - engine, + self.engine, self.locator.next(&placed.span()), styles, self.regions.base(), )? .into_frame(); + frame.post_process(styles); - let item = FlowItem::Placed { frame, x_align, y_align, delta, float, clearance }; - self.layout_item(engine, item) + + self.handle_item(FlowItem::Placed { + frame, + x_align, + y_align, + delta, + float, + clearance, + }) } - /// Attach currently pending metadata to the frame. - fn drain_tag(&mut self, frame: &mut Frame) { - if !self.pending_tags.is_empty() && !frame.is_empty() { - frame.prepend_multiple( - self.pending_tags - .drain(..) - .map(|tag| (Point::zero(), FrameItem::Tag(tag))), - ); + /// Lays out all floating elements before continuing with other content. + fn handle_flush(&mut self, _: &'a Packed) -> SourceResult<()> { + for item in std::mem::take(&mut self.pending_floats) { + self.handle_item(item)?; } + while !self.pending_floats.is_empty() { + self.finish_region(false)?; + } + Ok(()) } /// Layout a finished frame. - fn layout_item( - &mut self, - engine: &mut Engine, - mut item: FlowItem, - ) -> SourceResult<()> { + fn handle_item(&mut self, mut item: FlowItem) -> SourceResult<()> { match item { FlowItem::Absolute(v, weak) => { if weak @@ -439,24 +448,24 @@ impl<'a> FlowLayouter<'a> { FlowItem::Frame { ref frame, movable, .. } => { let height = frame.height(); while !self.regions.size.y.fits(height) && !self.regions.in_last() { - self.finish_region(engine, false)?; + self.finish_region(false)?; } let in_last = self.regions.in_last(); self.regions.size.y -= height; if self.root && movable { let mut notes = Vec::new(); - find_footnotes(&mut notes, frame); + collect_footnotes(&mut notes, frame); self.items.push(item); // When we are already in_last, we can directly force the // footnotes. - if !self.handle_footnotes(engine, &mut notes, true, in_last)? { + if !self.handle_footnotes(&mut notes, true, in_last)? { let item = self.items.pop(); - self.finish_region(engine, false)?; + self.finish_region(false)?; self.items.extend(item); self.regions.size.y -= height; - self.handle_footnotes(engine, &mut notes, true, true)?; + self.handle_footnotes(&mut notes, true, true)?; } return Ok(()); } @@ -504,8 +513,8 @@ impl<'a> FlowLayouter<'a> { // Find footnotes in the frame. if self.root { let mut notes = vec![]; - find_footnotes(&mut notes, frame); - self.try_handle_footnotes(engine, notes)?; + collect_footnotes(&mut notes, frame); + self.try_handle_footnotes(notes)?; } } FlowItem::Footnote(_) => {} @@ -515,12 +524,49 @@ impl<'a> FlowLayouter<'a> { Ok(()) } + /// Attach currently pending metadata to the frame. + fn drain_tag(&mut self, frame: &mut Frame) { + if !self.pending_tags.is_empty() && !frame.is_empty() { + frame.prepend_multiple( + self.pending_tags + .drain(..) + .map(|tag| (Point::zero(), FrameItem::Tag(tag.clone()))), + ); + } + } + + /// Finisht the region, migrating all sticky items to the next one. + /// + /// Returns whether we migrated into a last region. + fn finish_region_with_migration(&mut self) -> SourceResult { + // Find the suffix of sticky items. + let mut sticky = self.items.len(); + for (i, item) in self.items.iter().enumerate().rev() { + match *item { + FlowItem::Absolute(_, _) => {} + FlowItem::Frame { sticky: true, .. } => sticky = i, + _ => break, + } + } + + let carry: Vec<_> = self.items.drain(sticky..).collect(); + self.finish_region(false)?; + + let in_last = self.regions.in_last(); + for item in carry { + self.handle_item(item)?; + } + + Ok(in_last) + } + /// Finish the frame for one region. /// /// Set `force` to `true` to allow creating a frame for out-of-flow elements /// only (this is used to force the creation of a frame in case the /// remaining elements are all out-of-flow). - fn finish_region(&mut self, engine: &mut Engine, force: bool) -> SourceResult<()> { + fn finish_region(&mut self, force: bool) -> SourceResult<()> { + // Early return if we don't have any relevant items. if !force && !self.items.is_empty() && self.items.iter().all(FlowItem::is_out_of_flow) @@ -585,6 +631,13 @@ impl<'a> FlowLayouter<'a> { size.y = self.initial.y; } + if !self.regions.size.x.is_finite() && self.expand.x { + bail!(self.flow.span(), "cannot expand into infinite width"); + } + if !self.regions.size.y.is_finite() && self.expand.y { + bail!(self.flow.span(), "cannot expand into infinite height"); + } + let mut output = Frame::soft(size); let mut ruler = FixedAlignment::Start; let mut float_top_offset = Abs::zero(); @@ -653,7 +706,9 @@ impl<'a> FlowLayouter<'a> { if force && !self.pending_tags.is_empty() { let pos = Point::with_y(offset); output.push_multiple( - self.pending_tags.drain(..).map(|tag| (pos, FrameItem::Tag(tag))), + self.pending_tags + .drain(..) + .map(|tag| (pos, FrameItem::Tag(tag.clone()))), ); } @@ -665,62 +720,42 @@ impl<'a> FlowLayouter<'a> { // Try to place floats into the next region. for item in std::mem::take(&mut self.pending_floats) { - self.layout_item(engine, item)?; - } - - Ok(()) - } - - /// Lays out all floating elements before continuing with other content. - fn flush(&mut self, engine: &mut Engine) -> SourceResult<()> { - for item in std::mem::take(&mut self.pending_floats) { - self.layout_item(engine, item)?; - } - while !self.pending_floats.is_empty() { - self.finish_region(engine, false)?; + self.handle_item(item)?; } Ok(()) } /// Finish layouting and return the resulting fragment. - fn finish(mut self, engine: &mut Engine) -> SourceResult { + fn finish(mut self) -> SourceResult { if self.expand.y { while !self.regions.backlog.is_empty() { - self.finish_region(engine, true)?; + self.finish_region(true)?; } } - self.finish_region(engine, true)?; + self.finish_region(true)?; while !self.items.is_empty() { - self.finish_region(engine, true)?; + self.finish_region(true)?; } Ok(Fragment::frames(self.finished)) } -} -impl FlowLayouter<'_> { /// Tries to process all footnotes in the frame, placing them /// in the next region if they could not be placed in the current /// one. fn try_handle_footnotes( &mut self, - engine: &mut Engine, mut notes: Vec>, ) -> SourceResult<()> { // When we are already in_last, we can directly force the // footnotes. if self.root - && !self.handle_footnotes( - engine, - &mut notes, - false, - self.regions.in_last(), - )? + && !self.handle_footnotes(&mut notes, false, self.regions.in_last())? { - self.finish_region(engine, false)?; - self.handle_footnotes(engine, &mut notes, false, true)?; + self.finish_region(false)?; + self.handle_footnotes(&mut notes, false, true)?; } Ok(()) } @@ -731,7 +766,6 @@ impl FlowLayouter<'_> { /// regions. fn handle_footnotes( &mut self, - engine: &mut Engine, notes: &mut Vec>, movable: bool, force: bool, @@ -750,16 +784,16 @@ impl FlowLayouter<'_> { } if !self.has_footnotes { - self.layout_footnote_separator(engine)?; + self.layout_footnote_separator()?; } self.regions.size.y -= self.footnote_config.gap; let frames = FootnoteEntry::new(notes[k].clone()) .pack() .layout( - engine, + self.engine, Locator::synthesize(notes[k].location().unwrap()), - self.styles, + *self.styles, self.regions.with_root(false), )? .into_frames(); @@ -780,10 +814,10 @@ impl FlowLayouter<'_> { let prev = notes.len(); for (i, frame) in frames.into_iter().enumerate() { - find_footnotes(notes, &frame); + collect_footnotes(notes, &frame); if i > 0 { - self.finish_region(engine, false)?; - self.layout_footnote_separator(engine)?; + self.finish_region(false)?; + self.layout_footnote_separator()?; self.regions.size.y -= self.footnote_config.gap; } self.regions.size.y -= frame.height(); @@ -804,14 +838,14 @@ impl FlowLayouter<'_> { } /// Layout and save the footnote separator, typically a line. - fn layout_footnote_separator(&mut self, engine: &mut Engine) -> SourceResult<()> { + fn layout_footnote_separator(&mut self) -> SourceResult<()> { let expand = Axes::new(self.regions.expand.x, false); let pod = Regions::one(self.regions.base(), expand); let separator = &self.footnote_config.separator; // FIXME: Shouldn't use `root()` here. let mut frame = separator - .layout(engine, Locator::root(), self.styles, pod)? + .layout(self.engine, Locator::root(), *self.styles, pod)? .into_frame(); frame.size_mut().y += self.footnote_config.clearance; frame.translate(Point::with_y(self.footnote_config.clearance)); @@ -824,11 +858,11 @@ impl FlowLayouter<'_> { } } -/// Finds all footnotes in the frame. -fn find_footnotes(notes: &mut Vec>, frame: &Frame) { +/// Collect all footnotes in a frame. +fn collect_footnotes(notes: &mut Vec>, frame: &Frame) { for (_, item) in frame.items() { match item { - FrameItem::Group(group) => find_footnotes(notes, &group.frame), + FrameItem::Group(group) => collect_footnotes(notes, &group.frame), FrameItem::Tag(tag) if !notes.iter().any(|note| note.location() == tag.elem.location()) => { diff --git a/crates/typst/src/layout/inline/collect.rs b/crates/typst/src/layout/inline/collect.rs index 79d0d59f6..b6a847f57 100644 --- a/crates/typst/src/layout/inline/collect.rs +++ b/crates/typst/src/layout/inline/collect.rs @@ -79,7 +79,7 @@ impl<'a> Item<'a> { } /// The natural layouted width of the item. - pub fn width(&self) -> Abs { + pub fn natural_width(&self) -> Abs { match self { Self::Text(shaped) => shaped.width, Self::Absolute(v, _) => *v, @@ -201,7 +201,7 @@ pub fn collect<'a>( ); let peeked = iter.peek().and_then(|(child, _)| { if let Some(elem) = child.to_packed::() { - elem.text().chars().next() + elem.text().chars().find(|c| !is_default_ignorable(*c)) } else if child.is::() { Some('"') } else if child.is::() @@ -302,7 +302,7 @@ impl<'a> Collector<'a> { } fn push_segment(&mut self, segment: Segment<'a>, is_quote: bool) { - if let Some(last) = self.full.chars().last() { + if let Some(last) = self.full.chars().rev().find(|c| !is_default_ignorable(*c)) { self.quoter.last(last, is_quote); } diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst/src/layout/inline/line.rs index 2473f958c..12162ab16 100644 --- a/crates/typst/src/layout/inline/line.rs +++ b/crates/typst/src/layout/inline/line.rs @@ -1,11 +1,18 @@ -use unicode_bidi::BidiInfo; +use std::fmt::{self, Debug, Formatter}; +use std::ops::{Deref, DerefMut}; use super::*; use crate::engine::Engine; -use crate::layout::{Abs, Em, Fr, Frame, FrameItem, Point}; -use crate::text::TextElem; +use crate::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; +use crate::text::{Lang, TextElem}; use crate::utils::Numeric; +const SHY: char = '\u{ad}'; +const HYPHEN: char = '-'; +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 @@ -16,20 +23,9 @@ use crate::utils::Numeric; /// line, respectively. But even those can partially reuse previous results when /// the break index is safe-to-break per rustybuzz. pub struct Line<'a> { - /// Bidi information about the paragraph. - pub bidi: &'a BidiInfo<'a>, - /// The trimmed range the line spans in the paragraph. - pub trimmed: Range, - /// The untrimmed end where the line ends. - pub end: usize, - /// A reshaped text item if the line sliced up a text item at the start. - pub first: Option>, - /// Inner items which don't need to be reprocessed. - pub inner: &'a [Item<'a>], - /// A reshaped text item if the line sliced up a text item at the end. If - /// there is only one text item, this takes precedence over `first`. - pub last: Option>, - /// The width of the line. + /// The items the line is made of. + pub items: Items<'a>, + /// The exact natural width of the line. pub width: Abs, /// Whether the line should be justified. pub justify: bool, @@ -39,45 +35,27 @@ pub struct Line<'a> { } impl<'a> Line<'a> { - /// Iterate over the line's items. - pub fn items(&self) -> impl Iterator> { - self.first.iter().chain(self.inner).chain(&self.last) - } - - /// Return items that intersect the given `text_range`. - pub fn slice(&self, text_range: Range) -> impl Iterator> { - let mut cursor = self.trimmed.start; - let mut start = 0; - let mut end = 0; - - for (i, item) in self.items().enumerate() { - if cursor <= text_range.start { - start = i; - } - - let len = item.textual_len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - } else { - break; - } - - cursor += len; + /// Create an empty line. + pub fn empty() -> Self { + Self { + items: Items::new(), + width: Abs::zero(), + justify: false, + dash: None, } - - self.items().skip(start).take(end - start) } /// How many glyphs are in the text where we can insert additional /// space when encountering underfull lines. pub fn justifiables(&self) -> usize { let mut count = 0; - for shaped in self.items().filter_map(Item::text) { + for shaped in self.items.iter().filter_map(Item::text) { count += shaped.justifiables(); } + // CJK character at line end should not be adjusted. if self - .items() + .items .last() .and_then(Item::text) .map(|s| s.cjk_justifiable_at_last()) @@ -89,19 +67,37 @@ impl<'a> Line<'a> { count } - /// How much can the line stretch + /// How much the line can stretch. pub fn stretchability(&self) -> Abs { - self.items().filter_map(Item::text).map(|s| s.stretchability()).sum() + self.items + .iter() + .filter_map(Item::text) + .map(|s| s.stretchability()) + .sum() } - /// How much can the line shrink + /// How much the line can shrink. pub fn shrinkability(&self) -> Abs { - self.items().filter_map(Item::text).map(|s| s.shrinkability()).sum() + self.items + .iter() + .filter_map(Item::text) + .map(|s| s.shrinkability()) + .sum() + } + + /// Whether the line has items with negative width. + 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(), + _ => false, + }) } /// The sum of fractions in the line. pub fn fr(&self) -> Fr { - self.items() + self.items + .iter() .filter_map(|item| match item { Item::Fractional(fr, _) => Some(*fr), _ => None, @@ -113,232 +109,299 @@ impl<'a> Line<'a> { /// A dash at the end of a line. #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum Dash { - /// A hyphen added to break a word. - SoftHyphen, - /// Regular hyphen, present in a compound word, e.g. beija-flor. - HardHyphen, - /// An em dash. - Long, - /// An en dash. - Short, + /// A soft hyphen added to break a word. + Soft, + /// A regular hyphen, present in a compound word, e.g. beija-flor. + Hard, + /// Another kind of dash. Only relevant for cost computation. + Other, } /// Create a line which spans the given range. pub fn line<'a>( engine: &Engine, p: &'a Preparation, - mut range: Range, + range: Range, breakpoint: Breakpoint, - prepend_hyphen: bool, + pred: Option<&Line>, ) -> Line<'a> { - let end = range.end; - let mut justify = - p.justify && end < p.bidi.text.len() && breakpoint != Breakpoint::Mandatory; + // The line's full text. + let full = &p.text[range.clone()]; + // Whether the line is justified. + let justify = full.ends_with(LINE_SEPARATOR) + || (p.justify && breakpoint != Breakpoint::Mandatory); + + // Process dashes. + let dash = if breakpoint == Breakpoint::Hyphen || full.ends_with(SHY) { + Some(Dash::Soft) + } else if full.ends_with(HYPHEN) { + Some(Dash::Hard) + } else if full.ends_with([EN_DASH, EM_DASH]) { + Some(Dash::Other) + } else { + None + }; + + // Trim the line at the end, if necessary for this breakpoint. + let trim = range.start + breakpoint.trim(full).len(); + + // Collect the items for the line. + 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 let Some(shaped) = items.first_text_mut() { + shaped.prepend_hyphen(engine, p.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); + } + } + + // Deal with CJ characters at line boundaries. + adjust_cj_at_line_boundaries(p, full, &mut items); + + // Compute the line's width. + let width = items.iter().map(Item::natural_width).sum(); + + Line { items, width, justify, dash } +} + +/// Collects / reshapes all items for the line with the given `range`. +/// +/// The `trim` defines an end position to which text items are trimmed. For +/// example, the `range` may span "hello\n", but the `trim` specifies that the +/// linebreak is trimmed. +/// +/// We do not factor the `trim` diredctly into the `range` because we still want +/// to keep non-text items after the trim (e.g. tags). +fn collect_items<'a>( + engine: &Engine, + p: &'a Preparation, + range: Range, + trim: usize, +) -> Items<'a> { + let mut items = Items::new(); + let mut fallback = None; + + // Collect the items for each consecutively ordered run. + reorder(p, range.clone(), |subrange, rtl| { + let from = items.len(); + collect_range(engine, p, subrange, trim, &mut items, &mut fallback); + if rtl { + items.reorder(from); + } + }); + + // Trim weak spacing at the start of the line. + let prefix = items + .iter() + .take_while(|item| matches!(item, Item::Absolute(_, true))) + .count(); + if prefix > 0 { + items.drain(..prefix); + } + + // Trim weak spacing at the end of the line. + while matches!(items.last(), Some(Item::Absolute(_, true))) { + items.pop(); + } + + // Add fallback text to expand the line height, if necessary. + if !items.iter().any(|item| matches!(item, Item::Text(_))) { + if let Some(fallback) = fallback { + items.push(fallback); + } + } + + items +} + +/// Calls `f` for the the BiDi-reordered ranges of a line. +fn reorder(p: &Preparation, range: Range, mut f: F) +where + F: FnMut(Range, bool), +{ + // If there is nothing bidirectional going on, skip reordering. + let Some(bidi) = &p.bidi else { + f(range, p.dir == Dir::RTL); + return; + }; + + // The bidi crate panics for empty lines. if range.is_empty() { - return Line { - bidi: &p.bidi, - end, - trimmed: range, - first: None, - inner: &[], - last: None, - width: Abs::zero(), - justify, - dash: None, + f(range, p.dir == Dir::RTL); + return; + } + + // Find the paragraph that contains the line. + let para = bidi + .paragraphs + .iter() + .find(|para| para.range.contains(&range.start)) + .unwrap(); + + // Compute the reordered ranges in visual order (left to right). + let (levels, runs) = bidi.visual_runs(para, range.clone()); + + // Call `f` for each run. + for run in runs { + let rtl = levels[run.start].is_rtl(); + f(run, rtl) + } +} + +/// Collects / reshapes all items for the given `subrange` with continous +/// direction. +fn collect_range<'a>( + engine: &Engine, + p: &'a Preparation, + range: Range, + trim: usize, + items: &mut Items<'a>, + fallback: &mut Option>, +) { + for (subrange, item) in p.slice(range.clone()) { + // All non-text items are just kept, they can't be split. + let Item::Text(shaped) = item else { + items.push(item); + continue; }; - } - // Slice out the relevant items. - let (mut expanded, mut inner) = p.slice(range.clone()); - let mut width = Abs::zero(); + // The intersection range of the item, the subrange, and the line's + // trimming. + let sliced = + range.start.max(subrange.start)..range.end.min(subrange.end).min(trim); - // Weak space (`Absolute(_, true)`) is removed at the end of the line - while let Some((Item::Absolute(_, true), before)) = inner.split_last() { - inner = before; - range.end -= 1; - expanded.end -= 1; - } - // Weak space (`Absolute(_, true)`) is removed at the beginning of the line - while let Some((Item::Absolute(_, true), after)) = inner.split_first() { - inner = after; - range.start += 1; - expanded.end += 1; - } + // Whether the item is split by the line. + let split = subrange.start < sliced.start || sliced.end < subrange.end; - // Reshape the last item if it's split in half or hyphenated. - let mut last = None; - let mut dash = None; - if let Some((Item::Text(shaped), before)) = inner.split_last() { - // Compute the range we want to shape, trimming whitespace at the - // end of the line. - let base = expanded.end - shaped.text.len(); - let start = range.start.max(base); - let text = &p.bidi.text[start..range.end]; - // U+200B ZERO WIDTH SPACE is used to provide a line break opportunity, - // we want to trim it too. - let trimmed = text.trim_end().trim_end_matches('\u{200B}'); - range.end = start + trimmed.len(); - - // Deal with hyphens, dashes and justification. - let shy = trimmed.ends_with('\u{ad}'); - let hyphen = breakpoint == Breakpoint::Hyphen; - dash = if hyphen || shy { - Some(Dash::SoftHyphen) - } else if trimmed.ends_with('-') { - Some(Dash::HardHyphen) - } else if trimmed.ends_with('โ€“') { - Some(Dash::Short) - } else if trimmed.ends_with('โ€”') { - Some(Dash::Long) + if sliced.is_empty() { + // When there is no text, still keep this as a fallback item, which + // we can use to force a non-zero line-height when the line doesn't + // contain any other text. + *fallback = Some(ItemEntry::from(Item::Text(shaped.empty()))); + } else if split { + // When the item is split in half, reshape it. + let reshaped = shaped.reshape(engine, sliced); + items.push(Item::Text(reshaped)); } else { - None - }; - justify |= text.ends_with('\u{2028}'); + // When the item is fully contained, just keep it. + items.push(item); + } + } +} - // Deal with CJK punctuation at line ends. - let gb_style = cjk_punct_style(shaped.lang, shaped.region); - let maybe_adjust_last_glyph = trimmed.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && trimmed.ends_with(is_of_cj_script)); +/// Add spacing around punctuation marks for CJ glyphs at line boundaries. +/// +/// See Requirements for Chinese Text Layout, Section 3.1.6.3 Compression of +/// 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)) + { + adjust_cj_at_line_start(p, items); + } - // Usually, we don't want to shape an empty string because: - // - We don't want the height of trimmed whitespace in a different font - // to be considered for the line height. - // - Even if it's in the same font, its unnecessary. + if text.ends_with(END_PUNCT_PAT) + || (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) + { + adjust_cj_at_line_end(p, items); + } +} + +/// Add spacing around punctuation marks for CJ glyphs at the line start. +fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { + let Some(shaped) = items.first_text_mut() else { return }; + let Some(glyph) = shaped.glyphs.first() else { return }; + + if glyph.is_cjk_right_aligned_punctuation() { + // If the first glyph is a CJK punctuation, we want to + // shrink it. + let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); + 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() { + // 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(); + let shrink = glyph.x_offset; + glyph.x_advance -= shrink; + glyph.x_offset = Em::zero(); + glyph.adjustability.shrinkability.0 = Em::zero(); + shaped.width -= shrink.at(shaped.size); + } +} + +/// Add spacing around punctuation marks for CJ glyphs at the line end. +fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { + let Some(shaped) = items.last_text_mut() else { return }; + let Some(glyph) = shaped.glyphs.last() else { return }; + + // Deal with CJK punctuation at line ends. + let style = cjk_punct_style(shaped.lang, shaped.region); + + if glyph.is_cjk_left_aligned_punctuation(style) { + // If the last glyph is a CJK punctuation, we want to + // shrink it. + let shrink = glyph.shrinkability().1; + 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 + && glyph.is_cj_script() + && (glyph.x_advance - glyph.x_offset) > Em::one() + { + // If the last glyph is a CJK character adjusted by + // [`add_cjk_latin_spacing`], restore the original width. + let shrink = glyph.x_advance - glyph.x_offset - Em::one(); + let glyph = shaped.glyphs.to_mut().last_mut().unwrap(); + glyph.x_advance -= shrink; + glyph.adjustability.shrinkability.1 = Em::zero(); + shaped.width -= shrink.at(shaped.size); + } +} + +/// Whether a hyphen should be inserted at the start of the next line. +fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { + // If the predecessor line does not end with a `Dash::Hard`, we shall + // not place a hyphen at the start of the next line. + if pred_line.dash != Some(Dash::Hard) { + return false; + } + + // The hyphen should repeat only in the languages that require that feature. + // For more information see the discussion at https://github.com/typst/typst/issues/3235 + let Some(Item::Text(shaped)) = pred_line.items.last() else { return false }; + + match shaped.lang { + // - Lower Sorbian: see https://dolnoserbski.de/ortografija/psawidla/K3 + // - Czech: see https://prirucka.ujc.cas.cz/?id=164 + // - Croatian: see http://pravopis.hr/pravilo/spojnica/68/ + // - Polish: see https://www.ortograf.pl/zasady-pisowni/lacznik-zasady-pisowni + // - Portuguese: see https://www2.senado.leg.br/bdsf/bitstream/handle/id/508145/000997415.pdf (Base XX) + // - Slovak: see https://www.zones.sk/studentske-prace/gramatika/10620-pravopis-rozdelovanie-slov/ + Lang::LOWER_SORBIAN + | Lang::CZECH + | Lang::CROATIAN + | Lang::POLISH + | Lang::PORTUGUESE + | Lang::SLOVAK => true, + + // In Spanish the hyphen is required only if the word next to hyphen is + // not capitalized. Otherwise, the hyphen must not be repeated. // - // There is one exception though. When the whole line is empty, we need - // the shaped empty string to make the line the appropriate height. That - // is the case exactly if the string is empty and there are no other - // items in the line. - if hyphen - || start + shaped.text.len() > range.end - || maybe_adjust_last_glyph - || prepend_hyphen - { - if hyphen || start < range.end || before.is_empty() { - let mut reshaped = shaped.reshape(engine, &p.spans, start..range.end); - if hyphen || shy { - reshaped.push_hyphen(engine, p.fallback); - } + // 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()), - if let Some(last_glyph) = reshaped.glyphs.last() { - if last_glyph.is_cjk_left_aligned_punctuation(gb_style) { - // If the last glyph is a CJK punctuation, we want to - // shrink it. See Requirements for Chinese Text Layout, - // Section 3.1.6.3 Compression of punctuation marks at - // line start or line end - let shrink_amount = last_glyph.shrinkability().1; - let punct = reshaped.glyphs.to_mut().last_mut().unwrap(); - punct.shrink_right(shrink_amount); - reshaped.width -= shrink_amount.at(reshaped.size); - } else if p.cjk_latin_spacing - && last_glyph.is_cj_script() - && (last_glyph.x_advance - last_glyph.x_offset) > Em::one() - { - // If the last glyph is a CJK character adjusted by - // [`add_cjk_latin_spacing`], restore the original - // width. - let shrink_amount = - last_glyph.x_advance - last_glyph.x_offset - Em::one(); - let glyph = reshaped.glyphs.to_mut().last_mut().unwrap(); - glyph.x_advance -= shrink_amount; - glyph.adjustability.shrinkability.1 = Em::zero(); - reshaped.width -= shrink_amount.at(reshaped.size); - } - } - - width += reshaped.width; - last = Some(Item::Text(reshaped)); - } - - inner = before; - } - } - - // Deal with CJ characters at line starts. - let text = &p.bidi.text[range.start..end]; - let maybe_adjust_first_glyph = text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)); - - // Reshape the start item if it's split in half. - let mut first = None; - if let Some((Item::Text(shaped), after)) = inner.split_first() { - // Compute the range we want to shape. - let base = expanded.start; - let end = range.end.min(base + shaped.text.len()); - - // Reshape if necessary. - if range.start + shaped.text.len() > end - || maybe_adjust_first_glyph - || prepend_hyphen - { - // If the range is empty, we don't want to push an empty text item. - if range.start < end { - let reshaped = shaped.reshape(engine, &p.spans, range.start..end); - width += reshaped.width; - first = Some(Item::Text(reshaped)); - } - - inner = after; - } - } - - if prepend_hyphen { - let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut); - if let Some(reshaped) = reshaped { - let width_before = reshaped.width; - reshaped.prepend_hyphen(engine, p.fallback); - width += reshaped.width - width_before; - } - } - - if maybe_adjust_first_glyph { - let reshaped = first.as_mut().or(last.as_mut()).and_then(Item::text_mut); - if let Some(reshaped) = reshaped { - if let Some(first_glyph) = reshaped.glyphs.first() { - if first_glyph.is_cjk_right_aligned_punctuation() { - // If the first glyph is a CJK punctuation, we want to - // shrink it. - let shrink_amount = first_glyph.shrinkability().0; - let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); - glyph.shrink_left(shrink_amount); - let amount_abs = shrink_amount.at(reshaped.size); - reshaped.width -= amount_abs; - width -= amount_abs; - } else if p.cjk_latin_spacing - && first_glyph.is_cj_script() - && first_glyph.x_offset > Em::zero() - { - // If the first glyph is a CJK character adjusted by - // [`add_cjk_latin_spacing`], restore the original width. - let shrink_amount = first_glyph.x_offset; - let glyph = reshaped.glyphs.to_mut().first_mut().unwrap(); - glyph.x_advance -= shrink_amount; - glyph.x_offset = Em::zero(); - glyph.adjustability.shrinkability.0 = Em::zero(); - let amount_abs = shrink_amount.at(reshaped.size); - reshaped.width -= amount_abs; - width -= amount_abs; - } - } - } - } - - // Measure the inner items. - for item in inner { - width += item.width(); - } - - Line { - bidi: &p.bidi, - trimmed: range, - end, - first, - inner, - last, - width, - justify, - dash, + _ => false, } } @@ -354,18 +417,19 @@ pub fn commit( let mut remaining = width - line.width - p.hang; let mut offset = Abs::zero(); - // Reorder the line from logical to visual order. - let (reordered, starts_rtl) = reorder(line); - if !starts_rtl { + // 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 + // hanging indent arises naturally due to the line width. + if p.dir == Dir::LTR { offset += p.hang; } // Handle hanging punctuation to the left. - if let Some(Item::Text(text)) = reordered.first() { + if let Some(Item::Text(text)) = line.items.first() { if let Some(glyph) = text.glyphs.first() { if !text.dir.is_positive() && TextElem::overhang_in(text.styles) - && (reordered.len() > 1 || text.glyphs.len() > 1) + && (line.items.len() > 1 || text.glyphs.len() > 1) { let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); offset -= amount; @@ -375,11 +439,11 @@ pub fn commit( } // Handle hanging punctuation to the right. - if let Some(Item::Text(text)) = reordered.last() { + if let Some(Item::Text(text)) = line.items.last() { if let Some(glyph) = text.glyphs.last() { if text.dir.is_positive() && TextElem::overhang_in(text.styles) - && (reordered.len() > 1 || text.glyphs.len() > 1) + && (line.items.len() > 1 || text.glyphs.len() > 1) { let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); remaining += amount; @@ -397,16 +461,16 @@ pub fn commit( let mut extra_justification = Abs::zero(); let shrinkability = line.shrinkability(); - let stretch = line.stretchability(); + let stretchability = line.stretchability(); if remaining < Abs::zero() && shrinkability > Abs::zero() && shrink { // Attempt to reduce the length of the line, using shrinkability. justification_ratio = (remaining / shrinkability).max(-1.0); remaining = (remaining + shrinkability).min(Abs::zero()); } else if line.justify && fr.is_zero() { // Attempt to increase the length of the line, using stretchability. - if stretch > Abs::zero() { - justification_ratio = (remaining / stretch).min(1.0); - remaining = (remaining - stretch).max(Abs::zero()); + if stretchability > Abs::zero() { + justification_ratio = (remaining / stretchability).min(1.0); + remaining = (remaining - stretchability).max(Abs::zero()); } let justifiables = line.justifiables(); @@ -422,7 +486,7 @@ pub fn commit( // Build the frames and determine the height and baseline. let mut frames = vec![]; - for item in reordered { + for item in line.items.iter() { let mut push = |offset: &mut Abs, frame: Frame| { let width = frame.width(); top.set_max(frame.baseline()); @@ -449,8 +513,12 @@ pub fn commit( } } Item::Text(shaped) => { - let mut frame = - shaped.build(engine, justification_ratio, extra_justification); + let mut frame = shaped.build( + engine, + &p.spans, + justification_ratio, + extra_justification, + ); frame.post_process(shaped.styles); push(&mut offset, frame); } @@ -488,46 +556,6 @@ pub fn commit( Ok(output) } -/// Return a line's items in visual order. -fn reorder<'a>(line: &'a Line<'a>) -> (Vec<&Item<'a>>, bool) { - let mut reordered = vec![]; - - // The bidi crate doesn't like empty lines. - if line.trimmed.is_empty() { - return (line.slice(line.trimmed.clone()).collect(), false); - } - - // Find the paragraph that contains the line. - let para = line - .bidi - .paragraphs - .iter() - .find(|para| para.range.contains(&line.trimmed.start)) - .unwrap(); - - // Compute the reordered ranges in visual order (left to right). - let (levels, runs) = line.bidi.visual_runs(para, line.trimmed.clone()); - let starts_rtl = levels.first().is_some_and(|level| level.is_rtl()); - - // Collect the reordered items. - for run in runs { - // Skip reset L1 runs because handling them would require reshaping - // again in some cases. - if line.bidi.levels[run.start] != levels[run.start] { - continue; - } - - let prev = reordered.len(); - reordered.extend(line.slice(run.clone())); - - if levels[run.start].is_rtl() { - reordered[prev..].reverse(); - } - } - - (reordered, starts_rtl) -} - /// How much a character should hang into the end margin. /// /// For more discussion, see: @@ -548,3 +576,119 @@ fn overhang(c: char) -> f64 { _ => 0.0, } } + +/// A collection of owned or borrowed paragraph items. +pub struct Items<'a>(Vec>); + +impl<'a> Items<'a> { + /// Create empty items. + pub fn new() -> Self { + Self(vec![]) + } + + /// Push a new item. + pub fn push(&mut self, entry: impl Into>) { + self.0.push(entry.into()); + } + + /// Iterate over the items + pub fn iter(&self) -> impl Iterator> { + self.0.iter().map(|item| &**item) + } + + /// Access the first item. + pub fn first(&self) -> Option<&Item<'a>> { + self.0.first().map(|item| &**item) + } + + /// Access the last item. + pub fn last(&self) -> Option<&Item<'a>> { + self.0.last().map(|item| &**item) + } + + /// Access the first item mutably, if it is text. + pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { + self.0.first_mut()?.text_mut() + } + + /// Access the last item mutably, if it is text. + pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { + self.0.last_mut()?.text_mut() + } + + /// Reorder the items starting at the given index to RTL. + pub fn reorder(&mut self, from: usize) { + self.0[from..].reverse() + } +} + +impl<'a> FromIterator> for Items<'a> { + fn from_iter>>(iter: I) -> Self { + Self(iter.into_iter().collect()) + } +} + +impl<'a> Deref for Items<'a> { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> DerefMut for Items<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// A reference to or a boxed item. +pub enum ItemEntry<'a> { + Ref(&'a Item<'a>), + Box(Box>), +} + +impl<'a> ItemEntry<'a> { + fn text_mut(&mut self) -> Option<&mut ShapedText<'a>> { + match self { + Self::Ref(item) => { + let text = item.text()?; + *self = Self::Box(Box::new(Item::Text(text.clone()))); + match self { + Self::Box(item) => item.text_mut(), + _ => unreachable!(), + } + } + Self::Box(item) => item.text_mut(), + } + } +} + +impl<'a> Deref for ItemEntry<'a> { + type Target = Item<'a>; + + fn deref(&self) -> &Self::Target { + match self { + Self::Ref(item) => item, + Self::Box(item) => item, + } + } +} + +impl Debug for ItemEntry<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl<'a> From<&'a Item<'a>> for ItemEntry<'a> { + fn from(item: &'a Item<'a>) -> Self { + Self::Ref(item) + } +} + +impl<'a> From> for ItemEntry<'a> { + fn from(item: Item<'a>) -> Self { + Self::Box(Box::new(item)) + } +} diff --git a/crates/typst/src/layout/inline/linebreak.rs b/crates/typst/src/layout/inline/linebreak.rs index ddf7937bf..075d24b33 100644 --- a/crates/typst/src/layout/inline/linebreak.rs +++ b/crates/typst/src/layout/inline/linebreak.rs @@ -1,42 +1,61 @@ -use icu_properties::maps::CodePointMapData; +use std::ops::{Add, Sub}; + +use icu_properties::maps::{CodePointMapData, CodePointMapDataBorrowed}; +use icu_properties::sets::CodePointSetData; use icu_properties::LineBreak; use icu_provider::AsDeserializingBufferProvider; use icu_provider_adapters::fork::ForkByKeyProvider; use icu_provider_blob::BlobDataProvider; use icu_segmenter::LineSegmenter; use once_cell::sync::Lazy; +use unicode_segmentation::UnicodeSegmentation; use super::*; use crate::engine::Engine; -use crate::layout::Abs; +use crate::layout::{Abs, Em}; use crate::model::Linebreaks; use crate::syntax::link_prefix; use crate::text::{Lang, TextElem}; +/// The cost of a line or paragraph layout. +type Cost = f64; + +// Cost parameters. +const DEFAULT_HYPH_COST: Cost = 0.5; +const DEFAULT_RUNT_COST: Cost = 0.5; +const CONSECUTIVE_DASH_COST: Cost = 0.3; +const MAX_COST: Cost = 1_000_000.0; +const MIN_RATIO: f64 = -1.0; +const MIN_APPROX_RATIO: f64 = -0.5; +const BOUND_EPS: f64 = 1e-3; + +/// The ICU blob data. +fn blob() -> BlobDataProvider { + BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap() +} + /// The general line break segmenter. -static SEGMENTER: Lazy = Lazy::new(|| { - let provider = - BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap(); - LineSegmenter::try_new_lstm_with_buffer_provider(&provider).unwrap() -}); +static SEGMENTER: Lazy = + Lazy::new(|| LineSegmenter::try_new_lstm_with_buffer_provider(&blob()).unwrap()); /// The line break segmenter for Chinese/Japanese text. static CJ_SEGMENTER: Lazy = Lazy::new(|| { - let provider = - BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap(); let cj_blob = BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU_CJ_SEGMENT) .unwrap(); - let cj_provider = ForkByKeyProvider::new(cj_blob, provider); + let cj_provider = ForkByKeyProvider::new(cj_blob, blob()); LineSegmenter::try_new_lstm_with_buffer_provider(&cj_provider).unwrap() }); /// The Unicode line break properties for each code point. static LINEBREAK_DATA: Lazy> = Lazy::new(|| { - let provider = - BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU).unwrap(); - let deser_provider = provider.as_deserializing(); - icu_properties::maps::load_line_break(&deser_provider).unwrap() + icu_properties::maps::load_line_break(&blob().as_deserializing()).unwrap() +}); + +/// The set of Unicode default ignorables. +static DEFAULT_IGNORABLE_DATA: Lazy = Lazy::new(|| { + icu_properties::sets::load_default_ignorable_code_point(&blob().as_deserializing()) + .unwrap() }); /// A line break opportunity. @@ -50,6 +69,37 @@ pub enum Breakpoint { Hyphen, } +impl Breakpoint { + /// Trim a line before this breakpoint. + pub fn trim(self, line: &str) -> &str { + // Trim default ignorables. + let ignorable = DEFAULT_IGNORABLE_DATA.as_borrowed(); + let line = line.trim_end_matches(|c| ignorable.contains(c)); + + match self { + // Trim whitespace. + Self::Normal => line.trim_end_matches(char::is_whitespace), + + // Trim linebreaks. + Self::Mandatory => { + let lb = LINEBREAK_DATA.as_borrowed(); + line.trim_end_matches(|c| { + matches!( + lb.get(c), + LineBreak::MandatoryBreak + | LineBreak::CarriageReturn + | LineBreak::LineFeed + | LineBreak::NextLine + ) + }) + } + + // Trim nothing further. + Self::Hyphen => line, + } + } +} + /// Breaks the paragraph into lines. pub fn linebreak<'a>( engine: &Engine, @@ -84,10 +134,8 @@ fn linebreak_simple<'a>( let mut last = None; breakpoints(p, |end, breakpoint| { - let prepend_hyphen = lines.last().map(should_repeat_hyphen).unwrap_or(false); - // Compute the line and its size. - let mut attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); + let mut attempt = line(engine, p, start..end, breakpoint, lines.last()); // If the line doesn't fit anymore, we push the last fitting attempt // into the stack and rebuild the line from the attempt's end. The @@ -96,7 +144,7 @@ fn linebreak_simple<'a>( if let Some((last_attempt, last_end)) = last.take() { lines.push(last_attempt); start = last_end; - attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); + attempt = line(engine, p, start..end, breakpoint, lines.last()); } } @@ -142,144 +190,140 @@ fn linebreak_optimized<'a>( p: &'a Preparation<'a>, width: Abs, ) -> Vec> { - /// The cost of a line or paragraph layout. - type Cost = f64; + let metrics = CostMetrics::compute(p); - /// An entry in the dynamic programming table. + // Determines the exact costs of a likely good layout through Knuth-Plass + // with approximate metrics. We can use this cost as an upper bound to prune + // the search space in our proper optimization pass below. + let upper_bound = linebreak_optimized_approximate(engine, p, width, &metrics); + + // Using the upper bound, perform exact optimized linebreaking. + linebreak_optimized_bounded(engine, p, width, &metrics, upper_bound) +} + +/// Performs line breaking in optimized Knuth-Plass style, but with an upper +/// bound on the cost. This allows us to skip many parts of the search space. +#[typst_macros::time] +fn linebreak_optimized_bounded<'a>( + engine: &Engine, + p: &'a Preparation<'a>, + width: Abs, + metrics: &CostMetrics, + upper_bound: Cost, +) -> Vec> { + /// An entry in the dynamic programming table for paragraph optimization. struct Entry<'a> { pred: usize, total: Cost, line: Line<'a>, + end: usize, } - // Cost parameters. - const DEFAULT_HYPH_COST: Cost = 0.5; - const DEFAULT_RUNT_COST: Cost = 0.5; - const CONSECUTIVE_DASH_COST: Cost = 0.3; - const MAX_COST: Cost = 1_000_000.0; - const MIN_RATIO: f64 = -1.0; - - let hyph_cost = DEFAULT_HYPH_COST * p.costs.hyphenation().get(); - let runt_cost = DEFAULT_RUNT_COST * p.costs.runt().get(); - // Dynamic programming table. - let mut active = 0; - let mut table = vec![Entry { - pred: 0, - total: 0.0, - line: line(engine, p, 0..0, Breakpoint::Mandatory, false), - }]; + let mut table = vec![Entry { pred: 0, total: 0.0, line: Line::empty(), end: 0 }]; + + let mut active = 0; + let mut prev_end = 0; - let em = p.size; - let mut lines = Vec::with_capacity(16); breakpoints(p, |end, breakpoint| { - let k = table.len(); - let is_end = end == p.bidi.text.len(); + // Find the optimal predecessor. let mut best: Option = None; - // Find the optimal predecessor. - for (i, pred) in table.iter().enumerate().skip(active) { - // Layout the line. - let start = pred.line.end; - let prepend_hyphen = should_repeat_hyphen(&pred.line); + // A lower bound for the cost of all following line attempts. + let mut line_lower_bound = None; - let attempt = line(engine, p, start..end, breakpoint, prepend_hyphen); + for (pred_index, pred) in table.iter().enumerate().skip(active) { + let start = pred.end; + let unbreakable = prev_end == start; - // Determine how much the line's spaces would need to be stretched - // to make it the desired width. - let delta = width - attempt.width; - // Determine how much stretch are permitted. - let adjust = if delta >= Abs::zero() { - attempt.stretchability() - } else { - attempt.shrinkability() - }; - // Ideally, the ratio should between -1.0 and 1.0, but sometimes a - // value above 1.0 is possible, in which case the line is underfull. - let mut ratio = delta / adjust; - if ratio.is_nan() { - // The line is not stretchable, but it just fits. This often - // happens with monospace fonts and CJK texts. - ratio = 0.0; - } - if ratio > 1.0 { - // We should stretch the line above its stretchability. Now - // calculate the extra amount. Also, don't divide by zero. - let extra_stretch = - (delta - adjust) / attempt.justifiables().max(1) as f64; - // Normalize the amount by half Em size. - ratio = 1.0 + extra_stretch / (em / 2.0); + // If the minimum cost we've established for the line is already + // too much, skip this attempt. + if line_lower_bound + .is_some_and(|lower| pred.total + lower > upper_bound + BOUND_EPS) + { + continue; } - // Determine the cost of the line. - let min_ratio = if p.justify { MIN_RATIO } else { 0.0 }; - let mut cost = if ratio < min_ratio { - // The line is overfull. This is the case if - // - justification is on, but we'd need to shrink too much - // - justification is off and the line just doesn't fit - // - // If this is the earliest breakpoint in the active set - // (active == i), remove it from the active set. If there is an - // earlier one (active < i), then the logically shorter line was - // in fact longer (can happen with negative spacing) and we - // can't trim the active set just yet. - if active == i { - active += 1; - } - MAX_COST - } else if breakpoint == Breakpoint::Mandatory || is_end { - // This is a mandatory break and the line is not overfull, so - // all breakpoints before this one become inactive since no line - // can span above the mandatory break. - active = k; - // - If ratio > 0, we need to stretch the line only when justify - // is needed. - // - If ratio < 0, we always need to shrink the line. - if (ratio > 0.0 && attempt.justify) || ratio < 0.0 { - ratio.powi(3).abs() - } else { - 0.0 - } - } else { - // Normal line with cost of |ratio^3|. - ratio.powi(3).abs() - }; + // Build the line. + let attempt = line(engine, p, start..end, breakpoint, Some(&pred.line)); - // Penalize runts. - if k == i + 1 && is_end { - cost += runt_cost; - } + // Determine the cost of the line and its stretch ratio. + let (line_ratio, line_cost) = ratio_and_cost( + p, + metrics, + width, + &pred.line, + &attempt, + end, + breakpoint, + unbreakable, + ); - // Penalize hyphens. - if breakpoint == Breakpoint::Hyphen { - cost += hyph_cost; - } - - // In Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, - // where r is the ratio, p=50 is the penalty, and a=3000 is - // consecutive the penalty. We divide the whole formula by 10, - // resulting (0.01 + |r|^3 + p)^2 + a, where p=0.5 and a=0.3 - cost = (0.01 + cost).powi(2); - - // Penalize two consecutive dashes (not necessarily hyphens) extra. - if attempt.dash.is_some() && pred.line.dash.is_some() { - cost += CONSECUTIVE_DASH_COST; + // If the line is overfull, we adjust the set of active candidate + // line starts. This is the case if + // - justification is on, but we'd need to shrink too much + // - justification is off and the line just doesn't fit + // + // If this is the earliest breakpoint in the active set + // (active == i), remove it from the active set. If there is an + // earlier one (active < i), then the logically shorter line was + // in fact longer (can happen with negative spacing) and we + // can't trim the active set just yet. + if line_ratio < metrics.min_ratio && active == pred_index { + active += 1; } // The total cost of this line and its chain of predecessors. - let total = pred.total + cost; + let total = pred.total + line_cost; + + // If the line is already underfull (`line_ratio > 0`), any shorter + // slice of the line will be even more underfull. So it'll only get + // worse from here and further attempts would also have a cost + // exceeding `bound`. There is one exception: When the line has + // negative spacing, we can't know for sure, so we don't assign the + // lower bound in that case. + if line_ratio > 0.0 + && line_lower_bound.is_none() + && !attempt.has_negative_width_items() + { + line_lower_bound = Some(line_cost); + } + + // If the cost already exceeds the upper bound, we don't need to + // integrate this result into the table. + if total > upper_bound + BOUND_EPS { + continue; + } // If this attempt is better than what we had before, take it! if best.as_ref().map_or(true, |best| best.total >= total) { - best = Some(Entry { pred: i, total, line: attempt }); + best = Some(Entry { pred: pred_index, total, line: attempt, end }); } } - table.push(best.unwrap()); + // If this is a mandatory break, all breakpoints before this one become + // inactive since no line can span over the mandatory break. + if breakpoint == Breakpoint::Mandatory { + active = table.len(); + } + + table.extend(best); + prev_end = end; }); // Retrace the best path. + let mut lines = Vec::with_capacity(16); let mut idx = table.len() - 1; + + // 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"); + + #[cfg(not(debug_assertions))] + return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY); + } + while idx != 0 { table.truncate(idx + 1); let entry = table.pop().unwrap(); @@ -291,6 +335,293 @@ fn linebreak_optimized<'a>( lines } +/// Runs the normal Knuth-Plass algorithm, but instead of building proper lines +/// (which is costly) to determine costs, it determines approximate costs using +/// cummulative arrays. +/// +/// This results in a likely good paragraph 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] +fn linebreak_optimized_approximate( + engine: &Engine, + p: &Preparation, + width: Abs, + metrics: &CostMetrics, +) -> Cost { + // Determine the cummulative estimation metrics. + let estimates = Estimates::compute(p); + + /// An entry in the dynamic programming table for paragraph optimization. + struct Entry { + pred: usize, + total: Cost, + end: usize, + unbreakable: bool, + breakpoint: Breakpoint, + } + + // Dynamic programming table. + let mut table = vec![Entry { + pred: 0, + total: 0.0, + end: 0, + unbreakable: false, + breakpoint: Breakpoint::Mandatory, + }]; + + let mut active = 0; + let mut prev_end = 0; + + breakpoints(p, |end, breakpoint| { + let at_end = end == p.text.len(); + + // Find the optimal predecessor. + let mut best: Option = None; + for (pred_index, pred) in table.iter().enumerate().skip(active) { + let start = pred.end; + let unbreakable = prev_end == start; + + // Whether the line is justified. This is not 100% accurate w.r.t + // to line()'s behaviour, but good enough. + let justify = p.justify && !at_end && 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 + // just an estimate. + let consecutive_dash = + pred.breakpoint == Breakpoint::Hyphen && breakpoint == Breakpoint::Hyphen; + + // Estimate how much the line's spaces would need to be stretched to + // make it the desired width. We trim at the end to not take into + // account trailing spaces. This is, again, only an approximation of + // the real behaviour of `line`. + let trimmed_end = start + p.text[start..end].trim_end().len(); + let line_ratio = raw_ratio( + p, + width, + estimates.widths.estimate(start..trimmed_end) + + if breakpoint == Breakpoint::Hyphen { + metrics.approx_hyphen_width + } else { + Abs::zero() + }, + estimates.stretchability.estimate(start..trimmed_end), + estimates.shrinkability.estimate(start..trimmed_end), + estimates.justifiables.estimate(start..trimmed_end), + ); + + // Determine the line's cost. + let line_cost = raw_cost( + metrics, + breakpoint, + line_ratio, + at_end, + justify, + unbreakable, + consecutive_dash, + true, + ); + + // Adjust the set of active breakpoints. + // See `linebreak_optimized` for details. + if line_ratio < metrics.min_ratio && active == pred_index { + active += 1; + } + + // The total cost of this line and its chain of predecessors. + 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) { + best = Some(Entry { + pred: pred_index, + total, + end, + unbreakable, + breakpoint, + }); + } + } + + // If this is a mandatory break, all breakpoints before this one become + // inactive. + if breakpoint == Breakpoint::Mandatory { + active = table.len(); + } + + table.extend(best); + prev_end = end; + }); + + // Retrace the best path. + let mut indices = Vec::with_capacity(16); + let mut idx = table.len() - 1; + while idx != 0 { + indices.push(idx); + idx = table[idx].pred; + } + + let mut pred = Line::empty(); + let mut start = 0; + let mut exact = 0.0; + + // The cost that we optimized was only an approximate cost, so the layout we + // got here is only likely to be good, not guaranteed to be the best. We now + // computes its exact cost as that gives us a sound upper bound for the + // proper optimization pass. + for idx in indices.into_iter().rev() { + let Entry { end, breakpoint, unbreakable, .. } = table[idx]; + + let attempt = line(engine, p, start..end, breakpoint, Some(&pred)); + + let (_, line_cost) = ratio_and_cost( + p, + metrics, + width, + &pred, + &attempt, + end, + breakpoint, + unbreakable, + ); + + pred = attempt; + start = end; + exact += line_cost; + } + + exact +} + +/// Compute the stretch ratio and cost of a line. +#[allow(clippy::too_many_arguments)] +fn ratio_and_cost( + p: &Preparation, + metrics: &CostMetrics, + available_width: Abs, + pred: &Line, + attempt: &Line, + end: usize, + breakpoint: Breakpoint, + unbreakable: bool, +) -> (f64, Cost) { + let ratio = raw_ratio( + p, + available_width, + attempt.width, + attempt.stretchability(), + attempt.shrinkability(), + attempt.justifiables(), + ); + + let cost = raw_cost( + metrics, + breakpoint, + ratio, + end == p.text.len(), + attempt.justify, + unbreakable, + pred.dash.is_some() && attempt.dash.is_some(), + false, + ); + + (ratio, cost) +} + +/// Determine the stretch ratio for a line given raw metrics. +fn raw_ratio( + p: &Preparation, + available_width: Abs, + line_width: Abs, + stretchability: Abs, + shrinkability: Abs, + justifiables: usize, +) -> f64 { + // Determine how much the line's spaces would need to be stretched + // to make it the desired width. + let delta = available_width - line_width; + + // Determine how much stretch is permitted. + let adjust = if delta >= Abs::zero() { stretchability } else { shrinkability }; + + // Ideally, the ratio should between -1.0 and 1.0. + // + // A ratio above 1.0 is possible for an underfull line, but a ratio below + // -1.0 is forbidden because the line would overflow. + let mut ratio = delta / adjust; + + // The line is not stretchable, but it just fits. This often happens with + // monospace fonts and CJK texts. + if ratio.is_nan() { + ratio = 0.0; + } + + if ratio > 1.0 { + // We should stretch the line above its stretchability. Now + // calculate the extra amount. Also, don't divide by zero. + let extra_stretch = (delta - adjust) / justifiables.max(1) as f64; + // Normalize the amount by half the em size. + ratio = 1.0 + extra_stretch / (p.size / 2.0); + } + + ratio +} + +/// Compute the cost of a line given raw metrics. +#[allow(clippy::too_many_arguments)] +fn raw_cost( + metrics: &CostMetrics, + breakpoint: Breakpoint, + ratio: f64, + at_end: bool, + justify: bool, + unbreakable: bool, + consecutive_dash: bool, + approx: bool, +) -> Cost { + // Determine the cost of the line. + let mut cost = if ratio < metrics.min_ratio(approx) { + // Overfull line always has maximum cost. + MAX_COST + } else if breakpoint == Breakpoint::Mandatory || at_end { + // - If ratio < 0, we always need to shrink the line (even the last one). + // - If ratio > 0, we need to stretch the line only when it is justified + // (last line is not justified by default even if `p.justify` is true). + if ratio < 0.0 || (ratio > 0.0 && justify) { + ratio.powi(3).abs() + } else { + 0.0 + } + } else { + // Normal line with cost of |ratio^3|. + ratio.powi(3).abs() + }; + + // Penalize runts (lone words in the last line). + if unbreakable && at_end { + cost += metrics.runt_cost; + } + + // Penalize hyphenation. + if breakpoint == Breakpoint::Hyphen { + cost += metrics.hyph_cost; + } + + // In the Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, + // where r is the ratio, p=50 is the penalty, and a=3000 is + // consecutive the penalty. We divide the whole formula by 10, + // resulting (0.01 + |r|^3 + p)^2 + a, where p=0.5 and a=0.3 + let mut cost = (0.01 + cost).powi(2); + + // Penalize two consecutive dashes (not necessarily hyphens) extra. + if consecutive_dash { + cost += CONSECUTIVE_DASH_COST; + } + + cost +} + /// Calls `f` for all possible points in the text where lines can broken. /// /// Yields for each breakpoint the text index, whether the break is mandatory @@ -300,8 +631,15 @@ fn linebreak_optimized<'a>( /// This is an internal instead of an external iterator because it makes the /// code much simpler and the consumers of this function don't need the /// composability and flexibility of external iteration anyway. -fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint)) { - let text = p.bidi.text; +fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { + let text = p.text; + + // Single breakpoint at the end for empty text. + if text.is_empty() { + f(0, Breakpoint::Mandatory); + return; + } + let hyphenate = p.hyphenate != Some(false); let lb = LINEBREAK_DATA.as_borrowed(); let segmenter = match p.lang { @@ -324,7 +662,7 @@ fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint)) } } - // Get the UAX #14 linebreak opportunities. + // Get the next UAX #14 linebreak opportunity. let Some(point) = iter.next() else { break }; // Skip breakpoint if there is no char before it. icu4x generates one @@ -349,46 +687,13 @@ fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint)) }; // Hyphenate between the last and current breakpoint. - 'hyphenate: { - if !hyphenate { - break 'hyphenate; - } - - // Extract a hyphenatable "word". - let word = &text[last..point].trim_end_matches(|c: char| !c.is_alphabetic()); - if word.is_empty() { - break 'hyphenate; - } - - let end = last + word.len(); + if hyphenate { let mut offset = last; - - // Determine the language to hyphenate this word in. - let Some(lang) = lang_at(p, last) else { break 'hyphenate }; - - for syllable in hypher::hyphenate(word, lang) { - // Don't hyphenate after the final syllable. - offset += syllable.len(); - if offset == end { - continue; + for segment in text[last..point].split_word_bounds() { + if !segment.is_empty() && segment.chars().all(char::is_alphabetic) { + hyphenations(p, &lb, offset, segment, &mut f); } - - // Filter out hyphenation opportunities where hyphenation was - // actually disabled. - if !hyphenate_at(p, offset) { - continue; - } - - // Filter out forbidden hyphenation opportunities. - if matches!( - syllable.chars().next_back().map(|c| lb.get(c)), - Some(LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ) - ) { - continue; - } - - // Call `f` for the word-internal hyphenation opportunity. - f(offset, Breakpoint::Hyphen); + offset += segment.len(); } } @@ -399,6 +704,44 @@ fn breakpoints<'a>(p: &'a Preparation<'a>, mut f: impl FnMut(usize, Breakpoint)) } } +/// Generate breakpoints for hyphenations within a word. +fn hyphenations( + p: &Preparation, + lb: &CodePointMapDataBorrowed, + mut offset: usize, + word: &str, + mut f: impl FnMut(usize, Breakpoint), +) { + let Some(lang) = lang_at(p, offset) else { return }; + let end = offset + word.len(); + + for syllable in hypher::hyphenate(word, lang) { + offset += syllable.len(); + + // Don't hyphenate after the final syllable. + if offset == end { + continue; + } + + // Filter out hyphenation opportunities where hyphenation was actually + // disabled. + if !hyphenate_at(p, offset) { + continue; + } + + // Filter out forbidden hyphenation opportunities. + if matches!( + syllable.chars().next_back().map(|c| lb.get(c)), + Some(LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ) + ) { + continue; + } + + // Call `f` for the word-internal hyphenation opportunity. + f(offset, Breakpoint::Hyphen); + } +} + /// Produce linebreak opportunities for a link. fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { #[derive(PartialEq)] @@ -433,7 +776,7 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { // - other -> other // - alphabetic -> numeric // - numeric -> alphabetic - // Never before after opening delimiters. + // Never before/after opening delimiters. if end > 0 && prev != Class::Open && if class == Class::Other { prev == Class::Other } else { class != prev } @@ -461,8 +804,9 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { fn hyphenate_at(p: &Preparation, offset: usize) -> bool { p.hyphenate .or_else(|| { - let shaped = p.find(offset)?.text()?; - Some(TextElem::hyphenate_in(shaped.styles)) + let (_, item) = p.get(offset); + let styles = item.text()?.styles; + Some(TextElem::hyphenate_in(styles)) }) .unwrap_or(false) } @@ -470,56 +814,147 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool { /// The text language at the given offset. fn lang_at(p: &Preparation, offset: usize) -> Option { let lang = p.lang.or_else(|| { - let shaped = p.find(offset)?.text()?; - Some(TextElem::lang_in(shaped.styles)) + let (_, item) = p.get(offset); + let styles = item.text()?.styles; + Some(TextElem::lang_in(styles)) })?; let bytes = lang.as_str().as_bytes().try_into().ok()?; hypher::Lang::from_iso(bytes) } -/// Whether the hyphen should repeat at the start of the next line. -fn should_repeat_hyphen(pred_line: &Line) -> bool { - // If the predecessor line does not end with a Dash::HardHyphen, we shall - // not place a hyphen at the start of the next line. - if pred_line.dash != Some(Dash::HardHyphen) { - return false; +/// Resolved metrics relevant for cost computation. +struct CostMetrics { + min_ratio: f64, + min_approx_ratio: f64, + hyph_cost: Cost, + runt_cost: Cost, + approx_hyphen_width: Abs, +} + +impl CostMetrics { + /// Compute shared metrics for paragraph 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 }, + hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), + runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), + // Approximate hyphen width for estimates. + approx_hyphen_width: Em::new(0.33).at(p.size), + } } - // If there's a trimmed out space, we needn't repeat the hyphen. That's the - // case of a text like "...kebab รฉ a -melhor- comida que existe", where the - // hyphens are a kind of emphasis marker. - if pred_line.trimmed.end != pred_line.end { - return false; - } - - // The hyphen should repeat only in the languages that require that feature. - // For more information see the discussion at https://github.com/typst/typst/issues/3235 - let Some(Item::Text(shape)) = pred_line.last.as_ref() else { return false }; - - match shape.lang { - // - Lower Sorbian: see https://dolnoserbski.de/ortografija/psawidla/K3 - // - Czech: see https://prirucka.ujc.cas.cz/?id=164 - // - Croatian: see http://pravopis.hr/pravilo/spojnica/68/ - // - Polish: see https://www.ortograf.pl/zasady-pisowni/lacznik-zasady-pisowni - // - Portuguese: see https://www2.senado.leg.br/bdsf/bitstream/handle/id/508145/000997415.pdf (Base XX) - // - Slovak: see https://www.zones.sk/studentske-prace/gramatika/10620-pravopis-rozdelovanie-slov/ - Lang::LOWER_SORBIAN - | Lang::CZECH - | Lang::CROATIAN - | Lang::POLISH - | Lang::PORTUGUESE - | Lang::SLOVAK => true, - // In Spanish the hyphen is required only if the word next to hyphen is - // not capitalized. Otherwise, the hyphen must not be repeated. - // - // 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 => pred_line.bidi.text[pred_line.end..] - .chars() - .next() - .map(|c| !c.is_uppercase()) - .unwrap_or(false), - _ => false, + /// The minimum line ratio we allow for shrinking. For approximate layout, + /// we allow less because otherwise we get an invalid layout fairly often, + /// which makes our bound useless. + fn min_ratio(&self, approx: bool) -> f64 { + if approx { + self.min_approx_ratio + } else { + self.min_ratio + } } } + +/// Estimated line metrics. +/// +/// Allows to get a quick estimate of a metric for a line between two byte +/// positions. +struct Estimates { + widths: CummulativeVec, + stretchability: CummulativeVec, + shrinkability: CummulativeVec, + justifiables: CummulativeVec, +} + +impl Estimates { + /// Compute estimations for approximate Knuth-Plass layout. + fn compute(p: &Preparation) -> Self { + let cap = p.text.len(); + + let mut widths = CummulativeVec::with_capacity(cap); + let mut stretchability = CummulativeVec::with_capacity(cap); + let mut shrinkability = CummulativeVec::with_capacity(cap); + let mut justifiables = CummulativeVec::with_capacity(cap); + + for (range, item) in p.items.iter() { + if let Item::Text(shaped) = item { + for g in shaped.glyphs.iter() { + let byte_len = g.range.len(); + let stretch = g.stretchability().0 + g.stretchability().1; + let shrink = g.shrinkability().0 + g.shrinkability().1; + widths.push(byte_len, g.x_advance.at(shaped.size)); + stretchability.push(byte_len, stretch.at(shaped.size)); + shrinkability.push(byte_len, shrink.at(shaped.size)); + justifiables.push(byte_len, g.is_justifiable() as usize); + } + } else { + widths.push(range.len(), item.natural_width()); + } + + widths.adjust(range.end); + stretchability.adjust(range.end); + shrinkability.adjust(range.end); + justifiables.adjust(range.end); + } + + Self { + widths, + stretchability, + shrinkability, + justifiables, + } + } +} + +/// An accumulative array of a metric. +struct CummulativeVec { + total: T, + summed: Vec, +} + +impl CummulativeVec +where + T: Default + Copy + Add + Sub, +{ + /// Create a new instance with the given capacity. + fn with_capacity(capacity: usize) -> Self { + let total = T::default(); + let mut summed = Vec::with_capacity(capacity); + summed.push(total); + Self { total, summed } + } + + /// Adjust to cover the given byte length. + fn adjust(&mut self, len: usize) { + self.summed.resize(len, self.total); + } + + /// Adds a new segment with the given byte length and metric. + fn push(&mut self, byte_len: usize, metric: T) { + self.total = self.total + metric; + for _ in 0..byte_len { + self.summed.push(self.total); + } + } + + /// Estimates the metrics for the line spanned by the range. + fn estimate(&self, range: Range) -> T { + self.get(range.end) - self.get(range.start) + } + + /// Get the metric at the given byte position. + fn get(&self, index: usize) -> T { + match index.checked_sub(1) { + None => T::default(), + Some(i) => self.summed[i], + } + } +} + +/// Whether a codepoint is Unicode `Default_Ignorable`. +pub fn is_default_ignorable(c: char) -> bool { + DEFAULT_IGNORABLE_DATA.as_borrowed().contains(c) +} diff --git a/crates/typst/src/layout/inline/mod.rs b/crates/typst/src/layout/inline/mod.rs index 94ac89f0d..821b4f57e 100644 --- a/crates/typst/src/layout/inline/mod.rs +++ b/crates/typst/src/layout/inline/mod.rs @@ -9,8 +9,8 @@ use comemo::{Track, Tracked, TrackedMut}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::finalize::finalize; -use self::line::{commit, line, Dash, Line}; -use self::linebreak::{linebreak, Breakpoint}; +use self::line::{commit, line, Line}; +use self::linebreak::{is_default_ignorable, linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ cjk_punct_style, is_of_cj_script, shape_range, ShapedGlyph, ShapedText, diff --git a/crates/typst/src/layout/inline/prepare.rs b/crates/typst/src/layout/inline/prepare.rs index 90d8d5a47..59682b2c8 100644 --- a/crates/typst/src/layout/inline/prepare.rs +++ b/crates/typst/src/layout/inline/prepare.rs @@ -13,16 +13,24 @@ use crate::text::{Costs, Lang, TextElem}; /// 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. + pub text: &'a str, /// Bidirectional text embedding levels for the paragraph. - pub bidi: BidiInfo<'a>, + /// + /// This is `None` if the paragraph is BiDi-uniform (all the base direction). + pub bidi: Option>, /// Text runs, spacing and layouted elements. - pub items: Vec>, + pub items: Vec<(Range, Item<'a>)>, + /// Maps from byte indices to item indices. + 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. @@ -44,46 +52,18 @@ pub struct Preparation<'a> { } impl<'a> Preparation<'a> { - /// Find the item that contains the given `text_offset`. - pub fn find(&self, text_offset: usize) -> Option<&Item<'a>> { - let mut cursor = 0; - for item in &self.items { - let end = cursor + item.textual_len(); - if (cursor..end).contains(&text_offset) { - return Some(item); - } - cursor = end; - } - None + /// Get the item that contains the given `text_offset`. + pub fn get(&self, offset: usize) -> &(Range, Item<'a>) { + let idx = self.indices.get(offset).copied().unwrap_or(0); + &self.items[idx] } - /// Return the items that intersect the given `text_range`. - /// - /// Returns the expanded range around the items and the items. - pub fn slice(&self, text_range: Range) -> (Range, &[Item<'a>]) { - let mut cursor = 0; - let mut start = 0; - let mut end = 0; - let mut expanded = text_range.clone(); - - for (i, item) in self.items.iter().enumerate() { - if cursor <= text_range.start { - start = i; - expanded.start = cursor; - } - - let len = item.textual_len(); - if cursor < text_range.end || cursor + len <= text_range.end { - end = i + 1; - expanded.end = cursor + len; - } else { - break; - } - - cursor += len; - } - - (expanded, &self.items[start..end]) + /// Iterate over the items that intersect the given `sliced` range. + pub fn slice(&self, sliced: Range) -> impl Iterator)> { + let start = self.indices.get(sliced.start).copied().unwrap_or(0); + self.items[start..].iter().take_while(move |(range, _)| { + range.start < sliced.end || range.end <= sliced.end + }) } } @@ -99,42 +79,57 @@ pub fn prepare<'a>( spans: SpanMapper, styles: StyleChain<'a>, ) -> SourceResult> { - let bidi = BidiInfo::new( - text, - match TextElem::dir_in(styles) { - Dir::LTR => Some(BidiLevel::ltr()), - Dir::RTL => Some(BidiLevel::rtl()), - _ => None, - }, - ); + let dir = TextElem::dir_in(styles); + let default_level = match dir { + Dir::RTL => BidiLevel::rtl(), + _ => BidiLevel::ltr(), + }; + + let bidi = BidiInfo::new(text, Some(default_level)); + let is_bidi = bidi + .levels + .iter() + .any(|level| level.is_ltr() != default_level.is_ltr()); let mut cursor = 0; let mut items = Vec::with_capacity(segments.len()); // Shape the text to finalize the items. for segment in segments { - let end = cursor + segment.textual_len(); + let len = segment.textual_len(); + let end = cursor + len; + let range = cursor..end; + match segment { Segment::Text(_, styles) => { - shape_range(&mut items, engine, &bidi, cursor..end, &spans, styles); + shape_range(&mut items, engine, text, &bidi, range, styles); } - Segment::Item(item) => items.push(item), + Segment::Item(item) => items.push((range, item)), } cursor = end; } + // Build the mapping from byte to item indices. + let mut indices = Vec::with_capacity(text.len()); + for (i, (range, _)) in items.iter().enumerate() { + indices.extend(range.clone().map(|_| i)); + } + let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); if cjk_latin_spacing { add_cjk_latin_spacing(&mut items); } Ok(Preparation { - bidi, + 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), @@ -150,10 +145,14 @@ pub fn prepare<'a>( /// Add some spacing between Han characters and western characters. See /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// in Horizontal Written Mode -fn add_cjk_latin_spacing(items: &mut [Item]) { - let mut items = items.iter_mut().filter(|x| !matches!(x, Item::Tag(_))).peekable(); +fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) { + let mut items = items + .iter_mut() + .filter(|(_, x)| !matches!(x, Item::Tag(_))) + .peekable(); + let mut prev: Option<&ShapedGlyph> = None; - while let Some(item) = items.next() { + while let Some((_, item)) = items.next() { let Some(text) = item.text_mut() else { prev = None; continue; @@ -168,7 +167,7 @@ fn add_cjk_latin_spacing(items: &mut [Item]) { let next = glyphs.peek().map(|n| n as _).or_else(|| { items .peek() - .and_then(|i| i.text()) + .and_then(|(_, i)| i.text()) .and_then(|shaped| shaped.glyphs.first()) }); diff --git a/crates/typst/src/layout/inline/shaping.rs b/crates/typst/src/layout/inline/shaping.rs index 44b653917..43dc351a5 100644 --- a/crates/typst/src/layout/inline/shaping.rs +++ b/crates/typst/src/layout/inline/shaping.rs @@ -14,7 +14,6 @@ use super::{Item, Range, SpanMapper}; use crate::engine::Engine; use crate::foundations::{Smart, StyleChain}; use crate::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; -use crate::syntax::Span; use crate::text::{ decorate, families, features, variant, Font, FontVariant, Glyph, Lang, Region, TextElem, TextItem, @@ -27,6 +26,7 @@ use crate::World; /// This type contains owned or borrowed shaped text runs, which can be /// measured, used to reshape substrings more quickly and converted into a /// frame. +#[derive(Clone)] pub struct ShapedText<'a> { /// The start of the text in the full paragraph. pub base: usize, @@ -80,8 +80,6 @@ pub struct ShapedGlyph { pub safe_to_break: bool, /// The first char in this glyph's cluster. pub c: char, - /// The source code location of the glyph and its byte offset within it. - pub span: (Span, u16), /// Whether this glyph is justifiable for CJK scripts. pub is_justifiable: bool, /// The script of the glyph. @@ -214,6 +212,7 @@ impl<'a> ShapedText<'a> { pub fn build( &self, engine: &Engine, + spans: &SpanMapper, justification_ratio: f64, extra_justification: Abs, ) -> Frame { @@ -268,7 +267,7 @@ impl<'a> ShapedText<'a> { // We may not be able to reach the offset completely if // it exceeds u16, but better to have a roughly correct // span offset than nothing. - let mut span = shaped.span; + let mut span = spans.span_at(shaped.range.start); span.1 = span.1.saturating_add(span_offset.saturating_as()); // |<---- a Glyph ---->| @@ -331,7 +330,7 @@ impl<'a> ShapedText<'a> { } /// Measure the top and bottom extent of this text. - fn measure(&self, engine: &Engine) -> (Abs, Abs) { + pub fn measure(&self, engine: &Engine) -> (Abs, Abs) { let mut top = Abs::zero(); let mut bottom = Abs::zero(); @@ -409,12 +408,7 @@ impl<'a> ShapedText<'a> { /// shaping process if possible. /// /// The text `range` is relative to the whole paragraph. - pub fn reshape( - &'a self, - engine: &Engine, - spans: &SpanMapper, - text_range: Range, - ) -> ShapedText<'a> { + pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> { let text = &self.text[text_range.start - self.base..text_range.end - self.base]; if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { #[cfg(debug_assertions)] @@ -436,7 +430,6 @@ impl<'a> ShapedText<'a> { engine, text_range.start, text, - spans, self.styles, self.dir, self.lang, @@ -445,6 +438,16 @@ impl<'a> ShapedText<'a> { } } + /// Derive an empty text run with the same properties as this one. + pub fn empty(&self) -> Self { + Self { + text: "", + width: Abs::zero(), + glyphs: Cow::Borrowed(&[]), + ..*self + } + } + /// Push a hyphen to end of the text. pub fn push_hyphen(&mut self, engine: &Engine, fallback: bool) { self.insert_hyphen(engine, fallback, Side::Right) @@ -493,7 +496,6 @@ impl<'a> ShapedText<'a> { range, safe_to_break: true, c: '-', - span: (Span::detached(), 0), is_justifiable: false, script: Script::Common, }; @@ -592,11 +594,11 @@ impl Debug for ShapedText<'_> { /// Group a range of text by BiDi level and script, shape the runs and generate /// items for them. pub fn shape_range<'a>( - items: &mut Vec>, + items: &mut Vec<(Range, Item<'a>)>, engine: &Engine, + text: &'a str, bidi: &BidiInfo<'a>, range: Range, - spans: &SpanMapper, styles: StyleChain<'a>, ) { let script = TextElem::script_in(styles); @@ -604,17 +606,9 @@ pub fn shape_range<'a>( let region = TextElem::region_in(styles); let mut process = |range: Range, level: BidiLevel| { let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; - let shaped = shape( - engine, - range.start, - &bidi.text[range], - spans, - styles, - dir, - lang, - region, - ); - items.push(Item::Text(shaped)); + let shaped = + shape(engine, range.start, &text[range.clone()], styles, dir, lang, region); + items.push((range, Item::Text(shaped))); }; let mut prev_level = BidiLevel::ltr(); @@ -625,14 +619,14 @@ pub fn shape_range<'a>( // set (rather than inferred from the glyphs), we keep the script at an // unchanging `Script::Unknown` so that only level changes cause breaks. for i in range.clone() { - if !bidi.text.is_char_boundary(i) { + if !text.is_char_boundary(i) { continue; } let level = bidi.levels[i]; let curr_script = match script { Smart::Auto => { - bidi.text[i..].chars().next().map_or(Script::Unknown, |c| c.script()) + text[i..].chars().next().map_or(Script::Unknown, |c| c.script()) } Smart::Custom(_) => Script::Unknown, }; @@ -668,7 +662,6 @@ fn shape<'a>( engine: &Engine, base: usize, text: &'a str, - spans: &SpanMapper, styles: StyleChain<'a>, dir: Dir, lang: Lang, @@ -677,7 +670,6 @@ fn shape<'a>( let size = TextElem::size_in(styles); let mut ctx = ShapingContext { engine, - spans, size, glyphs: vec![], used: vec![], @@ -717,7 +709,6 @@ fn shape<'a>( /// Holds shaping results and metadata common to all shaped segments. struct ShapingContext<'a, 'v> { engine: &'a Engine<'v>, - spans: &'a SpanMapper, glyphs: Vec, used: Vec, styles: StyleChain<'a>, @@ -830,7 +821,6 @@ fn shape_segment<'a>( range: start..end, safe_to_break: !info.unsafe_to_break(), c, - span: ctx.spans.span_at(start), is_justifiable: is_justifiable( c, script, @@ -921,7 +911,6 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { range: start..end, safe_to_break: true, c, - span: ctx.spans.span_at(start), is_justifiable: is_justifiable( c, script, diff --git a/crates/typst/src/layout/mod.rs b/crates/typst/src/layout/mod.rs index 85cdbae7a..739d09224 100644 --- a/crates/typst/src/layout/mod.rs +++ b/crates/typst/src/layout/mod.rs @@ -106,7 +106,6 @@ pub fn define(global: &mut Scope) { global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); global.define_elem::(); global.define_elem::(); global.define_elem::(); diff --git a/crates/typst/src/layout/place.rs b/crates/typst/src/layout/place.rs index 78922c1bb..be211c150 100644 --- a/crates/typst/src/layout/place.rs +++ b/crates/typst/src/layout/place.rs @@ -46,7 +46,7 @@ pub struct PlaceElem { /// Floating elements are positioned at the top or bottom of the page, /// displacing in-flow content. They are always placed in the in-flow /// order relative to each other, as well as before any content following - /// a later [`flush`] element. + /// a later [`place.flush`] element. /// /// ```example /// #set page(height: 150pt) diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 6bda28668..f3f2a28f8 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -60,10 +60,12 @@ use std::collections::HashSet; use std::ops::{Deref, Range}; use comemo::{Track, Tracked, Validate}; -use ecow::{EcoString, EcoVec}; +use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use typst_timing::{timed, TimingScope}; -use crate::diag::{warning, FileResult, SourceDiagnostic, SourceResult, Warned}; +use crate::diag::{ + warning, FileError, FileResult, SourceDiagnostic, SourceResult, Warned, +}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ Array, Bytes, Datetime, Dict, Module, Scope, StyleChain, Styles, Value, @@ -110,13 +112,19 @@ fn compile_inner( let library = world.library(); let styles = StyleChain::new(&library.styles); + // Fetch the main source file once. + let main = world.main(); + let main = world + .source(main) + .map_err(|err| hint_invalid_main_file(world, err, main))?; + // First evaluate the main source file into a module. let content = crate::eval::eval( world, traced, sink.track_mut(), Route::default().track(), - &world.main(), + &main, )? .content(); @@ -205,8 +213,8 @@ pub trait World: Send + Sync { /// Metadata about all known fonts. fn book(&self) -> &LazyHash; - /// Access the main source file. - fn main(&self) -> Source; + /// Get the file id of the main source file. + fn main(&self) -> FileId; /// Try to access the specified source file. fn source(&self, id: FileId) -> FileResult; @@ -248,7 +256,7 @@ macro_rules! delegate_for_ptr { self.deref().book() } - fn main(&self) -> Source { + fn main(&self) -> FileId { self.deref().main() } @@ -404,3 +412,48 @@ fn prelude(global: &mut Scope) { global.define("horizon", Alignment::HORIZON); global.define("bottom", Alignment::BOTTOM); } + +/// Adds useful hints when the main source file couldn't be read +/// and returns the final diagnostic. +fn hint_invalid_main_file( + world: Tracked, + file_error: FileError, + input: FileId, +) -> EcoVec { + let is_utf8_error = matches!(file_error, FileError::InvalidUtf8); + let mut diagnostic = + SourceDiagnostic::error(Span::detached(), EcoString::from(file_error)); + + // Attempt to provide helpful hints for UTF-8 errors. Perhaps the user + // mistyped the filename. For example, they could have written "file.pdf" + // instead of "file.typ". + if is_utf8_error { + let path = input.vpath(); + let extension = path.as_rootless_path().extension(); + if extension.is_some_and(|extension| extension == "typ") { + // No hints if the file is already a .typ file. + // The file is indeed just invalid. + return eco_vec![diagnostic]; + } + + match extension { + Some(extension) => { + diagnostic.hint(eco_format!( + "a file with the `.{}` extension is not usually a Typst file", + extension.to_string_lossy() + )); + } + + None => { + diagnostic + .hint("a file without an extension is not usually a Typst file"); + } + }; + + if world.source(input.with_extension("typ")).is_ok() { + diagnostic.hint("check if you meant to use the `.typ` extension instead"); + } + } + + eco_vec![diagnostic] +} diff --git a/crates/typst/src/math/attach.rs b/crates/typst/src/math/attach.rs index 7cc03bbab..035b78125 100644 --- a/crates/typst/src/math/attach.rs +++ b/crates/typst/src/math/attach.rs @@ -52,31 +52,47 @@ pub struct AttachElem { impl LayoutMath for Packed { #[typst_macros::time(name = "math.attach", span = self.span())] fn layout_math(&self, ctx: &mut MathContext, styles: StyleChain) -> SourceResult<()> { - type GetAttachment = fn(&AttachElem, styles: StyleChain) -> Option; - - let layout_attachment = - |ctx: &mut MathContext, styles: StyleChain, getter: GetAttachment| { - getter(self, styles) - .map(|elem| ctx.layout_into_fragment(&elem, styles)) - .transpose() - }; - let base = ctx.layout_into_fragment(self.base(), styles)?; let sup_style = style_for_superscript(styles); - let tl = layout_attachment(ctx, styles.chain(&sup_style), AttachElem::tl)?; - let tr = layout_attachment(ctx, styles.chain(&sup_style), AttachElem::tr)?; - let t = layout_attachment(ctx, styles.chain(&sup_style), AttachElem::t)?; + let sup_style_chain = styles.chain(&sup_style); + let tl = self.tl(sup_style_chain); + let tr = self.tr(sup_style_chain); + let primed = tr.as_ref().is_some_and(|content| content.is::()); + let t = self.t(sup_style_chain); let sub_style = style_for_subscript(styles); - let bl = layout_attachment(ctx, styles.chain(&sub_style), AttachElem::bl)?; - let br = layout_attachment(ctx, styles.chain(&sub_style), AttachElem::br)?; - let b = layout_attachment(ctx, styles.chain(&sub_style), AttachElem::b)?; + let sub_style_chain = styles.chain(&sub_style); + let bl = self.bl(sub_style_chain); + let br = self.br(sub_style_chain); + let b = self.b(sub_style_chain); let limits = base.limits().active(styles); - let (t, tr) = if limits || tr.is_some() { (t, tr) } else { (None, t) }; + let (t, tr) = match (t, tr) { + (Some(t), Some(tr)) if primed && !limits => (None, Some(tr + t)), + (Some(t), None) if !limits => (None, Some(t)), + (t, tr) => (t, tr), + }; let (b, br) = if limits || br.is_some() { (b, br) } else { (None, b) }; - layout_attachments(ctx, styles, base, [tl, t, tr, bl, b, br]) + + macro_rules! layout { + ($content:ident, $style_chain:ident) => { + $content + .map(|elem| ctx.layout_into_fragment(&elem, $style_chain)) + .transpose() + }; + } + + let fragments = [ + layout!(tl, sup_style_chain)?, + layout!(t, sup_style_chain)?, + layout!(tr, sup_style_chain)?, + layout!(bl, sub_style_chain)?, + layout!(b, sub_style_chain)?, + layout!(br, sub_style_chain)?, + ]; + + layout_attachments(ctx, styles, base, fragments) } } @@ -127,7 +143,7 @@ impl LayoutMath for Packed { prime.clone(), ) } - ctx.push(FrameFragment::new(ctx, styles, frame)); + ctx.push(FrameFragment::new(ctx, styles, frame).with_text_like(true)); } } Ok(()) @@ -245,8 +261,11 @@ fn layout_attachments( base: MathFragment, [tl, t, tr, bl, b, br]: [Option; 6], ) -> SourceResult<()> { - let (shift_up, shift_down) = - compute_shifts_up_and_down(ctx, styles, &base, [&tl, &tr, &bl, &br]); + let (shift_up, shift_down) = if [&tl, &tr, &bl, &br].iter().all(|e| e.is_none()) { + (Abs::zero(), Abs::zero()) + } else { + compute_shifts_up_and_down(ctx, styles, &base, [&tl, &tr, &bl, &br]) + }; let sup_delta = Abs::zero(); let sub_delta = -base.italics_correction(); @@ -271,7 +290,11 @@ fn layout_attachments( let post_width_max = (sup_delta + measure!(tr, width)).max(sub_delta + measure!(br, width)); - let (center_frame, base_offset) = attach_top_and_bottom(ctx, styles, base, t, b); + let (center_frame, base_offset) = if t.is_none() && b.is_none() { + (base.into_frame(), Abs::zero()) + } else { + attach_top_and_bottom(ctx, styles, base, t, b) + }; if [&tl, &bl, &tr, &br].iter().all(|&e| e.is_none()) { ctx.push(FrameFragment::new(ctx, styles, center_frame).with_class(base_class)); return Ok(()); @@ -327,7 +350,7 @@ fn layout_attachments( } fn attach_top_and_bottom( - ctx: &mut MathContext, + ctx: &MathContext, styles: StyleChain, base: MathFragment, t: Option, diff --git a/crates/typst/src/math/cancel.rs b/crates/typst/src/math/cancel.rs index 405b03513..ef07a1c8e 100644 --- a/crates/typst/src/math/cancel.rs +++ b/crates/typst/src/math/cancel.rs @@ -65,13 +65,13 @@ pub struct CancelElem { /// How much to rotate the cancel line. /// + /// - If given an angle, the line is rotated by that angle clockwise with + /// respect to the y-axis. /// - If `{auto}`, the line assumes the default angle; that is, along the - /// diagonal line of the content box. - /// - If given an angle, the line is rotated by that angle clockwise w.r.t - /// the y-axis. - /// - If given a function `angle => angle`, the line is rotated by the angle - /// returned by that function. The function receives the default angle as - /// its input. + /// rising diagonal of the content box. + /// - If given a function `angle => angle`, the line is rotated, with + /// respect to the y-axis, by the angle returned by that function. The + /// function receives the default angle as its input. /// /// ```example /// >>> #set page(width: 140pt) diff --git a/crates/typst/src/math/fragment.rs b/crates/typst/src/math/fragment.rs index 63c42cbce..c6de24863 100644 --- a/crates/typst/src/math/fragment.rs +++ b/crates/typst/src/math/fragment.rs @@ -71,6 +71,13 @@ impl MathFragment { } } + pub fn is_ignorant(&self) -> bool { + match self { + Self::Frame(fragment) => fragment.ignorant, + _ => false, + } + } + pub fn class(&self) -> MathClass { match self { Self::Glyph(glyph) => glyph.class, @@ -120,17 +127,18 @@ impl MathFragment { } pub fn is_spaced(&self) -> bool { - self.class() == MathClass::Fence - || match self { - MathFragment::Frame(frame) => { - frame.spaced - && matches!( - frame.class, - MathClass::Normal | MathClass::Alphabetic - ) - } - _ => false, - } + if self.class() == MathClass::Fence { + return true; + } + + matches!( + self, + MathFragment::Frame(FrameFragment { + spaced: true, + class: MathClass::Normal | MathClass::Alphabetic, + .. + }) + ) } pub fn is_text_like(&self) -> bool { @@ -440,6 +448,7 @@ pub struct FrameFragment { pub italics_correction: Abs, pub accent_attach: Abs, pub text_like: bool, + pub ignorant: bool, } impl FrameFragment { @@ -458,6 +467,7 @@ impl FrameFragment { italics_correction: Abs::zero(), accent_attach, text_like: false, + ignorant: false, } } @@ -488,6 +498,10 @@ impl FrameFragment { pub fn with_text_like(self, text_like: bool) -> Self { Self { text_like, ..self } } + + pub fn with_ignorant(self, ignorant: bool) -> Self { + Self { ignorant, ..self } + } } #[derive(Debug, Clone)] diff --git a/crates/typst/src/math/mod.rs b/crates/typst/src/math/mod.rs index dc79f48b4..04db9efb1 100644 --- a/crates/typst/src/math/mod.rs +++ b/crates/typst/src/math/mod.rs @@ -49,6 +49,7 @@ use crate::foundations::{ }; use crate::introspection::TagElem; use crate::layout::{BoxElem, Frame, FrameItem, HElem, Point, Size, Spacing, VAlignment}; +use crate::realize::Behaviour; use crate::realize::{process, BehavedBuilder}; use crate::text::{LinebreakElem, SpaceElem, TextElem}; @@ -299,7 +300,7 @@ impl LayoutMath for Content { if let Some(elem) = self.to_packed::() { let mut frame = Frame::soft(Size::zero()); frame.push(Point::zero(), FrameItem::Tag(elem.tag.clone())); - ctx.push(FrameFragment::new(ctx, styles, frame)); + ctx.push(FrameFragment::new(ctx, styles, frame).with_ignorant(true)); return Ok(()); } @@ -312,7 +313,15 @@ impl LayoutMath for Content { let axis = scaled!(ctx, styles, axis_height); frame.set_baseline(frame.height() / 2.0 + axis); } - ctx.push(FrameFragment::new(ctx, styles, frame).with_spaced(true)); + + ctx.push( + FrameFragment::new(ctx, styles, frame) + .with_spaced(true) + .with_ignorant(matches!( + self.behaviour(), + Behaviour::Invisible | Behaviour::Ignorant + )), + ); Ok(()) } diff --git a/crates/typst/src/math/row.rs b/crates/typst/src/math/row.rs index cb909b0bc..60afe64e5 100644 --- a/crates/typst/src/math/row.rs +++ b/crates/typst/src/math/row.rs @@ -77,14 +77,17 @@ impl MathRun { fragment.set_class(MathClass::Binary); } - // Insert spacing between the last and this item. - if let Some(i) = last { - if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { - resolved.insert(i + 1, s); + // Insert spacing between the last and this non-ignorant item. + if !fragment.is_ignorant() { + if let Some(i) = last { + if let Some(s) = spacing(&resolved[i], space.take(), &fragment) { + resolved.insert(i + 1, s); + } } + + last = Some(resolved.len()); } - last = Some(resolved.len()); resolved.push(fragment); } @@ -156,10 +159,19 @@ impl MathRun { pub fn into_fragment(self, ctx: &MathContext, styles: StyleChain) -> MathFragment { if self.0.len() == 1 { - self.0.into_iter().next().unwrap() - } else { - FrameFragment::new(ctx, styles, self.into_frame(ctx, styles)).into() + return self.0.into_iter().next().unwrap(); } + + // Fragments without a math_size are ignored: the notion of size do not + // apply to them, so their text-likeness is meaningless. + let text_like = self + .iter() + .filter(|e| e.math_size().is_some()) + .all(|e| e.is_text_like()); + + FrameFragment::new(ctx, styles, self.into_frame(ctx, styles)) + .with_text_like(text_like) + .into() } /// Returns a builder that lays out the [`MathFragment`]s into a possibly diff --git a/crates/typst/src/model/footnote.rs b/crates/typst/src/model/footnote.rs index f8f36eb23..2aeaad1a7 100644 --- a/crates/typst/src/model/footnote.rs +++ b/crates/typst/src/model/footnote.rs @@ -93,6 +93,15 @@ impl FootnoteElem { Self::new(FootnoteBody::Reference(label)) } + /// Creates a new footnote referencing the footnote with the specified label, + /// with the other fields from the current footnote cloned. + pub fn into_ref(&self, label: Label) -> Self { + Self { + body: FootnoteBody::Reference(label), + ..self.clone() + } + } + /// Tests if this footnote is a reference to another footnote. pub fn is_ref(&self) -> bool { matches!(self.body(), FootnoteBody::Reference(_)) diff --git a/crates/typst/src/model/outline.rs b/crates/typst/src/model/outline.rs index 090472850..ec1e5f1b8 100644 --- a/crates/typst/src/model/outline.rs +++ b/crates/typst/src/model/outline.rs @@ -483,7 +483,7 @@ impl OutlineEntry { impl Show for Packed { #[typst_macros::time(name = "outline.entry", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let mut seq = vec![]; let elem = self.element(); @@ -500,7 +500,11 @@ impl Show for Packed { }; // The body text remains overridable. - seq.push(self.body().clone().linked(Destination::Location(location))); + crate::text::isolate( + self.body().clone().linked(Destination::Location(location)), + styles, + &mut seq, + ); // Add filler symbols between the section name and page number. if let Some(filler) = self.fill() { diff --git a/crates/typst/src/model/par.rs b/crates/typst/src/model/par.rs index 7f65a00fb..2110995f3 100644 --- a/crates/typst/src/model/par.rs +++ b/crates/typst/src/model/par.rs @@ -18,9 +18,9 @@ use crate::realize::StyleVec; /// /// # Example /// ```example -/// #show par: set block(spacing: 0.65em) /// #set par( /// first-line-indent: 1em, +/// spacing: 0.65em, /// justify: true, /// ) /// @@ -115,8 +115,7 @@ pub struct ParElem { /// 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 - /// `[#show par: set block(spacing: 0.65em)]`). + /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`). #[ghost] pub first_line_indent: Length, diff --git a/crates/typst/src/model/reference.rs b/crates/typst/src/model/reference.rs index 511ca3d9a..8194eec53 100644 --- a/crates/typst/src/model/reference.rs +++ b/crates/typst/src/model/reference.rs @@ -177,8 +177,8 @@ impl Show for Packed { let elem = elem.at(span)?; - if elem.func() == FootnoteElem::elem() { - return Ok(FootnoteElem::with_label(target).pack().spanned(span)); + if let Some(footnote) = elem.to_packed::() { + return Ok(footnote.into_ref(target).pack().spanned(span)); } let elem = elem.clone(); diff --git a/crates/typst/src/symbols/sym.rs b/crates/typst/src/symbols/sym.rs index 1dec0039e..802b9a7a7 100644 --- a/crates/typst/src/symbols/sym.rs +++ b/crates/typst/src/symbols/sym.rs @@ -480,7 +480,6 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! { checkmark: ['โœ“', light: '๐Ÿ—ธ', heavy: 'โœ”'], crossmark: ['โœ—', heavy: 'โœ˜'], floral: ['โฆ', l: 'โ˜™', r: 'โง'], - notes: [up: '๐ŸŽœ', down: '๐ŸŽ'], refmark: 'โ€ป', servicemark: 'โ„ ', maltese: 'โœ ', @@ -495,6 +494,51 @@ pub(crate) const SYM: &[(&str, Symbol)] = symbols! { spade.stroked: 'โ™ค', ], + // Music. + note: [ + up: '๐ŸŽœ', + down: '๐ŸŽ', + whole: '๐…', + half: '๐…ž', + quarter: '๐…Ÿ', + quarter.alt: 'โ™ฉ', + eighth: '๐… ', + eighth.alt: 'โ™ช', + eighth.beamed: 'โ™ซ', + sixteenth: '๐…ก', + sixteenth.beamed: 'โ™ฌ', + grace: '๐†•', + grace.slash: '๐†”', + ], + rest: [ + whole: '๐„ป', + multiple: '๐„บ', + multiple.measure: '๐„ฉ', + half: '๐„ผ', + quarter: '๐„ฝ', + eighth: '๐„พ', + sixteenth: '๐„ฟ', + ], + natural: [ + 'โ™ฎ', + t: '๐„ฎ', + b: '๐„ฏ', + ], + flat: [ + 'โ™ญ', + t: '๐„ฌ', + b: '๐„ญ', + double: '๐„ซ', + quarter: '๐„ณ', + ], + sharp: [ + 'โ™ฏ', + t: '๐„ฐ', + b: '๐„ฑ', + double: '๐„ช', + quarter: '๐„ฒ', + ], + // Shapes. bullet: 'โ€ข', circle: [ diff --git a/crates/typst/src/text/font/exceptions.rs b/crates/typst/src/text/font/exceptions.rs index 2e5e3edaf..642b6ea72 100644 --- a/crates/typst/src/text/font/exceptions.rs +++ b/crates/typst/src/text/font/exceptions.rs @@ -48,6 +48,16 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { // See https://corefonts.sourceforge.net/. "Arial-Black" => Exception::new() .weight(900), + // Archivo Narrow is different from Archivo and Archivo Black. Since Archivo Black seems + // identical to Archivo weight 900, only differentiate between Archivo and Archivo Narrow. + "ArchivoNarrow-Regular" => Exception::new() + .family("Archivo Narrow"), + "ArchivoNarrow-Italic" => Exception::new() + .family("Archivo Narrow"), + "ArchivoNarrow-Bold" => Exception::new() + .family("Archivo Narrow"), + "ArchivoNarrow-BoldItalic" => Exception::new() + .family("Archivo Narrow"), // Fandol fonts designed for Chinese typesetting. // See https://ctan.org/tex-archive/fonts/fandol/. "FandolHei-Bold" => Exception::new() diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index 70d514d0c..0bf572e5f 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -1300,3 +1300,13 @@ cast! { ret }, } + +/// Pushes `text` wrapped in LRE/RLE + PDF to `out`. +pub(crate) fn isolate(text: Content, styles: StyleChain, out: &mut Vec) { + out.push(TextElem::packed(match TextElem::dir_in(styles) { + Dir::RTL => "\u{202B}", + _ => "\u{202A}", + })); + out.push(text); + out.push(TextElem::packed("\u{202C}")); +} diff --git a/crates/typst/src/text/smartquote.rs b/crates/typst/src/text/smartquote.rs index 236d06363..797f0804b 100644 --- a/crates/typst/src/text/smartquote.rs +++ b/crates/typst/src/text/smartquote.rs @@ -123,7 +123,7 @@ impl SmartQuoter { /// Process the last seen character. pub fn last(&mut self, c: char, is_quote: bool) { - self.expect_opening = is_ignorable(c) || is_opening_bracket(c); + self.expect_opening = is_exterior_to_quote(c) || is_opening_bracket(c); self.last_num = c.is_numeric(); if !is_quote { self.prev_quote_type = None; @@ -150,7 +150,7 @@ impl SmartQuoter { self.prev_quote_type = Some(double); quotes.open(double) } else if self.quote_depth > 0 - && (peeked.is_ascii_punctuation() || is_ignorable(peeked)) + && (peeked.is_ascii_punctuation() || is_exterior_to_quote(peeked)) { self.quote_depth -= 1; quotes.close(double) @@ -168,7 +168,7 @@ impl Default for SmartQuoter { } } -fn is_ignorable(c: char) -> bool { +fn is_exterior_to_quote(c: char) -> bool { c.is_whitespace() || is_newline(c) } diff --git a/crates/typst/src/visualize/image/mod.rs b/crates/typst/src/visualize/image/mod.rs index fbbedfb3e..5d952b1a3 100644 --- a/crates/typst/src/visualize/image/mod.rs +++ b/crates/typst/src/visualize/image/mod.rs @@ -196,7 +196,7 @@ fn layout_image( format, elem.alt(styles), engine.world, - &families(styles).map(|s| s.into()).collect::>(), + &families(styles).collect::>(), ) .at(span)?; @@ -360,7 +360,7 @@ impl Image { format: ImageFormat, alt: Option, world: Tracked, - families: &[String], + families: &[&str], ) -> StrResult { let kind = match format { ImageFormat::Raster(format) => { diff --git a/crates/typst/src/visualize/image/raster.rs b/crates/typst/src/visualize/image/raster.rs index 995e34837..ff15432b6 100644 --- a/crates/typst/src/visualize/image/raster.rs +++ b/crates/typst/src/visualize/image/raster.rs @@ -30,11 +30,11 @@ impl RasterImage { /// Decode a raster image. #[comemo::memoize] pub fn new(data: Bytes, format: RasterFormat) -> StrResult { - fn decode_with<'a, T: ImageDecoder<'a>>( + fn decode_with( decoder: ImageResult, ) -> ImageResult<(image::DynamicImage, Option>)> { let mut decoder = decoder?; - let icc = decoder.icc_profile().filter(|icc| !icc.is_empty()); + 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)) diff --git a/crates/typst/src/visualize/image/svg.rs b/crates/typst/src/visualize/image/svg.rs index 09319ccde..f7a498a83 100644 --- a/crates/typst/src/visualize/image/svg.rs +++ b/crates/typst/src/visualize/image/svg.rs @@ -40,7 +40,7 @@ impl SvgImage { pub fn with_fonts( data: Bytes, world: Tracked, - families: &[String], + families: &[&str], ) -> StrResult { let book = world.book(); let resolver = Mutex::new(FontResolver::new(world, book, families)); @@ -142,7 +142,7 @@ struct FontResolver<'a> { /// The world we use to load fonts. world: Tracked<'a, dyn World + 'a>, /// The active list of font families at the location of the SVG. - families: &'a [String], + families: &'a [&'a str], /// A mapping from Typst font indices to fontdb IDs. to_id: HashMap>, /// The reverse mapping. @@ -156,7 +156,7 @@ impl<'a> FontResolver<'a> { fn new( world: Tracked<'a, dyn World + 'a>, book: &'a FontBook, - families: &'a [String], + families: &'a [&'a str], ) -> Self { Self { book, @@ -191,11 +191,11 @@ impl FontResolver<'_> { font.families() .iter() .filter_map(|family| match family { - usvg::FontFamily::Named(named) => Some(named), + usvg::FontFamily::Named(named) => Some(named.as_str()), // We don't support generic families at the moment. _ => None, }) - .chain(self.families) + .chain(self.families.iter().copied()) .filter_map(|named| self.book.select(&named.to_lowercase(), variant)) .find_map(|index| self.get_or_load(index, db)) } diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md index 1f3caef98..8c3b56013 100644 --- a/docs/guides/guide-for-latex-users.md +++ b/docs/guides/guide-for-latex-users.md @@ -593,10 +593,9 @@ The example below ```typ #set page(margin: 1.75in) -#set par(leading: 0.55em, first-line-indent: 1.8em, justify: true) +#set par(leading: 0.55em, spacing: 0.55em, first-line-indent: 1.8em, justify: true) #set text(font: "New Computer Modern") #show raw: set text(font: "New Computer Modern Mono") -#show par: set block(spacing: 0.55em) #show heading: set block(above: 1.4em, below: 1em) ``` diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md index 9a7dc3733..b63d17760 100644 --- a/docs/reference/syntax.md +++ b/docs/reference/syntax.md @@ -120,7 +120,7 @@ a table listing all syntax that is available in code mode: | Named function | `{let f(x) = 2 * x}` | [Function]($function) | | Set rule | `{set text(14pt)}` | [Styling]($styling/#set-rules) | | Set-if rule | `{set text(..) if .. }` | [Styling]($styling/#set-rules) | -| Show-set rule | `{show par: set block(..)}` | [Styling]($styling/#show-rules) | +| Show-set rule | `{show heading: set block(..)}` | [Styling]($styling/#show-rules) | | Show rule with function | `{show raw: it => {..}}` | [Styling]($styling/#show-rules) | | Show-everything rule | `{show: columns.with(2)}` | [Styling]($styling/#show-rules) | | Context expression | `{context text.lang}` | [Context]($context) | diff --git a/docs/src/html.rs b/docs/src/html.rs index ab140a902..58c8e54c2 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -6,7 +6,7 @@ use heck::{ToKebabCase, ToTitleCase}; use pulldown_cmark as md; use serde::{Deserialize, Serialize}; use typed_arena::Arena; -use typst::diag::{FileResult, StrResult}; +use typst::diag::{FileError, FileResult, StrResult}; use typst::foundations::{Bytes, Datetime}; use typst::layout::{Abs, Point, Size}; use typst::syntax::{FileId, Source, VirtualPath}; @@ -463,12 +463,16 @@ impl World for DocWorld { &FONTS.0 } - fn main(&self) -> Source { - self.0.clone() + fn main(&self) -> FileId { + self.0.id() } - fn source(&self, _: FileId) -> FileResult { - Ok(self.0.clone()) + fn source(&self, id: FileId) -> FileResult { + if id == self.0.id() { + Ok(self.0.clone()) + } else { + Err(FileError::NotFound(id.vpath().as_rootless_path().into())) + } } fn file(&self, id: FileId) -> FileResult { diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 7575817a7..030cb0a97 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -44,8 +44,8 @@ static GROUPS: Lazy> = Lazy::new(|| { .module() .scope() .iter() - .filter(|(_, v)| matches!(v, Value::Func(_))) - .map(|(k, _)| k.clone()) + .filter(|(_, v, _)| matches!(v, Value::Func(_))) + .map(|(k, _, _)| k.clone()) .collect(); } } @@ -249,7 +249,7 @@ fn category_page(resolver: &dyn Resolver, category: Category) -> PageModel { // Add values and types. let scope = module.scope(); - for (name, value) in scope.iter() { + for (name, value, _) in scope.iter() { if scope.get_category(name) != Some(category) { continue; } @@ -463,7 +463,7 @@ fn casts( fn scope_models(resolver: &dyn Resolver, name: &str, scope: &Scope) -> Vec { scope .iter() - .filter_map(|(_, value)| { + .filter_map(|(_, value, _)| { let Value::Func(func) = value else { return None }; Some(func_model(resolver, func, &[name], true)) }) @@ -649,7 +649,7 @@ 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() { + for (name, value, _) in group.module().scope().iter() { let Value::Symbol(symbol) = value else { continue }; let complete = |variant: &str| { if variant.is_empty() { diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index 98c300ce5..c9536150f 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -39,16 +39,20 @@ impl World for FuzzWorld { &self.book } - fn main(&self) -> Source { - self.source.clone() + fn main(&self) -> FileId { + self.source.id() } - fn source(&self, src: FileId) -> FileResult { - Err(FileError::NotFound(src.vpath().as_rootless_path().into())) + fn source(&self, id: FileId) -> FileResult { + if id == self.source.id() { + Ok(self.source.clone()) + } else { + Err(FileError::NotFound(id.vpath().as_rootless_path().into())) + } } - fn file(&self, src: FileId) -> FileResult { - Err(FileError::NotFound(src.vpath().as_rootless_path().into())) + fn file(&self, id: FileId) -> FileResult { + Err(FileError::NotFound(id.vpath().as_rootless_path().into())) } fn font(&self, _: usize) -> Option { diff --git a/tests/ref/baseline-box.png b/tests/ref/baseline-box.png index 8d7627c60..b85d0715d 100644 Binary files a/tests/ref/baseline-box.png and b/tests/ref/baseline-box.png differ diff --git a/tests/ref/bidi-whitespace-reset.png b/tests/ref/bidi-whitespace-reset.png index 7d64012f9..e9973798b 100644 Binary files a/tests/ref/bidi-whitespace-reset.png and b/tests/ref/bidi-whitespace-reset.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 e56e23a02..21f9f4e41 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/context-compatibility-locate.png b/tests/ref/context-compatibility-locate.png index 4c8944ab4..32516c00f 100644 Binary files a/tests/ref/context-compatibility-locate.png and b/tests/ref/context-compatibility-locate.png differ diff --git a/tests/ref/eval-mode.png b/tests/ref/eval-mode.png index 5edfa62dc..94357ff4f 100644 Binary files a/tests/ref/eval-mode.png and b/tests/ref/eval-mode.png differ diff --git a/tests/ref/eval-path-resolve-in-show-rule.png b/tests/ref/eval-path-resolve-in-show-rule.png index cf5c183ad..4d695d91a 100644 Binary files a/tests/ref/eval-path-resolve-in-show-rule.png and b/tests/ref/eval-path-resolve-in-show-rule.png differ diff --git a/tests/ref/eval-path-resolve.png b/tests/ref/eval-path-resolve.png index cf5c183ad..4d695d91a 100644 Binary files a/tests/ref/eval-path-resolve.png and b/tests/ref/eval-path-resolve.png differ diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png index 0fd0acc7f..3f8f50ca1 100644 Binary files a/tests/ref/footnote-in-table.png and b/tests/ref/footnote-in-table.png differ diff --git a/tests/ref/hide-image.png b/tests/ref/hide-image.png index 78bc690c8..36dbf6a81 100644 Binary files a/tests/ref/hide-image.png and b/tests/ref/hide-image.png differ diff --git a/tests/ref/hyphenate-outside-of-words.png b/tests/ref/hyphenate-outside-of-words.png new file mode 100644 index 000000000..57b11ed84 Binary files /dev/null and b/tests/ref/hyphenate-outside-of-words.png differ diff --git a/tests/ref/image-baseline-with-box.png b/tests/ref/image-baseline-with-box.png index 411280698..492570d01 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 6f12e8b47..6ecb7dcda 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 6f12e8b47..6ecb7dcda 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 5a3bdec18..24946fdbd 100644 Binary files a/tests/ref/image-fit.png and b/tests/ref/image-fit.png differ diff --git a/tests/ref/image-rgba-png-and-jpeg.png b/tests/ref/image-rgba-png-and-jpeg.png index 601271705..083722819 100644 Binary files a/tests/ref/image-rgba-png-and-jpeg.png and b/tests/ref/image-rgba-png-and-jpeg.png differ diff --git a/tests/ref/image-sizing.png b/tests/ref/image-sizing.png index 7419de141..e1201cf22 100644 Binary files a/tests/ref/image-sizing.png and b/tests/ref/image-sizing.png differ diff --git a/tests/ref/issue-3355-metadata-weak-spacing.png b/tests/ref/issue-3355-metadata-weak-spacing.png new file mode 100644 index 000000000..1ae8a1043 Binary files /dev/null and b/tests/ref/issue-3355-metadata-weak-spacing.png differ diff --git a/tests/ref/issue-3601-empty-raw.png b/tests/ref/issue-3601-empty-raw.png new file mode 100644 index 000000000..be5ea8fc2 Binary files /dev/null and b/tests/ref/issue-3601-empty-raw.png differ diff --git a/tests/ref/issue-4278-par-trim-before-equation.png b/tests/ref/issue-4278-par-trim-before-equation.png new file mode 100644 index 000000000..b05537190 Binary files /dev/null and b/tests/ref/issue-4278-par-trim-before-equation.png differ diff --git a/tests/ref/issue-4361-transparency-leak.png b/tests/ref/issue-4361-transparency-leak.png new file mode 100644 index 000000000..4060d43ac Binary files /dev/null and b/tests/ref/issue-4361-transparency-leak.png differ diff --git a/tests/ref/issue-4454-footnote-ref-numbering.png b/tests/ref/issue-4454-footnote-ref-numbering.png new file mode 100644 index 000000000..0e89dbd91 Binary files /dev/null and b/tests/ref/issue-4454-footnote-ref-numbering.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 new file mode 100644 index 000000000..09506966e Binary files /dev/null and b/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png differ diff --git a/tests/ref/justify-basically-empty.png b/tests/ref/justify-basically-empty.png new file mode 100644 index 000000000..3d1b50c13 Binary files /dev/null and b/tests/ref/justify-basically-empty.png differ diff --git a/tests/ref/math-attach-horizontal-align.png b/tests/ref/math-attach-horizontal-align.png index 507cb0ffd..8aa2a3767 100644 Binary files a/tests/ref/math-attach-horizontal-align.png and b/tests/ref/math-attach-horizontal-align.png differ diff --git a/tests/ref/math-attach-to-group.png b/tests/ref/math-attach-to-group.png index a3d1923eb..ea4115fce 100644 Binary files a/tests/ref/math-attach-to-group.png and b/tests/ref/math-attach-to-group.png differ diff --git a/tests/ref/math-primes-complex.png b/tests/ref/math-primes-complex.png index 5f5558eb2..0e85d08d3 100644 Binary files a/tests/ref/math-primes-complex.png and b/tests/ref/math-primes-complex.png differ diff --git a/tests/ref/math-primes-with-superscript.png b/tests/ref/math-primes-with-superscript.png new file mode 100644 index 000000000..88a892b97 Binary files /dev/null and b/tests/ref/math-primes-with-superscript.png differ diff --git a/tests/ref/math-root-large-body.png b/tests/ref/math-root-large-body.png index 3dd4d848e..b8d9edfbd 100644 Binary files a/tests/ref/math-root-large-body.png and b/tests/ref/math-root-large-body.png differ diff --git a/tests/ref/math-spacing-ignorant.png b/tests/ref/math-spacing-ignorant.png new file mode 100644 index 000000000..6fead62e3 Binary files /dev/null and b/tests/ref/math-spacing-ignorant.png differ diff --git a/tests/ref/pad-followed-by-content.png b/tests/ref/pad-followed-by-content.png index f0f06a6ce..199457ae3 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-metadata-after-trimmed-space.png b/tests/ref/par-metadata-after-trimmed-space.png new file mode 100644 index 000000000..b0de98eab Binary files /dev/null and b/tests/ref/par-metadata-after-trimmed-space.png differ diff --git a/tests/ref/par-trailing-whitespace.png b/tests/ref/par-trailing-whitespace.png new file mode 100644 index 000000000..10c22da5a Binary files /dev/null and b/tests/ref/par-trailing-whitespace.png differ diff --git a/tests/ref/place-background.png b/tests/ref/place-background.png index 7d732717b..e528445e3 100644 Binary files a/tests/ref/place-background.png and b/tests/ref/place-background.png differ diff --git a/tests/ref/place-basic.png b/tests/ref/place-basic.png index 07642c347..07845b7dd 100644 Binary files a/tests/ref/place-basic.png and b/tests/ref/place-basic.png differ diff --git a/tests/ref/place-float-figure.png b/tests/ref/place-float-figure.png index 5411178a0..be2071542 100644 Binary files a/tests/ref/place-float-figure.png and b/tests/ref/place-float-figure.png differ diff --git a/tests/ref/ref-supplements.png b/tests/ref/ref-supplements.png index 3bd8a30ff..8da442881 100644 Binary files a/tests/ref/ref-supplements.png and b/tests/ref/ref-supplements.png differ diff --git a/tests/ref/smartquote-with-embedding-chars.png b/tests/ref/smartquote-with-embedding-chars.png new file mode 100644 index 000000000..569bfb502 Binary files /dev/null and b/tests/ref/smartquote-with-embedding-chars.png differ diff --git a/tests/ref/transform-rotate-and-scale.png b/tests/ref/transform-rotate-and-scale.png index 0dcf67ed2..2487d4b50 100644 Binary files a/tests/ref/transform-rotate-and-scale.png and b/tests/ref/transform-rotate-and-scale.png differ diff --git a/tests/ref/transform-rotate-origin.png b/tests/ref/transform-rotate-origin.png index 152b1e1f8..5292d300c 100644 Binary files a/tests/ref/transform-rotate-origin.png and b/tests/ref/transform-rotate-origin.png differ diff --git a/tests/src/collect.rs b/tests/src/collect.rs index ee4f9db99..f10f4a2e3 100644 --- a/tests/src/collect.rs +++ b/tests/src/collect.rs @@ -48,7 +48,11 @@ impl FilePos { impl Display for FilePos { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}:{}", self.path.display(), self.line) + if self.line > 0 { + write!(f, "{}:{}", self.path.display(), self.line) + } else { + write!(f, "{}", self.path.display()) + } } } diff --git a/tests/src/world.rs b/tests/src/world.rs index ad925a538..47b77d7e5 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -43,8 +43,8 @@ impl World for TestWorld { &self.base.book } - fn main(&self) -> Source { - self.main.clone() + fn main(&self) -> FileId { + self.main.id() } fn source(&self, id: FileId) -> FileResult { diff --git a/tests/suite/foundations/version.typ b/tests/suite/foundations/version.typ index bf2cadb18..a4be7f13e 100644 --- a/tests/suite/foundations/version.typ +++ b/tests/suite/foundations/version.typ @@ -4,7 +4,7 @@ // Test version constructor. // Empty. -#version() +#test(array(version()), ()) // Plain. #test(version(1, 2).major, 1) diff --git a/tests/suite/layout/flow/flow.typ b/tests/suite/layout/flow/flow.typ index 9c48c9acc..7c8ade141 100644 --- a/tests/suite/layout/flow/flow.typ +++ b/tests/suite/layout/flow/flow.typ @@ -65,3 +65,10 @@ = Heading #lorem(6) + +--- issue-3355-metadata-weak-spacing --- +#set page(height: 50pt) +#block(width: 100%, height: 30pt, fill: aqua) +#metadata(none) +#v(10pt, weak: true) +Hi diff --git a/tests/suite/layout/inline/hyphenate.typ b/tests/suite/layout/inline/hyphenate.typ index c366b38f9..debce1da1 100644 --- a/tests/suite/layout/inline/hyphenate.typ +++ b/tests/suite/layout/inline/hyphenate.typ @@ -50,6 +50,16 @@ It's a #emph[Tree]beard. #set text(hyphenate: true) #h(6pt) networks, the rest. +--- hyphenate-outside-of-words --- +// More tests for hyphenation of non-words. +#set text(hyphenate: true) +#block(width: 0pt, "doesn't") +#block(width: 0pt, "(OneNote)") +#block(width: 0pt, "(present)") + +#set text(lang: "de") +#block(width: 0pt, "(bzw.)") + --- hyphenate-pt-repeat-hyphen-natural-word-breaking --- // The word breaker naturally breaks arco-da-velha at arco-/-da-velha, // so we shall repeat the hyphen, even that hyphenate is set to false. diff --git a/tests/suite/layout/spacing.typ b/tests/suite/layout/spacing.typ index dd0fced55..c32e6c8f9 100644 --- a/tests/suite/layout/spacing.typ +++ b/tests/suite/layout/spacing.typ @@ -47,14 +47,14 @@ Totally #h() ignored Hello #h(2cm, weak: true) --- issue-4087 --- -// weak space at the end of the line would be removed. +// Weak space at the end of the line is removed. This is the first line #h(2cm, weak: true) A new line -// non-weak space would be consume a specified width and push next line. +// Non-weak space consumes a specified width and pushes to next line. This is the first line #h(2cm, weak: false) A new line -// similarly weak space at the beginning of the line would be removed. -This is the first line\ #h(2cm, weak: true) A new line +// Similarly, weak space at the beginning of the line is removed. +This is the first line \ #h(2cm, weak: true) A new line -// non-spacing, on the other hand, is not removed. -This is the first line\ #h(2cm, weak: false) A new line +// Non-weak-spacing, on the other hand, is not removed. +This is the first line \ #h(2cm, weak: false) A new line diff --git a/tests/suite/math/attach.typ b/tests/suite/math/attach.typ index c9510c6a5..c5ca57357 100644 --- a/tests/suite/math/attach.typ +++ b/tests/suite/math/attach.typ @@ -84,9 +84,9 @@ $ (-1)^n + (1/2 + 3)^(-1/2) $ // Test that the attachments are aligned horizontally. $ x_1 p_1 frak(p)_1 2_1 dot_1 lg_1 !_1 \\_1 ]_1 "ip"_1 op("iq")_1 \ x^1 b^1 frak(b)^1 2^1 dot^1 lg^1 !^1 \\^1 ]^1 "ib"^1 op("id")^1 \ - x_1 y_1 "_"_1 x^1 l^1 "`"^1 attach(I,tl:1,bl:1,tr:1,br:1) + "_"_1 "`"^1 x_1 y_1 x^1 l^1 attach(I,tl:1,bl:1,tr:1,br:1) scripts(sum)_1^1 integral_1^1 abs(1/2)_1^1 \ - x^1_1, "("b y")"^1_1 != (b y)^1_1, "[โˆซ]"_1 [integral]_1 $ + x^1_1, ")"^1_1 (b y)^1_1, "[โˆซ]"_1 [integral]_1 $ --- math-attach-limit --- // Test limit. diff --git a/tests/suite/math/primes.typ b/tests/suite/math/primes.typ index e10f8876c..78bb915aa 100644 --- a/tests/suite/math/primes.typ +++ b/tests/suite/math/primes.typ @@ -12,7 +12,11 @@ $a'_b^c$, $a_b'^c$, $a_b^c'$, $a_b'^c'^d'$ $(a'_b')^(c'_d')$, $a'/b'$, $a_b'/c_d'$ -$โˆซ'$, $โˆ‘'$, $ โˆ‘'_S' $ +$โˆซ'$, $โˆ‘'$, $a'^2^2$, $a'_2_2$ + +$f_n'^a'$, $f^a'_n'$ + +$ โˆ‘'_S' $ --- math-primes-attach --- // Test attaching primes only @@ -48,3 +52,12 @@ $ #g''''''''''''''''' \ gg' $ + +--- math-primes-with-superscript --- +// Test prime symbols don't raise the superscript position +$ + sqrt(f)/f + sqrt(f^2)/f^2 + sqrt(f'^2)/f'^2 + sqrt(f''_n^2)/f''^2_n +$ diff --git a/tests/suite/math/spacing.typ b/tests/suite/math/spacing.typ index 2a387f929..707c09bb8 100644 --- a/tests/suite/math/spacing.typ +++ b/tests/suite/math/spacing.typ @@ -49,6 +49,16 @@ $integral f(x) thin dif x$, // Both are weak, collide $integral f(x) #h(0.166em, weak: true)dif x$ +--- math-spacing-ignorant --- +// Test spacing with ignorant elements +$#metadata(none) "text"$ \ +$#place(dx: 5em)[Placed] "text"$ \ +// Operator spacing +$#counter("test").update(3) + b$ \ +$#place(dx: 5em)[a] + b$ +// Validate that ignorant elements are layouted +#context test(counter("test").get(), (3,)) + --- issue-1052-math-number-spacing --- // Test spacing after numbers in math. $ diff --git a/tests/suite/model/footnote.typ b/tests/suite/model/footnote.typ index 34450ca4c..d72ca25a0 100644 --- a/tests/suite/model/footnote.typ +++ b/tests/suite/model/footnote.typ @@ -187,3 +187,7 @@ B #footnote[b] #set page(height: 50pt) #footnote[A] #footnote[B] + +--- issue-4454-footnote-ref-numbering --- +// Test that footnote references are numbered correctly. +A #footnote(numbering: "*")[B], C @fn, D @fn, E @fn. diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index 085e06ede..2409dbbe3 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -176,3 +176,10 @@ Ok ... // 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]) + +--- issue-4476-rtl-title-ending-in-ltr-text --- +#set text(lang: "he") +#outline() + += ื”ื•ืงื•ืก Pocus += ื–ื•ื”ื™ ื›ื•ืชืจืช ืฉืชื•ืจื’ืžื” ืขืœ ื™ื“ื™ ืžื—ืฉื‘ diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index f07c4c6ce..80bc9f3e1 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -78,3 +78,22 @@ Welcome \ here. Does this work well? #set text(dir: rtl) ู„ุขู† ูˆู‚ุฏ ุฃุธู„ู… ุงู„ู„ูŠู„ ูˆุจุฏุฃุช ุงู„ู†ุฌูˆู… ุชู†ุถุฎ ูˆุฌู‡ ุงู„ุทุจูŠุนุฉ ุงู„ุชูŠ ุฃุนู’ูŠูŽุชู’ ู…ู† ุทูˆู„ ู…ุง ุงู†ุจุนุซุช ููŠ ุงู„ู†ู‡ุงุฑ + +--- par-trailing-whitespace --- +// Ensure that trailing whitespace layouts as intended. +#box(fill: aqua, " ") + +--- par-empty-metadata --- +// Check that metadata still works in a zero length paragraph. +#block(height: 0pt)[#""#metadata(false)] +#context test(query().first().value, false) + +--- par-metadata-after-trimmed-space --- +// Ensure that metadata doesn't prevent trailing spaces from being trimmed. +#set par(justify: true, linebreaks: "simple") +#set text(hyphenate: false) +Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. + +--- issue-4278-par-trim-before-equation --- +#set par(justify: true) +#lorem(6) aa $a = c + b$ diff --git a/tests/suite/model/ref.typ b/tests/suite/model/ref.typ index 200f40aaa..d0881202c 100644 --- a/tests/suite/model/ref.typ +++ b/tests/suite/model/ref.typ @@ -54,3 +54,8 @@ $ A = 1 $ // Error: 1-7 label occurs in the document and its bibliography @arrgh #bibliography("/assets/bib/works.bib") + +--- issue-4536-non-whitespace-before-ref --- +// Test reference with non-whitespace before it. +#figure[] <1> +#test([(#ref(<1>))], [(@1)]) diff --git a/tests/suite/text/smartquote.typ b/tests/suite/text/smartquote.typ index 28fcba5b7..02ac4455e 100644 --- a/tests/suite/text/smartquote.typ +++ b/tests/suite/text/smartquote.typ @@ -69,6 +69,11 @@ Some people's thought on this would be #[#set smartquote(enabled: false); "stran "'test' statement" \ "statement 'test'" +--- smartquote-with-embedding-chars --- +#set text(lang: "fr") +"#"\u{202A}"bonjour#"\u{202C}"" \ +#"\u{202A}""bonjour"#"\u{202C}" + --- smartquote-custom --- // Use language quotes for missing keys, allow partial reset #set smartquote(quotes: "ยซยป") diff --git a/tests/suite/visualize/color.typ b/tests/suite/visualize/color.typ index 45000ab24..bc8f8be5b 100644 --- a/tests/suite/visualize/color.typ +++ b/tests/suite/visualize/color.typ @@ -333,3 +333,10 @@ --- issue-color-mix-luma --- // When mixing luma colors, we accidentally used the wrong component. #rect(fill: gradient.linear(black, silver, space: luma)) + +--- issue-4361-transparency-leak --- +// Ensure that transparency doesn't leak from shapes to images in PDF. The PNG +// test doesn't validate it, but at least we can discover regressions on the PDF +// output with a PDF comparison script. +#rect(fill: red.transparentize(50%)) +#image("/assets/images/tiger.jpg", width: 45pt)