Compare commits

..

100 Commits

Author SHA1 Message Date
Tobias Schmitz
d204a28818
Expand text link boxes vertically by half the leading spacing (#6252) 2025-05-12 18:12:35 +00:00
Tobias Schmitz
22a117a091
Prohibit some line break opportunities between LTR-ISOLATE and OBJECT-REPLACEMENT-CHARACTER (#6251)
Co-authored-by: Max <max@mkor.je>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-05-12 09:16:38 +00:00
Tobias Schmitz
26c19a49c8
Use the infer crate to determine if pdf embeds should be compressed (#6256) 2025-05-12 08:07:43 +00:00
Tobias Schmitz
54c5113a83
Catch indefinite loop in realization due to cycle between show and grouping rule (#6259) 2025-05-12 08:06:18 +00:00
Tobias Schmitz
9b09146a6b
Use list spacing for attach spacing in tight lists (#6242) 2025-05-06 14:03:48 +00:00
Tobias Schmitz
b322da930f
Respect RTL cell layouting order in grid layout (#6232)
Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
2025-05-06 08:26:55 +00:00
Malo
14241ec1aa
Use the right field name for figure.caption.position (#6226) 2025-05-01 15:43:07 +00:00
Andrew Voynov
3e6691a93b
Fix frac syntax section typo (#6193) 2025-04-18 14:27:07 +00:00
Max
7e072e2493
Add test for flattened accents in math (#6188) 2025-04-17 14:10:27 +00:00
Malo
c21c1c391b
Use measure width argument in layout doc (#6160) 2025-04-10 09:27:42 +00:00
Approximately Equal
94a497a01f
Add HTML meta tags for document authors and keywords (#6134) 2025-04-07 20:18:52 +00:00
alluring-mushroom
9829bd8326
Document exceptions and alternatives to using type (#6027)
Co-authored-by: Zedd Serjeant <Zedd.Serjeant@PumpkinEng.com.au>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-04-07 19:56:20 +00:00
Andrew Voynov
43c3d5d3af
Improved ratio and relative length docs (#5750)
Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-04-07 19:47:02 +00:00
+merlan #flirora
14a0565d95
Show warnings from eval (#6100)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-04-07 18:42:29 +00:00
Laurenz
bd2e76e11d
Bump OpenSSL (#6153) 2025-04-07 18:20:27 +00:00
Andrew Voynov
14928ef962
Fix typo in module docs (#6146)
Co-authored-by: Alberto Corbi <alberto_corbi@icloud.com>
2025-04-07 17:47:29 +00:00
Laurenz
d55abf0842
Update community section in README (#6150) 2025-04-07 17:46:46 +00:00
Markus Langgeng Iman Saputra
ea336a6ac7
Add Indonesian translation (#6108)
Co-authored-by: Malo <57839069+MDLC01@users.noreply.github.com>
2025-04-04 15:50:13 +00:00
Malo
387a8b4895
Display color spaces in the order in which they are presented in the doc (#6140) 2025-04-04 11:53:14 +00:00
Laurenz
bf8751c063
Switch to released krilla version (#6137) 2025-04-04 08:35:51 +00:00
Malo
ed2106e28d
Disallow empty font lists (#6049) 2025-04-02 11:47:42 +00:00
Malo
417f5846b6
Support comparison functions in array.sorted (#5627)
Co-authored-by: +merlan #flirora <uruwi@protonmail.com>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-04-02 09:41:45 +00:00
Ian Wrzesinski
12699eb7f4
Parse multi-character numbers consistently in math (#5996)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-04-02 09:30:04 +00:00
Laurenz Stampfl
96dd67e011
Switch PDF backend to krilla (#5420)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-04-01 14:42:52 +00:00
Max
012e14d40c
Unify layout of vec and cases with mat (#5934) 2025-03-31 09:38:04 +00:00
Max
4f0fbfb7e0
Add dotless parameter to math.accent (#5939)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-31 09:17:49 +00:00
+merlan #flirora
a64af130dc
Add default parameter for array.{first, last} (#5970) 2025-03-31 09:06:18 +00:00
Malo
1082181a6f
Improve french smartquotes (#5976) 2025-03-31 09:01:01 +00:00
+merlan #flirora
e60d3021a7
Add env setting for ignore_system_fonts (#6092) 2025-03-31 08:17:37 +00:00
Astra3
326bec1f0d
Correcting Czech translation in typst-library (#6101) 2025-03-31 08:16:47 +00:00
Myriad-Dreamin
758ee78ef5
Make World::font implementations safe (#6117) 2025-03-31 08:08:55 +00:00
Matt Fellenz
efdb75558f
IDE: complete jump-to-cursor impl (#6037) 2025-03-28 17:33:16 +00:00
frozolotl
20ee446eba
Fix descriptions of color maps (#6096) 2025-03-28 15:30:30 +00:00
Philipp Niedermayer
b7a4382a73
Fix typo (#6104) 2025-03-28 15:28:03 +00:00
Laurenz Stampfl
838a46dbb7
Test all exif rotation types and fix two of them (#6102) 2025-03-27 10:59:32 +00:00
PgBiel
1f1c133878
Refactor grid header and footer resolving (#5919) 2025-03-24 20:42:48 +00:00
Laurenz
1e591ac8dc
Bump zip (#6091) 2025-03-24 18:17:29 +00:00
Eduardo Sánchez Muñoz
38213ed534
Use u64 instead of usize to store counter and enumeration item numbers, so behavior does not vary from 64-bit to 32-bit platforms (#6026) 2025-03-24 18:16:33 +00:00
Andrew Voynov
636eea18bc
Expand page breaks' triggers for page(height: auto) in docs (#6081) 2025-03-24 18:08:39 +00:00
Ian Wrzesinski
91956d1f03
Use std::ops::ControlFlow in Content::traverse (#6053)
Co-authored-by: Max Mynter <maxmynter@me.com>
2025-03-24 18:07:19 +00:00
Wolf-SO
1b2714e1a7
Update 1-writing.md to improve readability (#6040) 2025-03-12 18:29:35 +00:00
Laurenz
95a7e28e25
Make two typst-kit functions private (#6045) 2025-03-12 12:46:03 +00:00
Kevin K.
37bb632d2e
Fix missing words and paren in docs (#6046) 2025-03-12 12:45:57 +00:00
Michael Fortunato
24b2f98bf9
Fix typo in 4-template.md (#6047) 2025-03-12 12:45:22 +00:00
Andrew Voynov
0214320087
Fix parallel package installation (#5979)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-11 20:20:41 +00:00
Max
96f6957371
Fix math.root frame size (#6021) 2025-03-11 10:18:15 +00:00
evie
3650859ae8
Fix cargo clippy warnings (mostly about .repeat.take and .next_back) (#6038) 2025-03-11 10:00:53 +00:00
Caleb Maclennan
bd531e08dc
Bump rustybuzz (and adjacent crates) (#5407) 2025-03-10 12:45:08 +00:00
Ludovico Gerardi
e66e190a21
Fix typo in docs (#6034) 2025-03-10 11:39:30 +00:00
Laurenz
db9a83d9fc Bump version on main
The tagged commit itself is on the 0.13 branch.
2025-03-07 11:19:12 +01:00
Laurenz
8d3488a07d
0.13.1 changelog (#6025) 2025-03-07 10:03:52 +00:00
Laurenz
476c2df312
Mark breaking symbol changes as breaking in 0.13.0 changelog (#6024) 2025-03-07 09:17:11 +00:00
Malo
e0b2c32a8e
Mention that sym.ohm was removed in the 0.13.0 changelog (#6017)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-07 09:05:16 +00:00
Laurenz
99b7d2898e
Replace par function call in tutorial (#6023) 2025-03-07 08:47:56 +00:00
Laurenz
e1a9166e1d
Hotfix for labels on symbols (#6015) 2025-03-07 08:22:42 +00:00
Andrew Voynov
6271cdceae
Fix debug implementation of Recipe (#5997) 2025-03-04 09:33:39 +00:00
LN Liberda
63fda9935f
Run tests on 32-bit via Ubuntu multilib (#5937)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-03 13:10:58 +00:00
3w36zj6
8820a00beb
Respect quotes: false in inline quote (#5991)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-03-03 11:50:47 +00:00
andis854
9a6ffbc7db
Added snap to installation instructions (#5984) 2025-03-03 11:40:58 +00:00
Andrew Voynov
bf0d45e2c0
Make array.chunks example more readable (#5975) 2025-03-03 11:31:39 +00:00
F2011
d4def09962
Correct typo (#5971) 2025-03-03 11:23:29 +00:00
Tijme
66679920b2
Fix docs example with type/string comparison (#5987) 2025-03-03 09:32:06 +00:00
Ian Wrzesinski
cfb3b1a270
Improve clarity of ast.rs for newcomers to the codebase (#5784)
Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com>
Co-authored-by: T0mstone <39707032+T0mstone@users.noreply.github.com>
2025-02-26 20:10:36 +00:00
Emmanuel Lesueur
52f1f53973
Fix curve with multiple non-closed components. (#5963) 2025-02-26 18:07:29 +00:00
Malo
d6b0d68ffa
Add more methods to direction (#5893) 2025-02-25 14:19:17 +00:00
Laurenz
8f039dd614
Only autocomplete methods which take self (#5824) 2025-02-25 14:10:01 +00:00
Malo
2eef9e84e1
Improve hints for show rule recursion depth (#5856) 2025-02-25 14:09:52 +00:00
evie
d11ad80dee
Add #str.normalize(form) (#5631)
Co-authored-by: +merlan #flirora <uruwi@protonmail.com>
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-25 14:01:01 +00:00
Laurenz
bad343748b
Fix paper name in page setup guide (#5956) 2025-02-25 13:00:22 +00:00
Laurenz
f31c971624
Deduplicate watcher update call (#5955) 2025-02-25 12:47:41 +00:00
aodenis
acd3a5b7a5
Fix high CPU usage due to inotify watch triggering itself (#5905)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-25 12:41:54 +00:00
Laurenz
225e845021
Fix introspection of HTML root sibling metadata (#5953) 2025-02-25 11:31:15 +00:00
Sharzy
36d83c8c09
HTML export: fix elem counting on classify_output (#5910)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-02-24 16:35:13 +00:00
Malo
3744c99b07
Override the default math class of some characters (#5949) 2025-02-24 16:15:17 +00:00
Max
81efc82d3c
Fix math accent base height calculation (#5941) 2025-02-24 16:05:36 +00:00
Laurenz
69c3f95705
Bump MSRV to 1.83 and Rust in CI to 1.85 (#5946) 2025-02-24 12:28:01 +00:00
Laurenz
ebe2543264
Fix comparison of Func and NativeFuncData (#5943) 2025-02-24 11:17:31 +00:00
Malo
56f4fa2b4d
Documentation improvements (#5888) 2025-02-23 11:31:28 +00:00
Max
55bc5f4c94
Make math shorthands noncontinuable (#5925) 2025-02-23 11:28:24 +00:00
PgBiel
240f238eee
Fix HTML export of table with gutter (#5920) 2025-02-23 11:26:14 +00:00
Laurenz
d199546f9f Bump version on main
The tagged commit itself is on the 0.13 branch.
2025-02-19 11:25:31 +01:00
Laurenz
a543ee9445
Update changelog (#5894) 2025-02-19 09:59:27 +00:00
Matthew Toohey
3de3813ca0
--make-deps fixes (#5873) 2025-02-18 18:04:40 +00:00
ᡥᠠᡳᡤᡳᠶᠠ ᡥᠠᠯᠠ·ᠨᡝᡴᠣ 猫
74e4f78687
HTML export: Use <code> for inline RawElem (#5884) 2025-02-18 10:16:19 +00:00
Laurenz
25c86accbb
More robust SVG auto-detection (#5878) 2025-02-17 10:56:00 +00:00
Laurenz
5fc679f3e7
Remove Linux Libertine warning (#5876) 2025-02-16 13:18:39 +00:00
Ana Gelez
19a12f379f
Lazy parsing of the package index (#5851) 2025-02-12 15:50:48 +00:00
+merlan #flirora
02cd43e27f
Gradient::repeat: Fix floating-point error in stop calculation (#5837) 2025-02-12 12:38:40 +00:00
+merlan #flirora
83ad407d3c
Update documentation for float.{to-bits, from-bits} (#5836) 2025-02-12 12:35:03 +00:00
Laurenz
a0cd89b478
Fix autocomplete and jumps in math (#5849) 2025-02-11 10:30:30 +00:00
Laurenz
81021fa1a2
Bump typst-assets (#5845) 2025-02-10 15:39:14 +00:00
Laurenz
89e71acecd
Respect par constructor arguments (#5842) 2025-02-10 14:37:19 +00:00
TwoF1nger
ee47cb8469
Add smart quotes for Bulgarian (#5807) 2025-02-10 10:42:16 +00:00
Malo
25e27169e1
Add warning for pdf.embed elem used with HTML (#5829) 2025-02-10 10:39:32 +00:00
PgBiel
3fba256405
Don't crash on image with zero DPI (#5835) 2025-02-10 10:39:04 +00:00
Laurenz
e4f8e57c53
Fix unnecessary import rename warning (#5828) 2025-02-06 21:10:43 +00:00
Laurenz
a1c73b41b8
Document removals in changelog (#5827) 2025-02-06 20:57:46 +00:00
Laurenz
d61f57365b
Fix docs outline for nested definitions (#5823) 2025-02-06 10:18:35 +00:00
Malo
ca702c7f82
Documentation fixes and improvements (#5816) 2025-02-06 10:18:10 +00:00
Laurenz
d897ab5e7d
Autocomplete content methods (#5822) 2025-02-06 09:34:28 +00:00
326 changed files with 6920 additions and 7039 deletions

View File

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

View File

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

323
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "adler2" name = "adler2"
@ -217,6 +217,20 @@ name = "bytemuck"
version = "1.21.0" version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "byteorder" name = "byteorder"
@ -735,11 +749,12 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.35" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"libz-rs-sys",
"miniz_oxide", "miniz_oxide",
] ]
@ -749,6 +764,15 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
[[package]]
name = "float-cmp"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -761,6 +785,15 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "font-types"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "fontconfig-parser" name = "fontconfig-parser"
version = "0.5.7" version = "0.5.7"
@ -772,9 +805,9 @@ dependencies = [
[[package]] [[package]]
name = "fontdb" name = "fontdb"
version = "0.21.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37be9fc20d966be438cd57a45767f73349477fb0f85ce86e000557f787298afb" checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
dependencies = [ dependencies = [
"fontconfig-parser", "fontconfig-parser",
"log", "log",
@ -829,6 +862,15 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]] [[package]]
name = "getopts" name = "getopts"
version = "0.2.21" version = "0.2.21"
@ -871,6 +913,12 @@ dependencies = [
"weezl", "weezl",
] ]
[[package]]
name = "glidesort"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2e102e6eb644d3e0b186fc161e4460417880a0a0b87d235f2e5b8fb30f2e9e0"
[[package]] [[package]]
name = "half" name = "half"
version = "2.4.1" version = "2.4.1"
@ -966,7 +1014,7 @@ checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"serde", "serde",
"yoke", "yoke 0.7.5",
"zerofrom", "zerofrom",
"zerovec", "zerovec",
] ]
@ -1064,7 +1112,7 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
"tinystr", "tinystr",
"writeable", "writeable",
"yoke", "yoke 0.7.5",
"zerofrom", "zerofrom",
"zerovec", "zerovec",
] ]
@ -1175,9 +1223,9 @@ dependencies = [
[[package]] [[package]]
name = "image-webp" name = "image-webp"
version = "0.1.3" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
dependencies = [ dependencies = [
"byteorder-lite", "byteorder-lite",
"quick-error", "quick-error",
@ -1211,6 +1259,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "infer"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.0" version = "0.11.0"
@ -1310,6 +1364,50 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "krilla"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9"
dependencies = [
"base64",
"bumpalo",
"comemo",
"flate2",
"float-cmp 0.10.0",
"fxhash",
"gif",
"image-webp",
"imagesize",
"once_cell",
"pdf-writer",
"png",
"rayon",
"rustybuzz",
"siphasher",
"skrifa",
"subsetter",
"tiny-skia-path",
"xmp-writer",
"yoke 0.8.0",
"zune-jpeg",
]
[[package]]
name = "krilla-svg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e"
dependencies = [
"flate2",
"fontdb",
"krilla",
"png",
"resvg",
"tiny-skia",
"usvg",
]
[[package]] [[package]]
name = "kurbo" name = "kurbo"
version = "0.11.1" version = "0.11.1"
@ -1371,6 +1469,15 @@ dependencies = [
"redox_syscall", "redox_syscall",
] ]
[[package]]
name = "libz-rs-sys"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "902bc563b5d65ad9bba616b490842ef0651066a1a1dc3ce1087113ffcb873c8d"
dependencies = [
"zlib-rs",
]
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
version = "0.5.6" version = "0.5.6"
@ -1458,9 +1565,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.3" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32", "simd-adler32",
@ -1601,9 +1708,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.70" version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"cfg-if", "cfg-if",
@ -1642,9 +1749,9 @@ dependencies = [
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.105" version = "0.9.107"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -1738,9 +1845,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]] [[package]]
name = "pdf-writer" name = "pdf-writer"
version = "0.12.1" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5df03c7d216de06f93f398ef06f1385a60f2c597bb96f8195c8d98e08a26b1d5" checksum = "3ea27c5015ab81753fc61e49f8cde74999346605ee148bb20008ef3d3150e0dc"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"itoa", "itoa",
@ -1804,9 +1911,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]] [[package]]
name = "pixglyph" name = "pixglyph"
version = "0.5.1" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d15afa937836bf3d876f5a04ce28810c06045857bf46c3d0d31073b8aada5494" checksum = "3c1106193bc18a4b840eb075ff6664c8a0b0270f0531bb12a7e9c803e53b55c5"
dependencies = [ dependencies = [
"ttf-parser", "ttf-parser",
] ]
@ -1997,6 +2104,16 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "read-fonts"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288"
dependencies = [
"bytemuck",
"font-types",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.8" version = "0.5.8"
@ -2048,9 +2165,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "resvg" name = "resvg"
version = "0.43.0" version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7314563c59c7ce31c18e23ad3dd092c37b928a0fa4e1c0a1a6504351ab411d1" checksum = "dd43d1c474e9dadf09a8fdf22d713ba668b499b5117b9b9079500224e26b5b29"
dependencies = [ dependencies = [
"gif", "gif",
"image-webp", "image-webp",
@ -2121,9 +2238,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]] [[package]]
name = "rustybuzz" name = "rustybuzz"
version = "0.18.0" version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"bytemuck", "bytemuck",
@ -2315,6 +2432,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "skrifa"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8"
dependencies = [
"bytemuck",
"read-fonts",
]
[[package]] [[package]]
name = "slotmap" name = "slotmap"
version = "1.0.7" version = "1.0.7"
@ -2361,7 +2488,7 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
dependencies = [ dependencies = [
"float-cmp", "float-cmp 0.9.0",
] ]
[[package]] [[package]]
@ -2404,28 +2531,11 @@ dependencies = [
[[package]] [[package]]
name = "subsetter" name = "subsetter"
version = "0.2.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f98178f34057d4d4de93d68104007c6dea4dfac930204a69ab4622daefa648" checksum = "35539e8de3dcce8dd0c01f3575f85db1e5ac1aea1b996d2d09d89f148bc91497"
[[package]]
name = "svg2pdf"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5014c9dadcf318fb7ef8c16438e95abcc9de1ae24d60d5bccc64c55100c50364"
dependencies = [ dependencies = [
"fontdb", "fxhash",
"image",
"log",
"miniz_oxide",
"once_cell",
"pdf-writer",
"resvg",
"siphasher",
"subsetter",
"tiny-skia",
"ttf-parser",
"usvg",
] ]
[[package]] [[package]]
@ -2709,9 +2819,9 @@ dependencies = [
[[package]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.24.1" version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
dependencies = [ dependencies = [
"core_maths", "core_maths",
] ]
@ -2735,7 +2845,7 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]] [[package]]
name = "typst" name = "typst"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2752,13 +2862,12 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.13.0-rc1" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/typst/typst-assets?rev=ab1295f#ab1295ff896444e51902e03c2669955e1d73604a"
checksum = "4e364df2dd61caf35f959a879e55654922a8cea77d4886103ed735c45c888445"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -2803,12 +2912,12 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.12.0" version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c" source = "git+https://github.com/typst/typst-dev-assets?rev=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"clap", "clap",
"ecow", "ecow",
@ -2831,7 +2940,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-eval" name = "typst-eval"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2849,7 +2958,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-fuzz" name = "typst-fuzz"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"libfuzzer-sys", "libfuzzer-sys",
@ -2861,7 +2970,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-html" name = "typst-html"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2875,7 +2984,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-ide" name = "typst-ide"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
@ -2892,16 +3001,19 @@ dependencies = [
[[package]] [[package]]
name = "typst-kit" name = "typst-kit"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"dirs", "dirs",
"ecow", "ecow",
"env_proxy", "env_proxy",
"fastrand",
"flate2", "flate2",
"fontdb", "fontdb",
"native-tls", "native-tls",
"once_cell", "once_cell",
"openssl", "openssl",
"serde",
"serde_json",
"tar", "tar",
"typst-assets", "typst-assets",
"typst-library", "typst-library",
@ -2913,7 +3025,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-layout" name = "typst-layout"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"az", "az",
"bumpalo", "bumpalo",
@ -2943,7 +3055,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-library" name = "typst-library"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"az", "az",
"bitflags 2.8.0", "bitflags 2.8.0",
@ -2956,6 +3068,7 @@ dependencies = [
"ecow", "ecow",
"flate2", "flate2",
"fontdb", "fontdb",
"glidesort",
"hayagriva", "hayagriva",
"icu_properties", "icu_properties",
"icu_provider", "icu_provider",
@ -2965,6 +3078,7 @@ dependencies = [
"kamadak-exif", "kamadak-exif",
"kurbo", "kurbo",
"lipsum", "lipsum",
"memchr",
"palette", "palette",
"phf", "phf",
"png", "png",
@ -2993,6 +3107,7 @@ dependencies = [
"typst-timing", "typst-timing",
"typst-utils", "typst-utils",
"unicode-math-class", "unicode-math-class",
"unicode-normalization",
"unicode-segmentation", "unicode-segmentation",
"unscanny", "unscanny",
"usvg", "usvg",
@ -3002,7 +3117,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-macros" name = "typst-macros"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -3012,33 +3127,27 @@ dependencies = [
[[package]] [[package]]
name = "typst-pdf" name = "typst-pdf"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"arrayvec",
"base64",
"bytemuck", "bytemuck",
"comemo", "comemo",
"ecow", "ecow",
"image", "image",
"indexmap 2.7.1", "infer",
"miniz_oxide", "krilla",
"pdf-writer", "krilla-svg",
"serde", "serde",
"subsetter",
"svg2pdf",
"ttf-parser",
"typst-assets", "typst-assets",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
"typst-syntax", "typst-syntax",
"typst-timing", "typst-timing",
"typst-utils", "typst-utils",
"xmp-writer",
] ]
[[package]] [[package]]
name = "typst-realize" name = "typst-realize"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bumpalo", "bumpalo",
@ -3054,7 +3163,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-render" name = "typst-render"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"comemo", "comemo",
@ -3070,7 +3179,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-svg" name = "typst-svg"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"base64", "base64",
"comemo", "comemo",
@ -3088,7 +3197,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-syntax" name = "typst-syntax"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"ecow", "ecow",
"serde", "serde",
@ -3104,7 +3213,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-tests" name = "typst-tests"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"clap", "clap",
"comemo", "comemo",
@ -3129,7 +3238,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-timing" name = "typst-timing"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"parking_lot", "parking_lot",
"serde", "serde",
@ -3139,7 +3248,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-utils" name = "typst-utils"
version = "0.13.0-rc1" version = "0.13.1"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"portable-atomic", "portable-atomic",
@ -3182,15 +3291,15 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]] [[package]]
name = "unicode-bidi-mirroring" name = "unicode-bidi-mirroring"
version = "0.3.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f" checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe"
[[package]] [[package]]
name = "unicode-ccc" name = "unicode-ccc"
version = "0.3.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
@ -3285,9 +3394,9 @@ dependencies = [
[[package]] [[package]]
name = "usvg" name = "usvg"
version = "0.43.0" version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6803057b5cbb426e9fb8ce2216f3a9b4ca1dd2c705ba3cbebc13006e437735fd" checksum = "2ac8e0e3e4696253dc06167990b3fe9a2668ab66270adf949a464db4088cb354"
dependencies = [ dependencies = [
"base64", "base64",
"data-url", "data-url",
@ -3657,9 +3766,9 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
[[package]] [[package]]
name = "xmp-writer" name = "xmp-writer"
version = "0.3.1" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb5954c9ca6dcc869e98d3e42760ed9dab08f3e70212b31d7ab8ae7f3b7a487" checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7"
[[package]] [[package]]
name = "xz2" name = "xz2"
@ -3697,7 +3806,19 @@ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
dependencies = [ dependencies = [
"serde", "serde",
"stable_deref_trait", "stable_deref_trait",
"yoke-derive", "yoke-derive 0.7.5",
"zerofrom",
]
[[package]]
name = "yoke"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive 0.8.0",
"zerofrom", "zerofrom",
] ]
@ -3713,6 +3834,18 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "yoke-derive"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"
@ -3774,7 +3907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [ dependencies = [
"serde", "serde",
"yoke", "yoke 0.7.5",
"zerofrom", "zerofrom",
"zerovec-derive", "zerovec-derive",
] ]
@ -3792,21 +3925,25 @@ dependencies = [
[[package]] [[package]]
name = "zip" name = "zip"
version = "2.2.2" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88"
dependencies = [ dependencies = [
"arbitrary", "arbitrary",
"crc32fast", "crc32fast",
"crossbeam-utils", "crossbeam-utils",
"displaydoc",
"flate2", "flate2",
"indexmap 2.7.1", "indexmap 2.7.1",
"memchr", "memchr",
"thiserror 2.0.11",
"zopfli", "zopfli",
] ]
[[package]]
name = "zlib-rs"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b20717f0917c908dc63de2e44e97f1e6b126ca58d0e391cee86d504eb8fbd05"
[[package]] [[package]]
name = "zopfli" name = "zopfli"
version = "0.8.1" version = "0.8.1"

View File

@ -4,8 +4,8 @@ default-members = ["crates/typst-cli"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.13.0-rc1" version = "0.13.1"
rust-version = "1.80" # also change in ci.yml rust-version = "1.83" # also change in ci.yml
authors = ["The Typst Project Developers"] authors = ["The Typst Project Developers"]
edition = "2021" edition = "2021"
homepage = "https://typst.app" homepage = "https://typst.app"
@ -16,24 +16,24 @@ keywords = ["typst"]
readme = "README.md" readme = "README.md"
[workspace.dependencies] [workspace.dependencies]
typst = { path = "crates/typst", version = "0.13.0-rc1" } typst = { path = "crates/typst", version = "0.13.1" }
typst-cli = { path = "crates/typst-cli", version = "0.13.0-rc1" } typst-cli = { path = "crates/typst-cli", version = "0.13.1" }
typst-eval = { path = "crates/typst-eval", version = "0.13.0-rc1" } typst-eval = { path = "crates/typst-eval", version = "0.13.1" }
typst-html = { path = "crates/typst-html", version = "0.13.0-rc1" } typst-html = { path = "crates/typst-html", version = "0.13.1" }
typst-ide = { path = "crates/typst-ide", version = "0.13.0-rc1" } typst-ide = { path = "crates/typst-ide", version = "0.13.1" }
typst-kit = { path = "crates/typst-kit", version = "0.13.0-rc1" } typst-kit = { path = "crates/typst-kit", version = "0.13.1" }
typst-layout = { path = "crates/typst-layout", version = "0.13.0-rc1" } typst-layout = { path = "crates/typst-layout", version = "0.13.1" }
typst-library = { path = "crates/typst-library", version = "0.13.0-rc1" } typst-library = { path = "crates/typst-library", version = "0.13.1" }
typst-macros = { path = "crates/typst-macros", version = "0.13.0-rc1" } typst-macros = { path = "crates/typst-macros", version = "0.13.1" }
typst-pdf = { path = "crates/typst-pdf", version = "0.13.0-rc1" } typst-pdf = { path = "crates/typst-pdf", version = "0.13.1" }
typst-realize = { path = "crates/typst-realize", version = "0.13.0-rc1" } typst-realize = { path = "crates/typst-realize", version = "0.13.1" }
typst-render = { path = "crates/typst-render", version = "0.13.0-rc1" } typst-render = { path = "crates/typst-render", version = "0.13.1" }
typst-svg = { path = "crates/typst-svg", version = "0.13.0-rc1" } typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.0-rc1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.0-rc1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.0-rc1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = "0.13.0-rc1" typst-assets = { git = "https://github.com/typst/typst-assets", rev = "ab1295f" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -55,9 +55,11 @@ ctrlc = "3.4.1"
dirs = "6" dirs = "6"
ecow = { version = "0.2", features = ["serde"] } ecow = { version = "0.2", features = ["serde"] }
env_proxy = "0.4" env_proxy = "0.4"
fastrand = "2.3"
flate2 = "1" flate2 = "1"
fontdb = { version = "0.21", default-features = false } fontdb = { version = "0.23", default-features = false }
fs_extra = "1.3" fs_extra = "1.3"
glidesort = "0.1.2"
hayagriva = "0.8.1" hayagriva = "0.8.1"
heck = "0.5" heck = "0.5"
hypher = "0.1.4" hypher = "0.1.4"
@ -69,23 +71,25 @@ icu_segmenter = { version = "1.4", features = ["serde"] }
if_chain = "1" if_chain = "1"
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] }
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6" kamadak-exif = "0.6"
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = "0.1.0"
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"
miniz_oxide = "0.8" memchr = "2"
native-tls = "0.2" native-tls = "0.2"
notify = "8" notify = "8"
once_cell = "1" once_cell = "1"
open = "5.0.1" open = "5.0.1"
openssl = "0.10" openssl = "0.10.72"
oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] } oxipng = { version = "9.0", default-features = false, features = ["filetime", "parallel", "zopfli"] }
palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] } palette = { version = "0.7.3", default-features = false, features = ["approx", "libm"] }
parking_lot = "0.12.1" parking_lot = "0.12.1"
pathdiff = "0.2" pathdiff = "0.2"
pdf-writer = "0.12.1"
phf = { version = "0.11", features = ["macros"] } phf = { version = "0.11", features = ["macros"] }
pixglyph = "0.5.1" pixglyph = "0.6"
png = "0.17" png = "0.17"
portable-atomic = "1.6" portable-atomic = "1.6"
proc-macro2 = "1" proc-macro2 = "1"
@ -95,10 +99,10 @@ quote = "1"
rayon = "1.7.0" rayon = "1.7.0"
regex = "1" regex = "1"
regex-syntax = "0.8" regex-syntax = "0.8"
resvg = { version = "0.43", default-features = false, features = ["raster-images"] } resvg = { version = "0.45", default-features = false, features = ["raster-images"] }
roxmltree = "0.20" roxmltree = "0.20"
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] } rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
rustybuzz = "0.18" rustybuzz = "0.20"
same-file = "1" same-file = "1"
self-replace = "1.3.7" self-replace = "1.3.7"
semver = "1" semver = "1"
@ -110,8 +114,6 @@ sigpipe = "0.1"
siphasher = "1" siphasher = "1"
smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] } smallvec = { version = "1.11.1", features = ["union", "const_generics", "const_new"] }
stacker = "0.1.15" stacker = "0.1.15"
subsetter = "0.2"
svg2pdf = "0.12"
syn = { version = "2", features = ["full", "extra-traits"] } syn = { version = "2", features = ["full", "extra-traits"] }
syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] } syntect = { version = "5", default-features = false, features = ["parsing", "regex-fancy", "plist-load", "yaml-load"] }
tar = "0.4" tar = "0.4"
@ -121,26 +123,26 @@ time = { version = "0.3.20", features = ["formatting", "macros", "parsing"] }
tiny_http = "0.12" tiny_http = "0.12"
tiny-skia = "0.11" tiny-skia = "0.11"
toml = { version = "0.8", default-features = false, features = ["parse", "display"] } toml = { version = "0.8", default-features = false, features = ["parse", "display"] }
ttf-parser = "0.24.1" ttf-parser = "0.25.0"
two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] } two-face = { version = "0.4.3", default-features = false, features = ["syntect-fancy"] }
typed-arena = "2" typed-arena = "2"
unicode-bidi = "0.3.18" unicode-bidi = "0.3.18"
unicode-ident = "1.0" unicode-ident = "1.0"
unicode-math-class = "0.1" unicode-math-class = "0.1"
unicode-script = "0.5" unicode-script = "0.5"
unicode-normalization = "0.1.24"
unicode-segmentation = "1" unicode-segmentation = "1"
unscanny = "0.1" unscanny = "0.1"
ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] } ureq = { version = "2", default-features = false, features = ["native-tls", "gzip", "json"] }
usvg = { version = "0.43", default-features = false, features = ["text"] } usvg = { version = "0.45", default-features = false, features = ["text"] }
walkdir = "2" walkdir = "2"
wasmi = "0.40.0" wasmi = "0.40.0"
web-sys = "0.3" web-sys = "0.3"
xmlparser = "0.13.5" xmlparser = "0.13.5"
xmlwriter = "0.1.0" xmlwriter = "0.1.0"
xmp-writer = "0.3.1"
xz2 = { version = "0.1", features = ["static"] } xz2 = { version = "0.1", features = ["static"] }
yaml-front-matter = "0.1" yaml-front-matter = "0.1"
zip = { version = "2", default-features = false, features = ["deflate"] } zip = { version = "2.5", default-features = false, features = ["deflate"] }
[profile.dev.package."*"] [profile.dev.package."*"]
opt-level = 2 opt-level = 2

View File

@ -113,7 +113,9 @@ Typst's CLI is available from different sources:
- You can install Typst through different package managers. Note that the - You can install Typst through different package managers. Note that the
versions in the package managers might lag behind the latest release. versions in the package managers might lag behind the latest release.
- Linux: View [Typst on Repology][repology] - Linux:
- View [Typst on Repology][repology]
- View [Typst's Snap][snap]
- macOS: `brew install typst` - macOS: `brew install typst`
- Windows: `winget install --id Typst.Typst` - Windows: `winget install --id Typst.Typst`
@ -175,22 +177,22 @@ If you prefer an integrated IDE-like experience with autocompletion and instant
preview, you can also check out [Typst's free web app][app]. preview, you can also check out [Typst's free web app][app].
## Community ## Community
The main place where the community gathers is our [Discord server][discord]. The main places where the community gathers are our [Forum][forum] and our
Feel free to join there to ask questions, help out others, share cool things [Discord server][discord]. The Forum is a great place to ask questions, help
you created with Typst, or just to chat. others, and share cool things you created with Typst. The Discord server is more
suitable for quicker questions, discussions about contributing, or just to chat.
We'd be happy to see you there!
Aside from that there are a few places where you can find things built by [Typst Universe][universe] is where the community shares templates and packages.
the community: If you want to share your own creations, you can submit them to our
[package repository][packages].
- The official [package list](https://typst.app/docs/packages)
- The [Awesome Typst](https://github.com/qjcg/awesome-typst) repository
If you had a bad experience in our community, please [reach out to us][contact]. If you had a bad experience in our community, please [reach out to us][contact].
## Contributing ## Contributing
We would love to see contributions from the community. If you experience bugs, We love to see contributions from the community. If you experience bugs, feel
feel free to open an issue. If you would like to implement a new feature or bug free to open an issue. If you would like to implement a new feature or bug fix,
fix, please follow the steps outlined in the [contribution guide][contributing]. please follow the steps outlined in the [contribution guide][contributing].
To build Typst yourself, first ensure that you have the To build Typst yourself, first ensure that you have the
[latest stable Rust][rust] installed. Then, clone this repository and build the [latest stable Rust][rust] installed. Then, clone this repository and build the
@ -241,6 +243,8 @@ instant preview. To achieve these goals, we follow three core design principles:
[docs]: https://typst.app/docs/ [docs]: https://typst.app/docs/
[app]: https://typst.app/ [app]: https://typst.app/
[discord]: https://discord.gg/2uDybryKPe [discord]: https://discord.gg/2uDybryKPe
[forum]: https://forum.typst.app/
[universe]: https://typst.app/universe/
[tutorial]: https://typst.app/docs/tutorial/ [tutorial]: https://typst.app/docs/tutorial/
[show]: https://typst.app/docs/reference/styling/#show-rules [show]: https://typst.app/docs/reference/styling/#show-rules
[math]: https://typst.app/docs/reference/math/ [math]: https://typst.app/docs/reference/math/
@ -254,3 +258,4 @@ instant preview. To achieve these goals, we follow three core design principles:
[contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md [contributing]: https://github.com/typst/typst/blob/main/CONTRIBUTING.md
[packages]: https://github.com/typst/packages/ [packages]: https://github.com/typst/packages/
[`comemo`]: https://github.com/typst/comemo/ [`comemo`]: https://github.com/typst/comemo/
[snap]: https://snapcraft.io/typst

View File

@ -361,7 +361,7 @@ pub struct FontArgs {
/// Ensures system fonts won't be searched, unless explicitly included via /// Ensures system fonts won't be searched, unless explicitly included via
/// `--font-path`. /// `--font-path`.
#[arg(long)] #[arg(long, env = "TYPST_IGNORE_SYSTEM_FONTS")]
pub ignore_system_fonts: bool, pub ignore_system_fonts: bool,
} }
@ -467,15 +467,45 @@ display_possible_values!(Feature);
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)] #[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum PdfStandard { pub enum PdfStandard {
/// PDF 1.4.
#[value(name = "1.4")]
V_1_4,
/// PDF 1.5.
#[value(name = "1.5")]
V_1_5,
/// PDF 1.5.
#[value(name = "1.6")]
V_1_6,
/// PDF 1.7. /// PDF 1.7.
#[value(name = "1.7")] #[value(name = "1.7")]
V_1_7, V_1_7,
/// PDF 2.0.
#[value(name = "2.0")]
V_2_0,
/// PDF/A-1b.
#[value(name = "a-1b")]
A_1b,
/// PDF/A-2b. /// PDF/A-2b.
#[value(name = "a-2b")] #[value(name = "a-2b")]
A_2b, A_2b,
/// PDF/A-3b. /// PDF/A-2u.
#[value(name = "a-2u")]
A_2u,
/// PDF/A-3u.
#[value(name = "a-3b")] #[value(name = "a-3b")]
A_3b, A_3b,
/// PDF/A-3u.
#[value(name = "a-3u")]
A_3u,
/// PDF/A-4.
#[value(name = "a-4")]
A_4,
/// PDF/A-4f.
#[value(name = "a-4f")]
A_4f,
/// PDF/A-4e.
#[value(name = "a-4e")]
A_4e,
} }
display_possible_values!(PdfStandard); display_possible_values!(PdfStandard);

View File

@ -6,8 +6,9 @@ use std::path::{Path, PathBuf};
use chrono::{DateTime, Datelike, Timelike, Utc}; use chrono::{DateTime, Datelike, Timelike, Utc};
use codespan_reporting::diagnostic::{Diagnostic, Label}; use codespan_reporting::diagnostic::{Diagnostic, Label};
use codespan_reporting::term; use codespan_reporting::term;
use ecow::{eco_format, EcoString}; use ecow::eco_format;
use parking_lot::RwLock; use parking_lot::RwLock;
use pathdiff::diff_paths;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
@ -62,8 +63,7 @@ pub struct CompileConfig {
/// Opens the output file with the default viewer or a specific program after /// Opens the output file with the default viewer or a specific program after
/// compilation. /// compilation.
pub open: Option<Option<String>>, pub open: Option<Option<String>>,
/// One (or multiple comma-separated) PDF standards that Typst will enforce /// A list of standards the PDF should conform to.
/// conformance with.
pub pdf_standards: PdfStandards, pub pdf_standards: PdfStandards,
/// A path to write a Makefile rule describing the current compilation. /// A path to write a Makefile rule describing the current compilation.
pub make_deps: Option<PathBuf>, pub make_deps: Option<PathBuf>,
@ -129,18 +129,9 @@ impl CompileConfig {
PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect()) PageRanges::new(export_ranges.iter().map(|r| r.0.clone()).collect())
}); });
let pdf_standards = { let pdf_standards = PdfStandards::new(
let list = args &args.pdf_standard.iter().copied().map(Into::into).collect::<Vec<_>>(),
.pdf_standard )?;
.iter()
.map(|standard| match standard {
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
})
.collect::<Vec<_>>();
PdfStandards::new(&list)?
};
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
let server = match watch { let server = match watch {
@ -188,7 +179,7 @@ pub fn compile_once(
match output { match output {
// Export the PDF / PNG. // Export the PDF / PNG.
Ok(()) => { Ok(outputs) => {
let duration = start.elapsed(); let duration = start.elapsed();
if config.watching { if config.watching {
@ -202,7 +193,7 @@ pub fn compile_once(
print_diagnostics(world, &[], &warnings, config.diagnostic_format) print_diagnostics(world, &[], &warnings, config.diagnostic_format)
.map_err(|err| eco_format!("failed to print diagnostics ({err})"))?; .map_err(|err| eco_format!("failed to print diagnostics ({err})"))?;
write_make_deps(world, config)?; write_make_deps(world, config, outputs)?;
open_output(config)?; open_output(config)?;
} }
@ -226,12 +217,15 @@ pub fn compile_once(
fn compile_and_export( fn compile_and_export(
world: &mut SystemWorld, world: &mut SystemWorld,
config: &mut CompileConfig, config: &mut CompileConfig,
) -> Warned<SourceResult<()>> { ) -> Warned<SourceResult<Vec<Output>>> {
match config.output_format { match config.output_format {
OutputFormat::Html => { OutputFormat::Html => {
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world); let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
let result = output.and_then(|document| export_html(&document, config)); let result = output.and_then(|document| export_html(&document, config));
Warned { output: result, warnings } Warned {
output: result.map(|()| vec![config.output.clone()]),
warnings,
}
} }
_ => { _ => {
let Warned { output, warnings } = typst::compile::<PagedDocument>(world); let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
@ -257,9 +251,14 @@ fn export_html(document: &HtmlDocument, config: &CompileConfig) -> SourceResult<
} }
/// Export to a paged target format. /// Export to a paged target format.
fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> { fn export_paged(
document: &PagedDocument,
config: &CompileConfig,
) -> SourceResult<Vec<Output>> {
match config.output_format { match config.output_format {
OutputFormat::Pdf => export_pdf(document, config), OutputFormat::Pdf => {
export_pdf(document, config).map(|()| vec![config.output.clone()])
}
OutputFormat::Png => { OutputFormat::Png => {
export_image(document, config, ImageExportFormat::Png).at(Span::detached()) export_image(document, config, ImageExportFormat::Png).at(Span::detached())
} }
@ -286,6 +285,7 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<
}) })
} }
}; };
let options = PdfOptions { let options = PdfOptions {
ident: Smart::Auto, ident: Smart::Auto,
timestamp, timestamp,
@ -327,7 +327,7 @@ fn export_image(
document: &PagedDocument, document: &PagedDocument,
config: &CompileConfig, config: &CompileConfig,
fmt: ImageExportFormat, fmt: ImageExportFormat,
) -> StrResult<()> { ) -> StrResult<Vec<Output>> {
// Determine whether we have indexable templates in output // Determine whether we have indexable templates in output
let can_handle_multiple = match config.output { let can_handle_multiple = match config.output {
Output::Stdout => false, Output::Stdout => false,
@ -341,7 +341,7 @@ fn export_image(
.iter() .iter()
.enumerate() .enumerate()
.filter(|(i, _)| { .filter(|(i, _)| {
config.pages.as_ref().map_or(true, |exported_page_ranges| { config.pages.as_ref().is_none_or(|exported_page_ranges| {
exported_page_ranges.includes_page_index(*i) exported_page_ranges.includes_page_index(*i)
}) })
}) })
@ -383,7 +383,7 @@ fn export_image(
&& config.export_cache.is_cached(*i, &page.frame) && config.export_cache.is_cached(*i, &page.frame)
&& path.exists() && path.exists()
{ {
return Ok(()); return Ok(Output::Path(path.to_path_buf()));
} }
Output::Path(path.to_owned()) Output::Path(path.to_owned())
@ -392,11 +392,9 @@ fn export_image(
}; };
export_image_page(config, page, &output, fmt)?; export_image_page(config, page, &output, fmt)?;
Ok(()) Ok(output)
}) })
.collect::<Result<Vec<()>, EcoString>>()?; .collect::<StrResult<Vec<Output>>>()
Ok(())
} }
mod output_template { mod output_template {
@ -501,14 +499,25 @@ impl ExportCache {
/// Writes a Makefile rule describing the relationship between the output and /// Writes a Makefile rule describing the relationship between the output and
/// its dependencies to the path specified by the --make-deps argument, if it /// its dependencies to the path specified by the --make-deps argument, if it
/// was provided. /// was provided.
fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult<()> { fn write_make_deps(
world: &mut SystemWorld,
config: &CompileConfig,
outputs: Vec<Output>,
) -> StrResult<()> {
let Some(ref make_deps_path) = config.make_deps else { return Ok(()) }; let Some(ref make_deps_path) = config.make_deps else { return Ok(()) };
let Output::Path(output_path) = &config.output else { let Ok(output_paths) = outputs
bail!("failed to create make dependencies file because output was stdout") .into_iter()
}; .filter_map(|o| match o {
let Some(output_path) = output_path.as_os_str().to_str() else { Output::Path(path) => Some(path.into_os_string().into_string()),
Output::Stdout => None,
})
.collect::<Result<Vec<_>, _>>()
else {
bail!("failed to create make dependencies file because output path was not valid unicode") bail!("failed to create make dependencies file because output path was not valid unicode")
}; };
if output_paths.is_empty() {
bail!("failed to create make dependencies file because output was stdout")
}
// Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't // Based on `munge` in libcpp/mkdeps.cc from the GCC source code. This isn't
// perfect as some special characters can't be escaped. // perfect as some special characters can't be escaped.
@ -522,6 +531,10 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
res.push('$'); res.push('$');
slashes = 0; slashes = 0;
} }
':' => {
res.push('\\');
slashes = 0;
}
' ' | '\t' => { ' ' | '\t' => {
// `munge`'s source contains a comment here that says: "A // `munge`'s source contains a comment here that says: "A
// space or tab preceded by 2N+1 backslashes represents N // space or tab preceded by 2N+1 backslashes represents N
@ -544,18 +557,29 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
fn write( fn write(
make_deps_path: &Path, make_deps_path: &Path,
output_path: &str, output_paths: Vec<String>,
root: PathBuf, root: PathBuf,
dependencies: impl Iterator<Item = PathBuf>, dependencies: impl Iterator<Item = PathBuf>,
) -> io::Result<()> { ) -> io::Result<()> {
let mut file = File::create(make_deps_path)?; let mut file = File::create(make_deps_path)?;
let current_dir = std::env::current_dir()?;
let relative_root = diff_paths(&root, &current_dir).unwrap_or(root.clone());
file.write_all(munge(output_path).as_bytes())?; for (i, output_path) in output_paths.into_iter().enumerate() {
if i != 0 {
file.write_all(b" ")?;
}
file.write_all(munge(&output_path).as_bytes())?;
}
file.write_all(b":")?; file.write_all(b":")?;
for dependency in dependencies { for dependency in dependencies {
let Some(dependency) = let relative_dependency = match dependency.strip_prefix(&root) {
dependency.strip_prefix(&root).unwrap_or(&dependency).to_str() Ok(root_relative_dependency) => {
else { relative_root.join(root_relative_dependency)
}
Err(_) => dependency,
};
let Some(relative_dependency) = relative_dependency.to_str() else {
// Silently skip paths that aren't valid unicode so we still // Silently skip paths that aren't valid unicode so we still
// produce a rule that will work for the other paths that can be // produce a rule that will work for the other paths that can be
// processed. // processed.
@ -563,14 +587,14 @@ fn write_make_deps(world: &mut SystemWorld, config: &CompileConfig) -> StrResult
}; };
file.write_all(b" ")?; file.write_all(b" ")?;
file.write_all(munge(dependency).as_bytes())?; file.write_all(munge(relative_dependency).as_bytes())?;
} }
file.write_all(b"\n")?; file.write_all(b"\n")?;
Ok(()) Ok(())
} }
write(make_deps_path, output_path, world.root().to_owned(), world.dependencies()) write(make_deps_path, output_paths, world.root().to_owned(), world.dependencies())
.map_err(|err| { .map_err(|err| {
eco_format!("failed to create make dependencies file due to IO error ({err})") eco_format!("failed to create make dependencies file due to IO error ({err})")
}) })
@ -732,3 +756,23 @@ impl<'a> codespan_reporting::files::Files<'a> for SystemWorld {
}) })
} }
} }
impl From<PdfStandard> for typst_pdf::PdfStandard {
fn from(standard: PdfStandard) -> Self {
match standard {
PdfStandard::V_1_4 => typst_pdf::PdfStandard::V_1_4,
PdfStandard::V_1_5 => typst_pdf::PdfStandard::V_1_5,
PdfStandard::V_1_6 => typst_pdf::PdfStandard::V_1_6,
PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7,
PdfStandard::V_2_0 => typst_pdf::PdfStandard::V_2_0,
PdfStandard::A_1b => typst_pdf::PdfStandard::A_1b,
PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b,
PdfStandard::A_2u => typst_pdf::PdfStandard::A_2u,
PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b,
PdfStandard::A_3u => typst_pdf::PdfStandard::A_3u,
PdfStandard::A_4 => typst_pdf::PdfStandard::A_4,
PdfStandard::A_4f => typst_pdf::PdfStandard::A_4f,
PdfStandard::A_4e => typst_pdf::PdfStandard::A_4e,
}
}
}

View File

@ -2,6 +2,7 @@ use comemo::Track;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use serde::Serialize; use serde::Serialize;
use typst::diag::{bail, HintedStrResult, StrResult, Warned}; use typst::diag::{bail, HintedStrResult, StrResult, Warned};
use typst::engine::Sink;
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
use typst::layout::PagedDocument; use typst::layout::PagedDocument;
use typst::syntax::Span; use typst::syntax::Span;
@ -58,6 +59,8 @@ fn retrieve(
let selector = eval_string( let selector = eval_string(
&typst::ROUTINES, &typst::ROUTINES,
world.track(), world.track(),
// TODO: propagate warnings
Sink::new().track_mut(),
&command.selector, &command.selector,
Span::detached(), Span::detached(),
EvalMode::Code, EvalMode::Code,

View File

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

View File

@ -210,7 +210,9 @@ impl World for SystemWorld {
} }
fn font(&self, index: usize) -> Option<Font> { fn font(&self, index: usize) -> Option<Font> {
self.fonts[index].get() // comemo's validation may invoke this function with an invalid index. This is
// impossible in typst-cli but possible if a custom tool mutates the fonts.
self.fonts.get(index)?.get()
} }
fn today(&self, offset: Option<i64>) -> Option<Datetime> { fn today(&self, offset: Option<i64>) -> Option<Datetime> {

View File

@ -466,7 +466,7 @@ impl<'a> CapturesVisitor<'a> {
} }
// Code and content blocks create a scope. // Code and content blocks create a scope.
Some(ast::Expr::Code(_) | ast::Expr::Content(_)) => { Some(ast::Expr::CodeBlock(_) | ast::Expr::ContentBlock(_)) => {
self.internal.enter(); self.internal.enter();
for child in node.children() { for child in node.children() {
self.visit(child); self.visit(child);
@ -516,7 +516,7 @@ impl<'a> CapturesVisitor<'a> {
// A let expression contains a binding, but that binding is only // A let expression contains a binding, but that binding is only
// active after the body is evaluated. // active after the body is evaluated.
Some(ast::Expr::Let(expr)) => { Some(ast::Expr::LetBinding(expr)) => {
if let Some(init) = expr.init() { if let Some(init) = expr.init() {
self.visit(init.to_untyped()); self.visit(init.to_untyped());
} }
@ -529,7 +529,7 @@ impl<'a> CapturesVisitor<'a> {
// A for loop contains one or two bindings in its pattern. These are // A for loop contains one or two bindings in its pattern. These are
// active after the iterable is evaluated but before the body is // active after the iterable is evaluated but before the body is
// evaluated. // evaluated.
Some(ast::Expr::For(expr)) => { Some(ast::Expr::ForLoop(expr)) => {
self.visit(expr.iterable().to_untyped()); self.visit(expr.iterable().to_untyped());
self.internal.enter(); self.internal.enter();
@ -544,7 +544,7 @@ impl<'a> CapturesVisitor<'a> {
// An import contains items, but these are active only after the // An import contains items, but these are active only after the
// path is evaluated. // path is evaluated.
Some(ast::Expr::Import(expr)) => { Some(ast::Expr::ModuleImport(expr)) => {
self.visit(expr.source().to_untyped()); self.visit(expr.source().to_untyped());
if let Some(ast::Imports::Items(items)) = expr.imports() { if let Some(ast::Imports::Items(items)) = expr.imports() {
for item in items.iter() { for item in items.iter() {

View File

@ -30,7 +30,7 @@ fn eval_code<'a>(
while let Some(expr) = exprs.next() { while let Some(expr) = exprs.next() {
let span = expr.span(); let span = expr.span();
let value = match expr { let value = match expr {
ast::Expr::Set(set) => { ast::Expr::SetRule(set) => {
let styles = set.eval(vm)?; let styles = set.eval(vm)?;
if vm.flow.is_some() { if vm.flow.is_some() {
break; break;
@ -39,7 +39,7 @@ fn eval_code<'a>(
let tail = eval_code(vm, exprs)?.display(); let tail = eval_code(vm, exprs)?.display();
Value::Content(tail.styled_with_map(styles)) Value::Content(tail.styled_with_map(styles))
} }
ast::Expr::Show(show) => { ast::Expr::ShowRule(show) => {
let recipe = show.eval(vm)?; let recipe = show.eval(vm)?;
if vm.flow.is_some() { if vm.flow.is_some() {
break; break;
@ -94,9 +94,9 @@ impl Eval for ast::Expr<'_> {
Self::Label(v) => v.eval(vm), Self::Label(v) => v.eval(vm),
Self::Ref(v) => v.eval(vm).map(Value::Content), Self::Ref(v) => v.eval(vm).map(Value::Content),
Self::Heading(v) => v.eval(vm).map(Value::Content), Self::Heading(v) => v.eval(vm).map(Value::Content),
Self::List(v) => v.eval(vm).map(Value::Content), Self::ListItem(v) => v.eval(vm).map(Value::Content),
Self::Enum(v) => v.eval(vm).map(Value::Content), Self::EnumItem(v) => v.eval(vm).map(Value::Content),
Self::Term(v) => v.eval(vm).map(Value::Content), Self::TermItem(v) => v.eval(vm).map(Value::Content),
Self::Equation(v) => v.eval(vm).map(Value::Content), Self::Equation(v) => v.eval(vm).map(Value::Content),
Self::Math(v) => v.eval(vm).map(Value::Content), Self::Math(v) => v.eval(vm).map(Value::Content),
Self::MathText(v) => v.eval(vm).map(Value::Content), Self::MathText(v) => v.eval(vm).map(Value::Content),
@ -116,8 +116,8 @@ impl Eval for ast::Expr<'_> {
Self::Float(v) => v.eval(vm), Self::Float(v) => v.eval(vm),
Self::Numeric(v) => v.eval(vm), Self::Numeric(v) => v.eval(vm),
Self::Str(v) => v.eval(vm), Self::Str(v) => v.eval(vm),
Self::Code(v) => v.eval(vm), Self::CodeBlock(v) => v.eval(vm),
Self::Content(v) => v.eval(vm).map(Value::Content), Self::ContentBlock(v) => v.eval(vm).map(Value::Content),
Self::Array(v) => v.eval(vm).map(Value::Array), Self::Array(v) => v.eval(vm).map(Value::Array),
Self::Dict(v) => v.eval(vm).map(Value::Dict), Self::Dict(v) => v.eval(vm).map(Value::Dict),
Self::Parenthesized(v) => v.eval(vm), Self::Parenthesized(v) => v.eval(vm),
@ -126,19 +126,19 @@ impl Eval for ast::Expr<'_> {
Self::Closure(v) => v.eval(vm), Self::Closure(v) => v.eval(vm),
Self::Unary(v) => v.eval(vm), Self::Unary(v) => v.eval(vm),
Self::Binary(v) => v.eval(vm), Self::Binary(v) => v.eval(vm),
Self::Let(v) => v.eval(vm), Self::LetBinding(v) => v.eval(vm),
Self::DestructAssign(v) => v.eval(vm), Self::DestructAssignment(v) => v.eval(vm),
Self::Set(_) => bail!(forbidden("set")), Self::SetRule(_) => bail!(forbidden("set")),
Self::Show(_) => bail!(forbidden("show")), Self::ShowRule(_) => bail!(forbidden("show")),
Self::Contextual(v) => v.eval(vm).map(Value::Content), Self::Contextual(v) => v.eval(vm).map(Value::Content),
Self::Conditional(v) => v.eval(vm), Self::Conditional(v) => v.eval(vm),
Self::While(v) => v.eval(vm), Self::WhileLoop(v) => v.eval(vm),
Self::For(v) => v.eval(vm), Self::ForLoop(v) => v.eval(vm),
Self::Import(v) => v.eval(vm), Self::ModuleImport(v) => v.eval(vm),
Self::Include(v) => v.eval(vm).map(Value::Content), Self::ModuleInclude(v) => v.eval(vm).map(Value::Content),
Self::Break(v) => v.eval(vm), Self::LoopBreak(v) => v.eval(vm),
Self::Continue(v) => v.eval(vm), Self::LoopContinue(v) => v.eval(vm),
Self::Return(v) => v.eval(vm), Self::FuncReturn(v) => v.eval(vm),
}? }?
.spanned(span); .spanned(span);

View File

@ -44,11 +44,10 @@ impl Eval for ast::ModuleImport<'_> {
} }
// If there is a rename, import the source itself under that name. // If there is a rename, import the source itself under that name.
let bare_name = self.bare_name();
let new_name = self.new_name(); let new_name = self.new_name();
if let Some(new_name) = new_name { if let Some(new_name) = new_name {
if let Ok(source_name) = &bare_name { if let ast::Expr::Ident(ident) = self.source() {
if source_name == new_name.as_str() { if ident.as_str() == new_name.as_str() {
// Warn on `import x as x` // Warn on `import x as x`
vm.engine.sink.warn(warning!( vm.engine.sink.warn(warning!(
new_name.span(), new_name.span(),
@ -57,6 +56,7 @@ impl Eval for ast::ModuleImport<'_> {
} }
} }
// Define renamed module on the scope.
vm.define(new_name, source.clone()); vm.define(new_name, source.clone());
} }

View File

@ -101,6 +101,7 @@ pub fn eval(
pub fn eval_string( pub fn eval_string(
routines: &Routines, routines: &Routines,
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
sink: TrackedMut<Sink>,
string: &str, string: &str,
span: Span, span: Span,
mode: EvalMode, mode: EvalMode,
@ -121,7 +122,6 @@ pub fn eval_string(
} }
// Prepare the engine. // Prepare the engine.
let mut sink = Sink::new();
let introspector = Introspector::default(); let introspector = Introspector::default();
let traced = Traced::default(); let traced = Traced::default();
let engine = Engine { let engine = Engine {
@ -129,7 +129,7 @@ pub fn eval_string(
world, world,
introspector: introspector.track(), introspector: introspector.track(),
traced: traced.track(), traced: traced.track(),
sink: sink.track_mut(), sink,
route: Route::default(), route: Route::default(),
}; };

View File

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

View File

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

View File

@ -83,8 +83,8 @@ fn html_document_impl(
)?; )?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?; let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector }) Ok(HtmlDocument { info, root, introspector })
} }
@ -263,13 +263,13 @@ fn handle(
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted, /// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
/// supplying a suitable `<head>`. /// supplying a suitable `<head>`.
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> { fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
let head = head_element(info);
let body = match classify_output(output)? { let body = match classify_output(output)? {
OutputKind::Html(element) => return Ok(element), OutputKind::Html(element) => return Ok(element),
OutputKind::Body(body) => body, OutputKind::Body(body) => body,
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
}; };
Ok(HtmlElement::new(tag::html) Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()]))
.with_children(vec![head_element(info).into(), body.into()]))
} }
/// Generate a `<head>` element. /// Generate a `<head>` element.
@ -302,23 +302,41 @@ fn head_element(info: &DocumentInfo) -> HtmlElement {
); );
} }
if !info.author.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "authors")
.with_attr(attr::content, info.author.join(", "))
.into(),
)
}
if !info.keywords.is_empty() {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "keywords")
.with_attr(attr::content, info.keywords.join(", "))
.into(),
)
}
HtmlElement::new(tag::head).with_children(children) HtmlElement::new(tag::head).with_children(children)
} }
/// Determine which kind of output the user generated. /// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> { fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let len = output.len(); let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output { for node in &mut output {
let HtmlNode::Element(elem) = node else { continue }; let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag; let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, len) { match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())), (tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())), (tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!( (tag::html | tag::body, _) => bail!(
elem.span, elem.span,
"`{}` element must be the only element in the document", "`{}` element must be the only element in the document",
elem.tag elem.tag,
), ),
_ => {} _ => {}
} }

View File

@ -26,7 +26,7 @@ pub fn analyze_expr(
ast::Expr::Str(v) => Value::Str(v.get().into()), ast::Expr::Str(v) => Value::Str(v.get().into()),
_ => { _ => {
if node.kind() == SyntaxKind::Contextual { if node.kind() == SyntaxKind::Contextual {
if let Some(child) = node.children().last() { if let Some(child) = node.children().next_back() {
return analyze_expr(world, &child); return analyze_expr(world, &child);
} }
} }

View File

@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool {
} }
// Behind existing atom or identifier: "$a|$" or "$abc|$". // Behind existing atom or identifier: "$a|$" or "$abc|$".
if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { if matches!(
ctx.leaf.kind(),
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent
) {
ctx.from = ctx.leaf.offset(); ctx.from = ctx.leaf.offset();
math_completions(ctx); math_completions(ctx);
return true; return true;
@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
// Behind an expression plus dot: "emoji.|". // Behind an expression plus dot: "emoji.|".
if_chain! { if_chain! {
if ctx.leaf.kind() == SyntaxKind::Dot if ctx.leaf.kind() == SyntaxKind::Dot
|| (ctx.leaf.kind() == SyntaxKind::Text || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText)
&& ctx.leaf.text() == "."); && ctx.leaf.text() == ".");
if ctx.leaf.range().end == ctx.cursor; if ctx.leaf.range().end == ctx.cursor;
if let Some(prev) = ctx.leaf.prev_sibling(); if let Some(prev) = ctx.leaf.prev_sibling();
@ -398,9 +401,27 @@ fn field_access_completions(
value: &Value, value: &Value,
styles: &Option<Styles>, styles: &Option<Styles>,
) { ) {
for (name, binding) in value.ty().scope().iter() { let scopes = {
let ty = value.ty().scope();
let elem = match value {
Value::Content(content) => Some(content.elem().scope()),
_ => None,
};
elem.into_iter().chain(Some(ty))
};
// Autocomplete methods from the element's or type's scope. We only complete
// those which have a `self` parameter.
for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
let Ok(func) = binding.read().clone().cast::<Func>() else { continue };
if func
.params()
.and_then(|params| params.first())
.is_some_and(|param| param.name == "self")
{
ctx.call_completion(name.clone(), binding.read()); ctx.call_completion(name.clone(), binding.read());
} }
}
if let Some(scope) = value.scope() { if let Some(scope) = value.scope() {
for (name, binding) in scope.iter() { for (name, binding) in scope.iter() {
@ -496,7 +517,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
// "#import "path.typ": a, b, |". // "#import "path.typ": a, b, |".
if_chain! { if_chain! {
if let Some(prev) = ctx.leaf.prev_sibling(); if let Some(prev) = ctx.leaf.prev_sibling();
if let Some(ast::Expr::Import(import)) = prev.get().cast(); if let Some(ast::Expr::ModuleImport(import)) = prev.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>()); if let Some(source) = prev.children().find(|child| child.is::<ast::Expr>());
then { then {
@ -515,7 +536,7 @@ fn complete_imports(ctx: &mut CompletionContext) -> bool {
if let Some(grand) = parent.parent(); if let Some(grand) = parent.parent();
if grand.kind() == SyntaxKind::ImportItems; if grand.kind() == SyntaxKind::ImportItems;
if let Some(great) = grand.parent(); if let Some(great) = grand.parent();
if let Some(ast::Expr::Import(import)) = great.get().cast(); if let Some(ast::Expr::ModuleImport(import)) = great.get().cast();
if let Some(ast::Imports::Items(items)) = import.imports(); if let Some(ast::Imports::Items(items)) = import.imports();
if let Some(source) = great.children().find(|child| child.is::<ast::Expr>()); if let Some(source) = great.children().find(|child| child.is::<ast::Expr>());
then { then {
@ -656,10 +677,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
if let Some(args) = parent.get().cast::<ast::Args>(); if let Some(args) = parent.get().cast::<ast::Args>();
if let Some(grand) = parent.parent(); if let Some(grand) = parent.parent();
if let Some(expr) = grand.get().cast::<ast::Expr>(); if let Some(expr) = grand.get().cast::<ast::Expr>();
let set = matches!(expr, ast::Expr::Set(_)); let set = matches!(expr, ast::Expr::SetRule(_));
if let Some(callee) = match expr { if let Some(callee) = match expr {
ast::Expr::FuncCall(call) => Some(call.callee()), ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()), ast::Expr::SetRule(set) => Some(set.target()),
_ => None, _ => None,
}; };
then { then {
@ -1442,7 +1463,7 @@ impl<'a> CompletionContext<'a> {
let mut defined = BTreeMap::<EcoString, Option<Value>>::new(); let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
named_items(self.world, self.leaf.clone(), |item| { named_items(self.world, self.leaf.clone(), |item| {
let name = item.name(); let name = item.name();
if !name.is_empty() && item.value().as_ref().map_or(true, filter) { if !name.is_empty() && item.value().as_ref().is_none_or(filter) {
defined.insert(name.clone(), item.value()); defined.insert(name.clone(), item.value());
} }
@ -1747,4 +1768,26 @@ mod tests {
.must_include(["this", "that"]) .must_include(["this", "that"])
.must_exclude(["*", "figure"]); .must_exclude(["*", "figure"]);
} }
#[test]
fn test_autocomplete_type_methods() {
test("#\"hello\".", -1).must_include(["len", "contains"]);
test("#table().", -1).must_exclude(["cell"]);
}
#[test]
fn test_autocomplete_content_methods() {
test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
.must_include(["indented", "body", "page"]);
}
#[test]
fn test_autocomplete_symbol_variants() {
test("#sym.arrow.", -1)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
test("$ arrow. $", -3)
.must_include(["r", "dashed"])
.must_exclude(["cases"]);
}
} }

View File

@ -3,7 +3,7 @@ use std::num::NonZeroUsize;
use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size}; use typst::layout::{Frame, FrameItem, PagedDocument, Point, Position, Size};
use typst::model::{Destination, Url}; use typst::model::{Destination, Url};
use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind}; use typst::syntax::{FileId, LinkedNode, Side, Source, Span, SyntaxKind};
use typst::visualize::Geometry; use typst::visualize::{Curve, CurveItem, FillRule, Geometry};
use typst::WorldExt; use typst::WorldExt;
use crate::IdeWorld; use crate::IdeWorld;
@ -53,10 +53,20 @@ pub fn jump_from_click(
for (mut pos, item) in frame.items().rev() { for (mut pos, item) in frame.items().rev() {
match item { match item {
FrameItem::Group(group) => { FrameItem::Group(group) => {
// TODO: Handle transformation. let pos = click - pos;
if let Some(span) = if let Some(clip) = &group.clip {
jump_from_click(world, document, &group.frame, click - pos) if !clip.contains(FillRule::NonZero, pos) {
{ continue;
}
}
// Realistic transforms should always be invertible.
// An example of one that isn't is a scale of 0, which would
// not be clickable anyway.
let Some(inv_transform) = group.transform.invert() else {
continue;
};
let pos = pos.transform_inf(inv_transform);
if let Some(span) = jump_from_click(world, document, &group.frame, pos) {
return Some(span); return Some(span);
} }
} }
@ -73,7 +83,10 @@ pub fn jump_from_click(
let Some(id) = span.id() else { continue }; let Some(id) = span.id() else { continue };
let source = world.source(id).ok()?; let source = world.source(id).ok()?;
let node = source.find(span)?; let node = source.find(span)?;
let pos = if node.kind() == SyntaxKind::Text { let pos = if matches!(
node.kind(),
SyntaxKind::Text | SyntaxKind::MathText
) {
let range = node.range(); let range = node.range();
let mut offset = range.start + usize::from(span_offset); let mut offset = range.start + usize::from(span_offset);
if (click.x - pos.x) > width / 2.0 { if (click.x - pos.x) > width / 2.0 {
@ -91,12 +104,35 @@ pub fn jump_from_click(
} }
FrameItem::Shape(shape, span) => { FrameItem::Shape(shape, span) => {
let Geometry::Rect(size) = shape.geometry else { continue }; if shape.fill.is_some() {
if is_in_rect(pos, size, click) { let within = match &shape.geometry {
Geometry::Line(..) => false,
Geometry::Rect(size) => is_in_rect(pos, *size, click),
Geometry::Curve(curve) => {
curve.contains(shape.fill_rule, click - pos)
}
};
if within {
return Jump::from_span(world, *span); return Jump::from_span(world, *span);
} }
} }
if let Some(stroke) = &shape.stroke {
let within = !stroke.thickness.approx_empty() && {
// This curve is rooted at (0, 0), not `pos`.
let base_curve = match &shape.geometry {
Geometry::Line(to) => &Curve(vec![CurveItem::Line(*to)]),
Geometry::Rect(size) => &Curve::rect(*size),
Geometry::Curve(curve) => curve,
};
base_curve.stroke_contains(stroke, click - pos)
};
if within {
return Jump::from_span(world, *span);
}
}
}
FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => { FrameItem::Image(_, size, span) if is_in_rect(pos, *size, click) => {
return Jump::from_span(world, *span); return Jump::from_span(world, *span);
} }
@ -115,7 +151,7 @@ pub fn jump_from_cursor(
cursor: usize, cursor: usize,
) -> Vec<Position> { ) -> Vec<Position> {
fn is_text(node: &LinkedNode) -> bool { fn is_text(node: &LinkedNode) -> bool {
node.get().kind() == SyntaxKind::Text matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText)
} }
let root = LinkedNode::new(source.root()); let root = LinkedNode::new(source.root());
@ -143,9 +179,8 @@ pub fn jump_from_cursor(
fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> { fn find_in_frame(frame: &Frame, span: Span) -> Option<Point> {
for (mut pos, item) in frame.items() { for (mut pos, item) in frame.items() {
if let FrameItem::Group(group) = item { if let FrameItem::Group(group) = item {
// TODO: Handle transformation.
if let Some(point) = find_in_frame(&group.frame, span) { if let Some(point) = find_in_frame(&group.frame, span) {
return Some(point + pos); return Some(pos + point.transform(group.transform));
} }
} }
@ -261,6 +296,102 @@ mod tests {
test_click(s, point(21.0, 12.0), cursor(56)); test_click(s, point(21.0, 12.0), cursor(56));
} }
#[test]
fn test_jump_from_click_math() {
test_click("$a + b$", point(28.0, 14.0), cursor(5));
}
#[test]
fn test_jump_from_click_transform_clip() {
let margin = point(10.0, 10.0);
test_click(
"#rect(width: 20pt, height: 20pt, fill: black)",
point(10.0, 10.0) + margin,
cursor(1),
);
test_click(
"#rect(width: 60pt, height: 10pt, fill: black)",
point(5.0, 30.0) + margin,
None,
);
test_click(
"#rotate(90deg, origin: bottom + left, rect(width: 60pt, height: 10pt, fill: black))",
point(5.0, 30.0) + margin,
cursor(38),
);
test_click(
"#scale(x: 300%, y: 300%, origin: top + left, rect(width: 10pt, height: 10pt, fill: black))",
point(20.0, 20.0) + margin,
cursor(45),
);
test_click(
"#box(width: 10pt, height: 10pt, clip: true, scale(x: 300%, y: 300%, \
origin: top + left, rect(width: 10pt, height: 10pt, fill: black)))",
point(20.0, 20.0) + margin,
None,
);
test_click(
"#box(width: 10pt, height: 10pt, clip: false, rect(width: 30pt, height: 30pt, fill: black))",
point(20.0, 20.0) + margin,
cursor(45),
);
test_click(
"#box(width: 10pt, height: 10pt, clip: true, rect(width: 30pt, height: 30pt, fill: black))",
point(20.0, 20.0) + margin,
None,
);
test_click(
"#rotate(90deg, origin: bottom + left)[hello world]",
point(5.0, 15.0) + margin,
cursor(40),
);
}
#[test]
fn test_jump_from_click_shapes() {
let margin = point(10.0, 10.0);
test_click(
"#rect(width: 30pt, height: 30pt, fill: black)",
point(15.0, 15.0) + margin,
cursor(1),
);
let circle = "#circle(width: 30pt, height: 30pt, fill: black)";
test_click(circle, point(15.0, 15.0) + margin, cursor(1));
test_click(circle, point(1.0, 1.0) + margin, None);
let bowtie =
"#polygon(fill: black, (0pt, 0pt), (20pt, 20pt), (20pt, 0pt), (0pt, 20pt))";
test_click(bowtie, point(1.0, 2.0) + margin, cursor(1));
test_click(bowtie, point(2.0, 1.0) + margin, None);
test_click(bowtie, point(19.0, 10.0) + margin, cursor(1));
let evenodd = r#"#polygon(fill: black, fill-rule: "even-odd",
(0pt, 10pt), (30pt, 10pt), (30pt, 20pt), (20pt, 20pt),
(20pt, 0pt), (10pt, 0pt), (10pt, 30pt), (20pt, 30pt),
(20pt, 20pt), (0pt, 20pt))"#;
test_click(evenodd, point(15.0, 15.0) + margin, None);
test_click(evenodd, point(5.0, 15.0) + margin, cursor(1));
test_click(evenodd, point(15.0, 5.0) + margin, cursor(1));
}
#[test]
fn test_jump_from_click_shapes_stroke() {
let margin = point(10.0, 10.0);
let rect =
"#place(dx: 10pt, dy: 10pt, rect(width: 10pt, height: 10pt, stroke: 5pt))";
test_click(rect, point(15.0, 15.0) + margin, None);
test_click(rect, point(10.0, 15.0) + margin, cursor(27));
test_click(
"#line(angle: 45deg, length: 10pt, stroke: 2pt)",
point(2.0, 2.0) + margin,
cursor(1),
);
}
#[test] #[test]
fn test_jump_from_cursor() { fn test_jump_from_cursor() {
let s = "*Hello* #box[ABC] World"; let s = "*Hello* #box[ABC] World";
@ -268,6 +399,20 @@ mod tests {
test_cursor(s, 14, pos(1, 37.55, 16.58)); test_cursor(s, 14, pos(1, 37.55, 16.58));
} }
#[test]
fn test_jump_from_cursor_math() {
test_cursor("$a + b$", -3, pos(1, 27.51, 16.83));
}
#[test]
fn test_jump_from_cursor_transform() {
test_cursor(
r#"#rotate(90deg, origin: bottom + left, [hello world])"#,
-5,
pos(1, 10.0, 16.58),
);
}
#[test] #[test]
fn test_backlink() { fn test_backlink() {
let s = "#footnote[Hi]"; let s = "#footnote[Hi]";

View File

@ -232,7 +232,9 @@ pub fn deref_target(node: LinkedNode) -> Option<DerefTarget<'_>> {
ast::Expr::FuncCall(call) => { ast::Expr::FuncCall(call) => {
DerefTarget::Callee(expr_node.find(call.callee().span())?) DerefTarget::Callee(expr_node.find(call.callee().span())?)
} }
ast::Expr::Set(set) => DerefTarget::Callee(expr_node.find(set.target().span())?), ast::Expr::SetRule(set) => {
DerefTarget::Callee(expr_node.find(set.target().span())?)
}
ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => { ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FieldAccess(_) => {
DerefTarget::VarAccess(expr_node) DerefTarget::VarAccess(expr_node)
} }

View File

@ -97,7 +97,7 @@ impl World for TestWorld {
} }
fn font(&self, index: usize) -> Option<Font> { fn font(&self, index: usize) -> Option<Font> {
Some(self.base.fonts[index].clone()) self.base.fonts.get(index).cloned()
} }
fn today(&self, _: Option<i64>) -> Option<Datetime> { fn today(&self, _: Option<i64>) -> Option<Datetime> {

View File

@ -201,7 +201,7 @@ fn named_param_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Toolti
if let Some(expr) = grand_grand.cast::<ast::Expr>(); if let Some(expr) = grand_grand.cast::<ast::Expr>();
if let Some(ast::Expr::Ident(callee)) = match expr { if let Some(ast::Expr::Ident(callee)) = match expr {
ast::Expr::FuncCall(call) => Some(call.callee()), ast::Expr::FuncCall(call) => Some(call.callee()),
ast::Expr::Set(set) => Some(set.target()), ast::Expr::SetRule(set) => Some(set.target()),
_ => None, _ => None,
}; };

View File

@ -19,10 +19,13 @@ typst-utils = { workspace = true }
dirs = { workspace = true, optional = true } dirs = { workspace = true, optional = true }
ecow = { workspace = true } ecow = { workspace = true }
env_proxy = { workspace = true, optional = true } env_proxy = { workspace = true, optional = true }
fastrand = { workspace = true, optional = true }
flate2 = { workspace = true, optional = true } flate2 = { workspace = true, optional = true }
fontdb = { workspace = true, optional = true } fontdb = { workspace = true, optional = true }
native-tls = { workspace = true, optional = true } native-tls = { workspace = true, optional = true }
once_cell = { workspace = true } once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tar = { workspace = true, optional = true } tar = { workspace = true, optional = true }
ureq = { workspace = true, optional = true } ureq = { workspace = true, optional = true }
@ -41,7 +44,7 @@ fonts = ["dep:fontdb", "fontdb/memmap", "fontdb/fontconfig"]
downloads = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"] downloads = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"]
# Add package downloading utilities, implies `downloads` # Add package downloading utilities, implies `downloads`
packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar"] packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar", "dep:fastrand"]
# Embeds some fonts into the binary: # Embeds some fonts into the binary:
# - For text: Libertinus Serif, New Computer Modern # - For text: Libertinus Serif, New Computer Modern

View File

@ -1,14 +1,14 @@
//! Download and unpack packages and package indices. //! Download and unpack packages and package indices.
use std::fs; use std::fs;
use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use ecow::eco_format; use ecow::eco_format;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::Deserialize;
use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; use typst_library::diag::{bail, PackageError, PackageResult, StrResult};
use typst_syntax::package::{ use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec};
PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec,
};
use crate::download::{Downloader, Progress}; use crate::download::{Downloader, Progress};
@ -32,7 +32,7 @@ pub struct PackageStorage {
/// The downloader used for fetching the index and packages. /// The downloader used for fetching the index and packages.
downloader: Downloader, downloader: Downloader,
/// The cached index of the default namespace. /// The cached index of the default namespace.
index: OnceCell<Vec<PackageInfo>>, index: OnceCell<Vec<serde_json::Value>>,
} }
impl PackageStorage { impl PackageStorage {
@ -42,6 +42,18 @@ impl PackageStorage {
package_cache_path: Option<PathBuf>, package_cache_path: Option<PathBuf>,
package_path: Option<PathBuf>, package_path: Option<PathBuf>,
downloader: Downloader, downloader: Downloader,
) -> Self {
Self::with_index(package_cache_path, package_path, downloader, OnceCell::new())
}
/// Creates a new package storage with a pre-defined index.
///
/// Useful for testing.
fn with_index(
package_cache_path: Option<PathBuf>,
package_path: Option<PathBuf>,
downloader: Downloader,
index: OnceCell<Vec<serde_json::Value>>,
) -> Self { ) -> Self {
Self { Self {
package_cache_path: package_cache_path.or_else(|| { package_cache_path: package_cache_path.or_else(|| {
@ -51,7 +63,7 @@ impl PackageStorage {
dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR)) dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR))
}), }),
downloader, downloader,
index: OnceCell::new(), index,
} }
} }
@ -66,7 +78,8 @@ impl PackageStorage {
self.package_path.as_deref() self.package_path.as_deref()
} }
/// Make a package available in the on-disk. /// Makes a package available on-disk and returns the path at which it is
/// located (will be either in the cache or package directory).
pub fn prepare_package( pub fn prepare_package(
&self, &self,
spec: &PackageSpec, spec: &PackageSpec,
@ -89,7 +102,7 @@ impl PackageStorage {
// Download from network if it doesn't exist yet. // Download from network if it doesn't exist yet.
if spec.namespace == DEFAULT_NAMESPACE { if spec.namespace == DEFAULT_NAMESPACE {
self.download_package(spec, &dir, progress)?; self.download_package(spec, cache_dir, progress)?;
if dir.exists() { if dir.exists() {
return Ok(dir); return Ok(dir);
} }
@ -99,7 +112,7 @@ impl PackageStorage {
Err(PackageError::NotFound(spec.clone())) Err(PackageError::NotFound(spec.clone()))
} }
/// Try to determine the latest version of a package. /// Tries to determine the latest version of a package.
pub fn determine_latest_version( pub fn determine_latest_version(
&self, &self,
spec: &VersionlessPackageSpec, spec: &VersionlessPackageSpec,
@ -109,6 +122,7 @@ impl PackageStorage {
// version. // version.
self.download_index()? self.download_index()?
.iter() .iter()
.filter_map(|value| MinimalPackageInfo::deserialize(value).ok())
.filter(|package| package.name == spec.name) .filter(|package| package.name == spec.name)
.map(|package| package.version) .map(|package| package.version)
.max() .max()
@ -131,7 +145,7 @@ impl PackageStorage {
} }
/// Download the package index. The result of this is cached for efficiency. /// Download the package index. The result of this is cached for efficiency.
pub fn download_index(&self) -> StrResult<&[PackageInfo]> { fn download_index(&self) -> StrResult<&[serde_json::Value]> {
self.index self.index
.get_or_try_init(|| { .get_or_try_init(|| {
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
@ -152,10 +166,10 @@ impl PackageStorage {
/// ///
/// # Panics /// # Panics
/// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`. /// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`.
pub fn download_package( fn download_package(
&self, &self,
spec: &PackageSpec, spec: &PackageSpec,
package_dir: &Path, cache_dir: &Path,
progress: &mut dyn Progress, progress: &mut dyn Progress,
) -> PackageResult<()> { ) -> PackageResult<()> {
assert_eq!(spec.namespace, DEFAULT_NAMESPACE); assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
@ -179,10 +193,132 @@ impl PackageStorage {
} }
}; };
// The directory in which the package's version lives.
let base_dir = cache_dir.join(format!("{}/{}", spec.namespace, spec.name));
// The place at which the specific package version will live in the end.
let package_dir = base_dir.join(format!("{}", spec.version));
// To prevent multiple Typst instances from interferring, we download
// into a temporary directory first and then move this directory to
// its final destination.
//
// In the `rename` function's documentation it is stated:
// > This will not work if the new name is on a different mount point.
//
// By locating the temporary directory directly next to where the
// package directory will live, we are (trying our best) making sure
// that `tempdir` and `package_dir` are on the same mount point.
let tempdir = Tempdir::create(base_dir.join(format!(
".tmp-{}-{}",
spec.version,
fastrand::u32(..),
)))
.map_err(|err| error("failed to create temporary package directory", err))?;
// Decompress the archive into the temporary directory.
let decompressed = flate2::read::GzDecoder::new(data.as_slice()); let decompressed = flate2::read::GzDecoder::new(data.as_slice());
tar::Archive::new(decompressed).unpack(package_dir).map_err(|err| { tar::Archive::new(decompressed)
fs::remove_dir_all(package_dir).ok(); .unpack(&tempdir)
PackageError::MalformedArchive(Some(eco_format!("{err}"))) .map_err(|err| PackageError::MalformedArchive(Some(eco_format!("{err}"))))?;
})
// When trying to move (i.e., `rename`) the directory from one place to
// another and the target/destination directory is empty, then the
// operation will succeed (if it's atomic, or hardware doesn't fail, or
// power doesn't go off, etc.). If however the target directory is not
// empty, i.e., another instance already successfully moved the package,
// then we can safely ignore the `DirectoryNotEmpty` error.
//
// This means that we do not check the integrity of an existing moved
// package, just like we don't check the integrity if the package
// directory already existed in the first place. If situations with
// broken packages still occur even with the rename safeguard, we might
// consider more complex solutions like file locking or checksums.
match fs::rename(&tempdir, &package_dir) {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()),
Err(err) => Err(error("failed to move downloaded package directory", err)),
}
}
}
/// Minimal information required about a package to determine its latest
/// version.
#[derive(Deserialize)]
struct MinimalPackageInfo {
name: String,
version: PackageVersion,
}
/// A temporary directory that is a automatically cleaned up.
struct Tempdir(PathBuf);
impl Tempdir {
/// Creates a directory at the path and auto-cleans it.
fn create(path: PathBuf) -> io::Result<Self> {
std::fs::create_dir_all(&path)?;
Ok(Self(path))
}
}
impl Drop for Tempdir {
fn drop(&mut self) {
_ = fs::remove_dir_all(&self.0);
}
}
impl AsRef<Path> for Tempdir {
fn as_ref(&self) -> &Path {
&self.0
}
}
/// Enriches an I/O error with a message and turns it into a
/// `PackageError::Other`.
#[cold]
fn error(message: &str, err: io::Error) -> PackageError {
PackageError::Other(Some(eco_format!("{message}: {err}")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lazy_deser_index() {
let storage = PackageStorage::with_index(
None,
None,
Downloader::new("typst/test"),
OnceCell::with_value(vec![
serde_json::json!({
"name": "charged-ieee",
"version": "0.1.0",
"entrypoint": "lib.typ",
}),
serde_json::json!({
"name": "unequivocal-ams",
// This version number is currently not valid, so this package
// can't be parsed.
"version": "0.2.0-dev",
"entrypoint": "lib.typ",
}),
]),
);
let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec {
namespace: "preview".into(),
name: "charged-ieee".into(),
});
assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 }));
let ams_version = storage.determine_latest_version(&VersionlessPackageSpec {
namespace: "preview".into(),
name: "unequivocal-ams".into(),
});
assert_eq!(
ams_version,
Err("failed to find package @preview/unequivocal-ams".into())
)
} }
} }

View File

@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> {
styles, styles,
self.base, self.base,
self.expand, self.expand,
None,
)? )?
.into_frames(); .into_frames();
@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> {
self.output.push(Child::Tag(&elem.tag)); self.output.push(Child::Tag(&elem.tag));
} }
self.lines(lines, styles); let leading = ParElem::leading_in(styles);
self.lines(lines, leading, styles);
for (c, _) in &self.children[end..] { for (c, _) in &self.children[end..] {
let elem = c.to_packed::<TagElem>().unwrap(); let elem = c.to_packed::<TagElem>().unwrap();
@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> {
)? )?
.into_frames(); .into_frames();
let spacing = ParElem::spacing_in(styles); let spacing = elem.spacing(styles);
let leading = elem.leading(styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
self.lines(lines, styles); self.lines(lines, leading, styles);
self.output.push(Child::Rel(spacing.into(), 4)); self.output.push(Child::Rel(spacing.into(), 4));
self.par_situation = ParSituation::Consecutive; self.par_situation = ParSituation::Consecutive;
@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> {
} }
/// Collect laid-out lines. /// Collect laid-out lines.
fn lines(&mut self, lines: Vec<Frame>, styles: StyleChain<'a>) { fn lines(&mut self, lines: Vec<Frame>, leading: Abs, styles: StyleChain<'a>) {
let align = AlignElem::alignment_in(styles).resolve(styles); let align = AlignElem::alignment_in(styles).resolve(styles);
let leading = ParElem::leading_in(styles);
let costs = TextElem::costs_in(styles); let costs = TextElem::costs_in(styles);
// Determine whether to prevent widow and orphans. // Determine whether to prevent widow and orphans.

View File

@ -115,7 +115,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
let column_height = regions.size.y; let column_height = regions.size.y;
let backlog: Vec<_> = std::iter::once(&column_height) let backlog: Vec<_> = std::iter::once(&column_height)
.chain(regions.backlog) .chain(regions.backlog)
.flat_map(|&h| std::iter::repeat(h).take(self.config.columns.count)) .flat_map(|&h| std::iter::repeat_n(h, self.config.columns.count))
.skip(1) .skip(1)
.collect(); .collect();

View File

@ -197,7 +197,50 @@ pub fn layout_flow<'a>(
mode: FlowMode, mode: FlowMode,
) -> SourceResult<Fragment> { ) -> SourceResult<Fragment> {
// Prepare configuration that is shared across the whole flow. // Prepare configuration that is shared across the whole flow.
let config = Config { let config = configuration(shared, regions, columns, column_gutter, mode);
// Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements.
let bump = Bump::new();
let children = collect(
engine,
&bump,
children,
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
let mut finished = vec![];
// This loop runs once per region produced by the flow layout.
loop {
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
finished.push(frame);
// Terminate the loop when everything is processed, though draining the
// backlog if necessary.
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
break;
}
regions.next();
}
Ok(Fragment::frames(finished))
}
/// Determine the flow's configuration.
fn configuration<'x>(
shared: StyleChain<'x>,
regions: Regions,
columns: NonZeroUsize,
column_gutter: Rel<Abs>,
mode: FlowMode,
) -> Config<'x> {
Config {
mode, mode,
shared, shared,
columns: { columns: {
@ -235,39 +278,7 @@ pub fn layout_flow<'a>(
) )
}, },
}), }),
};
// Collect the elements into pre-processed children. These are much easier
// to handle than the raw elements.
let bump = Bump::new();
let children = collect(
engine,
&bump,
children,
locator.next(&()),
Size::new(config.columns.width, regions.full),
regions.expand.x,
mode,
)?;
let mut work = Work::new(&children);
let mut finished = vec![];
// This loop runs once per region produced by the flow layout.
loop {
let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?;
finished.push(frame);
// Terminate the loop when everything is processed, though draining the
// backlog if necessary.
if work.done() && (!regions.expand.y || regions.backlog.is_empty()) {
break;
} }
regions.next();
}
Ok(Fragment::frames(finished))
} }
/// The work that is left to do by flow layout. /// The work that is left to do by flow layout.

View File

@ -11,7 +11,7 @@ use typst_library::layout::{
use typst_library::text::TextElem; use typst_library::text::TextElem;
use typst_library::visualize::Geometry; use typst_library::visualize::Geometry;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{MaybeReverseIter, Numeric}; use typst_utils::Numeric;
use super::{ use super::{
generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row,
@ -574,7 +574,7 @@ impl<'a> GridLayouter<'a> {
// Reverse with RTL so that later columns start first. // Reverse with RTL so that later columns start first.
let mut dx = Abs::zero(); let mut dx = Abs::zero();
for (x, &col) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { for (x, &col) in self.rcols.iter().enumerate() {
let mut dy = Abs::zero(); let mut dy = Abs::zero();
for row in rows { for row in rows {
// We want to only draw the fill starting at the parent // We want to only draw the fill starting at the parent
@ -643,18 +643,13 @@ impl<'a> GridLayouter<'a> {
.sum() .sum()
}; };
let width = self.cell_spanned_width(cell, x); let width = self.cell_spanned_width(cell, x);
// In the grid, cell colspans expand to the right, let mut pos = Point::new(dx, dy);
// so we're at the leftmost (lowest 'x') column if self.is_rtl {
// spanned by the cell. However, in RTL, cells // In RTL cells expand to the left, thus the
// expand to the left. Therefore, without the // position must additionally be offset by the
// offset below, cell fills would start at the // cell's width.
// rightmost visual position of a cell and extend pos.x = self.width - (dx + width);
// over to unrelated columns to the right in RTL. }
// We avoid this by ensuring the fill starts at the
// very left of the cell, even with colspan > 1.
let offset =
if self.is_rtl { -width + col } else { Abs::zero() };
let pos = Point::new(dx + offset, dy);
let size = Size::new(width, height); let size = Size::new(width, height);
let rect = Geometry::Rect(size).filled(fill); let rect = Geometry::Rect(size).filled(fill);
fills.push((pos, FrameItem::Shape(rect, self.span))); fills.push((pos, FrameItem::Shape(rect, self.span)));
@ -1236,10 +1231,9 @@ impl<'a> GridLayouter<'a> {
} }
let mut output = Frame::soft(Size::new(self.width, height)); let mut output = Frame::soft(Size::new(self.width, height));
let mut pos = Point::zero(); let mut offset = Point::zero();
// Reverse the column order when using RTL. for (x, &rcol) in self.rcols.iter().enumerate() {
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) {
if let Some(cell) = self.grid.cell(x, y) { if let Some(cell) = self.grid.cell(x, y) {
// Rowspans have a separate layout step // Rowspans have a separate layout step
if cell.rowspan.get() == 1 { if cell.rowspan.get() == 1 {
@ -1257,25 +1251,17 @@ impl<'a> GridLayouter<'a> {
let frame = let frame =
layout_cell(cell, engine, disambiguator, self.styles, pod)? layout_cell(cell, engine, disambiguator, self.styles, pod)?
.into_frame(); .into_frame();
let mut pos = pos; let mut pos = offset;
if self.is_rtl { if self.is_rtl {
// In the grid, cell colspans expand to the right, // In RTL cells expand to the left, thus the position
// so we're at the leftmost (lowest 'x') column // must additionally be offset by the cell's width.
// spanned by the cell. However, in RTL, cells pos.x = self.width - (pos.x + width);
// expand to the left. Therefore, without the
// offset below, the cell's contents would be laid out
// starting at its rightmost visual position and extend
// over to unrelated cells to its right in RTL.
// We avoid this by ensuring the rendered cell starts at
// the very left of the cell, even with colspan > 1.
let offset = -width + rcol;
pos.x += offset;
} }
output.push_frame(pos, frame); output.push_frame(pos, frame);
} }
} }
pos.x += rcol; offset.x += rcol;
} }
Ok(output) Ok(output)
@ -1302,8 +1288,8 @@ impl<'a> GridLayouter<'a> {
pod.backlog = &heights[1..]; pod.backlog = &heights[1..];
// Layout the row. // Layout the row.
let mut pos = Point::zero(); let mut offset = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate().rev_if(self.is_rtl) { for (x, &rcol) in self.rcols.iter().enumerate() {
if let Some(cell) = self.grid.cell(x, y) { if let Some(cell) = self.grid.cell(x, y) {
// Rowspans have a separate layout step // Rowspans have a separate layout step
if cell.rowspan.get() == 1 { if cell.rowspan.get() == 1 {
@ -1314,17 +1300,19 @@ impl<'a> GridLayouter<'a> {
let fragment = let fragment =
layout_cell(cell, engine, disambiguator, self.styles, pod)?; layout_cell(cell, engine, disambiguator, self.styles, pod)?;
for (output, frame) in outputs.iter_mut().zip(fragment) { for (output, frame) in outputs.iter_mut().zip(fragment) {
let mut pos = pos; let mut pos = offset;
if self.is_rtl { if self.is_rtl {
let offset = -width + rcol; // In RTL cells expand to the left, thus the
pos.x += offset; // position must additionally be offset by the
// cell's width.
pos.x = self.width - (offset.x + width);
} }
output.push_frame(pos, frame); output.push_frame(pos, frame);
} }
} }
} }
pos.x += rcol; offset.x += rcol;
} }
Ok(Fragment::frames(outputs)) Ok(Fragment::frames(outputs))
@ -1377,7 +1365,7 @@ impl<'a> GridLayouter<'a> {
.footer .footer
.as_ref() .as_ref()
.and_then(Repeatable::as_repeated) .and_then(Repeatable::as_repeated)
.map_or(true, |footer| footer.start != header.end) .is_none_or(|footer| footer.start != header.end)
&& self.lrows.last().is_some_and(|row| row.index() < header.end) && self.lrows.last().is_some_and(|row| row.index() < header.end)
&& !in_last_with_offset( && !in_last_with_offset(
self.regions, self.regions,
@ -1446,7 +1434,7 @@ impl<'a> GridLayouter<'a> {
.iter_mut() .iter_mut()
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
.filter(|rowspan| { .filter(|rowspan| {
rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) rowspan.max_resolved_row.is_none_or(|max_row| y > max_row)
}) })
{ {
// If the first region wasn't defined yet, it will have the // If the first region wasn't defined yet, it will have the
@ -1469,7 +1457,7 @@ impl<'a> GridLayouter<'a> {
// last height is the one for the current region. // last height is the one for the current region.
rowspan rowspan
.heights .heights
.extend(std::iter::repeat(Abs::zero()).take(amount_missing_heights)); .extend(std::iter::repeat_n(Abs::zero(), amount_missing_heights));
// Ensure that, in this region, the rowspan will span at least // Ensure that, in this region, the rowspan will span at least
// this row. // this row.
@ -1494,7 +1482,7 @@ impl<'a> GridLayouter<'a> {
// laid out at the first frame of the row). // laid out at the first frame of the row).
// Any rowspans ending before this row are laid out even // Any rowspans ending before this row are laid out even
// on this row's first frame. // on this row's first frame.
if laid_out_footer_start.map_or(true, |footer_start| { if laid_out_footer_start.is_none_or(|footer_start| {
// If this is a footer row, then only lay out this rowspan // If this is a footer row, then only lay out this rowspan
// if the rowspan is contained within the footer. // if the rowspan is contained within the footer.
y < footer_start || rowspan.y >= footer_start y < footer_start || rowspan.y >= footer_start
@ -1580,5 +1568,5 @@ pub(super) fn points(
/// our case, headers). /// our case, headers).
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
regions.backlog.is_empty() regions.backlog.is_empty()
&& regions.last.map_or(true, |height| regions.size.y + offset == height) && regions.last.is_none_or(|height| regions.size.y + offset == height)
} }

View File

@ -463,7 +463,7 @@ pub fn hline_stroke_at_column(
// region, we have the last index, and (as a failsafe) we don't have the // region, we have the last index, and (as a failsafe) we don't have the
// last row of cells above us. // last row of cells above us.
let use_bottom_border_stroke = !in_last_region let use_bottom_border_stroke = !in_last_region
&& local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) && local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len())
&& y == grid.rows.len(); && y == grid.rows.len();
let bottom_y = let bottom_y =
if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };

View File

@ -3,7 +3,6 @@ use typst_library::engine::Engine;
use typst_library::foundations::Resolve; use typst_library::foundations::Resolve;
use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::grid::resolve::Repeatable;
use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing};
use typst_utils::MaybeReverseIter;
use super::layouter::{in_last_with_offset, points, Row, RowPiece}; use super::layouter::{in_last_with_offset, points, Row, RowPiece};
use super::{layout_cell, Cell, GridLayouter}; use super::{layout_cell, Cell, GridLayouter};
@ -23,6 +22,10 @@ pub struct Rowspan {
/// specified for the parent cell's `breakable` field. /// specified for the parent cell's `breakable` field.
pub is_effectively_unbreakable: bool, pub is_effectively_unbreakable: bool,
/// The horizontal offset of this rowspan in all regions. /// The horizontal offset of this rowspan in all regions.
///
/// This is the offset from the text direction start, meaning that, on RTL
/// grids, this is the offset from the right of the grid, whereas, on LTR
/// grids, it is the offset from the left.
pub dx: Abs, pub dx: Abs,
/// The vertical offset of this rowspan in the first region. /// The vertical offset of this rowspan in the first region.
pub dy: Abs, pub dy: Abs,
@ -118,10 +121,11 @@ impl GridLayouter<'_> {
// Nothing to layout. // Nothing to layout.
return Ok(()); return Ok(());
}; };
let first_column = self.rcols[x];
let cell = self.grid.cell(x, y).unwrap(); let cell = self.grid.cell(x, y).unwrap();
let width = self.cell_spanned_width(cell, x); let width = self.cell_spanned_width(cell, x);
let dx = if self.is_rtl { dx - width + first_column } else { dx }; // In RTL cells expand to the left, thus the position
// must additionally be offset by the cell's width.
let dx = if self.is_rtl { self.width - (dx + width) } else { dx };
// Prepare regions. // Prepare regions.
let size = Size::new(width, *first_height); let size = Size::new(width, *first_height);
@ -185,10 +189,8 @@ impl GridLayouter<'_> {
/// Checks if a row contains the beginning of one or more rowspan cells. /// Checks if a row contains the beginning of one or more rowspan cells.
/// If so, adds them to the rowspans vector. /// If so, adds them to the rowspans vector.
pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) { pub fn check_for_rowspans(&mut self, disambiguator: usize, y: usize) {
// We will compute the horizontal offset of each rowspan in advance. let offsets = points(self.rcols.iter().copied());
// For that reason, we must reverse the column order when using RTL. for (x, dx) in (0..self.rcols.len()).zip(offsets) {
let offsets = points(self.rcols.iter().copied().rev_if(self.is_rtl));
for (x, dx) in (0..self.rcols.len()).rev_if(self.is_rtl).zip(offsets) {
let Some(cell) = self.grid.cell(x, y) else { let Some(cell) = self.grid.cell(x, y) else {
continue; continue;
}; };
@ -588,7 +590,7 @@ impl GridLayouter<'_> {
measurement_data: &CellMeasurementData<'_>, measurement_data: &CellMeasurementData<'_>,
) -> bool { ) -> bool {
if sizes.len() <= 1 if sizes.len() <= 1
&& sizes.first().map_or(true, |&first_frame_size| { && sizes.first().is_none_or(|&first_frame_size| {
first_frame_size <= measurement_data.height_in_this_region first_frame_size <= measurement_data.height_in_this_region
}) })
{ {

View File

@ -95,6 +95,8 @@ pub fn layout_image(
} else { } else {
// If neither is forced, take the natural image size at the image's // If neither is forced, take the natural image size at the image's
// DPI bounded by the available space. // DPI bounded by the available space.
//
// Division by DPI is fine since it's guaranteed to be positive.
let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI);
let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi));
Size::new( Size::new(

View File

@ -2,10 +2,8 @@ use typst_library::diag::warning;
use typst_library::foundations::{Packed, Resolve}; use typst_library::foundations::{Packed, Resolve};
use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::introspection::{SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Abs, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing,
Spacing,
}; };
use typst_library::model::{EnumElem, ListElem, TermsElem};
use typst_library::routines::Pair; use typst_library::routines::Pair;
use typst_library::text::{ use typst_library::text::{
is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes,
@ -123,41 +121,21 @@ pub fn collect<'a>(
children: &[Pair<'a>], children: &[Pair<'a>],
engine: &mut Engine<'_>, engine: &mut Engine<'_>,
locator: &mut SplitLocator<'a>, locator: &mut SplitLocator<'a>,
styles: StyleChain<'a>, config: &Config,
region: Size, region: Size,
situation: Option<ParSituation>,
) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> { ) -> SourceResult<(String, Vec<Segment<'a>>, SpanMapper)> {
let mut collector = Collector::new(2 + children.len()); let mut collector = Collector::new(2 + children.len());
let mut quoter = SmartQuoter::new(); let mut quoter = SmartQuoter::new();
let outer_dir = TextElem::dir_in(styles); if !config.first_line_indent.is_zero() {
collector.push_item(Item::Absolute(config.first_line_indent, false));
if let Some(situation) = situation {
let first_line_indent = ParElem::first_line_indent_in(styles);
if !first_line_indent.amount.is_zero()
&& match situation {
// First-line indent for the first paragraph after a list bullet
// just looks bad.
ParSituation::First => first_line_indent.all && !in_list(styles),
ParSituation::Consecutive => true,
ParSituation::Other => first_line_indent.all,
}
&& AlignElem::alignment_in(styles).resolve(styles).x
== outer_dir.start().into()
{
collector.push_item(Item::Absolute(
first_line_indent.amount.resolve(styles),
false,
));
collector.spans.push(1, Span::detached()); collector.spans.push(1, Span::detached());
} }
let hang = ParElem::hanging_indent_in(styles); if !config.hanging_indent.is_zero() {
if !hang.is_zero() { collector.push_item(Item::Absolute(-config.hanging_indent, false));
collector.push_item(Item::Absolute(-hang, false));
collector.spans.push(1, Span::detached()); collector.spans.push(1, Span::detached());
} }
}
for &(child, styles) in children { for &(child, styles) in children {
let prev_len = collector.full.len(); let prev_len = collector.full.len();
@ -167,7 +145,7 @@ pub fn collect<'a>(
} else if let Some(elem) = child.to_packed::<TextElem>() { } else if let Some(elem) = child.to_packed::<TextElem>() {
collector.build_text(styles, |full| { collector.build_text(styles, |full| {
let dir = TextElem::dir_in(styles); let dir = TextElem::dir_in(styles);
if dir != outer_dir { if dir != config.dir {
// Insert "Explicit Directional Embedding". // Insert "Explicit Directional Embedding".
match dir { match dir {
Dir::LTR => full.push_str(LTR_EMBEDDING), Dir::LTR => full.push_str(LTR_EMBEDDING),
@ -182,7 +160,7 @@ pub fn collect<'a>(
full.push_str(&elem.text); full.push_str(&elem.text);
} }
if dir != outer_dir { if dir != config.dir {
// Insert "Pop Directional Formatting". // Insert "Pop Directional Formatting".
full.push_str(POP_EMBEDDING); full.push_str(POP_EMBEDDING);
} }
@ -265,16 +243,6 @@ pub fn collect<'a>(
Ok((collector.full, collector.segments, collector.spans)) Ok((collector.full, collector.segments, collector.spans))
} }
/// Whether we have a list ancestor.
///
/// When we support some kind of more general ancestry mechanism, this can
/// become more elegant.
fn in_list(styles: StyleChain) -> bool {
ListElem::depth_in(styles).0 > 0
|| !EnumElem::parents_in(styles).is_empty()
|| TermsElem::within_in(styles)
}
/// Collects segments. /// Collects segments.
struct Collector<'a> { struct Collector<'a> {
full: String, full: String,

View File

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

View File

@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use typst_library::engine::Engine; use typst_library::engine::Engine;
use typst_library::foundations::NativeElement;
use typst_library::introspection::{SplitLocator, Tag}; use typst_library::introspection::{SplitLocator, Tag};
use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point};
use typst_library::model::{ParLine, ParLineMarker}; use typst_library::model::ParLineMarker;
use typst_library::text::{Lang, TextElem}; use typst_library::text::{Lang, TextElem};
use typst_utils::Numeric; use typst_utils::Numeric;
@ -135,7 +134,7 @@ pub fn line<'a>(
// Whether the line is justified. // Whether the line is justified.
let justify = full.ends_with(LINE_SEPARATOR) let justify = full.ends_with(LINE_SEPARATOR)
|| (p.justify && breakpoint != Breakpoint::Mandatory); || (p.config.justify && breakpoint != Breakpoint::Mandatory);
// Process dashes. // Process dashes.
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
@ -155,16 +154,16 @@ pub fn line<'a>(
let mut items = collect_items(engine, p, range, trim); let mut items = collect_items(engine, p, range, trim);
// Add a hyphen at the line start, if a previous dash should be repeated. // Add a hyphen at the line start, if a previous dash should be repeated.
if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { if pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) {
if let Some(shaped) = items.first_text_mut() { if let Some(shaped) = items.first_text_mut() {
shaped.prepend_hyphen(engine, p.fallback); shaped.prepend_hyphen(engine, p.config.fallback);
} }
} }
// Add a hyphen at the line end, if we ended on a soft hyphen. // Add a hyphen at the line end, if we ended on a soft hyphen.
if dash == Some(Dash::Soft) { if dash == Some(Dash::Soft) {
if let Some(shaped) = items.last_text_mut() { if let Some(shaped) = items.last_text_mut() {
shaped.push_hyphen(engine, p.fallback); shaped.push_hyphen(engine, p.config.fallback);
} }
} }
@ -234,13 +233,13 @@ where
{ {
// If there is nothing bidirectional going on, skip reordering. // If there is nothing bidirectional going on, skip reordering.
let Some(bidi) = &p.bidi else { let Some(bidi) = &p.bidi else {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
}; };
// The bidi crate panics for empty lines. // The bidi crate panics for empty lines.
if range.is_empty() { if range.is_empty() {
f(range, p.dir == Dir::RTL); f(range, p.config.dir == Dir::RTL);
return; return;
} }
@ -308,13 +307,13 @@ fn collect_range<'a>(
/// punctuation marks at line start or line end. /// punctuation marks at line start or line end.
fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) {
if text.starts_with(BEGIN_PUNCT_PAT) if text.starts_with(BEGIN_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script))
{ {
adjust_cj_at_line_start(p, items); adjust_cj_at_line_start(p, items);
} }
if text.ends_with(END_PUNCT_PAT) if text.ends_with(END_PUNCT_PAT)
|| (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script))
{ {
adjust_cj_at_line_end(p, items); adjust_cj_at_line_end(p, items);
} }
@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) {
let shrink = glyph.shrinkability().0; let shrink = glyph.shrinkability().0;
glyph.shrink_left(shrink); glyph.shrink_left(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script()
&& glyph.x_offset > Em::zero()
{
// If the first glyph is a CJK character adjusted by // If the first glyph is a CJK character adjusted by
// [`add_cjk_latin_spacing`], restore the original width. // [`add_cjk_latin_spacing`], restore the original width.
let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); let glyph = shaped.glyphs.to_mut().first_mut().unwrap();
@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) {
let punct = shaped.glyphs.to_mut().last_mut().unwrap(); let punct = shaped.glyphs.to_mut().last_mut().unwrap();
punct.shrink_right(shrink); punct.shrink_right(shrink);
shaped.width -= shrink.at(shaped.size); shaped.width -= shrink.at(shaped.size);
} else if p.cjk_latin_spacing } else if p.config.cjk_latin_spacing
&& glyph.is_cj_script() && glyph.is_cj_script()
&& (glyph.x_advance - glyph.x_offset) > Em::one() && (glyph.x_advance - glyph.x_offset) > Em::one()
{ {
@ -404,7 +406,7 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
// //
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española" // See § 4.1.1.1.2.e on the "Ortografía de la lengua española"
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea // https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea
Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()), Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()),
_ => false, _ => false,
} }
@ -424,16 +426,15 @@ pub fn commit(
width: Abs, width: Abs,
full: Abs, full: Abs,
locator: &mut SplitLocator<'_>, locator: &mut SplitLocator<'_>,
styles: StyleChain,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let mut remaining = width - line.width - p.hang; let mut remaining = width - line.width - p.config.hanging_indent;
let mut offset = Abs::zero(); let mut offset = Abs::zero();
// We always build the line from left to right. In an LTR paragraph, we must // We always build the line from left to right. In an LTR paragraph, we must
// thus add the hanging indent to the offset. In an RTL paragraph, the // thus add the hanging indent to the offset. In an RTL paragraph, the
// hanging indent arises naturally due to the line width. // hanging indent arises naturally due to the line width.
if p.dir == Dir::LTR { if p.config.dir == Dir::LTR {
offset += p.hang; offset += p.config.hanging_indent;
} }
// Handle hanging punctuation to the left. // Handle hanging punctuation to the left.
@ -554,11 +555,13 @@ pub fn commit(
let mut output = Frame::soft(size); let mut output = Frame::soft(size);
output.set_baseline(top); output.set_baseline(top);
add_par_line_marker(&mut output, styles, engine, locator, top); if let Some(marker) = &p.config.numbering_marker {
add_par_line_marker(&mut output, marker, engine, locator, top);
}
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame) in frames {
let x = offset + p.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
} }
@ -575,26 +578,18 @@ pub fn commit(
/// number in the margin, is aligned to the line's baseline. /// number in the margin, is aligned to the line's baseline.
fn add_par_line_marker( fn add_par_line_marker(
output: &mut Frame, output: &mut Frame,
styles: StyleChain, marker: &Packed<ParLineMarker>,
engine: &mut Engine, engine: &mut Engine,
locator: &mut SplitLocator, locator: &mut SplitLocator,
top: Abs, top: Abs,
) { ) {
let Some(numbering) = ParLine::numbering_in(styles) else { return };
let margin = ParLine::number_margin_in(styles);
let align = ParLine::number_align_in(styles);
// Delay resolving the number clearance until line numbers are laid out to
// avoid inconsistent spacing depending on varying font size.
let clearance = ParLine::number_clearance_in(styles);
// Elements in tags must have a location for introspection to work. We do // Elements in tags must have a location for introspection to work. We do
// the work here instead of going through all of the realization process // the work here instead of going through all of the realization process
// just for this, given we don't need to actually place the marker as we // just for this, given we don't need to actually place the marker as we
// manually search for it in the frame later (when building a root flow, // manually search for it in the frame later (when building a root flow,
// where line numbers can be displayed), so we just need it to be in a tag // where line numbers can be displayed), so we just need it to be in a tag
// and to be valid (to have a location). // and to be valid (to have a location).
let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); let mut marker = marker.clone();
let key = typst_utils::hash128(&marker); let key = typst_utils::hash128(&marker);
let loc = locator.next_location(engine.introspector, key); let loc = locator.next_location(engine.introspector, key);
marker.set_location(loc); marker.set_location(loc);
@ -606,7 +601,7 @@ fn add_par_line_marker(
// line's general baseline. However, the line number will still need to // line's general baseline. However, the line number will still need to
// manually adjust its own 'y' position based on its own baseline. // manually adjust its own 'y' position based on its own baseline.
let pos = Point::with_y(top); let pos = Point::with_y(top);
output.push(pos, FrameItem::Tag(Tag::Start(marker))); output.push(pos, FrameItem::Tag(Tag::Start(marker.pack())));
output.push(pos, FrameItem::Tag(Tag::End(loc, key))); output.push(pos, FrameItem::Tag(Tag::End(loc, key)));
} }

View File

@ -110,15 +110,7 @@ pub fn linebreak<'a>(
p: &'a Preparation<'a>, p: &'a Preparation<'a>,
width: Abs, width: Abs,
) -> Vec<Line<'a>> { ) -> Vec<Line<'a>> {
let linebreaks = p.linebreaks.unwrap_or_else(|| { match p.config.linebreaks {
if p.justify {
Linebreaks::Optimized
} else {
Linebreaks::Simple
}
});
match linebreaks {
Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Simple => linebreak_simple(engine, p, width),
Linebreaks::Optimized => linebreak_optimized(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width),
} }
@ -298,7 +290,7 @@ fn linebreak_optimized_bounded<'a>(
} }
// If this attempt is better than what we had before, take it! // If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) { if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry { pred: pred_index, total, line: attempt, end }); best = Some(Entry { pred: pred_index, total, line: attempt, end });
} }
} }
@ -384,7 +376,7 @@ fn linebreak_optimized_approximate(
// Whether the line is justified. This is not 100% accurate w.r.t // Whether the line is justified. This is not 100% accurate w.r.t
// to line()'s behaviour, but good enough. // to line()'s behaviour, but good enough.
let justify = p.justify && breakpoint != Breakpoint::Mandatory; let justify = p.config.justify && breakpoint != Breakpoint::Mandatory;
// We don't really know whether the line naturally ends with a dash // We don't really know whether the line naturally ends with a dash
// here, so we can miss that case, but it's ok, since all of this // here, so we can miss that case, but it's ok, since all of this
@ -431,7 +423,7 @@ fn linebreak_optimized_approximate(
let total = pred.total + line_cost; let total = pred.total + line_cost;
// If this attempt is better than what we had before, take it! // If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) { if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry { best = Some(Entry {
pred: pred_index, pred: pred_index,
total, total,
@ -573,7 +565,7 @@ fn raw_ratio(
// calculate the extra amount. Also, don't divide by zero. // calculate the extra amount. Also, don't divide by zero.
let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64;
// Normalize the amount by half the em size. // Normalize the amount by half the em size.
ratio = 1.0 + extra_stretch / (p.size / 2.0); ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0);
} }
// The min value must be < MIN_RATIO, but how much smaller doesn't matter // The min value must be < MIN_RATIO, but how much smaller doesn't matter
@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
return; return;
} }
let hyphenate = p.hyphenate != Some(false); let hyphenate = p.config.hyphenate != Some(false);
let lb = LINEBREAK_DATA.as_borrowed(); let lb = LINEBREAK_DATA.as_borrowed();
let segmenter = match p.lang { let segmenter = match p.config.lang {
Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER,
_ => &SEGMENTER, _ => &SEGMENTER,
}; };
@ -698,13 +690,34 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
let breakpoint = if point == text.len() { let breakpoint = if point == text.len() {
Breakpoint::Mandatory Breakpoint::Mandatory
} else { } else {
const OBJ_REPLACE: char = '\u{FFFC}';
match lb.get(c) { match lb.get(c) {
// Fix for: https://github.com/unicode-org/icu4x/issues/4146
LineBreak::Glue | LineBreak::WordJoiner | LineBreak::ZWJ => continue,
LineBreak::MandatoryBreak LineBreak::MandatoryBreak
| LineBreak::CarriageReturn | LineBreak::CarriageReturn
| LineBreak::LineFeed | LineBreak::LineFeed
| LineBreak::NextLine => Breakpoint::Mandatory, | LineBreak::NextLine => Breakpoint::Mandatory,
// https://github.com/typst/typst/issues/5489
//
// OBJECT-REPLACEMENT-CHARACTERs provide Contingent Break
// opportunities before and after by default. This behaviour
// is however tailorable, see:
// https://www.unicode.org/reports/tr14/#CB
// https://www.unicode.org/reports/tr14/#TailorableBreakingRules
// https://www.unicode.org/reports/tr14/#LB20
//
// Don't provide a line breaking opportunity between a LTR-
// ISOLATE (or any other Combining Mark) and an OBJECT-
// REPLACEMENT-CHARACTER representing an inline item, if the
// LTR-ISOLATE could end up as the only character on the
// previous line.
LineBreak::CombiningMark
if text[point..].starts_with(OBJ_REPLACE)
&& last + c.len_utf8() == point =>
{
continue;
}
_ => Breakpoint::Normal, _ => Breakpoint::Normal,
} }
}; };
@ -830,18 +843,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
/// Whether hyphenation is enabled at the given offset. /// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(p: &Preparation, offset: usize) -> bool { fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.hyphenate p.config.hyphenate.unwrap_or_else(|| {
.or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
let styles = item.text()?.styles; match item.text() {
Some(TextElem::hyphenate_in(styles)) Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
None => false,
}
}) })
.unwrap_or(false)
} }
/// The text language at the given offset. /// The text language at the given offset.
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> { fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.lang.or_else(|| { let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset); let (_, item) = p.get(offset);
let styles = item.text()?.styles; let styles = item.text()?.styles;
Some(TextElem::lang_in(styles)) Some(TextElem::lang_in(styles))
@ -865,13 +878,13 @@ impl CostMetrics {
fn compute(p: &Preparation) -> Self { fn compute(p: &Preparation) -> Self {
Self { Self {
// When justifying, we may stretch spaces below their natural width. // When justifying, we may stretch spaces below their natural width.
min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 },
// Approximate hyphen width for estimates. // Approximate hyphen width for estimates.
approx_hyphen_width: Em::new(0.33).at(p.size), approx_hyphen_width: Em::new(0.33).at(p.config.font_size),
// Costs. // Costs.
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(),
} }
} }

View File

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

View File

@ -1,9 +1,4 @@
use typst_library::foundations::{Resolve, Smart}; use typst_library::layout::{Dir, Em};
use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment};
use typst_library::model::Linebreaks;
use typst_library::routines::Pair;
use typst_library::text::{Costs, Lang, TextElem};
use typst_utils::SliceExt;
use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*; use super::*;
@ -17,6 +12,8 @@ use super::*;
pub struct Preparation<'a> { pub struct Preparation<'a> {
/// The full text. /// The full text.
pub text: &'a str, pub text: &'a str,
/// Configuration for inline layout.
pub config: &'a Config,
/// Bidirectional text embedding levels. /// Bidirectional text embedding levels.
/// ///
/// This is `None` if all text directions are uniform (all the base /// This is `None` if all text directions are uniform (all the base
@ -28,28 +25,6 @@ pub struct Preparation<'a> {
pub indices: Vec<usize>, pub indices: Vec<usize>,
/// The span mapper. /// The span mapper.
pub spans: SpanMapper, pub spans: SpanMapper,
/// Whether to hyphenate if it's the same for all children.
pub hyphenate: Option<bool>,
/// Costs for various layout decisions.
pub costs: Costs,
/// The dominant direction.
pub dir: Dir,
/// The text language if it's the same for all children.
pub lang: Option<Lang>,
/// The resolved horizontal alignment.
pub align: FixedAlignment,
/// Whether to justify text.
pub justify: bool,
/// Hanging indent to apply.
pub hang: Abs,
/// Whether to add spacing between CJK and Latin characters.
pub cjk_latin_spacing: bool,
/// Whether font fallback is enabled.
pub fallback: bool,
/// How to determine line breaks.
pub linebreaks: Smart<Linebreaks>,
/// The text size.
pub size: Abs,
} }
impl<'a> Preparation<'a> { impl<'a> Preparation<'a> {
@ -80,15 +55,12 @@ impl<'a> Preparation<'a> {
#[typst_macros::time] #[typst_macros::time]
pub fn prepare<'a>( pub fn prepare<'a>(
engine: &mut Engine, engine: &mut Engine,
children: &[Pair<'a>], config: &'a Config,
text: &'a str, text: &'a str,
segments: Vec<Segment<'a>>, segments: Vec<Segment<'a>>,
spans: SpanMapper, spans: SpanMapper,
styles: StyleChain<'a>,
situation: Option<ParSituation>,
) -> SourceResult<Preparation<'a>> { ) -> SourceResult<Preparation<'a>> {
let dir = TextElem::dir_in(styles); let default_level = match config.dir {
let default_level = match dir {
Dir::RTL => BidiLevel::rtl(), Dir::RTL => BidiLevel::rtl(),
_ => BidiLevel::ltr(), _ => BidiLevel::ltr(),
}; };
@ -124,51 +96,20 @@ pub fn prepare<'a>(
indices.extend(range.clone().map(|_| i)); indices.extend(range.clone().map(|_| i));
} }
let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); if config.cjk_latin_spacing {
if cjk_latin_spacing {
add_cjk_latin_spacing(&mut items); add_cjk_latin_spacing(&mut items);
} }
// Only apply hanging indent to real paragraphs.
let hang = if situation.is_some() {
ParElem::hanging_indent_in(styles)
} else {
Abs::zero()
};
Ok(Preparation { Ok(Preparation {
config,
text, text,
bidi: is_bidi.then_some(bidi), bidi: is_bidi.then_some(bidi),
items, items,
indices, indices,
spans, spans,
hyphenate: shared_get(children, styles, TextElem::hyphenate_in),
costs: TextElem::costs_in(styles),
dir,
lang: shared_get(children, styles, TextElem::lang_in),
align: AlignElem::alignment_in(styles).resolve(styles).x,
justify: ParElem::justify_in(styles),
hang,
cjk_latin_spacing,
fallback: TextElem::fallback_in(styles),
linebreaks: ParElem::linebreaks_in(styles),
size: TextElem::size_in(styles),
}) })
} }
/// Get a style property, but only if it is the same for all of the children.
fn shared_get<T: PartialEq>(
children: &[Pair],
styles: StyleChain<'_>,
getter: fn(StyleChain) -> T,
) -> Option<T> {
let value = getter(styles);
children
.group_by_key(|&(_, s)| s)
.all(|(s, _)| getter(s) == value)
.then_some(value)
}
/// Add some spacing between Han characters and western characters. See /// Add some spacing between Han characters and western characters. See
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode /// in Horizontal Written Mode

View File

@ -20,7 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript}; use unicode_script::{Script, UnicodeScript};
use super::{decorate, Item, Range, SpanMapper}; use super::{decorate, Item, Range, SpanMapper};
use crate::modifiers::{FrameModifiers, FrameModify}; use crate::modifiers::FrameModifyText;
/// The result of shaping text. /// The result of shaping text.
/// ///
@ -327,7 +327,7 @@ impl<'a> ShapedText<'a> {
offset += width; offset += width;
} }
frame.modify(&FrameModifiers::get_in(self.styles)); frame.modify_text(self.styles);
frame frame
} }
@ -465,7 +465,7 @@ impl<'a> ShapedText<'a> {
None None
}; };
let mut chain = families(self.styles) let mut chain = families(self.styles)
.filter(|family| family.covers().map_or(true, |c| c.is_match("-"))) .filter(|family| family.covers().is_none_or(|c| c.is_match("-")))
.map(|family| book.select(family.as_str(), self.variant)) .map(|family| book.select(family.as_str(), self.variant))
.chain(fallback_func.iter().map(|f| f())) .chain(fallback_func.iter().map(|f| f()))
.flatten(); .flatten();
@ -570,7 +570,7 @@ impl<'a> ShapedText<'a> {
// for the next line. // for the next line.
let dec = if ltr { usize::checked_sub } else { usize::checked_add }; let dec = if ltr { usize::checked_sub } else { usize::checked_add };
while let Some(next) = dec(idx, 1) { while let Some(next) = dec(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) {
break; break;
} }
idx = next; idx = next;
@ -812,7 +812,7 @@ fn shape_segment<'a>(
.nth(1) .nth(1)
.map(|(i, _)| offset + i) .map(|(i, _)| offset + i)
.unwrap_or(text.len()); .unwrap_or(text.len());
covers.map_or(true, |cov| cov.is_match(&text[offset..end])) covers.is_none_or(|cov| cov.is_match(&text[offset..end]))
}; };
// Collect the shaped glyphs, doing fallback and shaping parts again with // Collect the shaped glyphs, doing fallback and shaping parts again with
@ -824,12 +824,42 @@ fn shape_segment<'a>(
// Add the glyph to the shaped output. // Add the glyph to the shaped output.
if info.glyph_id != 0 && is_covered(cluster) { if info.glyph_id != 0 && is_covered(cluster) {
// Determine the text range of the glyph. // Assume we have the following sequence of (glyph_id, cluster):
// [(120, 0), (80, 0), (3, 3), (755, 4), (69, 4), (424, 13),
// (63, 13), (193, 25), (80, 25), (3, 31)
//
// We then want the sequence of (glyph_id, text_range) to look as follows:
// [(120, 0..3), (80, 0..3), (3, 3..4), (755, 4..13), (69, 4..13),
// (424, 13..25), (63, 13..25), (193, 25..31), (80, 25..31), (3, 31..x)]
//
// Each glyph in the same cluster should be assigned the full text
// range. This is necessary because only this way krilla can
// properly assign `ActualText` attributes in complex shaping
// scenarios.
// The start of the glyph's text range.
let start = base + cluster; let start = base + cluster;
let end = base
+ if ltr { i.checked_add(1) } else { i.checked_sub(1) } // Determine the end of the glyph's text range.
.and_then(|last| infos.get(last)) let mut k = i;
.map_or(text.len(), |info| info.cluster as usize); let step: isize = if ltr { 1 } else { -1 };
let end = loop {
// If we've reached the end of the glyphs, the `end` of the
// range should be the end of the full text.
let Some((next, next_info)) = k
.checked_add_signed(step)
.and_then(|n| infos.get(n).map(|info| (n, info)))
else {
break base + text.len();
};
// If the cluster doesn't match anymore, we've reached the end.
if next_info.cluster != info.cluster {
break base + next_info.cluster as usize;
}
k = next;
};
let c = text[cluster..].chars().next().unwrap(); let c = text[cluster..].chars().next().unwrap();
let script = c.script(); let script = c.script();

View File

@ -96,9 +96,13 @@ pub fn layout_enum(
let mut cells = vec![]; let mut cells = vec![];
let mut locator = locator.split(); let mut locator = locator.split();
let mut number = let mut number = elem.start(styles).unwrap_or_else(|| {
elem.start(styles) if reversed {
.unwrap_or_else(|| if reversed { elem.children.len() } else { 1 }); elem.children.len() as u64
} else {
1
}
});
let mut parents = EnumElem::parents_in(styles); let mut parents = EnumElem::parents_in(styles);
let full = elem.full(styles); let full = elem.full(styles);

View File

@ -19,9 +19,11 @@ pub fn layout_accent(
let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?;
// Try to replace a glyph with its dotless variant. // Try to replace a glyph with its dotless variant.
if elem.dotless(styles) {
if let MathFragment::Glyph(glyph) = &mut base { if let MathFragment::Glyph(glyph) = &mut base {
glyph.make_dotless_form(ctx); glyph.make_dotless_form(ctx);
} }
}
// Preserve class to preserve automatic spacing. // Preserve class to preserve automatic spacing.
let base_class = base.class(); let base_class = base.class();
@ -34,7 +36,7 @@ pub fn layout_accent(
// Try to replace accent glyph with flattened variant. // Try to replace accent glyph with flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
if base.height() > flattened_base_height { if base.ascent() > flattened_base_height {
glyph.make_flattened_accent_form(ctx); glyph.make_flattened_accent_form(ctx);
} }
@ -50,7 +52,7 @@ pub fn layout_accent(
// minus the accent base height. Only if the base is very small, we need // minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low. // a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height); let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.height().min(accent_base_height); let gap = -accent.descent() - base.ascent().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height()); let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach); let accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap); let base_pos = Point::with_y(accent.height() + gap);

View File

@ -1,4 +1,4 @@
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, warning, SourceResult};
use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; use typst_library::foundations::{Content, Packed, Resolve, StyleChain};
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size, Abs, Axes, Em, FixedAlignment, Frame, FrameItem, Point, Ratio, Rel, Size,
@ -9,7 +9,7 @@ use typst_library::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape};
use typst_syntax::Span; use typst_syntax::Span;
use super::{ use super::{
alignments, delimiter_alignment, stack, style_for_denominator, AlignmentResult, alignments, delimiter_alignment, style_for_denominator, AlignmentResult,
FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL, FrameFragment, GlyphFragment, LeftRightAlternator, MathContext, DELIM_SHORT_FALL,
}; };
@ -23,67 +23,23 @@ pub fn layout_vec(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let delim = elem.delim(styles); let span = elem.span();
let frame = layout_vec_body(
let column: Vec<&Content> = elem.children.iter().collect();
let frame = layout_body(
ctx, ctx,
styles, styles,
&elem.children, &[column],
elem.align(styles), elem.align(styles),
elem.gap(styles),
LeftRightAlternator::Right, LeftRightAlternator::Right,
None,
Axes::with_y(elem.gap(styles)),
span,
"elements",
)?; )?;
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
}
/// Lays out a [`MatElem`].
#[typst_macros::time(name = "math.mat", span = elem.span())]
pub fn layout_mat(
elem: &Packed<MatElem>,
ctx: &mut MathContext,
styles: StyleChain,
) -> SourceResult<()> {
let augment = elem.augment(styles);
let rows = &elem.rows;
if let Some(aug) = &augment {
for &offset in &aug.hline.0 {
if offset == 0 || offset.unsigned_abs() >= rows.len() {
bail!(
elem.span(),
"cannot draw a horizontal line after row {} of a matrix with {} rows",
if offset < 0 { rows.len() as isize + offset } else { offset },
rows.len()
);
}
}
let ncols = rows.first().map_or(0, |row| row.len());
for &offset in &aug.vline.0 {
if offset == 0 || offset.unsigned_abs() >= ncols {
bail!(
elem.span(),
"cannot draw a vertical line after column {} of a matrix with {} columns",
if offset < 0 { ncols as isize + offset } else { offset },
ncols
);
}
}
}
let delim = elem.delim(styles); let delim = elem.delim(styles);
let frame = layout_mat_body( layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
ctx,
styles,
rows,
elem.align(styles),
augment,
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
elem.span(),
)?;
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), elem.span())
} }
/// Lays out a [`CasesElem`]. /// Lays out a [`CasesElem`].
@ -93,60 +49,100 @@ pub fn layout_cases(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
) -> SourceResult<()> { ) -> SourceResult<()> {
let delim = elem.delim(styles); let span = elem.span();
let frame = layout_vec_body(
let column: Vec<&Content> = elem.children.iter().collect();
let frame = layout_body(
ctx, ctx,
styles, styles,
&elem.children, &[column],
FixedAlignment::Start, FixedAlignment::Start,
elem.gap(styles),
LeftRightAlternator::None, LeftRightAlternator::None,
None,
Axes::with_y(elem.gap(styles)),
span,
"branches",
)?; )?;
let delim = elem.delim(styles);
let (open, close) = let (open, close) =
if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) };
layout_delimiters(ctx, styles, frame, open, close, span)
layout_delimiters(ctx, styles, frame, open, close, elem.span())
} }
/// Layout the inner contents of a vector. /// Lays out a [`MatElem`].
fn layout_vec_body( #[typst_macros::time(name = "math.mat", span = elem.span())]
pub fn layout_mat(
elem: &Packed<MatElem>,
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
column: &[Content], ) -> SourceResult<()> {
align: FixedAlignment, let span = elem.span();
row_gap: Rel<Abs>, let rows = &elem.rows;
alternator: LeftRightAlternator, let ncols = rows.first().map_or(0, |row| row.len());
) -> SourceResult<Frame> {
let gap = row_gap.relative_to(ctx.region.size.y);
let denom_style = style_for_denominator(styles); let augment = elem.augment(styles);
let mut flat = vec![]; if let Some(aug) = &augment {
for child in column { for &offset in &aug.hline.0 {
// We allow linebreaks in cases and vectors, which are functionally if offset == 0 || offset.unsigned_abs() >= rows.len() {
// identical to commas. bail!(
flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows()); span,
"cannot draw a horizontal line after row {} of a matrix with {} rows",
if offset < 0 { rows.len() as isize + offset } else { offset },
rows.len()
);
} }
// We pad ascent and descent with the ascent and descent of the paren }
// to ensure that normal vectors are aligned with others unless they are
// way too big. for &offset in &aug.vline.0 {
let paren = if offset == 0 || offset.unsigned_abs() >= ncols {
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); bail!(
Ok(stack(flat, align, gap, 0, alternator, Some((paren.ascent, paren.descent)))) span,
"cannot draw a vertical line after column {} of a matrix with {} columns",
if offset < 0 { ncols as isize + offset } else { offset },
ncols
);
}
}
}
// Transpose rows of the matrix into columns.
let mut row_iters: Vec<_> = rows.iter().map(|i| i.iter()).collect();
let columns: Vec<Vec<_>> = (0..ncols)
.map(|_| row_iters.iter_mut().map(|i| i.next().unwrap()).collect())
.collect();
let frame = layout_body(
ctx,
styles,
&columns,
elem.align(styles),
LeftRightAlternator::Right,
augment,
Axes::new(elem.column_gap(styles), elem.row_gap(styles)),
span,
"cells",
)?;
let delim = elem.delim(styles);
layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span)
} }
/// Layout the inner contents of a matrix. /// Layout the inner contents of a matrix, vector, or cases.
fn layout_mat_body( #[allow(clippy::too_many_arguments)]
fn layout_body(
ctx: &mut MathContext, ctx: &mut MathContext,
styles: StyleChain, styles: StyleChain,
rows: &[Vec<Content>], columns: &[Vec<&Content>],
align: FixedAlignment, align: FixedAlignment,
alternator: LeftRightAlternator,
augment: Option<Augment<Abs>>, augment: Option<Augment<Abs>>,
gap: Axes<Rel<Abs>>, gap: Axes<Rel<Abs>>,
span: Span, span: Span,
children: &str,
) -> SourceResult<Frame> { ) -> SourceResult<Frame> {
let ncols = rows.first().map_or(0, |row| row.len()); let nrows = columns.first().map_or(0, |col| col.len());
let nrows = rows.len(); let ncols = columns.len();
if ncols == 0 || nrows == 0 { if ncols == 0 || nrows == 0 {
return Ok(Frame::soft(Size::zero())); return Ok(Frame::soft(Size::zero()));
} }
@ -178,16 +174,11 @@ fn layout_mat_body(
// Before the full matrix body can be laid out, the // Before the full matrix body can be laid out, the
// individual cells must first be independently laid out // individual cells must first be independently laid out
// so we can ensure alignment across rows and columns. // so we can ensure alignment across rows and columns.
let mut cols = vec![vec![]; ncols];
// This variable stores the maximum ascent and descent for each row. // This variable stores the maximum ascent and descent for each row.
let mut heights = vec![(Abs::zero(), Abs::zero()); nrows]; let mut heights = vec![(Abs::zero(), Abs::zero()); nrows];
// We want to transpose our data layout to columns
// before final layout. For efficiency, the columns
// variable is set up here and newly generated
// individual cells are then added to it.
let mut cols = vec![vec![]; ncols];
let denom_style = style_for_denominator(styles); let denom_style = style_for_denominator(styles);
// We pad ascent and descent with the ascent and descent of the paren // We pad ascent and descent with the ascent and descent of the paren
// to ensure that normal matrices are aligned with others unless they are // to ensure that normal matrices are aligned with others unless they are
@ -195,10 +186,22 @@ fn layout_mat_body(
let paren = let paren =
GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached()); GlyphFragment::new(ctx, styles.chain(&denom_style), '(', Span::detached());
for (row, (ascent, descent)) in rows.iter().zip(&mut heights) { for (column, col) in columns.iter().zip(&mut cols) {
for (cell, col) in row.iter().zip(&mut cols) { for (cell, (ascent, descent)) in column.iter().zip(&mut heights) {
let cell_span = cell.span();
let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?; let cell = ctx.layout_into_run(cell, styles.chain(&denom_style))?;
// We ignore linebreaks in the cells as we can't differentiate
// alignment points for the whole body from ones for a specific
// cell, and multiline cells don't quite make sense at the moment.
if cell.is_multiline() {
ctx.engine.sink.warn(warning!(
cell_span,
"linebreaks are ignored in {}", children;
hint: "use commas instead to separate each line"
));
}
ascent.set_max(cell.ascent().max(paren.ascent)); ascent.set_max(cell.ascent().max(paren.ascent));
descent.set_max(cell.descent().max(paren.descent)); descent.set_max(cell.descent().max(paren.descent));
@ -222,7 +225,7 @@ fn layout_mat_body(
let mut y = Abs::zero(); let mut y = Abs::zero();
for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) { for (cell, &(ascent, descent)) in col.into_iter().zip(&heights) {
let cell = cell.into_line_frame(&points, LeftRightAlternator::Right); let cell = cell.into_line_frame(&points, alternator);
let pos = Point::new( let pos = Point::new(
if points.is_empty() { if points.is_empty() {
x + align.position(rcol - cell.width()) x + align.position(rcol - cell.width())

View File

@ -85,14 +85,15 @@ pub fn layout_root(
ascent.set_max(shift_up + index.ascent()); ascent.set_max(shift_up + index.ascent());
} }
let radicand_x = sqrt_offset + sqrt.width(); let sqrt_x = sqrt_offset.max(Abs::zero());
let radicand_x = sqrt_x + sqrt.width();
let radicand_y = ascent - radicand.ascent(); let radicand_y = ascent - radicand.ascent();
let width = radicand_x + radicand.width(); let width = radicand_x + radicand.width();
let size = Size::new(width, ascent + descent); let size = Size::new(width, ascent + descent);
// The extra "- thickness" comes from the fact that the sqrt is placed // The extra "- thickness" comes from the fact that the sqrt is placed
// in `push_frame` with respect to its top, not its baseline. // in `push_frame` with respect to its top, not its baseline.
let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness); let sqrt_pos = Point::new(sqrt_x, radicand_y - gap - thickness);
let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0)); let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0));
let radicand_pos = Point::new(radicand_x, radicand_y); let radicand_pos = Point::new(radicand_x, radicand_y);
@ -100,7 +101,8 @@ pub fn layout_root(
frame.set_baseline(ascent); frame.set_baseline(ascent);
if let Some(index) = index { if let Some(index) = index {
let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up); let index_x = -sqrt_offset.min(Abs::zero()) + kern_before;
let index_pos = Point::new(index_x, ascent - index.ascent() - shift_up);
frame.push_frame(index_pos, index); frame.push_frame(index_pos, index);
} }

View File

@ -117,7 +117,6 @@ pub fn stack(
gap: Abs, gap: Abs,
baseline: usize, baseline: usize,
alternator: LeftRightAlternator, alternator: LeftRightAlternator,
minimum_ascent_descent: Option<(Abs, Abs)>,
) -> Frame { ) -> Frame {
let AlignmentResult { points, width } = alignments(&rows); let AlignmentResult { points, width } = alignments(&rows);
let rows: Vec<_> = rows let rows: Vec<_> = rows
@ -125,13 +124,9 @@ pub fn stack(
.map(|row| row.into_line_frame(&points, alternator)) .map(|row| row.into_line_frame(&points, alternator))
.collect(); .collect();
let padded_height = |height: Abs| {
height.max(minimum_ascent_descent.map_or(Abs::zero(), |(a, d)| a + d))
};
let mut frame = Frame::soft(Size::new( let mut frame = Frame::soft(Size::new(
width, width,
rows.iter().map(|row| padded_height(row.height())).sum::<Abs>() rows.iter().map(|row| row.height()).sum::<Abs>()
+ rows.len().saturating_sub(1) as f64 * gap, + rows.len().saturating_sub(1) as f64 * gap,
)); ));
@ -142,14 +137,11 @@ pub fn stack(
} else { } else {
Abs::zero() Abs::zero()
}; };
let ascent_padded_part = minimum_ascent_descent let pos = Point::new(x, y);
.map_or(Abs::zero(), |(a, _)| (a - row.ascent()))
.max(Abs::zero());
let pos = Point::new(x, y + ascent_padded_part);
if i == baseline { if i == baseline {
frame.set_baseline(y + row.baseline() + ascent_padded_part); frame.set_baseline(y + row.baseline());
} }
y += padded_height(row.height()) + gap; y += row.height() + gap;
frame.push_frame(pos, row); frame.push_frame(pos, row);
} }

View File

@ -302,6 +302,6 @@ fn assemble(
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ { fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
assembly.parts.into_iter().flat_map(move |part| { assembly.parts.into_iter().flat_map(move |part| {
let count = if part.part_flags.extender() { repeat } else { 1 }; let count = if part.part_flags.extender() { repeat } else { 1 };
std::iter::repeat(part).take(count) std::iter::repeat_n(part, count)
}) })
} }

View File

@ -107,7 +107,6 @@ fn layout_inline_text(
styles, styles,
Size::splat(Abs::inf()), Size::splat(Abs::inf()),
false, false,
None,
)? )?
.into_frame(); .into_frame();

View File

@ -312,14 +312,8 @@ fn layout_underoverspreader(
} }
}; };
let frame = stack( let frame =
rows, stack(rows, FixedAlignment::Center, gap, baseline, LeftRightAlternator::Right);
FixedAlignment::Center,
gap,
baseline,
LeftRightAlternator::Right,
None,
);
ctx.push(FrameFragment::new(styles, frame).with_class(body_class)); ctx.push(FrameFragment::new(styles, frame).with_class(body_class));
Ok(()) Ok(())

View File

@ -1,6 +1,6 @@
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides};
use typst_library::model::{Destination, LinkElem}; use typst_library::model::{Destination, LinkElem, ParElem};
/// Frame-level modifications resulting from styles that do not impose any /// Frame-level modifications resulting from styles that do not impose any
/// layout structure. /// layout structure.
@ -52,14 +52,7 @@ pub trait FrameModify {
impl FrameModify for Frame { impl FrameModify for Frame {
fn modify(&mut self, modifiers: &FrameModifiers) { fn modify(&mut self, modifiers: &FrameModifiers) {
if let Some(dest) = &modifiers.dest { modify_frame(self, modifiers, None);
let size = self.size();
self.push(Point::zero(), FrameItem::Link(dest.clone(), size));
}
if modifiers.hidden {
self.hide();
}
} }
} }
@ -82,6 +75,41 @@ where
} }
} }
pub trait FrameModifyText {
/// Resolve and apply [`FrameModifiers`] for this text frame.
fn modify_text(&mut self, styles: StyleChain);
}
impl FrameModifyText for Frame {
fn modify_text(&mut self, styles: StyleChain) {
let modifiers = FrameModifiers::get_in(styles);
let expand_y = 0.5 * ParElem::leading_in(styles);
let outset = Sides::new(Abs::zero(), expand_y, Abs::zero(), expand_y);
modify_frame(self, &modifiers, Some(outset));
}
}
fn modify_frame(
frame: &mut Frame,
modifiers: &FrameModifiers,
link_box_outset: Option<Sides<Abs>>,
) {
if let Some(dest) = &modifiers.dest {
let mut pos = Point::zero();
let mut size = frame.size();
if let Some(outset) = link_box_outset {
pos.y -= outset.top;
pos.x -= outset.left;
size += outset.sum_by_axis();
}
frame.push(pos, FrameItem::Link(dest.clone(), size));
}
if modifiers.hidden {
frame.hide();
}
}
/// Performs layout and modification in one step. /// Performs layout and modification in one step.
/// ///
/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, /// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`,

View File

@ -284,6 +284,7 @@ impl<'a> CurveBuilder<'a> {
self.last_point = point; self.last_point = point;
self.last_control_from = point; self.last_control_from = point;
self.is_started = true; self.is_started = true;
self.is_empty = true;
} }
/// Add a line segment. /// Add a line segment.
@ -1281,7 +1282,7 @@ impl ControlPoints {
} }
} }
/// Helper to draw arcs with bezier curves. /// Helper to draw arcs with zier curves.
trait CurveExt { trait CurveExt {
fn arc(&mut self, start: Point, center: Point, end: Point); fn arc(&mut self, start: Point, center: Point, end: Point);
fn arc_move(&mut self, start: Point, center: Point, end: Point); fn arc_move(&mut self, start: Point, center: Point, end: Point);
@ -1305,7 +1306,7 @@ impl CurveExt for Curve {
} }
} }
/// Get the control points for a bezier curve that approximates a circular arc for /// Get the control points for a zier curve that approximates a circular arc for
/// a start point, an end point and a center of the circle whose arc connects /// a start point, an end point and a center of the circle whose arc connects
/// the two. /// the two.
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] { fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {

View File

@ -29,6 +29,7 @@ csv = { workspace = true }
ecow = { workspace = true } ecow = { workspace = true }
flate2 = { workspace = true } flate2 = { workspace = true }
fontdb = { workspace = true } fontdb = { workspace = true }
glidesort = { workspace = true }
hayagriva = { workspace = true } hayagriva = { workspace = true }
icu_properties = { workspace = true } icu_properties = { workspace = true }
icu_provider = { workspace = true } icu_provider = { workspace = true }
@ -38,6 +39,7 @@ indexmap = { workspace = true }
kamadak-exif = { workspace = true } kamadak-exif = { workspace = true }
kurbo = { workspace = true } kurbo = { workspace = true }
lipsum = { workspace = true } lipsum = { workspace = true }
memchr = { workspace = true }
palette = { workspace = true } palette = { workspace = true }
phf = { workspace = true } phf = { workspace = true }
png = { workspace = true } png = { workspace = true }
@ -60,6 +62,7 @@ ttf-parser = { workspace = true }
two-face = { workspace = true } two-face = { workspace = true }
typed-arena = { workspace = true } typed-arena = { workspace = true }
unicode-math-class = { workspace = true } unicode-math-class = { workspace = true }
unicode-normalization = { workspace = true }
unicode-segmentation = { workspace = true } unicode-segmentation = { workspace = true }
unscanny = { workspace = true } unscanny = { workspace = true }
usvg = { workspace = true } usvg = { workspace = true }

View File

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

View File

@ -172,17 +172,29 @@ impl Array {
} }
/// Returns the first item in the array. May be used on the left-hand side /// Returns the first item in the array. May be used on the left-hand side
/// of an assignment. Fails with an error if the array is empty. /// an assignment. Returns the default value if the array is empty
/// or fails with an error is no default value was specified.
#[func] #[func]
pub fn first(&self) -> StrResult<Value> { pub fn first(
self.0.first().cloned().ok_or_else(array_is_empty) &self,
/// A default value to return if the array is empty.
#[named]
default: Option<Value>,
) -> StrResult<Value> {
self.0.first().cloned().or(default).ok_or_else(array_is_empty)
} }
/// Returns the last item in the array. May be used on the left-hand side of /// Returns the last item in the array. May be used on the left-hand side of
/// an assignment. Fails with an error if the array is empty. /// an assignment. Returns the default value if the array is empty
/// or fails with an error is no default value was specified.
#[func] #[func]
pub fn last(&self) -> StrResult<Value> { pub fn last(
self.0.last().cloned().ok_or_else(array_is_empty) &self,
/// A default value to return if the array is empty.
#[named]
default: Option<Value>,
) -> StrResult<Value> {
self.0.last().cloned().or(default).ok_or_else(array_is_empty)
} }
/// Returns the item at the specified index in the array. May be used on the /// Returns the item at the specified index in the array. May be used on the
@ -751,7 +763,7 @@ impl Array {
/// ///
/// ```example /// ```example
/// #let array = (1, 2, 3, 4, 5, 6, 7, 8) /// #let array = (1, 2, 3, 4, 5, 6, 7, 8)
/// #array.chunks(3) /// #array.chunks(3) \
/// #array.chunks(3, exact: true) /// #array.chunks(3, exact: true)
/// ``` /// ```
#[func] #[func]
@ -796,7 +808,7 @@ impl Array {
/// function. The sorting algorithm used is stable. /// function. The sorting algorithm used is stable.
/// ///
/// Returns an error if two values could not be compared or if the key /// Returns an error if two values could not be compared or if the key
/// function (if given) yields an error. /// or comparison function (if given) yields an error.
/// ///
/// To sort according to multiple criteria at once, e.g. in case of equality /// To sort according to multiple criteria at once, e.g. in case of equality
/// between some criteria, the key function can return an array. The results /// between some criteria, the key function can return an array. The results
@ -820,17 +832,116 @@ impl Array {
/// determine the keys to sort by. /// determine the keys to sort by.
#[named] #[named]
key: Option<Func>, key: Option<Func>,
/// If given, uses this function to compare elements in the array.
///
/// This function should return a boolean: `{true}` indicates that the
/// elements are in order, while `{false}` indicates that they should be
/// swapped. To keep the sort stable, if the two elements are equal, the
/// function should return `{true}`.
///
/// If this function does not order the elements properly (e.g., by
/// returning `{false}` for both `{(x, y)}` and `{(y, x)}`, or for
/// `{(x, x)}`), the resulting array will be in unspecified order.
///
/// When used together with `key`, `by` will be passed the keys instead
/// of the elements.
///
/// ```example
/// #(
/// "sorted",
/// "by",
/// "decreasing",
/// "length",
/// ).sorted(
/// key: s => s.len(),
/// by: (l, r) => l >= r,
/// )
/// ```
#[named]
by: Option<Func>,
) -> SourceResult<Array> { ) -> SourceResult<Array> {
match by {
Some(by) => {
let mut are_in_order = |mut x, mut y| {
if let Some(f) = &key {
// We rely on `comemo`'s memoization of function
// evaluation to not excessively reevaluate the key.
x = f.call(engine, context, [x])?;
y = f.call(engine, context, [y])?;
}
match by.call(engine, context, [x, y])? {
Value::Bool(b) => Ok(b),
x => {
bail!(
span,
"expected boolean from `by` function, got {}",
x.ty(),
)
}
}
};
// If a comparison function is provided, we use `glidesort`
// instead of the standard library sorting algorithm to prevent
// panics in case the comparison function does not define a
// valid order (see https://github.com/typst/typst/pull/5627).
let mut result = Ok(()); let mut result = Ok(());
let mut vec = self.0; let mut vec = self.0.into_iter().enumerate().collect::<Vec<_>>();
glidesort::sort_by(&mut vec, |(i, x), (j, y)| {
// Because we use booleans for the comparison function, in
// order to keep the sort stable, we need to compare in the
// right order.
if i < j {
// If `x` and `y` appear in this order in the original
// array, then we should change their order (i.e.,
// return `Ordering::Greater`) iff `y` is strictly less
// than `x` (i.e., `compare(x, y)` returns `false`).
// Otherwise, we should keep them in the same order
// (i.e., return `Ordering::Less`).
match are_in_order(x.clone(), y.clone()) {
Ok(false) => Ordering::Greater,
Ok(true) => Ordering::Less,
Err(err) => {
if result.is_ok() {
result = Err(err);
}
Ordering::Equal
}
}
} else {
// If `x` and `y` appear in the opposite order in the
// original array, then we should change their order
// (i.e., return `Ordering::Less`) iff `x` is strictly
// less than `y` (i.e., `compare(y, x)` returns
// `false`). Otherwise, we should keep them in the same
// order (i.e., return `Ordering::Less`).
match are_in_order(y.clone(), x.clone()) {
Ok(false) => Ordering::Less,
Ok(true) => Ordering::Greater,
Err(err) => {
if result.is_ok() {
result = Err(err);
}
Ordering::Equal
}
}
}
});
result.map(|()| vec.into_iter().map(|(_, x)| x).collect())
}
None => {
let mut key_of = |x: Value| match &key { let mut key_of = |x: Value| match &key {
// NOTE: We are relying on `comemo`'s memoization of function // We rely on `comemo`'s memoization of function evaluation
// evaluation to not excessively reevaluate the `key`. // to not excessively reevaluate the key.
Some(f) => f.call(engine, context, [x]), Some(f) => f.call(engine, context, [x]),
None => Ok(x), None => Ok(x),
}; };
// If no comparison function is provided, we know the order is
// valid, so we can use the standard library sort and prevent an
// extra allocation.
let mut result = Ok(());
let mut vec = self.0;
vec.make_mut().sort_by(|a, b| { vec.make_mut().sort_by(|a, b| {
// Until we get `try` blocks :)
match (key_of(a.clone()), key_of(b.clone())) { match (key_of(a.clone()), key_of(b.clone())) {
(Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| { (Ok(a), Ok(b)) => ops::compare(&a, &b).unwrap_or_else(|err| {
if result.is_ok() { if result.is_ok() {
@ -846,7 +957,9 @@ impl Array {
} }
} }
}); });
result.map(|_| vec.into()) result.map(|()| vec.into())
}
}
} }
/// Deduplicates all items in the array. /// Deduplicates all items in the array.

View File

@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::iter::{self, Sum}; use std::iter::{self, Sum};
use std::marker::PhantomData; use std::marker::PhantomData;
use std::ops::{Add, AddAssign, Deref, DerefMut}; use std::ops::{Add, AddAssign, ControlFlow, Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
use comemo::Tracked; use comemo::Tracked;
@ -414,10 +414,11 @@ impl Content {
/// Elements produced in `show` rules will not be included in the results. /// Elements produced in `show` rules will not be included in the results.
pub fn query(&self, selector: Selector) -> Vec<Content> { pub fn query(&self, selector: Selector) -> Vec<Content> {
let mut results = Vec::new(); let mut results = Vec::new();
self.traverse(&mut |element| { self.traverse(&mut |element| -> ControlFlow<()> {
if selector.matches(&element, None) { if selector.matches(&element, None) {
results.push(element); results.push(element);
} }
ControlFlow::Continue(())
}); });
results results
} }
@ -427,54 +428,58 @@ impl Content {
/// ///
/// Elements produced in `show` rules will not be included in the results. /// Elements produced in `show` rules will not be included in the results.
pub fn query_first(&self, selector: &Selector) -> Option<Content> { pub fn query_first(&self, selector: &Selector) -> Option<Content> {
let mut result = None; self.traverse(&mut |element| -> ControlFlow<Content> {
self.traverse(&mut |element| { if selector.matches(&element, None) {
if result.is_none() && selector.matches(&element, None) { ControlFlow::Break(element)
result = Some(element); } else {
ControlFlow::Continue(())
} }
}); })
result .break_value()
} }
/// Extracts the plain text of this content. /// Extracts the plain text of this content.
pub fn plain_text(&self) -> EcoString { pub fn plain_text(&self) -> EcoString {
let mut text = EcoString::new(); let mut text = EcoString::new();
self.traverse(&mut |element| { self.traverse(&mut |element| -> ControlFlow<()> {
if let Some(textable) = element.with::<dyn PlainText>() { if let Some(textable) = element.with::<dyn PlainText>() {
textable.plain_text(&mut text); textable.plain_text(&mut text);
} }
ControlFlow::Continue(())
}); });
text text
} }
/// Traverse this content. /// Traverse this content.
fn traverse<F>(&self, f: &mut F) fn traverse<F, B>(&self, f: &mut F) -> ControlFlow<B>
where where
F: FnMut(Content), F: FnMut(Content) -> ControlFlow<B>,
{ {
f(self.clone());
self.inner
.elem
.fields()
.into_iter()
.for_each(|(_, value)| walk_value(value, f));
/// Walks a given value to find any content that matches the selector. /// Walks a given value to find any content that matches the selector.
fn walk_value<F>(value: Value, f: &mut F) ///
/// Returns early if the function gives `ControlFlow::Break`.
fn walk_value<F, B>(value: Value, f: &mut F) -> ControlFlow<B>
where where
F: FnMut(Content), F: FnMut(Content) -> ControlFlow<B>,
{ {
match value { match value {
Value::Content(content) => content.traverse(f), Value::Content(content) => content.traverse(f),
Value::Array(array) => { Value::Array(array) => {
for value in array { for value in array {
walk_value(value, f); walk_value(value, f)?;
}
ControlFlow::Continue(())
}
_ => ControlFlow::Continue(()),
} }
} }
_ => {}
} // Call f on the element itself before recursively iterating its fields.
f(self.clone())?;
for (_, value) in self.inner.elem.fields() {
walk_value(value, f)?;
} }
ControlFlow::Continue(())
} }
} }

View File

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

View File

@ -112,7 +112,7 @@ use crate::foundations::{
/// it into another file by writing `{import "foo.typ": alert}`. /// it into another file by writing `{import "foo.typ": alert}`.
/// ///
/// # Unnamed functions { #unnamed } /// # Unnamed functions { #unnamed }
/// You can also created an unnamed function without creating a binding by /// You can also create an unnamed function without creating a binding by
/// specifying a parameter list followed by `=>` and the function body. If your /// specifying a parameter list followed by `=>` and the function body. If your
/// function has just one parameter, the parentheses around the parameter list /// function has just one parameter, the parentheses around the parameter list
/// are optional. Unnamed functions are mainly useful for show rules, but also /// are optional. Unnamed functions are mainly useful for show rules, but also
@ -437,10 +437,10 @@ impl PartialEq for Func {
} }
} }
impl PartialEq<&NativeFuncData> for Func { impl PartialEq<&'static NativeFuncData> for Func {
fn eq(&self, other: &&NativeFuncData) -> bool { fn eq(&self, other: &&'static NativeFuncData) -> bool {
match &self.repr { match &self.repr {
Repr::Native(native) => native.function == other.function, Repr::Native(native) => *native == Static(*other),
_ => false, _ => false,
} }
} }

View File

@ -77,6 +77,7 @@ pub use {
indexmap::IndexMap, indexmap::IndexMap,
}; };
use comemo::TrackedMut;
use ecow::EcoString; use ecow::EcoString;
use typst_syntax::Spanned; use typst_syntax::Spanned;
@ -297,5 +298,14 @@ pub fn eval(
for (key, value) in dict { for (key, value) in dict {
scope.bind(key.into(), Binding::new(value, span)); scope.bind(key.into(), Binding::new(value, span));
} }
(engine.routines.eval_string)(engine.routines, engine.world, &text, span, mode, scope)
(engine.routines.eval_string)(
engine.routines,
engine.world,
TrackedMut::reborrow_mut(&mut engine.sink),
&text,
span,
mode,
scope,
)
} }

View File

@ -7,7 +7,7 @@ use typst_syntax::FileId;
use crate::diag::{bail, DeprecationSink, StrResult}; use crate::diag::{bail, DeprecationSink, StrResult};
use crate::foundations::{repr, ty, Content, Scope, Value}; use crate::foundations::{repr, ty, Content, Scope, Value};
/// An module of definitions. /// A module of definitions.
/// ///
/// A module /// A module
/// - be built-in /// - be built-in

View File

@ -148,9 +148,7 @@ use crate::loading::{DataSource, Load};
#[func(scope)] #[func(scope)]
pub fn plugin( pub fn plugin(
engine: &mut Engine, engine: &mut Engine,
/// A path to a WebAssembly file or raw WebAssembly bytes. /// A [path]($syntax/#paths) to a WebAssembly file or raw WebAssembly bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Module> { ) -> SourceResult<Module> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -7,12 +7,13 @@ use comemo::Tracked;
use ecow::EcoString; use ecow::EcoString;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned};
use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, dict, func, repr, scope, ty, Array, Bytes, Context, Decimal, Dict, Func, cast, dict, func, repr, scope, ty, Array, Bytes, Cast, Context, Decimal, Dict, Func,
IntoValue, Label, Repr, Type, Value, Version, IntoValue, Label, Repr, Type, Value, Version,
}; };
use crate::layout::Alignment; use crate::layout::Alignment;
@ -286,6 +287,30 @@ impl Str {
Ok(c.into()) Ok(c.into())
} }
/// Normalizes the string to the given Unicode normal form.
///
/// This is useful when manipulating strings containing Unicode combining
/// characters.
///
/// ```typ
/// #assert.eq("é".normalize(form: "nfd"), "e\u{0301}")
/// #assert.eq("ſ́".normalize(form: "nfkc"), "ś")
/// ```
#[func]
pub fn normalize(
&self,
#[named]
#[default(UnicodeNormalForm::Nfc)]
form: UnicodeNormalForm,
) -> Str {
match form {
UnicodeNormalForm::Nfc => self.nfc().collect(),
UnicodeNormalForm::Nfd => self.nfd().collect(),
UnicodeNormalForm::Nfkc => self.nfkc().collect(),
UnicodeNormalForm::Nfkd => self.nfkd().collect(),
}
}
/// Whether the string contains the specified pattern. /// Whether the string contains the specified pattern.
/// ///
/// This method also has dedicated syntax: You can write `{"bc" in "abcd"}` /// This method also has dedicated syntax: You can write `{"bc" in "abcd"}`
@ -788,6 +813,25 @@ cast! {
v: Str => Self::Str(v), v: Str => Self::Str(v),
} }
/// A Unicode normalization form.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum UnicodeNormalForm {
/// Canonical composition where e.g. accented letters are turned into a
/// single Unicode codepoint.
#[string("nfc")]
Nfc,
/// Canonical decomposition where e.g. accented letters are split into a
/// separate base and diacritic.
#[string("nfd")]
Nfd,
/// Like NFC, but using the Unicode compatibility decompositions.
#[string("nfkc")]
Nfkc,
/// Like NFD, but using the Unicode compatibility decompositions.
#[string("nfkd")]
Nfkd,
}
/// Convert an item of std's `match_indices` to a dictionary. /// Convert an item of std's `match_indices` to a dictionary.
fn match_to_dict((start, text): (usize, &str)) -> Dict { fn match_to_dict((start, text): (usize, &str)) -> Dict {
dict! { dict! {

View File

@ -471,7 +471,8 @@ impl Debug for Recipe {
selector.fmt(f)?; selector.fmt(f)?;
f.write_str(", ")?; f.write_str(", ")?;
} }
self.transform.fmt(f) self.transform.fmt(f)?;
f.write_str(")")
} }
} }

View File

@ -21,6 +21,7 @@ use crate::foundations::{
/// be accessed using [field access notation]($scripting/#fields): /// be accessed using [field access notation]($scripting/#fields):
/// ///
/// - General symbols are defined in the [`sym` module]($category/symbols/sym) /// - General symbols are defined in the [`sym` module]($category/symbols/sym)
/// and are accessible without the `sym.` prefix in math mode.
/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) /// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
/// ///
/// Moreover, you can define custom symbols with this type's constructor /// Moreover, you can define custom symbols with this type's constructor
@ -410,7 +411,7 @@ fn find<'a>(
} }
let score = (matching, Reverse(total)); let score = (matching, Reverse(total));
if best_score.map_or(true, |b| score > b) { if best_score.is_none_or(|b| score > b) {
best = Some(candidate.1); best = Some(candidate.1);
best_score = Some(score); best_score = Some(score);
} }

View File

@ -39,11 +39,25 @@ use crate::foundations::{
/// #type(image("glacier.jpg")). /// #type(image("glacier.jpg")).
/// ``` /// ```
/// ///
/// The type of `10` is `int`. Now, what is the type of `int` or even `type`? /// The type of `{10}` is `int`. Now, what is the type of `int` or even `type`?
/// ```example /// ```example
/// #type(int) \ /// #type(int) \
/// #type(type) /// #type(type)
/// ``` /// ```
///
/// Unlike other types like `int`, [none] and [auto] do not have a name
/// representing them. To test if a value is one of these, compare your value to
/// them directly, e.g:
/// ```example
/// #let val = none
/// #if val == none [
/// Yep, it's none.
/// ]
/// ```
///
/// Note that `type` will return [`content`] for all document elements. To
/// programmatically determine which kind of content you are dealing with, see
/// [`content.func`].
#[ty(scope, cast)] #[ty(scope, cast)]
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct Type(Static<NativeTypeData>); pub struct Type(Static<NativeTypeData>);

View File

@ -229,10 +229,10 @@ impl Counter {
if self.is_page() { if self.is_page() {
let at_delta = let at_delta =
engine.introspector.page(location).get().saturating_sub(at_page.get()); engine.introspector.page(location).get().saturating_sub(at_page.get());
at_state.step(NonZeroUsize::ONE, at_delta); at_state.step(NonZeroUsize::ONE, at_delta as u64);
let final_delta = let final_delta =
engine.introspector.pages().get().saturating_sub(final_page.get()); engine.introspector.pages().get().saturating_sub(final_page.get());
final_state.step(NonZeroUsize::ONE, final_delta); final_state.step(NonZeroUsize::ONE, final_delta as u64);
} }
Ok(CounterState(smallvec![at_state.first(), final_state.first()])) Ok(CounterState(smallvec![at_state.first(), final_state.first()]))
} }
@ -250,7 +250,7 @@ impl Counter {
if self.is_page() { if self.is_page() {
let delta = let delta =
engine.introspector.page(location).get().saturating_sub(page.get()); engine.introspector.page(location).get().saturating_sub(page.get());
state.step(NonZeroUsize::ONE, delta); state.step(NonZeroUsize::ONE, delta as u64);
} }
Ok(state) Ok(state)
} }
@ -319,7 +319,7 @@ impl Counter {
let delta = page.get() - prev.get(); let delta = page.get() - prev.get();
if delta > 0 { if delta > 0 {
state.step(NonZeroUsize::ONE, delta); state.step(NonZeroUsize::ONE, delta as u64);
} }
} }
@ -500,7 +500,7 @@ impl Counter {
let (mut state, page) = sequence.last().unwrap().clone(); let (mut state, page) = sequence.last().unwrap().clone();
if self.is_page() { if self.is_page() {
let delta = engine.introspector.pages().get().saturating_sub(page.get()); let delta = engine.introspector.pages().get().saturating_sub(page.get());
state.step(NonZeroUsize::ONE, delta); state.step(NonZeroUsize::ONE, delta as u64);
} }
Ok(state) Ok(state)
} }
@ -616,13 +616,13 @@ pub trait Count {
/// Counts through elements with different levels. /// Counts through elements with different levels.
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub struct CounterState(pub SmallVec<[usize; 3]>); pub struct CounterState(pub SmallVec<[u64; 3]>);
impl CounterState { impl CounterState {
/// Get the initial counter state for the key. /// Get the initial counter state for the key.
pub fn init(page: bool) -> Self { pub fn init(page: bool) -> Self {
// Special case, because pages always start at one. // Special case, because pages always start at one.
Self(smallvec![usize::from(page)]) Self(smallvec![u64::from(page)])
} }
/// Advance the counter and return the numbers for the given heading. /// Advance the counter and return the numbers for the given heading.
@ -645,7 +645,7 @@ impl CounterState {
} }
/// Advance the number of the given level by the specified amount. /// Advance the number of the given level by the specified amount.
pub fn step(&mut self, level: NonZeroUsize, by: usize) { pub fn step(&mut self, level: NonZeroUsize, by: u64) {
let level = level.get(); let level = level.get();
while self.0.len() < level { while self.0.len() < level {
@ -657,7 +657,7 @@ impl CounterState {
} }
/// Get the first number of the state. /// Get the first number of the state.
pub fn first(&self) -> usize { pub fn first(&self) -> u64 {
self.0.first().copied().unwrap_or(1) self.0.first().copied().unwrap_or(1)
} }
@ -675,7 +675,7 @@ impl CounterState {
cast! { cast! {
CounterState, CounterState,
self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()), self => Value::Array(self.0.into_iter().map(IntoValue::into_value).collect()),
num: usize => Self(smallvec![num]), num: u64 => Self(smallvec![num]),
array: Array => Self(array array: Array => Self(array
.into_iter() .into_iter()
.map(Value::cast) .map(Value::cast)
@ -758,7 +758,7 @@ impl Show for Packed<CounterDisplayElem> {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct ManualPageCounter { pub struct ManualPageCounter {
physical: NonZeroUsize, physical: NonZeroUsize,
logical: usize, logical: u64,
} }
impl ManualPageCounter { impl ManualPageCounter {
@ -773,7 +773,7 @@ impl ManualPageCounter {
} }
/// Get the current logical page counter state. /// Get the current logical page counter state.
pub fn logical(&self) -> usize { pub fn logical(&self) -> u64 {
self.logical self.logical
} }

View File

@ -10,7 +10,7 @@ use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector}; use crate::foundations::{Content, Label, Repr, Selector};
use crate::html::{HtmlElement, HtmlNode}; use crate::html::HtmlNode;
use crate::introspection::{Location, Tag}; use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering; use crate::model::Numbering;
@ -55,8 +55,8 @@ impl Introspector {
/// Creates an introspector for HTML. /// Creates an introspector for HTML.
#[typst_macros::time(name = "introspect html")] #[typst_macros::time(name = "introspect html")]
pub fn html(root: &HtmlElement) -> Self { pub fn html(output: &[HtmlNode]) -> Self {
IntrospectorBuilder::new().build_html(root) IntrospectorBuilder::new().build_html(output)
} }
/// Iterates over all locatable elements. /// Iterates over all locatable elements.
@ -392,9 +392,9 @@ impl IntrospectorBuilder {
} }
/// Build an introspector for an HTML document. /// Build an introspector for an HTML document.
fn build_html(mut self, root: &HtmlElement) -> Introspector { fn build_html(mut self, output: &[HtmlNode]) -> Introspector {
let mut elems = Vec::new(); let mut elems = Vec::new();
self.discover_in_html(&mut elems, root); self.discover_in_html(&mut elems, output);
self.finalize(elems) self.finalize(elems)
} }
@ -434,16 +434,16 @@ impl IntrospectorBuilder {
} }
/// Processes the tags in the HTML element. /// Processes the tags in the HTML element.
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, elem: &HtmlElement) { fn discover_in_html(&mut self, sink: &mut Vec<Pair>, nodes: &[HtmlNode]) {
for child in &elem.children { for node in nodes {
match child { match node {
HtmlNode::Tag(tag) => self.discover_in_tag( HtmlNode::Tag(tag) => self.discover_in_tag(
sink, sink,
tag, tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() }, Position { page: NonZeroUsize::ONE, point: Point::zero() },
), ),
HtmlNode::Text(_, _) => {} HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => self.discover_in_html(sink, elem), HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
HtmlNode::Frame(frame) => self.discover_in_frame( HtmlNode::Frame(frame) => self.discover_in_frame(
sink, sink,
frame, frame,

View File

@ -50,6 +50,42 @@ impl Dir {
pub const TTB: Self = Self::TTB; pub const TTB: Self = Self::TTB;
pub const BTT: Self = Self::BTT; pub const BTT: Self = Self::BTT;
/// Returns a direction from a starting point.
///
/// ```example
/// direction.from(left) \
/// direction.from(right) \
/// direction.from(top) \
/// direction.from(bottom)
/// ```
#[func]
pub const fn from(side: Side) -> Dir {
match side {
Side::Left => Self::LTR,
Side::Right => Self::RTL,
Side::Top => Self::TTB,
Side::Bottom => Self::BTT,
}
}
/// Returns a direction from an end point.
///
/// ```example
/// direction.to(left) \
/// direction.to(right) \
/// direction.to(top) \
/// direction.to(bottom)
/// ```
#[func]
pub const fn to(side: Side) -> Dir {
match side {
Side::Right => Self::LTR,
Side::Left => Self::RTL,
Side::Bottom => Self::TTB,
Side::Top => Self::BTT,
}
}
/// The axis this direction belongs to, either `{"horizontal"}` or /// The axis this direction belongs to, either `{"horizontal"}` or
/// `{"vertical"}`. /// `{"vertical"}`.
/// ///
@ -65,6 +101,22 @@ impl Dir {
} }
} }
/// The corresponding sign, for use in calculations.
///
/// ```example
/// #ltr.sign() \
/// #rtl.sign() \
/// #ttb.sign() \
/// #btt.sign()
/// ```
#[func]
pub const fn sign(self) -> i64 {
match self {
Self::LTR | Self::TTB => 1,
Self::RTL | Self::BTT => -1,
}
}
/// The start point of this direction, as an alignment. /// The start point of this direction, as an alignment.
/// ///
/// ```example /// ```example

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,8 @@ use crate::layout::{BlockElem, Size};
/// #let text = lorem(30) /// #let text = lorem(30)
/// #layout(size => [ /// #layout(size => [
/// #let (height,) = measure( /// #let (height,) = measure(
/// block(width: size.width, text), /// width: size.width,
/// text,
/// ) /// )
/// This text is #height high with /// This text is #height high with
/// the current page width: \ /// the current page width: \

View File

@ -75,9 +75,10 @@ pub struct PageElem {
/// The height of the page. /// The height of the page.
/// ///
/// If this is set to `{auto}`, page breaks can only be triggered manually /// If this is set to `{auto}`, page breaks can only be triggered manually
/// by inserting a [page break]($pagebreak). Most examples throughout this /// by inserting a [page break]($pagebreak) or by adding another non-empty
/// documentation use `{auto}` for the height of the page to dynamically /// page set rule. Most examples throughout this documentation use `{auto}`
/// grow and shrink to fit their content. /// for the height of the page to dynamically grow and shrink to fit their
/// content.
#[resolve] #[resolve]
#[parse( #[parse(
args.named("height")? args.named("height")?
@ -483,7 +484,7 @@ pub struct Page {
pub supplement: Content, pub supplement: Content,
/// The logical page number (controlled by `counter(page)` and may thus not /// The logical page number (controlled by `counter(page)` and may thus not
/// match the physical number). /// match the physical number).
pub number: usize, pub number: u64,
} }
impl Page { impl Page {

View File

@ -8,15 +8,35 @@ use crate::foundations::{repr, ty, Repr};
/// A ratio of a whole. /// A ratio of a whole.
/// ///
/// Written as a number, followed by a percent sign. /// A ratio is written as a number, followed by a percent sign. Ratios most
/// often appear as part of a [relative length]($relative), to specify the size
/// of some layout element relative to the page or some container.
/// ///
/// # Example
/// ```example /// ```example
/// #set align(center) /// #rect(width: 25%)
/// #scale(x: 150%)[
/// Scaled apart.
/// ]
/// ``` /// ```
///
/// However, they can also describe any other property that is relative to some
/// base, e.g. an amount of [horizontal scaling]($scale.x) or the
/// [height of parentheses]($math.lr.size) relative to the height of the content
/// they enclose.
///
/// # Scripting
/// Within your own code, you can use ratios as you like. You can multiply them
/// with various other types as shown below:
///
/// | Multiply by | Example | Result |
/// |-----------------|-------------------------|-----------------|
/// | [`ratio`] | `{27% * 10%}` | `{2.7%}` |
/// | [`length`] | `{27% * 100pt}` | `{27pt}` |
/// | [`relative`] | `{27% * (10% + 100pt)}` | `{2.7% + 27pt}` |
/// | [`angle`] | `{27% * 100deg}` | `{27deg}` |
/// | [`int`] | `{27% * 2}` | `{54%}` |
/// | [`float`] | `{27% * 0.37037}` | `{10%}` |
/// | [`fraction`] | `{27% * 3fr}` | `{0.81fr}` |
///
/// When ratios are displayed in the document, they are rounded to two
/// significant digits for readability.
#[ty(cast)] #[ty(cast)]
#[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] #[derive(Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Ratio(Scalar); pub struct Ratio(Scalar);

View File

@ -14,17 +14,58 @@ use crate::layout::{Abs, Em, Length, Ratio};
/// addition and subtraction of a length and a ratio. Wherever a relative length /// addition and subtraction of a length and a ratio. Wherever a relative length
/// is expected, you can also use a bare length or ratio. /// is expected, you can also use a bare length or ratio.
/// ///
/// # Example /// # Relative to the page
/// ```example /// A common use case is setting the width or height of a layout element (e.g.,
/// #rect(width: 100% - 50pt) /// [block], [rect], etc.) as a certain percentage of the width of the page.
/// Here, the rectangle's width is set to `{25%}`, so it takes up one fourth of
/// the page's _inner_ width (the width minus margins).
/// ///
/// #(100% - 50pt).length \ /// ```example
/// #(100% - 50pt).ratio /// #rect(width: 25%)
/// ``` /// ```
/// ///
/// Bare lengths or ratios are always valid where relative lengths are expected,
/// but the two can also be freely mixed:
/// ```example
/// #rect(width: 25% + 1cm)
/// ```
///
/// If you're trying to size an element so that it takes up the page's _full_
/// width, you have a few options (this highly depends on your exact use case):
///
/// 1. Set page margins to `{0pt}` (`[#set page(margin: 0pt)]`)
/// 2. Multiply the ratio by the known full page width (`{21cm * 69%}`)
/// 3. Use padding which will negate the margins (`[#pad(x: -2.5cm, ...)]`)
/// 4. Use the page [background](page.background) or
/// [foreground](page.foreground) field as those don't take margins into
/// account (note that it will render the content outside of the document
/// flow, see [place] to control the content position)
///
/// # Relative to a container
/// When a layout element (e.g. a [rect]) is nested in another layout container
/// (e.g. a [block]) instead of being a direct descendant of the page, relative
/// widths become relative to the container:
///
/// ```example
/// #block(
/// width: 100pt,
/// fill: aqua,
/// rect(width: 50%),
/// )
/// ```
///
/// # Scripting
/// You can multiply relative lengths by [ratios]($ratio), [integers]($int), and
/// [floats]($float).
///
/// A relative length has the following fields: /// A relative length has the following fields:
/// - `length`: Its length component. /// - `length`: Its length component.
/// - `ratio`: Its ratio component. /// - `ratio`: Its ratio component.
///
/// ```example
/// #(100% - 50pt).length \
/// #(100% - 50pt).ratio
/// ```
#[ty(cast, name = "relative", title = "Relative Length")] #[ty(cast, name = "relative", title = "Relative Length")]
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Rel<T: Numeric = Length> { pub struct Rel<T: Numeric = Length> {

View File

@ -307,6 +307,20 @@ impl Transform {
Self { sx, sy, ..Self::identity() } Self { sx, sy, ..Self::identity() }
} }
/// A scale transform at a specific position.
pub fn scale_at(sx: Ratio, sy: Ratio, px: Abs, py: Abs) -> Self {
Self::translate(px, py)
.pre_concat(Self::scale(sx, sy))
.pre_concat(Self::translate(-px, -py))
}
/// A rotate transform at a specific position.
pub fn rotate_at(angle: Angle, px: Abs, py: Abs) -> Self {
Self::translate(px, py)
.pre_concat(Self::rotate(angle))
.pre_concat(Self::translate(-px, -py))
}
/// A rotate transform. /// A rotate transform.
pub fn rotate(angle: Angle) -> Self { pub fn rotate(angle: Angle) -> Self {
let cos = Ratio::new(angle.cos()); let cos = Ratio::new(angle.cos());

View File

@ -20,9 +20,7 @@ use crate::loading::{DataSource, Load};
#[func(scope, title = "CBOR")] #[func(scope, title = "CBOR")]
pub fn cbor( pub fn cbor(
engine: &mut Engine, engine: &mut Engine,
/// A path to a CBOR file or raw CBOR bytes. /// A [path]($syntax/#paths) to a CBOR file or raw CBOR bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -26,9 +26,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "CSV")] #[func(scope, title = "CSV")]
pub fn csv( pub fn csv(
engine: &mut Engine, engine: &mut Engine,
/// Path to a CSV file or raw CSV bytes. /// A [path]($syntax/#paths) to a CSV file or raw CSV bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
/// The delimiter that separates columns in the CSV file. /// The delimiter that separates columns in the CSV file.
/// Must be a single ASCII character. /// Must be a single ASCII character.

View File

@ -51,9 +51,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "JSON")] #[func(scope, title = "JSON")]
pub fn json( pub fn json(
engine: &mut Engine, engine: &mut Engine,
/// Path to a JSON file or raw JSON bytes. /// A [path]($syntax/#paths) to a JSON file or raw JSON bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -29,9 +29,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "TOML")] #[func(scope, title = "TOML")]
pub fn toml( pub fn toml(
engine: &mut Engine, engine: &mut Engine,
/// A path to a TOML file or raw TOML bytes. /// A [path]($syntax/#paths) to a TOML file or raw TOML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -34,14 +34,14 @@ use crate::loading::{DataSource, Load, Readable};
/// let author = find-child(elem, "author") /// let author = find-child(elem, "author")
/// let pars = find-child(elem, "content") /// let pars = find-child(elem, "content")
/// ///
/// heading(title.children.first()) /// [= #title.children.first()]
/// text(10pt, weight: "medium")[ /// text(10pt, weight: "medium")[
/// Published by /// Published by
/// #author.children.first() /// #author.children.first()
/// ] /// ]
/// ///
/// for p in pars.children { /// for p in pars.children {
/// if (type(p) == "dictionary") { /// if type(p) == dictionary {
/// parbreak() /// parbreak()
/// p.children.first() /// p.children.first()
/// } /// }
@ -50,7 +50,7 @@ use crate::loading::{DataSource, Load, Readable};
/// ///
/// #let data = xml("example.xml") /// #let data = xml("example.xml")
/// #for elem in data.first().children { /// #for elem in data.first().children {
/// if (type(elem) == "dictionary") { /// if type(elem) == dictionary {
/// article(elem) /// article(elem)
/// } /// }
/// } /// }
@ -58,9 +58,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "XML")] #[func(scope, title = "XML")]
pub fn xml( pub fn xml(
engine: &mut Engine, engine: &mut Engine,
/// A path to an XML file or raw XML bytes. /// A [path]($syntax/#paths) to an XML file or raw XML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -41,9 +41,7 @@ use crate::loading::{DataSource, Load, Readable};
#[func(scope, title = "YAML")] #[func(scope, title = "YAML")]
pub fn yaml( pub fn yaml(
engine: &mut Engine, engine: &mut Engine,
/// A path to a YAML file or raw YAML bytes. /// A [path]($syntax/#paths) to a YAML file or raw YAML bytes.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
source: Spanned<DataSource>, source: Spanned<DataSource>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let data = source.load(engine.world)?; let data = source.load(engine.world)?;

View File

@ -13,8 +13,8 @@ use crate::math::Mathy;
/// ``` /// ```
#[elem(Mathy)] #[elem(Mathy)]
pub struct AccentElem { pub struct AccentElem {
/// The base to which the accent is applied. /// The base to which the accent is applied. May consist of multiple
/// May consist of multiple letters. /// letters.
/// ///
/// ```example /// ```example
/// $arrow(A B C)$ /// $arrow(A B C)$
@ -51,9 +51,24 @@ pub struct AccentElem {
pub accent: Accent, pub accent: Accent,
/// The size of the accent, relative to the width of the base. /// The size of the accent, relative to the width of the base.
///
/// ```example
/// $dash(A, size: #150%)$
/// ```
#[resolve] #[resolve]
#[default(Rel::one())] #[default(Rel::one())]
pub size: Rel<Length>, pub size: Rel<Length>,
/// Whether to remove the dot on top of lowercase i and j when adding a top
/// accent.
///
/// This enables the `dtls` OpenType feature.
///
/// ```example
/// $hat(dotless: #false, i)$
/// ```
#[default(true)]
pub dotless: bool,
} }
/// An accent character. /// An accent character.
@ -103,11 +118,18 @@ macro_rules! accents {
/// The size of the accent, relative to the width of the base. /// The size of the accent, relative to the width of the base.
#[named] #[named]
size: Option<Rel<Length>>, size: Option<Rel<Length>>,
/// Whether to remove the dot on top of lowercase i and j when
/// adding a top accent.
#[named]
dotless: Option<bool>,
) -> Content { ) -> Content {
let mut accent = AccentElem::new(base, Accent::new($primary)); let mut accent = AccentElem::new(base, Accent::new($primary));
if let Some(size) = size { if let Some(size) = size {
accent = accent.with_size(size); accent = accent.with_size(size);
} }
if let Some(dotless) = dotless {
accent = accent.with_dotless(dotless);
}
accent.pack() accent.pack()
} }
)+ )+

View File

@ -15,7 +15,7 @@ use crate::math::Mathy;
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: Use a slash to turn neighbouring /// This function also has dedicated syntax: Use a slash to turn neighbouring
/// expressions into a fraction. Multiple atoms can be grouped into a single /// expressions into a fraction. Multiple atoms can be grouped into a single
/// expression using round grouping parenthesis. Such parentheses are removed /// expression using round grouping parentheses. Such parentheses are removed
/// from the output, but you can nest multiple to force them. /// from the output, but you can nest multiple to force them.
#[elem(title = "Fraction", Mathy)] #[elem(title = "Fraction", Mathy)]
pub struct FracElem { pub struct FracElem {

View File

@ -6,7 +6,7 @@ use std::num::NonZeroUsize;
use std::path::Path; use std::path::Path;
use std::sync::{Arc, LazyLock}; use std::sync::{Arc, LazyLock};
use comemo::Tracked; use comemo::{Track, Tracked};
use ecow::{eco_format, EcoString, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
use hayagriva::archive::ArchivedStyle; use hayagriva::archive::ArchivedStyle;
use hayagriva::io::BibLaTeXError; use hayagriva::io::BibLaTeXError;
@ -20,7 +20,7 @@ use typst_syntax::{Span, Spanned};
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::{Engine, Sink};
use crate::foundations::{ use crate::foundations::{
elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement,
OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles,
@ -94,7 +94,7 @@ pub struct BibliographyElem {
/// - A path string to load a bibliography file from the given path. For /// - A path string to load a bibliography file from the given path. For
/// more details about paths, see the [Paths section]($syntax/#paths). /// more details about paths, see the [Paths section]($syntax/#paths).
/// - Raw bytes from which the bibliography should be decoded. /// - Raw bytes from which the bibliography should be decoded.
/// - An array where each item is one the above. /// - An array where each item is one of the above.
#[required] #[required]
#[parse( #[parse(
let sources = args.expect("sources")?; let sources = args.expect("sources")?;
@ -999,6 +999,8 @@ impl ElemRenderer<'_> {
(self.routines.eval_string)( (self.routines.eval_string)(
self.routines, self.routines,
self.world, self.world,
// TODO: propagate warnings
Sink::new().track_mut(),
math, math,
self.span, self.span,
EvalMode::Math, EvalMode::Math,

View File

@ -129,7 +129,7 @@ pub struct EnumElem {
/// [Ahead], /// [Ahead],
/// ) /// )
/// ``` /// ```
pub start: Smart<usize>, pub start: Smart<u64>,
/// Whether to display the full numbering, including the numbers of /// Whether to display the full numbering, including the numbers of
/// all parent enumerations. /// all parent enumerations.
@ -217,7 +217,7 @@ pub struct EnumElem {
#[internal] #[internal]
#[fold] #[fold]
#[ghost] #[ghost]
pub parents: SmallVec<[usize; 4]>, pub parents: SmallVec<[u64; 4]>,
} }
#[scope] #[scope]
@ -259,10 +259,11 @@ impl Show for Packed<EnumElem> {
.spanned(self.span()); .spanned(self.span());
if tight { if tight {
let leading = ParElem::leading_in(styles); let spacing = self
let spacing = .spacing(styles)
VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); .unwrap_or_else(|| ParElem::leading_in(styles).into());
realized = spacing + realized; let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
} }
Ok(realized) Ok(realized)
@ -274,7 +275,7 @@ impl Show for Packed<EnumElem> {
pub struct EnumItem { pub struct EnumItem {
/// The item's number. /// The item's number.
#[positional] #[positional]
pub number: Option<usize>, pub number: Option<u64>,
/// The item's body. /// The item's body.
#[required] #[required]

View File

@ -457,7 +457,7 @@ impl Outlinable for Packed<FigureElem> {
/// customize the appearance of captions for all figures or figures of a /// customize the appearance of captions for all figures or figures of a
/// specific kind. /// specific kind.
/// ///
/// In addition to its `pos` and `body`, the `caption` also provides the /// In addition to its `position` and `body`, the `caption` also provides the
/// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These /// figure's `kind`, `supplement`, `counter`, and `numbering` as fields. These
/// parts can be used in [`where`]($function.where) selectors and show rules to /// parts can be used in [`where`]($function.where) selectors and show rules to
/// build a completely custom caption. /// build a completely custom caption.

View File

@ -11,7 +11,7 @@ use crate::foundations::{
use crate::html::{attr, tag, HtmlElem}; use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location; use crate::introspection::Location;
use crate::layout::Position; use crate::layout::Position;
use crate::text::{Hyphenate, TextElem}; use crate::text::TextElem;
/// Links to a URL or a location in the document. /// Links to a URL or a location in the document.
/// ///
@ -138,7 +138,7 @@ impl Show for Packed<LinkElem> {
impl ShowSet for Packed<LinkElem> { impl ShowSet for Packed<LinkElem> {
fn show_set(&self, _: StyleChain) -> Styles { fn show_set(&self, _: StyleChain) -> Styles {
let mut out = Styles::new(); let mut out = Styles::new();
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out out
} }
} }

View File

@ -166,10 +166,11 @@ impl Show for Packed<ListElem> {
.spanned(self.span()); .spanned(self.span());
if tight { if tight {
let leading = ParElem::leading_in(styles); let spacing = self
let spacing = .spacing(styles)
VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); .unwrap_or_else(|| ParElem::leading_in(styles).into());
realized = spacing + realized; let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack();
realized = v + realized;
} }
Ok(realized) Ok(realized)

View File

@ -1,7 +1,7 @@
use std::str::FromStr; use std::str::FromStr;
use chinese_number::{ use chinese_number::{
from_usize_to_chinese_ten_thousand as usize_to_chinese, ChineseCase, ChineseVariant, from_u64_to_chinese_ten_thousand as u64_to_chinese, ChineseCase, ChineseVariant,
}; };
use comemo::Tracked; use comemo::Tracked;
use ecow::{eco_format, EcoString, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
@ -85,7 +85,7 @@ pub fn numbering(
/// If `numbering` is a pattern and more numbers than counting symbols are /// If `numbering` is a pattern and more numbers than counting symbols are
/// given, the last counting symbol with its prefix is repeated. /// given, the last counting symbol with its prefix is repeated.
#[variadic] #[variadic]
numbers: Vec<usize>, numbers: Vec<u64>,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
numbering.apply(engine, context, &numbers) numbering.apply(engine, context, &numbers)
} }
@ -105,7 +105,7 @@ impl Numbering {
&self, &self,
engine: &mut Engine, engine: &mut Engine,
context: Tracked<Context>, context: Tracked<Context>,
numbers: &[usize], numbers: &[u64],
) -> SourceResult<Value> { ) -> SourceResult<Value> {
Ok(match self { Ok(match self {
Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()), Self::Pattern(pattern) => Value::Str(pattern.apply(numbers).into()),
@ -156,7 +156,7 @@ pub struct NumberingPattern {
impl NumberingPattern { impl NumberingPattern {
/// Apply the pattern to the given number. /// Apply the pattern to the given number.
pub fn apply(&self, numbers: &[usize]) -> EcoString { pub fn apply(&self, numbers: &[u64]) -> EcoString {
let mut fmt = EcoString::new(); let mut fmt = EcoString::new();
let mut numbers = numbers.iter(); let mut numbers = numbers.iter();
@ -185,7 +185,7 @@ impl NumberingPattern {
} }
/// Apply only the k-th segment of the pattern to a number. /// Apply only the k-th segment of the pattern to a number.
pub fn apply_kth(&self, k: usize, number: usize) -> EcoString { pub fn apply_kth(&self, k: usize, number: u64) -> EcoString {
let mut fmt = EcoString::new(); let mut fmt = EcoString::new();
if let Some((prefix, _)) = self.pieces.first() { if let Some((prefix, _)) = self.pieces.first() {
fmt.push_str(prefix); fmt.push_str(prefix);
@ -379,7 +379,7 @@ impl NumberingKind {
} }
/// Apply the numbering to the given number. /// Apply the numbering to the given number.
pub fn apply(self, n: usize) -> EcoString { pub fn apply(self, n: u64) -> EcoString {
match self { match self {
Self::Arabic => eco_format!("{n}"), Self::Arabic => eco_format!("{n}"),
Self::LowerRoman => roman_numeral(n, Case::Lower), Self::LowerRoman => roman_numeral(n, Case::Lower),
@ -392,9 +392,10 @@ impl NumberingKind {
} }
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; let n_symbols = SYMBOLS.len() as u64;
let amount = ((n - 1) / SYMBOLS.len()) + 1; let symbol = SYMBOLS[((n - 1) % n_symbols) as usize];
std::iter::repeat(symbol).take(amount).collect() let amount = ((n - 1) / n_symbols) + 1;
std::iter::repeat_n(symbol, amount.try_into().unwrap()).collect()
} }
Self::Hebrew => hebrew_numeral(n), Self::Hebrew => hebrew_numeral(n),
@ -489,18 +490,16 @@ impl NumberingKind {
} }
Self::LowerSimplifiedChinese => { Self::LowerSimplifiedChinese => {
usize_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into() u64_to_chinese(ChineseVariant::Simple, ChineseCase::Lower, n).into()
} }
Self::UpperSimplifiedChinese => { Self::UpperSimplifiedChinese => {
usize_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into() u64_to_chinese(ChineseVariant::Simple, ChineseCase::Upper, n).into()
} }
Self::LowerTraditionalChinese => { Self::LowerTraditionalChinese => {
usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n) u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Lower, n).into()
.into()
} }
Self::UpperTraditionalChinese => { Self::UpperTraditionalChinese => {
usize_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n) u64_to_chinese(ChineseVariant::Traditional, ChineseCase::Upper, n).into()
.into()
} }
Self::EasternArabic => decimal('\u{0660}', n), Self::EasternArabic => decimal('\u{0660}', n),
@ -512,7 +511,7 @@ impl NumberingKind {
} }
/// Stringify an integer to a Hebrew number. /// Stringify an integer to a Hebrew number.
fn hebrew_numeral(mut n: usize) -> EcoString { fn hebrew_numeral(mut n: u64) -> EcoString {
if n == 0 { if n == 0 {
return '-'.into(); return '-'.into();
} }
@ -566,7 +565,7 @@ fn hebrew_numeral(mut n: usize) -> EcoString {
} }
/// Stringify an integer to a Roman numeral. /// Stringify an integer to a Roman numeral.
fn roman_numeral(mut n: usize, case: Case) -> EcoString { fn roman_numeral(mut n: u64, case: Case) -> EcoString {
if n == 0 { if n == 0 {
return match case { return match case {
Case::Lower => 'n'.into(), Case::Lower => 'n'.into(),
@ -622,7 +621,7 @@ fn roman_numeral(mut n: usize, case: Case) -> EcoString {
/// ///
/// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm /// [converter]: https://www.russellcottrell.com/greek/utilities/GreekNumberConverter.htm
/// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/ /// [numbers]: https://mathshistory.st-andrews.ac.uk/HistTopics/Greek_numbers/
fn greek_numeral(n: usize, case: Case) -> EcoString { fn greek_numeral(n: u64, case: Case) -> EcoString {
let thousands = [ let thousands = [
["͵α", "͵Α"], ["͵α", "͵Α"],
["͵β", "͵Β"], ["͵β", "͵Β"],
@ -683,7 +682,7 @@ fn greek_numeral(n: usize, case: Case) -> EcoString {
let mut decimal_digits: Vec<usize> = Vec::new(); let mut decimal_digits: Vec<usize> = Vec::new();
let mut n = n; let mut n = n;
while n > 0 { while n > 0 {
decimal_digits.push(n % 10); decimal_digits.push((n % 10) as usize);
n /= 10; n /= 10;
} }
@ -778,18 +777,16 @@ fn greek_numeral(n: usize, case: Case) -> EcoString {
/// ///
/// You might be familiar with this scheme from the way spreadsheet software /// You might be familiar with this scheme from the way spreadsheet software
/// tends to label its columns. /// tends to label its columns.
fn zeroless<const N_DIGITS: usize>( fn zeroless<const N_DIGITS: usize>(alphabet: [char; N_DIGITS], mut n: u64) -> EcoString {
alphabet: [char; N_DIGITS],
mut n: usize,
) -> EcoString {
if n == 0 { if n == 0 {
return '-'.into(); return '-'.into();
} }
let n_digits = N_DIGITS as u64;
let mut cs = EcoString::new(); let mut cs = EcoString::new();
while n > 0 { while n > 0 {
n -= 1; n -= 1;
cs.push(alphabet[n % N_DIGITS]); cs.push(alphabet[(n % n_digits) as usize]);
n /= N_DIGITS; n /= n_digits;
} }
cs.chars().rev().collect() cs.chars().rev().collect()
} }
@ -797,7 +794,7 @@ fn zeroless<const N_DIGITS: usize>(
/// Stringify a number using a base-10 counting system with a zero digit. /// Stringify a number using a base-10 counting system with a zero digit.
/// ///
/// This function assumes that the digits occupy contiguous codepoints. /// This function assumes that the digits occupy contiguous codepoints.
fn decimal(start: char, mut n: usize) -> EcoString { fn decimal(start: char, mut n: u64) -> EcoString {
if n == 0 { if n == 0 {
return start.into(); return start.into();
} }

View File

@ -388,7 +388,7 @@ pub struct OutlineEntry {
/// space between the entry's body and the page number. When using show /// space between the entry's body and the page number. When using show
/// rules to override outline entries, it is thus recommended to wrap the /// rules to override outline entries, it is thus recommended to wrap the
/// fill in a [`box`] with fractional width, i.e. /// fill in a [`box`] with fractional width, i.e.
/// `{box(width: 1fr, it.fill}`. /// `{box(width: 1fr, it.fill)}`.
/// ///
/// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
/// to tweak the visual weight of the fill. /// to tweak the visual weight of the fill.
@ -623,7 +623,7 @@ impl OutlineEntry {
/// The content which is displayed in place of the referred element at its /// The content which is displayed in place of the referred element at its
/// entry in the outline. For a heading, this is its /// entry in the outline. For a heading, this is its
/// [`body`]($heading.body), for a figure a caption, and for equations it is /// [`body`]($heading.body); for a figure a caption and for equations, it is
/// empty. /// empty.
#[func] #[func]
pub fn body(&self) -> StrResult<Content> { pub fn body(&self) -> StrResult<Content> {

View File

@ -161,7 +161,7 @@ impl Show for Packed<QuoteElem> {
let block = self.block(styles); let block = self.block(styles);
let html = TargetElem::target_in(styles).is_html(); let html = TargetElem::target_in(styles).is_html();
if self.quotes(styles) == Smart::Custom(true) || !block { if self.quotes(styles).unwrap_or(!block) {
let quotes = SmartQuotes::get( let quotes = SmartQuotes::get(
SmartQuoteElem::quotes_in(styles), SmartQuoteElem::quotes_in(styles),
TextElem::lang_in(styles), TextElem::lang_in(styles),

View File

@ -282,7 +282,7 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack(); let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect(); let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
let tr = |tag, row: &[Entry]| { let tr = |tag, row: &[Entry]| {
let row = row let row = row

View File

@ -189,13 +189,15 @@ impl Show for Packed<TermsElem> {
.styled(TermsElem::set_within(true)); .styled(TermsElem::set_within(true));
if tight { if tight {
let leading = ParElem::leading_in(styles); let spacing = self
let spacing = VElem::new(leading.into()) .spacing(styles)
.unwrap_or_else(|| ParElem::leading_in(styles).into());
let v = VElem::new(spacing.into())
.with_weak(true) .with_weak(true)
.with_attach(true) .with_attach(true)
.pack() .pack()
.spanned(span); .spanned(span);
realized = spacing + realized; realized = v + realized;
} }
Ok(realized) Ok(realized)

View File

@ -1,9 +1,12 @@
use ecow::EcoString; use ecow::EcoString;
use typst_library::foundations::Target;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use crate::diag::{At, SourceResult}; use crate::diag::{warning, At, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; use crate::foundations::{
elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem,
};
use crate::introspection::Locatable; use crate::introspection::Locatable;
use crate::World; use crate::World;
@ -32,12 +35,10 @@ use crate::World;
/// embedded file conforms to PDF/A-1 or PDF/A-2. /// embedded file conforms to PDF/A-1 or PDF/A-2.
#[elem(Show, Locatable)] #[elem(Show, Locatable)]
pub struct EmbedElem { pub struct EmbedElem {
/// Path of the file to be embedded. /// The [path]($syntax/#paths) of the file to be embedded.
/// ///
/// Must always be specified, but is only read from if no data is provided /// Must always be specified, but is only read from if no data is provided
/// in the following argument. /// in the following argument.
///
/// For more details about paths, see the [Paths section]($syntax/#paths).
#[required] #[required]
#[parse( #[parse(
let Spanned { v: path, span } = let Spanned { v: path, span } =
@ -80,7 +81,12 @@ pub struct EmbedElem {
} }
impl Show for Packed<EmbedElem> { impl Show for Packed<EmbedElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles) == Target::Html {
engine
.sink
.warn(warning!(self.span(), "embed was ignored during HTML export"));
}
Ok(Content::empty()) Ok(Content::empty())
} }
} }

View File

@ -55,6 +55,7 @@ routines! {
fn eval_string( fn eval_string(
routines: &Routines, routines: &Routines,
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
sink: TrackedMut<Sink>,
string: &str, string: &str,
span: Span, span: Span,
mode: EvalMode, mode: EvalMode,

View File

@ -160,7 +160,7 @@ impl FontBook {
current.variant.weight.distance(variant.weight), current.variant.weight.distance(variant.weight),
); );
if best_key.map_or(true, |b| key < b) { if best_key.is_none_or(|b| key < b) {
best = Some(id); best = Some(id);
best_key = Some(key); best_key = Some(key);
} }

View File

@ -14,7 +14,7 @@ macro_rules! translation {
}; };
} }
const TRANSLATIONS: [(&str, &str); 38] = [ const TRANSLATIONS: [(&str, &str); 39] = [
translation!("ar"), translation!("ar"),
translation!("bg"), translation!("bg"),
translation!("ca"), translation!("ca"),
@ -31,6 +31,7 @@ const TRANSLATIONS: [(&str, &str); 38] = [
translation!("el"), translation!("el"),
translation!("he"), translation!("he"),
translation!("hu"), translation!("hu"),
translation!("id"),
translation!("is"), translation!("is"),
translation!("it"), translation!("it"),
translation!("ja"), translation!("ja"),
@ -82,6 +83,7 @@ impl Lang {
pub const HEBREW: Self = Self(*b"he ", 2); pub const HEBREW: Self = Self(*b"he ", 2);
pub const HUNGARIAN: Self = Self(*b"hu ", 2); pub const HUNGARIAN: Self = Self(*b"hu ", 2);
pub const ICELANDIC: Self = Self(*b"is ", 2); pub const ICELANDIC: Self = Self(*b"is ", 2);
pub const INDONESIAN: Self = Self(*b"id ", 2);
pub const ITALIAN: Self = Self(*b"it ", 2); pub const ITALIAN: Self = Self(*b"it ", 2);
pub const JAPANESE: Self = Self(*b"ja ", 2); pub const JAPANESE: Self = Self(*b"ja ", 2);
pub const LATIN: Self = Self(*b"la ", 2); pub const LATIN: Self = Self(*b"la ", 2);

View File

@ -42,7 +42,7 @@ use ttf_parser::Tag;
use typst_syntax::Spanned; use typst_syntax::Spanned;
use typst_utils::singleton; use typst_utils::singleton;
use crate::diag::{bail, warning, HintedStrResult, SourceResult}; use crate::diag::{bail, warning, HintedStrResult, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue, cast, dict, elem, Args, Array, Cast, Construct, Content, Dict, Fold, IntoValue,
@ -51,7 +51,6 @@ use crate::foundations::{
}; };
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
use crate::math::{EquationElem, MathSize}; use crate::math::{EquationElem, MathSize};
use crate::model::ParElem;
use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::visualize::{Color, Paint, RelativeTo, Stroke};
use crate::World; use crate::World;
@ -504,9 +503,8 @@ pub struct TextElem {
/// enabling hyphenation can /// enabling hyphenation can
/// improve justification. /// improve justification.
/// ``` /// ```
#[resolve]
#[ghost] #[ghost]
pub hyphenate: Hyphenate, pub hyphenate: Smart<bool>,
/// The "cost" of various choices when laying out text. A higher cost means /// The "cost" of various choices when laying out text. A higher cost means
/// the layout engine will make the choice less often. Costs are specified /// the layout engine will make the choice less often. Costs are specified
@ -893,9 +891,21 @@ cast! {
} }
/// Font family fallback list. /// Font family fallback list.
///
/// Must contain at least one font.
#[derive(Debug, Default, Clone, PartialEq, Hash)] #[derive(Debug, Default, Clone, PartialEq, Hash)]
pub struct FontList(pub Vec<FontFamily>); pub struct FontList(pub Vec<FontFamily>);
impl FontList {
pub fn new(fonts: Vec<FontFamily>) -> StrResult<Self> {
if fonts.is_empty() {
bail!("font fallback list must not be empty")
} else {
Ok(Self(fonts))
}
}
}
impl<'a> IntoIterator for &'a FontList { impl<'a> IntoIterator for &'a FontList {
type IntoIter = std::slice::Iter<'a, FontFamily>; type IntoIter = std::slice::Iter<'a, FontFamily>;
type Item = &'a FontFamily; type Item = &'a FontFamily;
@ -913,7 +923,7 @@ cast! {
self.0.into_value() self.0.into_value()
}, },
family: FontFamily => Self(vec![family]), family: FontFamily => Self(vec![family]),
values: Array => Self(values.into_iter().map(|v| v.cast()).collect::<HintedStrResult<_>>()?), values: Array => Self::new(values.into_iter().map(|v| v.cast()).collect::<HintedStrResult<_>>()?)?,
} }
/// Resolve a prioritized iterator over the font families. /// Resolve a prioritized iterator over the font families.
@ -1110,27 +1120,6 @@ impl Resolve for TextDir {
} }
} }
/// Whether to hyphenate text.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Hyphenate(pub Smart<bool>);
cast! {
Hyphenate,
self => self.0.into_value(),
v: Smart<bool> => Self(v),
}
impl Resolve for Hyphenate {
type Output = bool;
fn resolve(self, styles: StyleChain) -> Self::Output {
match self.0 {
Smart::Auto => ParElem::justify_in(styles),
Smart::Custom(v) => v,
}
}
}
/// A set of stylistic sets to enable. /// A set of stylistic sets to enable.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct StylisticSets(u32); pub struct StylisticSets(u32);
@ -1403,24 +1392,7 @@ pub fn is_default_ignorable(c: char) -> bool {
fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) { fn check_font_list(engine: &mut Engine, list: &Spanned<FontList>) {
let book = engine.world.book(); let book = engine.world.book();
for family in &list.v { for family in &list.v {
let found = book.contains_family(family.as_str()); if !book.contains_family(family.as_str()) {
if family.as_str() == "linux libertine" {
let mut warning = warning!(
list.span,
"Typst's default font has changed from Linux Libertine to its successor Libertinus Serif";
hint: "please set the font to `\"Libertinus Serif\"` instead"
);
if found {
warning.hint(
"Linux Libertine is available on your system - \
you can ignore this warning if you are sure you want to use it",
);
warning.hint("this warning will be removed in Typst 0.13");
}
engine.sink.warn(warning);
} else if !found {
engine.sink.warn(warning!( engine.sink.warn(warning!(
list.span, list.span,
"unknown font family: {}", "unknown font family: {}",

View File

@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem};
use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load}; use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem}; use crate::model::{Figurable, ParElem};
use crate::text::{ use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize};
FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize,
};
use crate::visualize::Color; use crate::visualize::Color;
use crate::World; use crate::World;
@ -190,7 +188,7 @@ pub struct RawElem {
/// - A path string to load a syntax file from the given path. For more /// - A path string to load a syntax file from the given path. For more
/// details about paths, see the [Paths section]($syntax/#paths). /// details about paths, see the [Paths section]($syntax/#paths).
/// - Raw bytes from which the syntax should be decoded. /// - Raw bytes from which the syntax should be decoded.
/// - An array where each item is one the above. /// - An array where each item is one of the above.
/// ///
/// ````example /// ````example
/// #set raw(syntaxes: "SExpressions.sublime-syntax") /// #set raw(syntaxes: "SExpressions.sublime-syntax")
@ -448,7 +446,11 @@ impl Show for Packed<RawElem> {
let mut realized = Content::sequence(seq); let mut realized = Content::sequence(seq);
if TargetElem::target_in(styles).is_html() { if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::pre) return Ok(HtmlElem::new(if self.block(styles) {
tag::pre
} else {
tag::code
})
.with_body(Some(realized)) .with_body(Some(realized))
.pack() .pack()
.spanned(self.span())); .spanned(self.span()));
@ -472,7 +474,7 @@ impl ShowSet for Packed<RawElem> {
let mut out = Styles::new(); let mut out = Styles::new();
out.set(TextElem::set_overhang(false)); out.set(TextElem::set_overhang(false));
out.set(TextElem::set_lang(Lang::ENGLISH)); out.set(TextElem::set_lang(Lang::ENGLISH));
out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_hyphenate(Smart::Custom(false)));
out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into())));
out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")])));
out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None)));

View File

@ -159,7 +159,7 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
{ {
let covers = family.covers(); let covers = family.covers();
return text.chars().all(|c| { return text.chars().all(|c| {
covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
&& font.ttf().glyph_index(c).is_some() && font.ttf().glyph_index(c).is_some()
}); });
} }

View File

@ -238,7 +238,7 @@ impl<'s> SmartQuotes<'s> {
"cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high, "cs" | "de" | "et" | "is" | "lt" | "lv" | "sk" | "sl" => low_high,
"da" => ("", "", "", ""), "da" => ("", "", "", ""),
"fr" | "ru" if alternative => default, "fr" | "ru" if alternative => default,
"fr" => ("\u{00A0}", "\u{00A0}", "«\u{00A0}", "\u{00A0}»"), "fr" => ("", "", "«\u{202F}", "\u{202F}»"),
"fi" | "sv" if alternative => ("", "", "»", "»"), "fi" | "sv" if alternative => ("", "", "»", "»"),
"bs" | "fi" | "sv" => ("", "", "", ""), "bs" | "fi" | "sv" => ("", "", "", ""),
"it" if alternative => default, "it" if alternative => default,
@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> {
"el" => ("", "", "«", "»"), "el" => ("", "", "«", "»"),
"he" => ("", "", "", ""), "he" => ("", "", "", ""),
"hr" => ("", "", "", ""), "hr" => ("", "", "", ""),
"bg" => ("", "", "", ""),
_ if lang.dir() == Dir::RTL => ("", "", "", ""), _ if lang.dir() == Dir::RTL => ("", "", "", ""),
_ => default, _ => default,
}; };

View File

@ -130,7 +130,7 @@ static TO_SRGB: LazyLock<qcms::Transform> = LazyLock::new(|| {
/// ///
/// # Predefined color maps /// # Predefined color maps
/// Typst also includes a number of preset color maps that can be used for /// Typst also includes a number of preset color maps that can be used for
/// [gradients]($gradient.linear). These are simply arrays of colors defined in /// [gradients]($gradient/#stops). These are simply arrays of colors defined in
/// the module `color.map`. /// the module `color.map`.
/// ///
/// ```example /// ```example
@ -148,11 +148,11 @@ static TO_SRGB: LazyLock<qcms::Transform> = LazyLock::new(|| {
/// | `magma` | A black to purple to yellow color map. | /// | `magma` | A black to purple to yellow color map. |
/// | `plasma` | A purple to pink to yellow color map. | /// | `plasma` | A purple to pink to yellow color map. |
/// | `rocket` | A black to red to white color map. | /// | `rocket` | A black to red to white color map. |
/// | `mako` | A black to teal to yellow color map. | /// | `mako` | A black to teal to white color map. |
/// | `vlag` | A light blue to white to red color map. | /// | `vlag` | A light blue to white to red color map. |
/// | `icefire` | A light teal to black to yellow color map. | /// | `icefire` | A light teal to black to orange color map. |
/// | `flare` | A orange to purple color map that is perceptually uniform. | /// | `flare` | A orange to purple color map that is perceptually uniform. |
/// | `crest` | A blue to white to red color map. | /// | `crest` | A light green to blue color map. |
/// ///
/// Some popular presets are not included because they are not available under a /// Some popular presets are not included because they are not available under a
/// free licence. Others, like /// free licence. Others, like

View File

@ -10,12 +10,14 @@ use crate::foundations::{
use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size};
use crate::visualize::{FillRule, Paint, Stroke}; use crate::visualize::{FillRule, Paint, Stroke};
/// A curve consisting of movements, lines, and Beziér segments. use super::FixedStroke;
/// A curve consisting of movements, lines, and Bézier segments.
/// ///
/// At any point in time, there is a conceptual pen or cursor. /// At any point in time, there is a conceptual pen or cursor.
/// - Move elements move the cursor without drawing. /// - Move elements move the cursor without drawing.
/// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new /// - Line/Quadratic/Cubic elements draw a segment from the cursor to a new
/// position, potentially with control point for a Beziér curve. /// position, potentially with control point for a Bézier curve.
/// - Close elements draw a straight or smooth line back to the start of the /// - Close elements draw a straight or smooth line back to the start of the
/// curve or the latest preceding move segment. /// curve or the latest preceding move segment.
/// ///
@ -26,7 +28,7 @@ use crate::visualize::{FillRule, Paint, Stroke};
/// or relative to the current pen/cursor position, that is, the position where /// or relative to the current pen/cursor position, that is, the position where
/// the previous segment ended. /// the previous segment ended.
/// ///
/// Beziér curve control points can be skipped by passing `{none}` or /// Bézier curve control points can be skipped by passing `{none}` or
/// automatically mirrored from the preceding segment by passing `{auto}`. /// automatically mirrored from the preceding segment by passing `{auto}`.
/// ///
/// # Example /// # Example
@ -88,7 +90,7 @@ pub struct CurveElem {
#[fold] #[fold]
pub stroke: Smart<Option<Stroke>>, pub stroke: Smart<Option<Stroke>>,
/// The components of the curve, in the form of moves, line and Beziér /// The components of the curve, in the form of moves, line and Bézier
/// segment, and closes. /// segment, and closes.
#[variadic] #[variadic]
pub components: Vec<CurveComponent>, pub components: Vec<CurveComponent>,
@ -225,7 +227,7 @@ pub struct CurveLine {
pub relative: bool, pub relative: bool,
} }
/// Adds a quadratic Beziér curve segment from the last point to `end`, using /// Adds a quadratic Bézier curve segment from the last point to `end`, using
/// `control` as the control point. /// `control` as the control point.
/// ///
/// ```example /// ```example
@ -245,9 +247,9 @@ pub struct CurveLine {
/// ``` /// ```
#[elem(name = "quad", title = "Curve Quadratic Segment")] #[elem(name = "quad", title = "Curve Quadratic Segment")]
pub struct CurveQuad { pub struct CurveQuad {
/// The control point of the quadratic Beziér curve. /// The control point of the quadratic Bézier curve.
/// ///
/// - If `{auto}` and this segment follows another quadratic Beziér curve, /// - If `{auto}` and this segment follows another quadratic Bézier curve,
/// the previous control point will be mirrored. /// the previous control point will be mirrored.
/// - If `{none}`, the control point defaults to `end`, and the curve will /// - If `{none}`, the control point defaults to `end`, and the curve will
/// be a straight line. /// be a straight line.
@ -272,7 +274,7 @@ pub struct CurveQuad {
pub relative: bool, pub relative: bool,
} }
/// Adds a cubic Beziér curve segment from the last point to `end`, using /// Adds a cubic Bézier curve segment from the last point to `end`, using
/// `control-start` and `control-end` as the control points. /// `control-start` and `control-end` as the control points.
/// ///
/// ```example /// ```example
@ -388,7 +390,7 @@ pub enum CloseMode {
Straight, Straight,
} }
/// A curve consisting of movements, lines, and Beziér segments. /// A curve consisting of movements, lines, and Bézier segments.
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Curve(pub Vec<CurveItem>); pub struct Curve(pub Vec<CurveItem>);
@ -530,3 +532,65 @@ impl Curve {
Size::new(max_x - min_x, max_y - min_y) Size::new(max_x - min_x, max_y - min_y)
} }
} }
impl Curve {
fn to_kurbo(&self) -> impl Iterator<Item = kurbo::PathEl> + '_ {
use kurbo::PathEl;
self.0.iter().map(|item| match *item {
CurveItem::Move(point) => PathEl::MoveTo(point_to_kurbo(point)),
CurveItem::Line(point) => PathEl::LineTo(point_to_kurbo(point)),
CurveItem::Cubic(point, point1, point2) => PathEl::CurveTo(
point_to_kurbo(point),
point_to_kurbo(point1),
point_to_kurbo(point2),
),
CurveItem::Close => PathEl::ClosePath,
})
}
/// When this curve is interpreted as a clip mask, would it contain `point`?
pub fn contains(&self, fill_rule: FillRule, needle: Point) -> bool {
let kurbo = kurbo::BezPath::from_vec(self.to_kurbo().collect());
let windings = kurbo::Shape::winding(&kurbo, point_to_kurbo(needle));
match fill_rule {
FillRule::NonZero => windings != 0,
FillRule::EvenOdd => windings % 2 != 0,
}
}
/// When this curve is stroked with `stroke`, would the stroke contain
/// `point`?
pub fn stroke_contains(&self, stroke: &FixedStroke, needle: Point) -> bool {
let width = stroke.thickness.to_raw();
let cap = match stroke.cap {
super::LineCap::Butt => kurbo::Cap::Butt,
super::LineCap::Round => kurbo::Cap::Round,
super::LineCap::Square => kurbo::Cap::Square,
};
let join = match stroke.join {
super::LineJoin::Miter => kurbo::Join::Miter,
super::LineJoin::Round => kurbo::Join::Round,
super::LineJoin::Bevel => kurbo::Join::Bevel,
};
let miter_limit = stroke.miter_limit.get();
let mut style = kurbo::Stroke::new(width)
.with_caps(cap)
.with_join(join)
.with_miter_limit(miter_limit);
if let Some(dash) = &stroke.dash {
style = style.with_dashes(
dash.phase.to_raw(),
dash.array.iter().copied().map(Abs::to_raw),
);
}
let opts = kurbo::StrokeOpts::default();
let tolerance = 0.01;
let expanded = kurbo::stroke(self.to_kurbo(), &style, &opts, tolerance);
kurbo::Shape::contains(&expanded, point_to_kurbo(needle))
}
}
fn point_to_kurbo(point: Point) -> kurbo::Point {
kurbo::Point::new(point.x.to_raw(), point.y.to_raw())
}

View File

@ -70,6 +70,9 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
/// the offsets when defining a gradient. In this case, Typst will space all /// the offsets when defining a gradient. In this case, Typst will space all
/// stops evenly. /// stops evenly.
/// ///
/// Typst predefines color maps that you can use as stops. See the
/// [`color`]($color/#predefined-color-maps) documentation for more details.
///
/// # Relativeness /// # Relativeness
/// The location of the `{0%}` and `{100%}` stops depends on the dimensions /// The location of the `{0%}` and `{100%}` stops depends on the dimensions
/// of a container. This container can either be the shape that it is being /// of a container. This container can either be the shape that it is being
@ -117,12 +120,12 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
/// #let spaces = ( /// #let spaces = (
/// ("Oklab", color.oklab), /// ("Oklab", color.oklab),
/// ("Oklch", color.oklch), /// ("Oklch", color.oklch),
/// ("linear-RGB", color.linear-rgb),
/// ("sRGB", color.rgb), /// ("sRGB", color.rgb),
/// ("linear-RGB", color.linear-rgb),
/// ("CMYK", color.cmyk), /// ("CMYK", color.cmyk),
/// ("Grayscale", color.luma),
/// ("HSL", color.hsl), /// ("HSL", color.hsl),
/// ("HSV", color.hsv), /// ("HSV", color.hsv),
/// ("Grayscale", color.luma),
/// ) /// )
/// ///
/// #for (name, space) in spaces { /// #for (name, space) in spaces {
@ -157,10 +160,6 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
/// ) /// )
/// ``` /// ```
/// ///
/// # Presets
/// Typst predefines color maps that you can use with your gradients. See the
/// [`color`]($color/#predefined-color-maps) documentation for more details.
///
/// # Note on file sizes /// # Note on file sizes
/// ///
/// Gradients can be quite large, especially if they have many stops. This is /// Gradients can be quite large, especially if they have many stops. This is
@ -288,7 +287,7 @@ impl Gradient {
/// )), /// )),
/// ) /// )
/// ``` /// ```
#[func] #[func(title = "Radial Gradient")]
fn radial( fn radial(
span: Span, span: Span,
/// The color [stops](#stops) of the gradient. /// The color [stops](#stops) of the gradient.
@ -402,7 +401,7 @@ impl Gradient {
/// )), /// )),
/// ) /// )
/// ``` /// ```
#[func] #[func(title = "Conic Gradient")]
pub fn conic( pub fn conic(
span: Span, span: Span,
/// The color [stops](#stops) of the gradient. /// The color [stops](#stops) of the gradient.
@ -575,19 +574,17 @@ impl Gradient {
} }
let n = repetitions.v; let n = repetitions.v;
let mut stops = std::iter::repeat(self.stops_ref()) let mut stops = std::iter::repeat_n(self.stops_ref(), n)
.take(n)
.enumerate() .enumerate()
.flat_map(|(i, stops)| { .flat_map(|(i, stops)| {
let mut stops = stops let mut stops = stops
.iter() .iter()
.map(move |&(color, offset)| { .map(move |&(color, offset)| {
let t = i as f64 / n as f64;
let r = offset.get(); let r = offset.get();
if i % 2 == 1 && mirror { if i % 2 == 1 && mirror {
(color, Ratio::new(t + (1.0 - r) / n as f64)) (color, Ratio::new((i as f64 + 1.0 - r) / n as f64))
} else { } else {
(color, Ratio::new(t + r / n as f64)) (color, Ratio::new((i as f64 + r) / n as f64))
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -1230,7 +1227,7 @@ fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ra
}; };
if stop.get() < last_stop { if stop.get() < last_stop {
bail!(*span, "offsets must be in strictly monotonic order"); bail!(*span, "offsets must be in monotonic order");
} }
last_stop = stop.get(); last_stop = stop.get();

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